実装フェーズ1:コア機能
認証、プロジェクト、タスクのCRUDなど、コア機能をClaude Codeで実装します。AIエージェントに的確な指示を出しながら、段階的にアプリケーションを構築していきましょう。
Phase 1 で実装する機能
1
Docker環境構築
2
ユーザー認証(JWT)
3
プロジェクトCRUD
4
タスクCRUD
5
カンバンボードUI
ステップ1:Docker環境構築
Claude Codeへの指示
TaskFlowプロジェクトのDocker開発環境を構築してください。 【コンテナ構成】 - db: MySQL 8.0(ポート3306) - データベース名: taskflow - ルートパスワード: rootpass - ユーザー: taskflow / taskflow_pass - ボリュームでデータ永続化 - backend: FastAPI(ポート8000) - Python 3.11ベース - ホットリロード有効 - dbコンテナに依存 - frontend: Next.js 14(ポート3000) - Node.js 20ベース - ホットリロード有効 - redis: Redis 7(ポート6379) - キャッシュおよびWebSocket用 - ボリュームでデータ永続化 【要件】 - compose.yamlを使用(docker-compose.ymlではなく) - 各サービスのDockerfileも作成 - .env.exampleにデフォルト値を記載 - ヘルスチェックを全コンテナに設定
Claude Codeが行うこと
- ✓
compose.yaml- Docker Compose設定ファイル - ✓
backend/Dockerfile- FastAPI用Dockerfile - ✓
frontend/Dockerfile- Next.js用Dockerfile - ✓
.env.example- 環境変数テンプレート - ✓
backend/requirements.txt- Python依存パッケージ - ✓
frontend/package.json- Node.js依存パッケージ
完成コード:compose.yaml
# compose.yaml
services:
db:
image: mysql:8.0
container_name: taskflow-db
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-rootpass}
MYSQL_DATABASE: ${DB_NAME:-taskflow}
MYSQL_USER: ${DB_USER:-taskflow}
MYSQL_PASSWORD: ${DB_PASSWORD:-taskflow_pass}
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: taskflow-redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: taskflow-backend
ports:
- "8000:8000"
volumes:
- ./backend:/app
environment:
DATABASE_URL: mysql+asyncmy://${DB_USER:-taskflow}:${DB_PASSWORD:-taskflow_pass}@db:3306/${DB_NAME:-taskflow}
REDIS_URL: redis://redis:6379/0
JWT_SECRET: ${JWT_SECRET:-dev-secret-key}
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 10s
timeout: 5s
retries: 5
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: taskflow-frontend
ports:
- "3000:3000"
volumes:
- ./frontend:/app
- /app/node_modules
environment:
NEXT_PUBLIC_API_URL: http://localhost:8000
depends_on:
backend:
condition: service_healthy
volumes:
mysql_data:
redis_data:
☑ 動作確認
# コンテナを起動 docker compose up -d # 全コンテナが起動しているか確認 docker compose ps # 期待される出力: # taskflow-db running (healthy) # taskflow-redis running (healthy) # taskflow-backend running (healthy) # taskflow-frontend running # バックエンドの疎通確認 curl http://localhost:8000/health # フロントエンドの疎通確認 curl http://localhost:3000
よくあるエラー
- ポート競合:
Bind for 0.0.0.0:3306 failed: port is already allocated→ ローカルのMySQLを停止するか、compose.yamlのポートを変更 - DB接続失敗:
Can't connect to MySQL server→depends_onのhealthcheckが正しく設定されているか確認。docker compose logs dbでログを確認 - ビルドエラー:
failed to solve: dockerfile parse error→ Dockerfileの構文を確認。Claude Codeに「Dockerfileのビルドエラーを修正して」と指示
ステップ2:認証機能の実装
Claude Codeへの指示
JWT認証機能を実装してください。 【バックエンド(FastAPI)】 - POST /auth/register - ユーザー登録 - メール、パスワード、表示名を受け取る - パスワードはbcryptでハッシュ化 - POST /auth/login - ログイン - メール+パスワードでJWTトークンを発行 - アクセストークン(15分)+リフレッシュトークン(7日) - GET /auth/me - 現在のユーザー情報 - Authorizationヘッダーからトークンを検証 - POST /auth/refresh - トークンリフレッシュ 【フロントエンド(Next.js)】 - /login ログインページ - /register 新規登録ページ - Zustandで認証状態を管理 - axiosインターセプターでトークン自動付与 - 未認証時は/loginにリダイレクト 【テーブル設計】 - usersテーブル: id, email, password_hash, display_name, created_at, updated_at
Claude Codeが行うこと
- ✓
backend/app/models/user.py- Userモデル - ✓
backend/app/routers/auth.py- 認証エンドポイント - ✓
backend/app/core/security.py- JWT・パスワード処理 - ✓
backend/app/core/deps.py- 認証依存関数 - ✓
frontend/src/stores/authStore.ts- Zustand認証ストア - ✓
frontend/src/app/login/page.tsx- ログインページ - ✓
frontend/src/app/register/page.tsx- 登録ページ - ✓
frontend/src/lib/axios.ts- axiosインスタンス設定
完成コード:バックエンド認証エンドポイント
# backend/app/routers/auth.py from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from app.core.deps import get_db, get_current_user from app.core.security import ( hash_password, verify_password, create_access_token, create_refresh_token ) from app.models.user import User from app.schemas.auth import ( UserRegister, UserLogin, TokenResponse, UserResponse ) router = APIRouter(prefix="/auth", tags=["auth"]) @router.post("/register", response_model=UserResponse) async def register(data: UserRegister, db: AsyncSession = Depends(get_db)): # メール重複チェック existing = await User.get_by_email(db, data.email) if existing: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="このメールアドレスは既に登録されています" ) user = User( email=data.email, password_hash=hash_password(data.password), display_name=data.display_name, ) db.add(user) await db.commit() await db.refresh(user) return user @router.post("/login", response_model=TokenResponse) async def login(data: UserLogin, db: AsyncSession = Depends(get_db)): user = await User.get_by_email(db, data.email) if not user or not verify_password(data.password, user.password_hash): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="メールアドレスまたはパスワードが正しくありません" ) return TokenResponse( access_token=create_access_token(user.id), refresh_token=create_refresh_token(user.id), token_type="bearer", ) @router.get("/me", response_model=UserResponse) async def get_me(current_user: User = Depends(get_current_user)): return current_user
完成コード:フロントエンド認証ストア
// frontend/src/stores/authStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import axios from '@/lib/axios';
interface User {
id: number;
email: string;
display_name: string;
}
interface AuthState {
user: User | null;
token: string | null;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
register: (email: string, password: string, displayName: string) => Promise<void>;
logout: () => void;
fetchUser: () => Promise<void>;
}
export const useAuthStore = create<AuthState>()(
persist(
(set, get) => ({
user: null,
token: null,
isLoading: false,
login: async (email, password) => {
const res = await axios.post('/auth/login', { email, password });
set({ token: res.data.access_token });
await get().fetchUser();
},
register: async (email, password, displayName) => {
await axios.post('/auth/register', {
email, password, display_name: displayName,
});
await get().login(email, password);
},
logout: () => {
set({ user: null, token: null });
},
fetchUser: async () => {
set({ isLoading: true });
try {
const res = await axios.get('/auth/me');
set({ user: res.data });
} catch {
set({ user: null, token: null });
} finally {
set({ isLoading: false });
}
},
}),
{ name: 'auth-storage' }
)
);
☑ 動作確認
# 1. Swagger UIで確認 # ブラウザで http://localhost:8000/docs を開く # 2. ユーザー登録をテスト curl -X POST http://localhost:8000/auth/register \ -H "Content-Type: application/json" \ -d '{"email":"test@example.com","password":"password123","display_name":"テストユーザー"}' # 3. ログインをテスト curl -X POST http://localhost:8000/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"test@example.com","password":"password123"}' # 4. トークンを使って /auth/me を確認 curl http://localhost:8000/auth/me \ -H "Authorization: Bearer <取得したトークン>" # 5. フロントエンド確認 # ブラウザで http://localhost:3000/register にアクセス # ユーザー登録 → 自動ログイン → ダッシュボードへリダイレクト
よくあるエラー
- 422 Validation Error:リクエストボディのフィールド名がスキーマと一致しているか確認。
display_nameのアンダースコアに注意 - 401 Unauthorized:トークンの有効期限切れ、または
Authorization: Bearerヘッダーの形式を確認 - CORS エラー:FastAPIの
CORSMiddlewareにhttp://localhost:3000が許可されているか確認
ステップ3:プロジェクトCRUD
Claude Codeへの指示
プロジェクト管理のCRUD機能を実装してください。
【エンドポイント】
- GET /projects - 自分が参加しているプロジェクト一覧
- POST /projects - プロジェクト新規作成(作成者が自動的にownerになる)
- GET /projects/{id} - プロジェクト詳細
- PUT /projects/{id} - プロジェクト更新(ownerのみ)
- DELETE /projects/{id} - プロジェクト削除(ownerのみ)
- POST /projects/{id}/members - メンバー追加(ownerのみ)
【テーブル設計】
- projects: id, name, description, owner_id, created_at, updated_at
- project_members: id, project_id, user_id, role(owner/admin/member), joined_at
【フロントエンド】
- /projects 一覧ページ(カード表示)
- /projects/new 新規作成フォーム
- /projects/{id} プロジェクト詳細ページ
認証済みユーザーのみアクセス可能にしてください。
既存のauth機能のパターンに合わせて実装してください。
Claude Codeが行うこと
- ✓
backend/app/models/project.py- Project, ProjectMemberモデル - ✓
backend/app/routers/projects.py- プロジェクトエンドポイント - ✓
backend/app/schemas/project.py- リクエスト/レスポンススキーマ - ✓
frontend/src/app/projects/page.tsx- プロジェクト一覧 - ✓
frontend/src/app/projects/new/page.tsx- 新規作成フォーム - ✓
frontend/src/app/projects/[id]/page.tsx- プロジェクト詳細
完成コード:プロジェクトモデル
# backend/app/models/project.py
from sqlalchemy import Column, Integer, String, Text, ForeignKey, DateTime, Enum
from sqlalchemy.orm import relationship
from datetime import datetime
import enum
from app.core.database import Base
class MemberRole(str, enum.Enum):
owner = "owner"
admin = "admin"
member = "member"
class Project(Base):
__tablename__ = "projects"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(255), nullable=False)
description = Column(Text, nullable=True)
owner_id = Column(Integer, ForeignKey("users.id"), nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
owner = relationship("User", back_populates="owned_projects")
members = relationship("ProjectMember", back_populates="project", cascade="all, delete-orphan")
tasks = relationship("Task", back_populates="project", cascade="all, delete-orphan")
class ProjectMember(Base):
__tablename__ = "project_members"
id = Column(Integer, primary_key=True, index=True)
project_id = Column(Integer, ForeignKey("projects.id"), nullable=False)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
role = Column(Enum(MemberRole), default=MemberRole.member)
joined_at = Column(DateTime, default=datetime.utcnow)
project = relationship("Project", back_populates="members")
user = relationship("User")
☑ 動作確認
# 1. プロジェクト作成 curl -X POST http://localhost:8000/projects \ -H "Authorization: Bearer <トークン>" \ -H "Content-Type: application/json" \ -d '{"name":"マイプロジェクト","description":"テスト用プロジェクト"}' # 2. プロジェクト一覧取得 curl http://localhost:8000/projects \ -H "Authorization: Bearer <トークン>" # 3. プロジェクト詳細取得 curl http://localhost:8000/projects/1 \ -H "Authorization: Bearer <トークン>" # 4. メンバー追加 curl -X POST http://localhost:8000/projects/1/members \ -H "Authorization: Bearer <トークン>" \ -H "Content-Type: application/json" \ -d '{"user_id":2,"role":"member"}' # 5. フロントエンドで確認 # http://localhost:3000/projects にアクセス # プロジェクトカードが表示されることを確認
ステップ4:タスクCRUD
Claude Codeへの指示
タスク管理のCRUD機能を実装してください。
【エンドポイント】
- GET /projects/{id}/tasks - プロジェクト内のタスク一覧(ステータス別)
- POST /projects/{id}/tasks - タスク新規作成
- PUT /tasks/{id} - タスク更新
- DELETE /tasks/{id} - タスク削除
- PUT /tasks/{id}/status - ステータス変更(カンバン移動用)
【テーブル設計】
- tasks: id, title, description, status, priority, assignee_id, project_id, position, created_at, updated_at
- status: "todo" | "in_progress" | "done"
- priority: "low" | "medium" | "high"
- positionはカンバンボード内の並び順(整数値)
【フロントエンド】
- タスク作成モーダル
- タスク編集モーダル
- タスク削除確認ダイアログ
プロジェクトメンバーのみタスク操作可能にしてください。
Claude Codeが行うこと
- ✓
backend/app/models/task.py- Taskモデル - ✓
backend/app/routers/tasks.py- タスクエンドポイント - ✓
backend/app/schemas/task.py- リクエスト/レスポンススキーマ - ✓
frontend/src/components/tasks/TaskCreateModal.tsx- タスク作成モーダル - ✓
frontend/src/components/tasks/TaskEditModal.tsx- タスク編集モーダル - ✓
frontend/src/stores/taskStore.ts- Zustandタスクストア
完成コード:タスクモデル
# backend/app/models/task.py
from sqlalchemy import Column, Integer, String, Text, ForeignKey, DateTime, Enum
from sqlalchemy.orm import relationship
from datetime import datetime
import enum
from app.core.database import Base
class TaskStatus(str, enum.Enum):
todo = "todo"
in_progress = "in_progress"
done = "done"
class TaskPriority(str, enum.Enum):
low = "low"
medium = "medium"
high = "high"
class Task(Base):
__tablename__ = "tasks"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(255), nullable=False)
description = Column(Text, nullable=True)
status = Column(Enum(TaskStatus), default=TaskStatus.todo)
priority = Column(Enum(TaskPriority), default=TaskPriority.medium)
assignee_id = Column(Integer, ForeignKey("users.id"), nullable=True)
project_id = Column(Integer, ForeignKey("projects.id"), nullable=False)
position = Column(Integer, default=0)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
project = relationship("Project", back_populates="tasks")
assignee = relationship("User")
完成コード:ステータス変更エンドポイント
# backend/app/routers/tasks.py(抜粋) @router.put("/{task_id}/status") async def update_task_status( task_id: int, data: TaskStatusUpdate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): task = await db.get(Task, task_id) if not task: raise HTTPException(status_code=404, detail="タスクが見つかりません") # プロジェクトメンバーか確認 await verify_project_member(db, task.project_id, current_user.id) task.status = data.status task.position = data.position task.updated_at = datetime.utcnow() await db.commit() await db.refresh(task) return task
☑ 動作確認
# 1. タスク作成 curl -X POST http://localhost:8000/projects/1/tasks \ -H "Authorization: Bearer <トークン>" \ -H "Content-Type: application/json" \ -d '{"title":"初めてのタスク","description":"テスト","priority":"high"}' # 2. タスク一覧取得 curl http://localhost:8000/projects/1/tasks \ -H "Authorization: Bearer <トークン>" # 3. ステータス変更(todo → in_progress) curl -X PUT http://localhost:8000/tasks/1/status \ -H "Authorization: Bearer <トークン>" \ -H "Content-Type: application/json" \ -d '{"status":"in_progress","position":0}' # 4. タスク更新 curl -X PUT http://localhost:8000/tasks/1 \ -H "Authorization: Bearer <トークン>" \ -H "Content-Type: application/json" \ -d '{"title":"更新されたタスク","priority":"medium"}' # 5. ステータス別にタスクが取得できることを確認 # レスポンスでtodo/in_progress/doneのグループ分けを確認
よくあるエラー
- 403 Forbidden:プロジェクトメンバーでないユーザーがタスクを操作しようとした場合。
project_membersテーブルにレコードがあるか確認 - position重複:カンバン移動時にpositionが重複する場合は、同一ステータス内のpositionを再計算するロジックを追加
ステップ5:カンバンボードUI
Claude Codeへの指示
カンバンボードUIを実装してください。
【コンポーネント構成】
- KanbanBoard - ボード全体(3カラム表示)
- KanbanColumn - 各ステータスのカラム(Todo / In Progress / Done)
- TaskCard - 個別タスクカード
【要件】
- TanStack Queryでタスクデータをフェッチ
- Zustandでローカルのタスク状態を管理
- カラム間のドラッグ&ドロップ(Phase 2で本格実装、今はボタンで移動)
- タスクカードにはタイトル、優先度バッジ、担当者アバターを表示
- 優先度に応じた色分け(high: 赤, medium: 黄, low: 緑)
- レスポンシブ対応(モバイルではカラムを縦並び)
【データフェッチ】
- useQueryでGET /projects/{id}/tasks を取得
- useMutationでステータス変更を即座に反映(楽観的更新)
プロジェクト詳細ページ /projects/{id} にカンバンボードを表示してください。
Claude Codeが行うこと
- ✓
frontend/src/components/kanban/KanbanBoard.tsx- ボード全体 - ✓
frontend/src/components/kanban/KanbanColumn.tsx- カラムコンポーネント - ✓
frontend/src/components/kanban/TaskCard.tsx- タスクカード - ✓
frontend/src/hooks/useTasks.ts- TanStack Queryフック - ✓
frontend/src/stores/taskStore.ts- Zustandストア更新
完成コード:カンバンボードコンポーネント
// frontend/src/components/kanban/KanbanBoard.tsx
'use client';
import { useTasks } from '@/hooks/useTasks';
import { KanbanColumn } from './KanbanColumn';
const COLUMNS = [
{ id: 'todo', title: 'Todo', color: 'bg-gray-100' },
{ id: 'in_progress', title: 'In Progress', color: 'bg-blue-50' },
{ id: 'done', title: 'Done', color: 'bg-green-50' },
] as const;
interface KanbanBoardProps {
projectId: number;
}
export function KanbanBoard({ projectId }: KanbanBoardProps) {
const { tasks, isLoading, updateTaskStatus } = useTasks(projectId);
if (isLoading) {
return <div className="text-center py-12">読み込み中...</div>;
}
const tasksByStatus = {
todo: tasks.filter(t => t.status === 'todo'),
in_progress: tasks.filter(t => t.status === 'in_progress'),
done: tasks.filter(t => t.status === 'done'),
};
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{COLUMNS.map(column => (
<KanbanColumn
key={column.id}
title={column.title}
color={column.color}
tasks={tasksByStatus[column.id]}
onMoveTask={updateTaskStatus}
/>
))}
</div>
);
}
完成コード:TanStack Queryフック
// frontend/src/hooks/useTasks.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import axios from '@/lib/axios'; import { Task, TaskStatus } from '@/types/task'; export function useTasks(projectId: number) { const queryClient = useQueryClient(); const { data: tasks = [], isLoading } = useQuery<Task[]>({ queryKey: ['tasks', projectId], queryFn: async () => { const res = await axios.get(`/projects/${projectId}/tasks`); return res.data; }, }); const { mutate: updateTaskStatus } = useMutation({ mutationFn: async ({ taskId, status, position, }: { taskId: number; status: TaskStatus; position: number; }) => { await axios.put(`/tasks/${taskId}/status`, { status, position }); }, // 楽観的更新 onMutate: async ({ taskId, status, position }) => { await queryClient.cancelQueries({ queryKey: ['tasks', projectId] }); const previous = queryClient.getQueryData<Task[]>(['tasks', projectId]); queryClient.setQueryData<Task[]>(['tasks', projectId], old => old?.map(t => t.id === taskId ? { ...t, status, position } : t ) ); return { previous }; }, onError: (_err, _vars, context) => { queryClient.setQueryData(['tasks', projectId], context?.previous); }, onSettled: () => { queryClient.invalidateQueries({ queryKey: ['tasks', projectId] }); }, }); return { tasks, isLoading, updateTaskStatus }; }
☑ 動作確認
# 1. ブラウザで http://localhost:3000/projects/1 にアクセス # 2. カンバンボードが3カラムで表示されることを確認 # 3. タスクカードが正しいカラムに表示されていることを確認 # 4. 「新しいタスク」ボタンからタスク作成モーダルを開く # 5. タスクを作成し、Todoカラムに追加されることを確認 # 6. タスクカードの移動ボタンでステータスを変更 # 例:Todoのタスクを「In Progress」に移動 # 7. 優先度バッジの色分けを確認(赤/黄/緑) # 8. モバイル表示(ブラウザ幅を縮小)でカラムが縦並びになることを確認 # # ※ ドラッグ&ドロップはPhase 2で実装します
Phase 1 実装のポイント
- ✓ 段階的に進める - 1機能ずつ動作確認しながら次へ進む。エラーを積み残さない
- ✓ エージェントモードを活用 - Claude CodeのPlan Modeで全体像を把握してからコード生成
- ✓ こまめにコミット - 機能単位でGitコミットを作成。
git commit -m "feat: 認証機能を追加" - ✓ テストも一緒に - 各エンドポイントの実装時にテストコードも依頼する
- ✓ エラーはAIに投げる - エラーメッセージをそのままClaude Codeに貼り付けて修正を依頼