丸ごとNext.jsでWebアプリケーションを作ってみた話
はじめに
こんにちは!株式会社マチス教育システムのいけふくろうです。
実務で商品管理画面を実装する機会があり、その際に得た知見をもとに、設計・実装プロセスや留意点について紹介させていただきます!!
フェーズ1の完成品
コスト(人的、予算)及びスケジュールの兼ね合いから、最低限のスコープでミニマムに設計・実装をおこなったため、至らない点は多々ありますが、作ったアプリケーションはこちらです💁
機能一覧
- アカウント管理
- ユーザーの管理機能とロール設定機能
- テナント管理
- 各オーナーに対して複数のテナントを紐づけ、オーナーごとにテナントを一元管理できる機能
- 商品管理
- 商品情報の基本機能として、カテゴリー、商品基本情報、属性情報、商品画像の管理機能
※商品基幹システムとの連携部分は未実装、ユーザー向けの注文サイト連携部分は未実装であることからJANコード管理などの機能は含めていない - 在庫管理
- 主力商品は食品であることから、販売単位(g、個 ...etc)を属性情報として付与することになるが、現状SKUはひとつひとつの商品として管理
- 商品情報の基本機能として、カテゴリー、商品基本情報、属性情報、商品画像の管理機能
- 注文管理
- 売上管理
- オーナー管轄の全テナントの売上及び各テナントごとの売上状況を把握する機能
対象読者
- Next.jsでフロントエンドとバックエンドの両方を活用したいと考えている方
- Next.jsでフロントエンドとバックエンドが両方できると知っているが、どのように活用するかの解像度が低い方
- Next.jsのApp Routerを活用した実装例を学びたい方
環境
共通
- Node.js 20.17.0
- Next.js 14.2.7
フロントエンド
- React 18
- React Hook Form 7.53.0(バリデーションライブラリ)
- CSS Modules
- Next.jsに標準で組み込まれているスタイル定義
バックエンド
- InversifyJS 6.0.2(DIライブラリ)
- Prisma 5.19.1(ORMライブラリ)
データベース
- MySQL
技術スタック
全体ディレクトリ構成
主要なディレクトリ/ファイル群を下記に示します。
├── src
│ ├── __test__
│ │ └── backend
│ ├── app
│ │ ├── (site)
│ │ │ ├── (auth)
│ │ │ │ └── login
│ │ │ │ ├── _components
│ │ │ │ │ └── LoginForm
│ │ │ │ │ ├── action.tsx
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── state.ts
│ │ │ │ │ └── style.module.css
│ │ │ │ ├── layout.module.css
│ │ │ │ ├── layout.tsx
│ │ │ │ └── page.tsx
│ │ │ └── (main)
│ │ │ ├── accounts
│ │ │ │ ├── [userId]
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── _components
│ │ │ │ │ ├── AccountForm
│ │ │ │ │ │ ├── action.ts
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ ├── state.ts
│ │ │ │ │ │ └── style.module.css
│ │ │ │ │ ├── AccountList
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── style.module.css
│ │ │ │ │ ├── EditAccountDetail
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── style.module.css
│ │ │ │ │ └── NewAccountDetail
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── style.module.css
│ │ │ │ ├── loading.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── layout.tsx
│ │ ├── (static)
│ │ │ └── system-error
│ │ │ └── page.tsx
│ │ ├── _components
│ │ │ └── Button
│ │ │ ├── index.tsx
│ │ │ └── style.module.css
│ │ ├── _config
│ │ │ └── unauthenticatedRoutes.ts
│ │ ├── _hooks
│ │ │ └── useModal.ts
│ │ ├── _infrastructure
│ │ │ ├── auth
│ │ │ │ └── postLogin
│ │ │ │ └── index.ts
│ │ │ ├── client
│ │ │ │ ├── apiClient.ts
│ │ │ │ ├── errorHandlers.ts
│ │ │ │ ├── formatResponse.ts
│ │ │ │ ├── handleResponse.ts
│ │ │ │ ├── requestUtils.ts
│ │ │ │ └── result.ts
│ │ │ ├── constants
│ │ │ │ └── index.ts
│ │ ├── _types
│ │ │ └── user.ts
│ │ ├── _utils
│ │ │ └── formatter.ts
│ │ ├── api
│ │ │ └── v1
│ │ │ └── admin
│ │ │ ├── platform
│ │ │ │ ├── auth
│ │ │ │ │ └── login
│ │ │ │ │ └── route.ts
│ │ │ │ └── users
│ │ │ │ ├── [userId]
│ │ │ │ │ └── route.ts
│ │ │ │ ├── route.ts
│ │ │ │ └── search
│ │ │ │ └── route.ts
│ │ │ └── tenants
│ │ │ └── route.ts
│ │ ├── error.tsx
│ │ ├── layout.tsx
│ │ ├── not-found.tsx
│ │ ├── page.module.css
│ │ ├── page.tsx
│ │ └── styles
│ │ ├── globals.css
│ │ └── theme.css
│ ├── application
│ │ ├── auth
│ │ │ ├── AuthService.ts
│ │ │ ├── IAuthService.ts
│ │ │ └── TokenProvider.ts
│ │ ├── platform
│ │ │ └── user
│ │ │ ├── IUserService.ts
│ │ │ ├── UserService.ts
│ │ │ ├── UserDto.ts
│ │ │ └── UserCommandDto.ts
│ ├── config
│ │ ├── inversify.config.ts
│ │ └── types.ts
│ ├── domain
│ │ ├── error
│ │ │ ├── ErrorResponse.ts
│ │ │ └── exception
│ │ │ ├── ArgumentException.ts
│ │ │ ├── BaseError.ts
│ │ ├── platform
│ │ │ └── user
│ │ │ ├── IUserRepository.ts
│ │ │ └── User.ts
│ ├── infrastructure
│ │ ├── prisma
│ │ │ ├── generated
│ │ │ │ ├── platform
│ │ │ │ └── tenant-common
│ │ │ ├── platform
│ │ │ │ ├── migrations
│ │ │ │ ├── schema.prisma
│ │ │ │ ├── seed-platform.ts
│ │ │ ├── tenant
│ │ │ │ ├── generated
│ │ │ │ ├── schema.prisma.template
│ │ │ │ └── t00000000000000
│ │ │ └── tenant-common
│ │ │ ├── schema.prisma
│ │ │ └── seed-tenant.ts
│ │ ├── repository
│ │ │ ├── client
│ │ │ │ ├── IPrismaClient.ts
│ │ │ │ └── PrismaClient.ts
│ │ │ ├── platform
│ │ │ │ └── user
│ │ │ │ └── UserRepository.ts
│ ├── interface
│ │ ├── authorization
│ │ │ └── Authorization.ts
│ │ └── controller
│ │ └── admin
│ │ └── platform
│ │ ├── auth
│ │ │ └── AuthController.ts
│ │ └── user
│ │ └── UserController.ts
│ ├── middleware.ts
│ ├── type
│ │ └── CustomNextRequest.ts
│ └── util
│ ├── exception
│ │ └── AllErrorHandler.ts
│ └── security
├── public
├── tsconfig.json
├── docker-compose.local.yml
├── jest.backend.setup.js
├── jest.config.backend.js
├── next-env.d.ts
├── next.config.mjs
├── package-lock.json
└── package.json
フロントエンド
App Routerを使用しているため、src/app
ディレクトリ配下を管理ディレクトリとしています。
Private Folder _(アンダーバー)
始まりのディレクトリに関しては、責務に応じて払い出しています。
なお、App Routerの規約に即したルーティング定義(Segment構成フォルダ・ファイル)に関しては、実践Next.js ——App Routerで進化するWebアプリ開発の書籍を参考にして設計しました。
とても素晴らしい書籍です!!手に取っていただけると学びが多くありますので是非🙌
バックエンド
アーキテクチャーパターンとしては、オニオンアーキテクチャー(後述)をベースとしているため、src
配下は責務に応じたディレクトリ構成としました。
層 | ディレクトリ | 内容 |
---|---|---|
アプリケーション層 | application | ビジネスロジック、サービスクラス |
ドメイン層 | domain | ドメインモデル、リポジトリインターフェース、ValueObject |
インフラストラクチャー層 | infrastructure | リポジトリ、ORM(Prisma) |
リポジトリ | repository | リポジトリ実装クラス |
ORM | prisma | Prisma関連ファイル、エンティティ定義 |
インターフェース / プレゼンテーション層 | interface | コントローラー、認証認可機能 |
コントローラー | controller | エンドポイント、リクエスト/レスポンス |
認証・認可 | authrization | アクセス制御、認可 |
エンドポイント | app/api | APIエンドポイント、App RouterのRoute Handler機能を活用 |
バックエンド
私自身も本アプリを設計・実装するまでは、既述のNext.jsでフロントエンドとバックエンドが両方できると知っているが、どのように活用するかの解像度が低い方でした。
フロントエンドはNext.jsのPages RouterとApp Router(Next.js v13.3.0)を活用したアプリは数年前から実装経験があるため、イメージはついていたもののバックエンドをNext.jsの中で含めたアーキテクチャーはどうするのが良いのだろうか?とぼんやりでした。
調査や実際にコードを書いて試行錯誤をしたのですが、「あれ?アーキテクチャーパターンはオニオンアーキテクチャーにしても、Next.jsにほぼ依存することなく構築できるのではないか」と自身で納得して、今までの知見をもとに下記のような責務分けで設計・実装する意思決定をしました。
なお、別記事のNode.js/Expressバージョンと基本方針は同じです
エンドポイントの例
エンドポイント
/api/v1/admin/platform/auth/me
ルーティング
src/app/api/v1/admin/platform/auth/me/route.ts
ValueObjectの例
export class TenantCode {
public value: string;
constructor(value: string) {
if (!value.match(/^t[0-9]{14}$/)) {
throw new ArgumentException(
`テナントコードはt始まりの頭0埋め15文字の文字列である必要があります: ${value}`,
);
}
this.value = value;
}
}
認証・認可
下記3つを満たす共通認証・認可機構を設計・実装しました。
- Cookieからトークン(アクセスorリフレッシュ)を取得し、認証する
- ロールによるAPI認可をする
- 上記1.2をパスした場合には、リクエストオブジェクトにユーザー情報を付加する
全てのAPIリクエスト時に必ず通る制御が必要になります。Next.jsでは、Middlewareの機能がありますので、最初はそちらで試しました。
※Springのfilter機能のように共通の認証機能を割り込ませたい
Middleware
ミドルウェア内での実装を試みて、しくじったことが2つありました!!
しくじったことNo.1
Next.jsのミドルウェアは、エッジランタイム環境下で実行されます。つまり、利用できるAPIは限定されることになります。
CookieあるいはHTTPヘッダーのAuthrizationからトークンを取得し、デコードなどの処理を経て検証させることがよくあるケースのひとつです。
jsonwebtoken
ライブラリを導入していましたが、NodeのAPI依存があるようで、エッジランタイム環境下では動きませんでした。
⨯ Error: The edge runtime does not support Node.js 'crypto' module.
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime
そのため、代替としてjose
へ切り替えました。既にjsonwebtoken
ライブラリを活用したTokenProviderクラス内にトークン生成、検証メソッドを実装済みであったが、切り替えによるコードの改修は容易だったので大きな手戻りになることはありませんでした。
公式ドキュメントにもあるとおり、実装を考えているロジックがエッジランタイム環境下にて動くかどうかを事前に調査することが大切であることを学びました✍️
しくじったことNo.2
ミドルウェア内にて、Requestオブジェクトにカスタムプロパティーの型定義を追加し、値のマッピングはできても後続のRoute Handlerでは参照できませんでした。
export interface CustomNextRequest extends NextRequest {
authUser?: PlatformTokenPayload;
}
3. 上記1.2をパスした場合には、リクエストオブジェクトにユーザー情報を付加する
の制御を加えることが困難と判断しました。
Route Handler内での共通認証認可機構の構築
Route Handlerのコードが実行される前に全てのAPIで呼び出す必要があります。そのため、ラッパーメソッドを作ることにしました。
実装イメージ
export type HandlerFunction = (
req: CustomNextRequest,
context?: { params?: Record<string, string>; query?: Record<string, string> },
) => Promise<NextResponse>;
export const withAuthrization = (
handler: HandlerFunction,
allowedRoles: RoleCode[],
): ((
req: NextRequest,
context?: { params: Record<string, string>; query: Record<string, string> },
) => Promise<NextResponse>) => {
return async (
req: CustomNextRequest,
context = {
params: {} as Record<string, never>,
query: {} as Record<string, never>,
},
) => {
try {
const { pathname, searchParams } = req.nextUrl;
context.query = Object.fromEntries(searchParams.entries());
// 認証処理
const token = req.cookies.get("token")?.value;
if (!token) throw new InvalidTokenException("Invalid token");
// トークンの検証
const decoded = await TokenProvider.verifyPlatformToken(token);
if (!decoded) throw new InvalidTokenException("Invalid token");
// 認可処理
const roles = decoded.roles;
if (!roles) throw new ForbiddenException("Forbidden");
// ユーザー情報からロールコードを取得して、許可されたロールコードかどうかを確認する
const userRoleCodes = roles.map((role) => role.code);
const isAllowed = allowedRoles.some((role) =>
userRoleCodes.includes(role),
);
if (!isAllowed) throw new ForbiddenException("Forbidden");
// 認可をパスした場合はカスタムリクエストにユーザー情報をマッピングしてハンドラーを返却
req.platformUser = {
...decoded,
isAdmin: userRoleCodes.includes("ADMIN"),
isOwner: userRoleCodes.includes("OWNER"),
};
return handler(req, context);
} catch (error) {
return allErrorHandler(error, req);
}
};
};
利用例
const getHandler: HandlerFunction = async (
req: CustomNextRequest,
context = { params: {}, query: {} },
) => {
try {
const userId = parseInt(context?.params?.userId ?? "0", 10);
if (!userId) throw new ForbiddenException("Invalid userId");
const user = await userController.getUserById(userId);
return NextResponse.json(user);
} catch (error) {
return allErrorHandler(error, req);
}
};
export const GET = withAuthrization(getHandler, ["ADMIN", "OWNER"]);
ポイントとしては、withAuthorization(getHandler, ["ADMIN", "OWNER"])
とすることで、Route Handler の制御をラップし、共通の認証・認可メソッドで許可するロールを柔軟に指定できるようにしました。
これにより、エンドポイントごとに許可するロールを利用サイドで指定でき、各エンドポイントに対して適切なアクセス制御が可能になります。メンテナンス性も高まり、ロールごとのアクセス管理が容易になるため、今後の変更に対応しやすい設計を意識しました。
エラーハンドリング
共通のエラハンを考える際に意識しているのは各レイヤーで例外を発生させた場合に例外を捕捉し、共通のエラーレスポンスを生成して、クライアントに返却させることです。
私はエラー処理などをコントローラーの共通処理として一元管理できるSpring BootのControllerAdviceの制御がイメージにあります。
根元であるRoute Handlerにて、例外を捕捉できるようにしてRoute Handlerでは共通のエラーハンドリングメソッドを呼び出すようにしました。
Route Handler
const getHandler: HandlerFunction = async (
req: CustomNextRequest,
context = { params: {}, query: {} },
) => {
try {
const userId = parseInt(context?.params?.userId ?? "0", 10);
if (!userId) throw new ForbiddenException("Invalid userId");
const user = await userController.getUserById(userId);
return NextResponse.json(user);
} catch (error) {
return allErrorHandler(error, req);
}
};
サービスクラス
async getUserById(userId: number): Promise<UserWithRolesAndTenantsDto> {
const user = await this._userRepository.findUserById(userId);
if (!user) throw new NotFoundException(`User not found. userId: ${userId}`);
return new UserWithRolesAndTenantsDto(user);
}
リポジトリクラス
async findUserById(userId: number): Promise<UserWithRolesAndTenants | null> {
try {
...
return userEntity ? new UserWithRolesAndTenants(userEntity) : null;
} catch (error: unknown) {
return handleServerError(error, "@UserRepositoryImpl#findUserById");
}
}
共通のエラーハンドリングメソッド
export const allErrorHandler = (error: unknown, req: Request | NextRequest) => {
if (error instanceof NotFoundException) {
const errorResponse = new ErrorResponse(
StatusCodes.NOT_FOUND,
"NotFoundException",
[new ErrorDetail("", error.message)],
req.url,
);
return NextResponse.json(errorResponse, { status: StatusCodes.NOT_FOUND});
} else {
const errorResponse = new ErrorResponse(
StatusCodes.INTERNAL_SERVER_ERROR,
"InternalServerError",
[new ErrorDetail("", "Unexpected error occurred.")],
req.url,
);
return NextResponse.json(errorResponse, {
status: StatusCodes.INTERNAL_SERVER_ERROR,
});
}
};
なお、下記の記事に書かれている通り、try catchのerrorでanyを指定することはできなくなりました。
そのため、下記のように別定義したメソッドにerrorをそのまま渡すようにしました。
} catch (error: unknown) {
return handleServerError(error, "@UserRepositoryImpl#findUserById");
}
単純ですが、下記のようにして、Error
判定を入れた上で例外をスローさせるようにしました。
export const handleServerError = (error: unknown, contextMessage: string) => {
if (error instanceof Error) {
throw new Error(`${contextMessage}: ${error.message}`);
} else {
throw new Error(`${contextMessage}: An unknown error occurred`);
}
};
フロントエンド
既述しましたが、本アプリケーションの設計・実装にあたって、以下の書籍が本当に参考となりました!!ありがとうございます🙌
共通レイアウト
共通レイアウトの実装イメージ
type Props = {
children: React.ReactNode;
};
export default async function SiteLayout({ children }: Props) {
return (
<Layout.Root>
<Navigation>
<Layout.Main>{children}</Layout.Main>
</Navigation>
</Layout.Root>
);
}
RootLayout
-> SiteLayout
を用意して、後述の基本構成としました。
ヘルパーコンポーネント
各画面・コンポーネントで複数回利用されるコンポーネントに関しては、src/app/_components
ディレクトリ内に配置しました。
1ページの基本構成
下図のようにサーバーコンポーネントを起点として、インタラクティブな機能を提供するコンポーネントに関しては、クライアントコンポーネントとする。バックエンドとの接続部は、サーバーサイドにて実行するようにしました。
APIコーラー
Next.jsに組み込まれている(内部で拡張)Web標準APIのfetch
メソッドの利用を前提とします。
Route HandlerのAPIをコールすることになりますが、利用サイドとしては、クライアントコンポーネント、サーバーコンポーネントのどちらからコールするかを留意する必要があります。
サーバーコンポーネント=Cookieへアクセスして取得するのか あるいは クライアントコンポーネント=Cookie情報を送るのか です。
サーバーコンポーネントの場合には、別記事に記載した方法を活用できます。
クライアントコンポーネントの場合には、ヘッダーにcredentials: "include"
を明示的に指定することを忘れないようにします。
共通APIクライアント
特別なことをしておりませんが、やめておければ良かったと感じたのは、Result型
の導入でした...
Result型の定義
export type Result<T, E> = Success<T, E> | Failure<T, E>;
export class Success<T, E> {
constructor(readonly value: T) {}
type = "success" as const;
isSuccess(): this is Success<T, E> {
return true;
}
isFailure(): this is Failure<T, E> {
return false;
}
}
export class Failure<T, E> {
constructor(readonly error: E) {}
type = "failure" as const;
isSuccess(): this is Success<T, E> {
return false;
}
isFailure(): this is Failure<T, E> {
return true;
}
}
フォーマットしたAPIレスポンスに対して、成功or失敗の型にマッピングして返却させるようにしました。
APIレスポンスの生成
export const handleResponse = async <T>(
response: Response,
resType: ResponseType = "json",
): Promise<Result<T, ErrorResponse>> => {
try {
const data = await formatResponse<T>(response, resType);
if (response.ok) {
return new Success<T, ErrorResponse>(data);
}
... エラーレスポンスを生成
return new Failure<T, ErrorResponse>(errorResponse);
} catch (error) {
... エラーレスポンスを生成
return new Failure<T, ErrorResponse>(errorResponse);
}
};
成功、失敗の判定をおこない、それぞれの型にマッピングします。
APIクライアント
export const get = async <T>(
url: string,
options?: RequestInit,
responseType: ResponseType = "json",
): Promise<Result<T, ErrorResponse>> => {
try {
const response = await fetch(url, {
...getRequestInit(options),
});
return await handleResponse<T>(response, responseType);
} catch (error) {
... エラーレスポンスを生成
return new Failure<T, ErrorResponse>(errorResponse);
}
};
利用サイド
Success / Failureの判定が入るので、準正常系エラーハンドリングの場合には、if
が目立つようになります。
export async function postTenantAction(
prevState: FormState,
tenantData: TenantFormData,
): Promise<FormState> {
const response = await registerTenant({
...
});
if (response.isSuccess()) {
return handleSuccess(prevState);
}
const { statusCode } = response.error;
if (statusCode === 409) {
return handleWarning(prevState, "このテナント名は既に登録されています。");
} else {
throw new Error(response.error.reason || "想定外のエラーが発生しました");
}
}
export default async function TenantPage() {
const tenants = await getAllTenants();
if (!tenants.isSuccess()) throw new Error(tenants.error.reason);
return (
<>
<TenantList initialTenants={tenants.value} />
</>
);
}
素直に例外をスローさせてハンドリングすることで、コードのシンプルさと可読性を保ちつつ、エラーハンドリングの統一性を提供できると振り返りました📝
エラーハンドリング
実装したアプリケーションにはServer Component、Client Component、Server Actionが存在します。Next.jsが提供するエラーハンドリングの仕様に沿って、これらの各コンポーネントで適切なエラーハンドリングを整備しました。
error.tsx
- サーバーサイドで発生したエラーのみ捕捉される
- 同じ階層にある
lauout.js
もしくはtemplate.js
でのエラーをキャッチすることができない - Client Componentとして実装する必要がある
最終的なエラーハンドリングの方針
-
Server Component
-
error.tsx
で捕捉されるようにエラーハンドリングを実装する
-
export default async function AccountPage() {
const users = await getAllUsers();
const usersResult = handle401ErrorsInArray(users);
if (!usersResult.isSuccess()) throw new Error(usersResult.error.reason);
return (
<>
<AccountList initialUsers={usersResult.value} />
</>
);
}
サーバーコンポーネント内で例外をスローさせることで、error.tsx
ページの内容に表示コンテンツを切り替えます。
-
Server Action
- コンポーネント内で例外をスローさせる
"use server";
export async function postUserAction(
prevState: FormState,
userData: AccountFormData,
): Promise<FormState> {
const response = await registerUser({
...
});
if (response.isSuccess()) {
return handleSuccess(prevState);
} else {
throw new Error(response.error.reason || "想定外のエラーが発生しました");
}
}
-
Client Component
-
error.tsx
では捕捉できないので、react-error-boundary
ライブラリや自作して捕捉する必要がある - ただし、こちらの記事にあるとおり、捕捉できないケースがある
- Next.jsとしては推奨ではないと推察するが、システムエラーページを新設してリダイレクトさせるようにする
-
"use client";
export function AccountForm({ userId, initialValues, tanants }: Props) {
const { push, refresh, replace } = useRouter();
const onSubmit: SubmitHandler<AccountFormData> = async (data) => {
try {
// 登録/更新処理のコール
const response = userId
? await putUserAction(formState, userId, data)
: await postUserAction(formState, data);
setFormState(response);
push("/accounts");
refresh();
} catch (error) {
setFormState(initialFormState());
replace("/system-error");
}
};
Client ComponentからServer Actionをコールし、エラーが返却された場合には、システムエラーページへリダイレクトさせるようにしました。つまり、既述のバックエンドと同様に根元で例外を捕捉するようにしました。
おわりに
本アプリケーションの設計・実装をコアにおこなったことで、Next.jsのApp Routerを活用した実装パターンを自分なりの理解が深まり、言語化ができる部分が増えたと感じています!キャッシュ戦略やパフォーマンスなどについては改善点がまだまだあると思いますので、より良いアプリケーションにしていきたいと思いました!
また、プロジェクト実行中には一緒に設計・実装しているメンバーと知見の共有をおこなったり、社内のナレッジ共有ツールに記事を投稿したりといったアウトプットもしながら進めた(職場の互酬性規範を意識的に高めるためのひとつ)ので、プロジェクトを通じてチームとして知見の蓄積とスキルアップに繋がったかなと思います!
個人のスキルアップがチームを強くし、チームが強くなることで組織が強くなる。この好循環を共通認識として継続することで、より社会貢献ができる組織へと成長できると信じて日々精進して参りたいと思います!(個人->チーム->組織->日本->世界 と視座を上げていく🆙)
以上です。
本記事が何かの一助になれば幸いです。
Discussion