Manager Feature-Based Monorepo Architecture
Feature-Based + RSC-First + Server Actions
設計の特徴
| 要素 | 採用元 | 目的 |
|---|
features/ 構造 | Bulletproof React | ドメイン単位の凝集 |
| Server Components デフォルト | RSC-First | パフォーマンス向上 |
| Server Actions | Next.js 15+ | フォーム処理のシンプル化 |
| パッケージ分割 | Turborepo | 共通ロジックの再利用 |
モノレポ構造
text
1tumiki/
2├── apps/
3│ ├── manager/ # Next.js フロントエンド
4│ │ └── src/
5│ │ ├── app/ # ルーティング
6│ │ ├── features/ # ドメイン機能
7│ │ ├── components/ # アプリ固有UI
8│ │ ├── hooks/
9│ │ ├── lib/
10│ │ └── config/
11│ │
12│ └── mcp-proxy/ # APIサーバー
13│
14├── packages/
15│ ├── ui/ # 共通UIコンポーネント
16│ ├── ai/ # AI SDK統合
17│ ├── shared/ # 共通ユーティリティ
18│ ├── db/ # Prisma + DB
19│ ├── keycloak/ # 認証
20│ ├── mailer/ # メール
21│ ├── oauth-token-manager/ # OAuthトークン
22│ └── slack/ # Slack統合
23│
24└── tooling/ # ビルド設定
apps/manager 内部構造
text
1apps/manager/src/
2├── app/ # Next.js App Router(ルーティングのみ)
3│ ├── [orgSlug]/
4│ │ ├── agents/
5│ │ ├── mcps/
6│ │ ├── dashboard/
7│ │ └── ...
8│ ├── api/
9│ │ └── trpc/
10│ └── layout.tsx
11│
12├── features/ # ドメイン単位で完結
13│ ├── agents/
14│ │ ├── components/ # UI(Server/Client混在)
15│ │ ├── actions/ # Server Actions
16│ │ ├── hooks/ # Client hooks
17│ │ ├── api/ # tRPCルーター
18│ │ ├── types/
19│ │ └── index.ts # 公開API
20│ ├── mcps/
21│ ├── organization/
22│ ├── chat/
23│ └── dashboard/
24│
25├── components/ # アプリ固有の共通コンポーネント
26├── hooks/ # アプリ固有の共通フック
27├── lib/ # ユーティリティ
28├── config/ # 設定
29└── types/ # アプリ固有の型
Feature 内部構造
text
1features/{domain}/
2├── components/ # UI コンポーネント
3│ ├── AgentCard.tsx # Server Component
4│ ├── AgentList.tsx # Server Component
5│ └── CreateAgentForm.tsx # 'use client'
6│
7├── actions/ # Server Actions
8│ ├── createAgent.ts # 'use server'
9│ └── deleteAgent.ts
10│
11├── hooks/ # Client側ロジック
12│ └── useAgentForm.ts
13│
14├── api/ # tRPCルーター
15│ └── router.ts
16│
17├── types/ # 型定義
18│ └── index.ts
19│
20└── index.ts # 公開API
依存関係ルール
text
1app/ → features/ → components/, hooks/, lib/, types/
2 ↓
3 packages/ (@tumiki/ui, @tumiki/ai, @tumiki/db, ...)
禁止される依存
| From | To | 理由 |
|---|
features/A | features/B | Feature間の直接依存禁止 |
packages/ | apps/ | パッケージはアプリに依存しない |
components/ | features/ | 共通コンポーネントはFeatureに依存しない |
Feature間で共通ロジックが必要な場合
- 共通化:
lib/ または packages/ に抽出
- 上位レイヤー:
app/ レベルで統合
Server Components vs Client Components
| 種類 | 用途 |
|---|
| Server(デフォルト) | データ取得、一覧表示 |
Client('use client') | フォーム、モーダル、hooks使用 |
コード例
Server Component
typescript
1// features/agents/components/AgentList.tsx
2// 'use client' なし = Server Component
3
4import { db } from "@tumiki/db";
5import { Card } from "@tumiki/ui";
6import { AgentCard } from "./AgentCard";
7import { CreateAgentButton } from "./CreateAgentButton";
8
9type Props = {
10 organizationId: string;
11};
12
13export const AgentList = async ({ organizationId }: Props) => {
14 const agents = await db.agent.findMany({
15 where: { organizationId },
16 orderBy: { createdAt: "desc" },
17 });
18
19 return (
20 <div>
21 <div className="flex justify-between mb-4">
22 <h2 className="text-xl font-bold">エージェント一覧</h2>
23 <CreateAgentButton />
24 </div>
25 <div className="grid gap-4">
26 {agents.map((agent) => (
27 <AgentCard key={agent.id} agent={agent} />
28 ))}
29 </div>
30 </div>
31 );
32};
Client Component
typescript
1// features/agents/components/CreateAgentForm.tsx
2"use client";
3
4import { useActionState } from "react";
5import { Button, Input, Label } from "@tumiki/ui";
6import { createAgentAction } from "../actions/createAgent";
7
8export const CreateAgentForm = () => {
9 const [state, formAction, pending] = useActionState(createAgentAction, null);
10
11 return (
12 <form action={formAction}>
13 <div>
14 <Label htmlFor="name">エージェント名</Label>
15 <Input id="name" name="name" required />
16 </div>
17
18 {state?.error && <p className="text-destructive">{state.error}</p>}
19
20 <Button type="submit" disabled={pending}>
21 {pending ? "作成中..." : "作成"}
22 </Button>
23 </form>
24 );
25};
Server Action
typescript
1// features/agents/actions/createAgent.ts
2"use server";
3
4import { revalidatePath } from "next/cache";
5import { redirect } from "next/navigation";
6import { db } from "@tumiki/db";
7import { auth } from "@/lib/auth";
8import { z } from "zod";
9
10const schema = z.object({
11 name: z.string().min(1, "名前は必須です"),
12});
13
14type ActionState = { error?: string } | null;
15
16export const createAgentAction = async (
17 _prevState: ActionState,
18 formData: FormData,
19): Promise<ActionState> => {
20 const session = await auth();
21 if (!session?.user) {
22 return { error: "認証が必要です" };
23 }
24
25 const parsed = schema.safeParse({
26 name: formData.get("name"),
27 });
28
29 if (!parsed.success) {
30 return { error: parsed.error.errors[0]?.message };
31 }
32
33 const agent = await db.agent.create({
34 data: {
35 name: parsed.data.name,
36 organizationId: session.user.organizationId,
37 status: "active",
38 },
39 });
40
41 revalidatePath("/[orgSlug]/agents");
42 redirect(`/${session.user.orgSlug}/agents/${agent.slug}`);
43};
Page
typescript
1// app/[orgSlug]/agents/page.tsx
2// Server Component
3
4import { AgentList } from "@/features/agents";
5import { getOrganization } from "@/features/organization";
6
7type Props = {
8 params: Promise<{ orgSlug: string }>;
9};
10
11const AgentsPage = async ({ params }: Props) => {
12 const { orgSlug } = await params;
13 const org = await getOrganization(orgSlug);
14
15 return (
16 <div className="container py-6">
17 <h1 className="text-2xl font-bold mb-6">エージェント管理</h1>
18 <AgentList organizationId={org.id} />
19 </div>
20 );
21};
22
23export default AgentsPage;
Feature index.ts
typescript
1// features/agents/index.ts
2
3// Components
4export { AgentCard } from "./components/AgentCard";
5export { AgentList } from "./components/AgentList";
6export { CreateAgentForm } from "./components/CreateAgentForm";
7
8// Actions
9export { createAgentAction } from "./actions/createAgent";
10export { deleteAgentAction } from "./actions/deleteAgent";
11
12// Hooks
13export { useAgentForm } from "./hooks/useAgentForm";
14
15// Types
16export type { Agent, CreateAgentInput } from "./types";
17
18// API (tRPC)
19export { agentRouter } from "./api/router";
tRPC ルーター統合
Feature 内のルーター定義
typescript
1// features/agents/api/router.ts
2
3import { createTRPCRouter, protectedProcedure } from "@/lib/trpc";
4import { z } from "zod";
5import { db } from "@tumiki/db";
6
7export const agentRouter = createTRPCRouter({
8 list: protectedProcedure
9 .input(z.object({ organizationId: z.string() }))
10 .query(async ({ input }) => {
11 return db.agent.findMany({
12 where: { organizationId: input.organizationId },
13 });
14 }),
15
16 getById: protectedProcedure
17 .input(z.object({ id: z.string() }))
18 .query(async ({ input }) => {
19 return db.agent.findUnique({ where: { id: input.id } });
20 }),
21});
Server Actions vs tRPC 使い分け
| ユースケース | 推奨 |
|---|
| フォーム送信 | Server Actions |
| ページ初期データ | 直接DBクエリ(Server Component内) |
| クライアントからの動的クエリ | tRPC |
| リアルタイム更新 | tRPC + React Query |
パッケージ間依存関係
text
1apps/manager
2├── @tumiki/ui
3├── @tumiki/ai
4├── @tumiki/shared
5├── @tumiki/db
6└── @tumiki/keycloak
7
8apps/mcp-proxy
9├── @tumiki/ai
10├── @tumiki/shared
11└── @tumiki/db
ルール:
apps/ は packages/ に依存可能
packages/ は apps/ に依存禁止
packages/ 間は下位パッケージにのみ依存
Feature 一覧
| Feature | 説明 | サブ機能 |
|---|
agents | AIエージェント管理 | 作成、実行、スケジュール |
mcps | MCPサーバー管理 | 追加、設定、ツール更新 |
organization | 組織管理 | メンバー、ロール、グループ |
chat | チャット機能 | メッセージ、履歴 |
dashboard | ダッシュボード | 統計、アクティビティ |
notification | 通知機能 | - |
feedback | フィードバック | - |
ファイル命名規則
| 種類 | 規則 | 例 |
|---|
| コンポーネント | PascalCase | AgentCard.tsx |
| Server Action | camelCase | createAgent.ts |
| フック | camelCase + use | useAgentForm.ts |
| tRPCルーター | camelCase + router | router.ts |
| 型定義 | PascalCase | types/index.ts |
| テスト | 元ファイル名 + .test | createAgent.test.ts |
| EE機能 | 元ファイル名 + .ee | create.ee.ts |
チェックリスト
Feature 移行完了条件
品質チェック