Files
opengroupware/docs/development-guidelines.md
Ken Yasue 80e195b3dc chore: initialize repository with project docs and tooling
- Add persistent docs: PRD, functional design, architecture, repository structure, development guidelines, milestones

- Add brainstorming notes (docs/ideas/)

- Configure tooling: package.json, tsconfig, eslint, prettier, vitest

- Add opencode configuration (commands, skills, agents)

- Add .gitignore for node_modules, .env, data/, backups/, build outputs
2026-06-24 23:31:06 +02:00

25 KiB
Raw Blame History

開発ガイドライン (Development Guidelines)

本書はシンプルグループウェアのチーム開発におけるコーディング規約と開発プロセスを定義する。技術スタックは docs/architecture.md、ディレクトリ構成は docs/repository-structure.md に基づく。

コーディング規約

命名規約

変数・関数

// ✅ 良い例: 役割が明確
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_CASEMAX_PAGE_SIZE, DEFAULT_PAGE_SIZE
  • 真偽値: is, has, should, can 始まり(isMember, hasPermission

クラス・インターフェース・型

// クラス: 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
  • 型定義: PascalCaseTodoItem.ts
  • Reactコンポーネント: PascalCaseKanbanBoard.tsx
  • Route Handler/画面: Next.js固定名route.ts, page.tsx, layout.tsx
  • Migration: NNN_description.sql001_initial.sql

コードフォーマット

  • インデント: 2スペース
  • 行長: 最大100文字
  • セミコロン: 必須
  • クォート: シングルクォート
  • ツール: Prettier.prettierrc+ ESLinteslint.config.js Flat Configで自動整形

TypeScript規約

型定義

// ✅ 良い例: 明示的な型注釈
function findByProject(projectId: number): ProjectMember[] { }

// ❌ 悪い例: 型推論への過度な依存暗黙のany
function findByProject(projectId) { }
  • 公開APIの引数・戻り値には明示的な型注釈を付ける
  • any は原則禁止。やむを得ない場合は unknown + 型ガードを使用
  • オブジェクト型は interface、共用型・プリミティブ型は type エイリアスを使用
  • Entity型は lib/types/ に集約し、レイヤ間で共有する

関数設計

// ✅ 良い例: 単一責任・パラメータをオブジェクトに集約
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

/**
 * プロジェクトにメンバーを追加する
 *
 * @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<void> { }

インラインコメント

// ✅ 良い例: 理由を説明する
// 論理削除済みデータを除外するため deleted_at IS NULL を付与
const threads = db.query<BoardThread>(`
  SELECT * FROM board_threads
  WHERE project_id = @projectId AND deleted_at IS NULL
`, { projectId });

// ❌ 悪い例: コードの再述
// スレッドを取得する
const threads = db.query(...);
  • コードから自明な内容は書かない
  • 「なぜ」その処理をするかを書く
  • TODO/FIXMEは課題番号と共に記載// TODO: キャッシュを実装する (Issue #123)
  • コメントアウトされたコードは残さない(削除する)

エラーハンドリング

カスタムエラークラス

// 期待されるエラー: 適切なエラークラスを定義
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';
  }
}

エラー処理パターン

// ✅ 良い例: 期待されるエラーは適切に処理、予期せぬエラーは伝播
async function getThread(threadId: number): Promise<BoardThread> {
  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<BoardThread | null> {
  try {
    return await boardRepository.findThreadById(threadId);
  } catch (error) {
    return null; // エラー情報が失われる
  }
}

原則:

  • 期待されるエラー(バリデーション・権限・存在確認)は適切なエラークラスで表現
  • 予期せぬエラーは上位に伝播しログに記録
  • エラーを無視空catchしない
  • エラーメッセージは具体的で解決策を示す('タイトルは1-200文字で入力してください。現在: 250文字'

プロジェクト固有規約

Repository層の規約

SQLは必ずパラメータバインド

// ✅ 良い例: パラメータバインド
const user = db.get<User>('SELECT * FROM users WHERE email = @email', { email });

// ❌ 悪い例: 文字列結合SQLインジェクション脆弱性
const user = db.get<User>(`SELECT * FROM users WHERE email = '${email}'`);

論理削除テーブルの取得には必ず deleted_at IS NULL

// ✅ 良い例
const notes = db.query<ProjectNote>(`
  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<ProjectNote>(`SELECT * FROM project_notes WHERE project_id = @projectId`, { projectId });

Repositoryは直接SQLiteライブラリを触らない

// ✅ 良い例: 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)し、全件取得しない。

function findThreads(projectId: number, page: number, pageSize: number = 20) {
  const offset = (page - 1) * pageSize;
  return db.query<BoardThread>(`
    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層の規約

権限チェックを必ず実施

// ✅ 良い例: 操作前に権限チェック
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('プロジェクト管理者のみメンバー追加が可能です');
  }
  // ...メンバー追加処理
}

トランザクション境界の明示

複数テーブルを更新する場合はトランザクション内で実行する。

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配信は専用ServiceNotificationService, ActivityLogService, SseHub)に委譲し、業務ロジックと分離する。

Next.js規約

Node.js Runtimeの明示

// app/api/.../route.ts の先頭
export const runtime = 'nodejs';  // Edge Runtime使用禁止

Server Componentsを優先

データ取得は可能な限りServer Componentsで行い、クライアント送信量を削減する。インタラクティブな要素ドラッグ&ドロップ・SSE受信・フォームのみClient Componentsとする。

Markdownレンダリングの安全性

// ✅ 必ず rehype-sanitize を通す
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeSanitize from 'rehype-sanitize';

<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeSanitize]}>
  {bodyMd}
</ReactMarkdown>

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

<type>(<scope>): <subject>

<body>

<footer>

type:

  • feat: 新機能
  • fix: バグ修正
  • docs: ドキュメント
  • style: フォーマット(コード挙動への影響なし)
  • refactor: リファクタリング
  • perf: パフォーマンス改善
  • test: テスト追加・修正
  • build: ビルドシステム
  • ci: CI/CD設定
  • chore: その他(依存更新等)

:

feat(chat): SSEによるリアルタイムメッセージ配信を実装

プロジェクト別のSSEエンドポイントを追加し、メッセージ送信時に
参加メンバーへリアルタイム配信する。

- SseHubクラスをlib/sse/hub.tsに追加
- ChatService.sendMessageでブロードキャスト
- chat.message.created/updated/deletedイベント定義

Closes #42

Pull Requestプロセス

PR作成前チェック:

  • 全テスト成功(npm test, npm run test:e2e
  • Lintエラーなしnpm run lint
  • 型チェック成功(npm run typecheck
  • コンフリクト解消済み

PRテンプレート:

## 変更種別
- [ ] 新機能 (feat)
- [ ] バグ修正 (fix)
- [ ] リファクタリング (refactor)
- [ ] ドキュメント (docs)
- [ ] その他 (chore)

## 概要
[変更内容の簡潔な説明]

## 変更理由
[なぜこの変更が必要か]

## 変更内容
- [変更1]
- [変更2]

## テスト
- [ ] Unit Test追加
- [ ] E2E Test追加
- [ ] 手動テスト実施

テスト結果: [説明]

## 関連Issue
Closes #[番号]

## レビューポイント
[特に確認してほしい点]

レビュープロセス:

  1. セルフレビュー
  2. 自動テスト実行CI
  3. レビュアー割当
  4. レビュー指摘対応
  5. 承認後にマージ

PRサイズ目安:

  • 小PR100行以下: 推奨
  • 中PR100-300行: 許容
  • 大PR300行超: 分割を検討

テスト戦略

テスト実装の必須条件

本プロジェクトでは、品質担保のため Unit TestVitestと E2E TestPlaywrightの実装を必須とする。 テストが未実装・未成功のPRはマージ不可。

Unit TestVitestの実装【必須】

以下の対象について Unit Test の実装を必須とする。新規実装・修正時に対応する Unit Test を必ず作成すること。

  • SQLラッパー
  • Migration実行
  • 全RepositoryクラスUser / Project / ProjectMember / Board / Chat / Todo / File / Calendar / Meeting / ProjectNote / Notification / ActivityLog
  • 全ServiceクラスAuth / Project / Chat / Meeting / Schedule / FileStorage / Backup
  • 権限チェック・バリデーション・スケジュール重複判定・通知作成ロジック・アクティビティログ作成ロジック・マイルストーン進捗計算

合格基準:

  • npm test で全件成功すること
  • Repository/Service層のカバレッジ 80%以上を維持すること
  • 正常系・異常系(権限エラー・バリデーションエラー・存在確認)を網羅すること

E2E TestPlaywrightの実装【必須】

主要ユーザーフローについて Playwright による E2E Test の実装を必須とする。機能追加時は該当フローの E2E Test を必ず作成すること。

対象フロー:

  • 認証(ログイン・ログアウト・未ログインの保護)
  • プロジェクト管理(作成・編集・メンバー追加/削除・アーカイブ)
  • 掲示板(スレッド作成・編集・コメント・検索)
  • チャット送信・SSEリアルタイム受信・編集・削除
  • ToDo / Kanbanカラム作成・タスク作成・編集・移動・担当者/期限設定・完了)
  • ファイル共有アップロード・一覧・Lightbox閲覧・PDFプレビュー・削除
  • Markdownメモ作成・編集・プレビュー・ピン留め・検索・削除
  • カレンダーToDo期限・マイルストーン・ミーティング表示・イベント作成/編集)
  • ミーティング(作成・参加メンバー設定・予定重複警告・アジェンダ/議事録・関連付け)
  • 通知ToDo担当者・メンション・ミーティング参加者への通知・既読化
  • アクティビティログ(各操作の記録)
  • バックアップ(作成・一覧表示・ダウンロード)

合格基準:

  • npm run test:e2e で全件成功すること
  • 主要フローのカバレッジ 100%を維持すること

テストピラミッド

       /\
      /E2E\       少数(遅い・高コスト)
     /------\
    / Integ. \    中程度
   /----------\
  /   Unit    \  多数(高速・低コスト)
 /--------------\

対象比率:

  • Unit Test: 70%
  • Integration Test: 20%
  • E2E Test: 10%

Unit TestVitest

対象: SQLラッパー・Migration・全Repository・全Service・権限チェック・バリデーション・スケジュール重複判定・通知作成ロジック・アクティビティログ作成ロジック・マイルストーン進捗計算

カバレッジ目標: Repository/Service層 80%以上

構造Given-When-Then:

describe('ChatService', () => {
  describe('sendMessage', () => {
    it('有効なデータでメッセージを作成できる', async () => {
      // Given: セットアップ
      const chatService = new ChatService(mockChatRepo, mockSseHub, mockNotificationService);
      const input = { body: 'テストメッセージ' };

      // When: 実行
      const result = await chatService.sendMessage(actorId, projectId, input.body);

      // Then: 検証
      expect(result.id).toBeDefined();
      expect(result.body).toBe('テストメッセージ');
      expect(mockSseHub.broadcast).toHaveBeenCalledWith(projectId, expect.objectContaining({ type: 'chat.message.created' }));
    });

    it('プロジェクト非参加者はメッセージ送信時にForbiddenErrorを投げる', async () => {
      // Given
      const chatService = new ChatService(...);
      mockProjectMemberRepository.isMember.mockReturnValue(false);

      // When/Then
      await expect(chatService.sendMessage(nonMemberId, projectId, 'body')).rejects.toThrow(ForbiddenError);
    });
  });
});

テスト命名: [対象]_[条件]_[期待結果]

it('findById_existingId_returnsThread', () => { });
it('findById_nonExistentId_returnsNull', () => { });
it('create_emptyTitle_throwsValidationError', () => { });

モック原則: 外部依存DB・ファイルシステム・SSEはモック化、業務ロジックは実体を使用。

Integration TestVitest

対象: 複数コンポーネントの連携。実際のSQLite一時ファイルを使用。

describe('プロジェクトメンバー権限', () => {
  it('非参加者はプロジェクトデータにアクセスできない', async () => {
    // 実DBでプロジェクト・ユーザー作成
    const project = await projectService.createProject(ownerId, { name: 'P1' });
    await expect(boardService.listThreads(nonMemberId, project.id)).rejects.toThrow(ForbiddenError);
  });
});

E2E TestPlaywright

対象: 主要ユーザーフローの全体検証。tests/e2e/*.spec.ts

test('チャットメッセージがSSEでリアルタイム配信される', async ({ browser }) => {
  const contextA = await browser.newContext();
  const contextB = await browser.newContext();
  const pageA = await contextA.newPage();
  const pageB = await contextB.newPage();

  // 両ブラウザで同プロジェクトのチャット画面を開く
  await pageA.goto('/projects/1/chat');
  await pageB.goto('/projects/1/chat');

  // Aがメッセージ送信
  await pageA.fill('input[name=message]', 'こんにちは');
  await pageA.click('button[type=submit]');

  // Bにリアルタイム表示される
  await expect(pageB.locator('text=こんにちは')).toBeVisible();
});

カバレッジ目標: 主要フロー100%認証・プロジェクト管理・掲示板・チャット・ToDo・ファイル・メモ・カレンダー・ミーティング・通知・アクティビティ・バックアップ

実行コマンド

コマンド 説明
npm test Unit Test実行Vitest
npm run test:e2e E2E Test実行Playwright
npm run lint Lint実行ESLint
npm run typecheck 型チェック実行tsc --noEmit
npm run build ビルド

コードレビュー基準

レビューポイント

機能性:

  • 要件を満たしているか
  • エッジケースが考慮されているか
  • エラーハンドリングが適切か

可読性:

  • 命名が明確か
  • コメントが適切か
  • 複雑なロジックに説明があるか

保守性:

  • 重複コードがないか
  • 責務が分離されているか
  • 変更の影響範囲が限定的か

パフォーマンス:

  • 不要な計算がないか
  • N+1クエリになっていないか
  • 一覧取得がページネーションされているか

セキュリティ:

  • 入力バリデーションが適切か
  • SQLがパラメータバインドされているか
  • 権限チェックが実装されているか
  • 機密情報のハードコードがないか
  • Markdownがサニタイズされているか

レビューコメントの書き方

建設的フィードバック:

// ✅ 良い例
この実装だとメンバー数増加時にN+1クエリになります。
JOINで一括取得するのはいかがでしょうか

// ❌ 悪い例
この書き方は良くないです。

優先度の明示:

  • [必須]: 修正必須(セキュリティ・バグ等)
  • [推奨]: 修正推奨
  • [提案]: 検討提案
  • [質問]: 意図の確認

開発環境セットアップ

必須ツール

ツール バージョン インストール方法
Node.js v24.11.0 公式インストーラ/nvm
npm 11.x Node.jsにバンドル
devcontainer - VS Code拡張開発環境統一

セットアップ手順

# 1. リポジトリクローン
git clone [URL]
cd [project-name]

# 2. 依存関係インストール
npm install

# 3. 環境変数設定
cp .env.example .env
# .env を編集SQLITE_PATH等

# 4. DB初期化Migration実行
npm run migrate

# 5. 開発サーバ起動
npm run dev

品質自動化

Pre-commitHusky + lint-staged: コミット前にLint・フォーマット・型チェックを自動実行

// package.json
{
  "scripts": {
    "lint": "eslint .",
    "format": "prettier --write .",
    "typecheck": "tsc --noEmit",
    "test": "vitest run",
    "test:e2e": "playwright test",
    "migrate": "tsx lib/db/run-migrations.ts",
    "dev": "next dev",
    "build": "next build"
  },
  "lint-staged": {
    "*.{ts,tsx}": ["eslint --fix", "prettier --write"]
  }
}

CIGitHub Actions: PR作成時にLint・型チェック・Unit Test・ビルドを自動実行

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '24' }
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm run test
      - run: npm run build

効果: 欠陥コードの混入防止・早期発見による修正コスト削減・CIによる品質担保。

チェックリスト

実装完了前

  • 命名が明確で一貫している
  • 関数が単一責務
  • マジックナンバーがない
  • 型注釈が適切
  • エラーハンドリングが実装されている

セキュリティ

  • 入力バリデーション実装
  • 機密情報のハードコードなし
  • SQLパラメータバインド
  • 権限チェック実装
  • Markdownサニタイズ

パフォーマンス

  • 適切なデータ構造
  • N+1クエリ回避
  • 一覧のページネーション

テスト【必須】

  • Unit TestVitest作成
  • E2E TestPlaywright作成
  • npm test 全件成功
  • npm run test:e2e 全件成功
  • エッジケース網羅

ツール

  • Lintエラーなし
  • 型チェック成功
  • フォーマット統一