# 開発ガイドライン (Development Guidelines) > 本書はシンプルグループウェアのチーム開発におけるコーディング規約と開発プロセスを定義する。技術スタックは `docs/architecture.md`、ディレクトリ構成は `docs/repository-structure.md` に基づく。 ## コーディング規約 ### 命名規約 #### 変数・関数 ```typescript // ✅ 良い例: 役割が明確 const projectMembers = await projectMemberRepository.findByProject(projectId); function formatDueDate(dueDate: string): string { } const hasPermission = await projectMemberRepository.isMember(projectId, userId); // ❌ 悪い例: 曖昧 const data = await repo.find(id); function calc(arr: any[]): number { } ``` **原則**: - 変数: camelCase・名詞または名詞句 - 関数: camelCase・動詞始まり(`find`, `create`, `update`, `delete`, `format`, `validate`) - 定数: UPPER_SNAKE_CASE(`MAX_PAGE_SIZE`, `DEFAULT_PAGE_SIZE`) - 真偽値: `is`, `has`, `should`, `can` 始まり(`isMember`, `hasPermission`) #### クラス・インターフェース・型 ```typescript // クラス: PascalCase + 役割接尾 class UserRepository { } class ChatService { } class SqliteDatabase { } // インターフェース: PascalCase(接尾辞Iは付けない) interface ProjectMember { } interface CreateMeetingInput { } // 型エイリアス: PascalCase type ProjectStatus = 'active' | 'on_hold' | 'completed' | 'archived'; type NotificationType = 'mention' | 'todo_assigned' | ...; ``` #### ファイル名 - Repository/Serviceクラス: PascalCase + 接尾(`UserRepository.ts`, `ChatService.ts`) - 関数・ユーティリティ: camelCase・動詞始まり(`formatDate.ts`, `validateEmail.ts`) - 型定義: PascalCase(`TodoItem.ts`) - Reactコンポーネント: PascalCase(`KanbanBoard.tsx`) - Route Handler/画面: Next.js固定名(`route.ts`, `page.tsx`, `layout.tsx`) - Migration: `NNN_description.sql`(`001_initial.sql`) ### コードフォーマット - **インデント**: 2スペース - **行長**: 最大100文字 - **セミコロン**: 必須 - **クォート**: シングルクォート - **ツール**: Prettier(`.prettierrc`)+ ESLint(`eslint.config.js` Flat Config)で自動整形 ### TypeScript規約 #### 型定義 ```typescript // ✅ 良い例: 明示的な型注釈 function findByProject(projectId: number): ProjectMember[] { } // ❌ 悪い例: 型推論への過度な依存(暗黙のany) function findByProject(projectId) { } ``` - 公開APIの引数・戻り値には明示的な型注釈を付ける - `any` は原則禁止。やむを得ない場合は `unknown` + 型ガードを使用 - オブジェクト型は `interface`、共用型・プリミティブ型は `type` エイリアスを使用 - Entity型は `lib/types/` に集約し、レイヤ間で共有する #### 関数設計 ```typescript // ✅ 良い例: 単一責任・パラメータをオブジェクトに集約 interface CreateTodoInput { title: string; description?: string; assigneeId?: number; dueDate?: string; priority?: TodoPriority; } function createTodo(input: CreateTodoInput): TodoItem { } // ❌ 悪い例: 多すぎるパラメータ function createTodo(title, description, assigneeId, dueDate, priority, milestoneId): TodoItem { } ``` - 1関数の責務は単一に(目安20行以内・50行推奨上限) - パラメータが4つ超の場合はオブジェクトに集約 - 1ファイル300行以下推奨・500行超は分割 ### コメント規約 #### ドキュメントコメント(TSDoc) ```typescript /** * プロジェクトにメンバーを追加する * * @param actorId - 操作実行者のユーザーID(権限チェックに使用) * @param projectId - 対象プロジェクトID * @param userId - 追加するユーザーID * @param role - プロジェクト内ロール * @throws {ForbiddenError} 実行者に権限がない場合 * @throws {NotFoundError} プロジェクトまたはユーザーが存在しない場合 */ async function addMember( actorId: number, projectId: number, userId: number, role: ProjectMemberRole ): Promise { } ``` #### インラインコメント ```typescript // ✅ 良い例: 理由を説明する // 論理削除済みデータを除外するため deleted_at IS NULL を付与 const threads = db.query(` SELECT * FROM board_threads WHERE project_id = @projectId AND deleted_at IS NULL `, { projectId }); // ❌ 悪い例: コードの再述 // スレッドを取得する const threads = db.query(...); ``` - コードから自明な内容は書かない - 「なぜ」その処理をするかを書く - TODO/FIXMEは課題番号と共に記載(`// TODO: キャッシュを実装する (Issue #123)`) - コメントアウトされたコードは残さない(削除する) ### エラーハンドリング #### カスタムエラークラス ```typescript // 期待されるエラー: 適切なエラークラスを定義 class ValidationError extends Error { constructor(message: string, public field: string, public value: unknown) { super(message); this.name = 'ValidationError'; } } class ForbiddenError extends Error { constructor(message: string) { super(message); this.name = 'ForbiddenError'; } } class NotFoundError extends Error { constructor(public resource: string, public id: number | string) { super(`${resource} not found: ${id}`); this.name = 'NotFoundError'; } } ``` #### エラー処理パターン ```typescript // ✅ 良い例: 期待されるエラーは適切に処理、予期せぬエラーは伝播 async function getThread(threadId: number): Promise { const thread = await boardRepository.findThreadById(threadId); if (!thread) { throw new NotFoundError('BoardThread', threadId); } return thread; } // Route HandlerでのHTTPステータスへの変換 try { const thread = await boardService.getThread(threadId); return Response.json(thread); } catch (error) { if (error instanceof ValidationError) return Response.json({ error: { message: error.message } }, { status: 400 }); if (error instanceof ForbiddenError) return Response.json({ error: { message: error.message } }, { status: 403 }); if (error instanceof NotFoundError) return Response.json({ error: { message: error.message } }, { status: 404 }); console.error('Unexpected error:', error); return Response.json({ error: { message: '内部エラーが発生しました' } }, { status: 500 }); } // ❌ 悪い例: エラーを無視して null を返す async function getThread(threadId: number): Promise { try { return await boardRepository.findThreadById(threadId); } catch (error) { return null; // エラー情報が失われる } } ``` **原則**: - 期待されるエラー(バリデーション・権限・存在確認)は適切なエラークラスで表現 - 予期せぬエラーは上位に伝播しログに記録 - エラーを無視(空catch)しない - エラーメッセージは具体的で解決策を示す(`'タイトルは1-200文字で入力してください。現在: 250文字'`) ## プロジェクト固有規約 ### Repository層の規約 #### SQLは必ずパラメータバインド ```typescript // ✅ 良い例: パラメータバインド const user = db.get('SELECT * FROM users WHERE email = @email', { email }); // ❌ 悪い例: 文字列結合(SQLインジェクション脆弱性) const user = db.get(`SELECT * FROM users WHERE email = '${email}'`); ``` #### 論理削除テーブルの取得には必ず deleted_at IS NULL ```typescript // ✅ 良い例 const notes = db.query(` SELECT * FROM project_notes WHERE project_id = @projectId AND deleted_at IS NULL ORDER BY is_pinned DESC, updated_at DESC LIMIT @limit OFFSET @offset `, { projectId, limit, offset }); // ❌ 悪い例: 削除済みデータが混入 const notes = db.query(`SELECT * FROM project_notes WHERE project_id = @projectId`, { projectId }); ``` #### Repositoryは直接SQLiteライブラリを触らない ```typescript // ✅ 良い例: SQLラッパー経由 import { getDb } from '@/lib/db/sqlite'; const db = getDb(); db.execute('INSERT INTO projects ...', params); // ❌ 悪い例: better-sqlite3を直接操作 import Database from 'better-sqlite3'; const db = new Database('./data/app.db'); ``` #### ページネーション 一覧取得APIは必ずページネーション(`LIMIT`/`OFFSET`)し、全件取得しない。 ```typescript function findThreads(projectId: number, page: number, pageSize: number = 20) { const offset = (page - 1) * pageSize; return db.query(` SELECT * FROM board_threads WHERE project_id = @projectId AND deleted_at IS NULL ORDER BY is_pinned DESC, created_at DESC LIMIT @pageSize OFFSET @offset `, { projectId, pageSize, offset }); } ``` ### Service層の規約 #### 権限チェックを必ず実施 ```typescript // ✅ 良い例: 操作前に権限チェック async function addMember(actorId: number, projectId: number, userId: number, role: ProjectMemberRole) { const actorRole = await projectMemberRepository.getRole(projectId, actorId); if (!actorRole || actorRole !== 'admin') { throw new ForbiddenError('プロジェクト管理者のみメンバー追加が可能です'); } // ...メンバー追加処理 } ``` #### トランザクション境界の明示 複数テーブルを更新する場合はトランザクション内で実行する。 ```typescript async function createMeeting(actorId: number, projectId: number, input: MeetingInput) { return db.transaction(() => { const meeting = meetingRepository.create({ ...input, projectId, createdById: actorId }); meetingRepository.addMembers(meeting.id, input.memberIds); activityLogService.log({ projectId, actorId, action: 'meeting_created', targetType: 'meeting', targetId: meeting.id }); return meeting; }); } ``` #### 副作用の分離 通知生成・アクティビティログ記録・SSE配信は専用Service(`NotificationService`, `ActivityLogService`, `SseHub`)に委譲し、業務ロジックと分離する。 ### Next.js規約 #### Node.js Runtimeの明示 ```typescript // app/api/.../route.ts の先頭 export const runtime = 'nodejs'; // Edge Runtime使用禁止 ``` #### Server Componentsを優先 データ取得は可能な限りServer Componentsで行い、クライアント送信量を削減する。インタラクティブな要素(ドラッグ&ドロップ・SSE受信・フォーム)のみClient Componentsとする。 #### Markdownレンダリングの安全性 ```typescript // ✅ 必ず rehype-sanitize を通す import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import rehypeSanitize from 'rehype-sanitize'; {bodyMd} ``` HTML直接入力は無効化し、危険なURLスキーム(`javascript:`等)は除外する。 ### セキュリティ規約 - 機密情報(パスワード・APIキー)はコードにハードコードしない。`.env` で管理 - パスワードは bcrypt でハッシュ化保存 - ファイルアップロードはMIMEタイプチェック・ファイル名サニタイズ・保存名の一意化 - ファイルアクセスAPIでもプロジェクト参加権限をチェック - 管理者機能(バックアップ・Migration状態)は `role='system_admin'` のみ許可 ## Gitワークフロー規則 ### ブランチ戦略(Git Flow) ``` main (本番環境) └── develop (開発統合) ├── feature/task-management ├── feature/user-auth ├── fix/chat-sse-reconnect └── refactor/todo-repository ``` **運用ルール**: - `main`: リリース済みの安定コードのみ。タグでバージョン管理 - `develop`: 次回リリースの最新開発コード。CIで自動テスト実行 - `feature/*`, `fix/*`: developから分岐しPR経由でdevelopへマージ - `release/*`: リリース準備(必要に応じて) - `hotfix/*`: 本番障害対応(mainから分岐しmain/develop両方へマージ) - 直接コミット禁止: 全ブランチでPRレビュー必須 - マージ方針: feature→develop はsquash merge推奨、develop→main はmerge commit ### コミットメッセージ規約(Conventional Commits) ``` ():