From e637b144c037dede818429080ce9a9f0ae4a3368 Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Sun, 8 Feb 2026 09:44:57 +0000 Subject: [PATCH 01/14] feat: Introduce claude code (#3143) --- .devcontainer/devcontainer.json | 12 +- CLAUDE.md | 303 ++++++++++++++++++ .../2026-02-08/introduce-claude-code/plan.md | 110 +++++++ .../2026-02-08/simplify-claude-md/plan.md | 164 ++++++++++ 4 files changed, 587 insertions(+), 2 deletions(-) create mode 100644 CLAUDE.md create mode 100644 docs/dev-notes/2026-02-08/introduce-claude-code/plan.md create mode 100644 docs/dev-notes/2026-02-08/simplify-claude-md/plan.md diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ca9edd849..7adc7ca3d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,7 +6,10 @@ "workspaceFolder": "/usr/src/app", // Use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile "dockerComposeFile": ["../compose.yaml"], - "mounts": ["source=${localEnv:HOME}/.ssh,target=/home/node/.ssh,type=bind,consistency=cached"], + "mounts": [ + "source=${localEnv:HOME}/.ssh,target=/home/node/.ssh,type=bind,consistency=cached", + "source=${localEnv:HOME}/.claude,target=/home/node/.claude,type=bind,consistency=cached" + ], // Features to add to the dev container. More info: https://containers.dev/features. // "features": {}, // @@ -19,7 +22,7 @@ // "shutdownAction": "none", // // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "yarn install", + "postCreateCommand": "npm install -g @anthropic-ai/claude-code", // // Configure tool-specific properties. "customizations": { @@ -72,6 +75,7 @@ "svelte.enable-ts-plugin": true }, "extensions": [ + "anthropic.claude-code", "bradlc.vscode-tailwindcss", "christian-kohler.path-intellisense", "csstools.postcss", @@ -85,6 +89,10 @@ "vscode-icons-team.vscode-icons" ] } + }, + "containerEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096 --dns-result-order=ipv4first", + "CLAUDE_CONFIG_DIR": "/home/node/.claude" } // // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..d778dc058 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,303 @@ +# AtCoder NoviSteps + +This unofficial web service enables users to track their submissions on [AtCoder](https://atcoder.jp/) and other competitive programming platforms. Users can track the status of their submissions for problems graded on a scale from Q11 to D6 (17 levels). + +**Main Features:** + +- Track submission status (AC, AC with editorial, Trying, Pending) +- Browse problems by contest type and difficulty +- Problem collections (workbooks) organized by topic/algorithm + +## Technology Stack + +**Framework & Language:** + +- SvelteKit 2.x (file-based routing) +- Svelte 5.x with Runes mode enabled +- TypeScript with strict mode + +**Backend & Database:** + +- PostgreSQL (via Supabase) +- Prisma ORM (client generated with `@prisma/client` and `@quramy/prisma-fabbrica`) +- Lucia v2 for authentication + +**UI & Styling:** + +- Flowbite Svelte (component library) +- Tailwind CSS 4.x +- Lucide icons + +**Testing:** + +- Vitest for unit tests (`src/**/*.test.ts`) +- Playwright for E2E tests (`tests/`) +- Nock for HTTP mocking in tests + +**Deployment:** + +- Vercel (Tokyo region: hnd1) +- GitHub Actions for CI/CD + +## Essential Commands + +### Development + +```bash +# Start development server (auto-opens browser at localhost:5174) +pnpm dev + +# Start with manual browser opening +pnpm dev --open + +# Build for production +pnpm build + +# Preview production build +pnpm preview +``` + +### Database + +```bash +# Generate Prisma client (auto-runs after pnpm install) +pnpm exec prisma generate + +# Push schema changes to database (for development) +pnpm exec prisma db push + +# Apply migrations (for production) +pnpm exec prisma migrate deploy + +# Seed database with initial data +pnpm db:seed + +# Open Prisma Studio (database GUI) +pnpm db:studio +``` + +### Testing + +```bash +# Run all tests (integration + unit) +pnpm test + +# Unit tests only (Vitest) +pnpm test:unit + +# Integration tests only (Playwright) +pnpm test:integration + +# Coverage report +pnpm coverage +``` + +### Code Quality + +```bash +# Lint code (ESLint) +pnpm lint + +# Format code (Prettier) +pnpm format + +# Type-check Svelte components +pnpm check + +# Type-check in watch mode +pnpm check:watch +``` + +**Note:** Git hooks (via lefthook) automatically run `prettier` and `eslint` on staged files before commit. To bypass: `LEFTHOOK=0 git commit -m "message"` + +### Single Test Execution + +```bash +# Run specific unit test file +pnpm exec vitest run path/to/test.test.ts + +# Run specific test in watch mode +pnpm exec vitest path/to/test.test.ts + +# Run specific Playwright test +pnpm exec playwright test tests/specific.test.ts +``` + +## Architecture + +### SvelteKit Routing Structure + +Routes are organized using SvelteKit's file-based routing with route groups: + +``` +src/routes/ +├── +layout.svelte # Root layout (navbar, common UI) +├── +layout.server.ts # Root layout data (user session) +├── +page.svelte # Home page +├── (auth)/ # Auth route group (no layout impact) +│ ├── login/+page.svelte +│ ├── signup/+page.svelte +│ └── logout/+page.server.ts +├── (admin)/ # Admin route group +│ ├── tasks/ +│ └── tags/ +├── problems/ # Problem listing +│ ├── +page.server.ts +│ └── [slug]/+page.svelte # Dynamic problem detail +├── workbooks/ # Problem collections +│ ├── +page.server.ts +│ ├── [slug]/ # View workbook +│ ├── create/ # Create new workbook +│ └── edit/[slug]/ # Edit workbook +└── users/ + ├── [username]/ # User profile + └── edit/ # Edit profile +``` + +**Key Pattern:** `+page.server.ts` files handle server-side data loading and form actions. Data flows to `+page.svelte` via the `data` prop. + +### Authentication Flow + +Authentication uses Lucia v2 with Prisma adapter: + +1. **Session validation** happens in `src/hooks.server.ts` for every request +2. Session data is attached to `event.locals.user` (id, name, role, atcoder_name, is_validated) +3. Protected routes check `event.locals.user` in `+page.server.ts` load functions +4. Auth forms use Superforms + Zod for validation + +**Key files:** + +- `src/lib/server/auth.ts` - Lucia configuration +- `src/hooks.server.ts` - Global request handler +- `prisma/schema.prisma` - User, Session, Key models + +### Database Schema (Prisma) + +**Core models:** + +- `User` - User accounts with AtCoder validation status +- `Task` - Problems from AtCoder with difficulty grades (Q11-D6) +- `TaskAnswer` - User submission status per problem +- `WorkBook` - Problem collections (curriculum, solution-based, user-created) +- `WorkBookTask` - Many-to-many relation between workbooks and tasks +- `Tag` / `TaskTag` - Categorization system for problems + +**Important enums:** + +- `TaskGrade` - Q11 (easiest) to D6 (hardest) +- `ContestType` - ABC, ARC, AGC, JOI, etc. +- `WorkBookType` - CURRICULUM, SOLUTION, CREATED_BY_USER + +**Note:** Schema uses camelCase for field names (Lucia v3 migration requirement). Generate types after schema changes: `pnpm exec prisma generate` + +### Directory Structure + +``` +src/lib/ +├── actions/ # Svelte actions (use:action directives) +├── clients/ # HTTP clients for external APIs (AtCoder Problems, AOJ) +│ └── cache.ts # Response caching with TTL +├── components/ # Reusable Svelte components +├── constants/ # App constants (URLs, links, status codes) +├── server/ # Server-only code (auth, database) +├── services/ # Business logic (users, tags, task_tags) +├── stores/ # Svelte stores (Runes mode: .svelte.ts files) +├── types/ # TypeScript type definitions +├── utils/ # Pure utility functions +└── zod/ # Zod validation schemas +``` + +**Testing convention:** Tests live in `src/test/` mirroring the `src/lib/` structure + +### External API Integration + +The app fetches problem data from: + +- **AtCoder Problems API** - Contest and task metadata +- **Aizu Online Judge API** - Additional problem sources + +**Implementation:** `src/lib/clients/` contains typed HTTP clients with caching (`cache.ts`). Use Nock for mocking in tests (see `src/test/lib/clients/`). + +### Svelte 5 Runes Mode + +All new Svelte code uses Runes mode: + +- Use `$props()`, `$state()`, `$derived()` instead of `export let`, `let x = $state()` +- Stores in `.svelte.ts` files use `$state()` runes +- Legacy components in `node_modules` use compatibility mode (see `svelte.config.js`) + +### Deployment Architecture + +**Branches:** + +- `staging` (default) - Preview environment with staging database +- `main` - Production environment with production database + +**CI/CD (GitHub Actions):** + +1. Build + Lint + Unit tests on all PRs and pushes +2. Preview deployment to Vercel (on staging branch) +3. Production deployment to Vercel (on main branch) +4. Database migrations run automatically before deployment + +**Vercel config:** Tokyo region (hnd1), 3008MB memory, 30s max duration + +## Important Conventions + +### Naming + +- Database fields: `snake_case` (legacy) or `camelCase` (new, preferred) +- TypeScript: `camelCase` for variables/functions, `PascalCase` for types/components +- Files: `kebab-case.ts` or `PascalCase.svelte` for components + +### Testing + +- Unit tests: `*.test.ts` files alongside source code +- E2E tests: `tests/*.test.ts` (Playwright) +- Use factories from `@quramy/prisma-fabbrica` for test data + +### Form Handling + +- Use Superforms with Zod schemas for all forms +- Server actions return structured success/error responses +- Client-side validation mirrors server-side schemas + +### Code Quality + +- Pre-commit hooks enforce formatting and linting +- All new code must pass TypeScript strict mode +- Coverage reports available via `pnpm coverage` + +## Development Environment + +**Requirements:** + +- Node.js 24.x +- pnpm (auto-detected from `package.json`) +- Docker Desktop (for PostgreSQL via Dev Containers) +- VS Code with Dev Containers extension (recommended) + +**Setup:** + +1. Clone repository +2. Open in VS Code Dev Container (auto-installs dependencies) +3. Run `pnpm exec prisma db push` to initialize database +4. Run `pnpm db:seed` to populate initial data +5. Run `pnpm dev` to start development server + +**Docker setup (alternative):** + +```bash +docker compose up -d +docker compose exec web pnpm install +docker compose exec web pnpm exec prisma db push +docker compose exec web pnpm dev --host +``` + +## Special Notes + +- **Memory leak prevention:** Vite config excludes large directories from watch (node_modules, .svelte-kit, etc.) +- **Vercel memory:** Increased to 3008MB for `/workbooks/{slug}` route to prevent OOM +- **AtCoder validation:** Users can link AtCoder accounts via validation code +- **Guest account:** Username `guest`, password `Hell0Guest` for demo purposes +- **Difficulty system:** Problems graded Q11-D6 (11 Q grades + 6 D grades = 17 levels) diff --git a/docs/dev-notes/2026-02-08/introduce-claude-code/plan.md b/docs/dev-notes/2026-02-08/introduce-claude-code/plan.md new file mode 100644 index 000000000..44d3ab919 --- /dev/null +++ b/docs/dev-notes/2026-02-08/introduce-claude-code/plan.md @@ -0,0 +1,110 @@ +# DevContainer で Claude Code を利用するための設定変更 + +## Context + +DevContainer 内で Claude Code (CLI + VSCode 拡張) を利用したい。`pnpm install -g @anthropic-ai/claude-code` を追加するだけでは不十分であり、以下の対応が必要。ファイアウォールは導入せず、認証は `claude login` で行う。 + +## 「pnpm install -g だけで十分か?」: 不十分な理由 + +1. **VSCode 拡張が未追加** — エディタ上で Claude Code を使うには `anthropic.claude-code` 拡張が必要 +2. **設定の永続化がない** — コンテナ再構築のたびに認証情報・設定が消失する。`~/.claude` をボリュームマウントで永続化すべき +3. **メモリ不足のリスク** — Claude Code は大量メモリを消費。公式は `NODE_OPTIONS=--max-old-space-size=4096` を設定 +4. **インストール場所** — ルート `Dockerfile` は複数人で共有しており Claude Code を使わない開発者もいるため変更しない。`postCreateCommand` はコンテナ作成時のみ実行されるため devcontainer 側で制御するのに適切 +5. **npm vs pnpm** — グローバルインストールは `npm` が安定(公式も `npm` を使用) + +## 公式サンプルとの比較: 取り入れる / 取り入れない + +| 公式の要素 | 判断 | 理由 | +| ---------------------------------------- | -------- | -------------------------------------------------- | +| `anthropic.claude-code` 拡張 | **採用** | 必須 | +| `~/.claude` ボリュームマウント | **採用** | `claude login` の認証を永続化 | +| `CLAUDE_CONFIG_DIR` 環境変数 | **採用** | Claude Code が設定ディレクトリを認識 | +| `NODE_OPTIONS=--max-old-space-size=4096` | **採用** | メモリ不足防止 | +| `postCreateCommand` で `npm install -g` | **採用** | ルート Dockerfile を変更せず devcontainer 側で制御 | +| ファイアウォール (init-firewall.sh) | 不採用 | 個人開発では過剰 | +| zsh + powerline10k | 不採用 | 好みの問題。既に fish を導入済み | +| git-delta | 不採用 | Claude Code に直接関係なし | + +--- + +## 変更対象ファイルと内容 + +### 1. `.devcontainer/devcontainer.json` (修正) + +#### (a) postCreateCommand を追加 + +ルート `Dockerfile` は複数人で共有しているため変更しない。コンテナ作成時のみ実行される `postCreateCommand` で Claude Code をインストール: + +```json +"postCreateCommand": "npm install -g @anthropic-ai/claude-code" +``` + +#### (b) extensions に追加 + +```json +"anthropic.claude-code" +``` + +#### (c) mounts に追加(~/.claude をホストからバインドマウント) + +ホストの `~/.claude` をバインドマウントで共有。ホスト側で `claude login` した認証情報がコンテナ内でも使える。 + +```json +"source=${localEnv:HOME}/.claude,target=/home/node/.claude,type=bind,consistency=cached" +``` + +> **当初は Docker ボリューム (`type=volume`) を使用していたが、コンテナ内での `claude login` が失敗するため変更した。** 詳細は「教訓」セクション参照。 + +#### (d) containerEnv を追加 + +compose.yaml の `environment` とは別に、devcontainer 固有の環境変数として設定: + +```json +"containerEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096 --dns-result-order=ipv4first", + "CLAUDE_CONFIG_DIR": "/home/node/.claude" +} +``` + +- `--max-old-space-size=4096`: メモリ不足防止(既存の dev server 等にも影響するが、4096MB は開発環境として妥当) +- `--dns-result-order=ipv4first`: コンテナ内で認証が必要な場合の IPv4/IPv6 ミスマッチ対策 + +--- + +## 変更しないファイル + +- `Dockerfile` — 複数人で共有しているため変更しない。Claude Code のインストールは `postCreateCommand` で制御 +- `compose.yaml` — API キーの環境変数追加は不要(`claude login` で認証) +- `init-firewall.sh` — 作成しない(ファイアウォール不採用) + +--- + +## 認証手順(初回のみ) + +**ホスト(Mac ターミナル)で実行:** + +```bash +npm install -g @anthropic-ai/claude-code # 未インストールの場合 +claude login +``` + +ホストの `~/.claude` がバインドマウントでコンテナに共有されるため、コンテナ内での追加認証は不要。 + +## 検証方法 + +1. `Dev Containers: Rebuild Container` でコンテナをリビルド +2. ターミナルで `claude --version` → バージョンが表示されること +3. `claude login` で認証 +4. VSCode のアクティビティバーに Claude Code アイコンが表示されること +5. コンテナを再起動 → `claude --version` で設定が保持されていること + +## 教訓 + +- **CLI インストール ≠ 利用可能**: VSCode 拡張・設定永続化・メモリ設定が揃って初めて実用的になる。公式サンプルを事前に確認すべき +- **共有 Dockerfile に個人ツールを入れない**: `postCreateCommand` で devcontainer 利用者だけに閉じた制御ができる。`postCreateCommand` はコンテナ作成時のみ実行され、再起動のたびには走らない +- **公式サンプルは全部入りなので取捨選択が必要**: ファイアウォール・zsh・git-delta など、Claude Code の動作に直接関係しないものは省いてシンプルに保つ +- **コンテナ内での `claude login` は失敗しやすい**: OAuth コールバックがランダムポートを使うため、コンテナ→ホストのポート転送が間に合わない。さらに IPv4/IPv6 ミスマッチも起きる。対策として、ホストで認証し `~/.claude` をバインドマウントで共有する方式が確実 + +## 参考 + +- [Official settings - Claude Code](https://github.com/anthropics/claude-code/tree/main/.devcontainer) diff --git a/docs/dev-notes/2026-02-08/simplify-claude-md/plan.md b/docs/dev-notes/2026-02-08/simplify-claude-md/plan.md new file mode 100644 index 000000000..bbee361b9 --- /dev/null +++ b/docs/dev-notes/2026-02-08/simplify-claude-md/plan.md @@ -0,0 +1,164 @@ +# CLAUDE.md 簡素化計画 + +## 背景 + +現在の CLAUDE.md(約230行)は `/init` コマンドで生成されたもので、毎セッション読み込まれるためコンテキストを圧迫する。AGENTS.md のベストプラクティスやmizchi氏のリポジトリ(67〜94行)を参考に、有用性を維持しつつコンテキスト消費を最小化するよう再構成する。 + +## 方針: AGENTS.md をメインに + +- **AGENTS.md**(約50行): Codex / Claude Code 共通のメイン指示ファイル +- **CLAUDE.md**(約10行): `@AGENTS.md` で import し、Claude 固有の設定のみ記載 +- **`.claude/rules/*.md`**: パス条件付きの詳細ルール(該当ファイル操作時のみ読み込み) + +この構成により: + +1. 単一の情報源(AGENTS.md)でマルチエージェント対応 +2. セッションごとのコンテキスト消費を最小化 +3. 詳細ガイダンスはパス条件でオンデマンド読み込み + +## 最終構成 + +``` +AGENTS.md # メイン(約50行)- Codex/Claude 共通 +CLAUDE.md # @AGENTS.md を import(約10行) +.claude/ +└── rules/ + ├── svelte-components.md # paths: ["src/**/*.svelte"] + ├── prisma-db.md # paths: ["prisma/**", "src/lib/server/**"] + ├── testing.md # paths: ["**/*.test.ts", "tests/**"] + └── auth.md # paths: ["src/lib/server/auth/**"] +.github/instructions/ # 削除(Copilot専用、情報が古い) +``` + +## AGENTS.md の内容(約50行) + +- プロジェクト概要(1〜2行) +- 技術スタック要約(1行) +- 主要コマンド(10行) +- プロジェクト構成(10行) +- 主要な規約(5行) +- 詳細は docs/package.json を参照 + +## .claude/rules/ のガイドライン + +- **各ファイル50行以内** +- **コード例は書かない** — 実際のソースファイルを参照 +- **命令形で具体的に** — 「$props() を使用する」のように +- YAML frontmatter でパス条件を指定 + +## 移行手順 + +1. AGENTS.md を新規作成(約50行) +2. CLAUDE.md を import のみに縮小(約10行) +3. .claude/rules/ に4ファイル作成 +4. .github/instructions/ を削除(8ファイル) +5. CONTRIBUTING.md に Codex 設定案内を追加(Codex 導入時に対応) + +## CLAUDE.md から削除する項目 + +- ゲストアカウント情報(`guest`/`Hell0Guest`)→ README.md のみに記載 +- 詳細なアーキテクチャ説明 → ソースコード参照 +- バージョン番号 → package.json を直接確認 +- コード例 → 実際のファイルを参照 + +--- + +## .github/instructions/ の有用コンテンツ分析 + +8ファイルを分析し、移管先を決定した。 + +### docs/guides/ に移管(人間向けドキュメント) + +| 元ファイル | 有用なセクション | 移管先 | +| ------------------------------ | ---------------------------- | --------------------------------------- | +| source-code.instructions.md | 命名規則(ファイル/変数/型) | `docs/guides/naming-conventions.md` | +| authentication.instructions.md | セキュリティチェックリスト | `docs/guides/security-checklist.md` | +| docs.instructions.md | ドキュメント種別・執筆原則 | `docs/guides/documentation-strategy.md` | + +### .claude/rules/ に圧縮して移管(エージェント向け) + +| 元ファイル | 有用なセクション | 移管先 | +| ----------------------------- | ---------------------------------- | ------------------------------------ | +| tests.instructions.md | テスト種別テーブル、カバレッジ目標 | `.claude/rules/testing.md` | +| ui-components.instructions.md | Svelte 5 Runes の使い方 | `.claude/rules/svelte-components.md` | + +### 削除(陳腐化 or 価値低い) + +| ファイル | 理由 | +| ------------------------------- | -------------------------------------------------------------------- | +| global.instructions.md | 設定ファイル名が古い(`.eslintrc.cjs` → 実際は `eslint.config.mjs`) | +| ci.instructions.md | 内容が薄い。`.github/workflows/` を見れば十分 | +| performance-seo.instructions.md | コード例が多いがソースと乖離リスク | +| ui-components.instructions.md | バージョンテーブルが古い(Tailwind 3.x → 実際は 4.x)、635行は長すぎ | +| authentication.instructions.md | 認証フロー図が実態と異なる(後述) | + +### 認証フロー図の調査結果 + +instructions に記載の認証フロー: + +``` +A[ユーザー登録] → B[AtCoder認証コード生成] → C[AtCoder側でコード確認] +→ D[認証ステータス更新] → E[セッション作成] → F[ログイン完了] +``` + +**実態**: + +| ステップ | 状態 | 根拠 | +| ------------------------ | ------------------- | ----------------------------------------------------------------------------------------- | +| A: ユーザー登録 | ✅ 機能 | Lucia v2 + Prisma で実装済み | +| B: AtCoder認証コード生成 | ⚠️ 実装済・UI非公開 | `validateApiService.generate()` 存在。ただし `AtCoderUserValidationForm` がコメントアウト | +| C: AtCoder側でコード確認 | ⚠️ 実装済・UI非公開 | `validateApiService.confirm()` 存在 | +| D: 認証ステータス更新 | ⚠️ 実装済・UI非公開 | `validateApiService.validate()` 存在 | +| E: セッション作成 | ✅ 機能 | Lucia v2 のセッション管理 | +| F: ログイン完了 | ✅ 機能 | `event.locals.user` にセット | + +B〜D は「回答状況が正しく取得されないバグ」のため UI を非公開にしている(`users/edit/+page.svelte` のコメント参照)。 + +**結論**: 認証フロー図は実態と異なるため、docs への移管は行わず削除する。 + +--- + +## Q&A まとめ + +### Q: 詳細ドキュメントを docs/guides/ に置くのはどうか? + +**A**: エージェントは docs/ を自動で読み込まない。AGENTS.md / CLAUDE.md / .claude/rules/ のみ自動読み込み対象。docs/ にエージェント向け指示を置いても効果がない。 + +### Q: Codex の Skills と Rules の違いは? + +**A**: + +- **Skills** = エージェントの能力を拡張(新しいワークフロー、スクリプト) +- **Rules** = コマンドのセキュリティ制御(allow/prompt/forbidden) + +Claude Code の `.claude/rules/` はこれらとは異なり、パス条件付きの指示ファイル。 + +### Q: .agents/skills/ を作成すべきか? + +**A**: 現時点では不要。Codex 導入時に再検討。 + +### Q: AGENTS.md を CLAUDE.md から import vs 内容を複製? + +**A**: `@AGENTS.md` で import。単一の情報源で保守性向上。 + +### Q: 命名規則の `I` prefix は使うか? + +**A**: 使わない。実際のコードベースで使用されておらず、モダン TypeScript では非推奨。 + +### Q: Core Web Vitals 目標値は残すか? + +**A**: 削除。performance-seo.instructions.md ごと削除。 + +### Q: 認証フロー図は docs に移管するか? + +**A**: 削除。A, E, F のみ機能しており、B〜D は UI 非公開のため図が実態と異なる。 + +--- + +## 参考資料 + +- [AGENTS.md](https://agents.md/) +- [Manage Claude's memory](https://code.claude.com/docs/en/memory) +- [Custom instructions with AGENTS.md](https://developers.openai.com/codex/guides/agents-md) +- [mizchi/playwright.mbt](https://github.com/mizchi/playwright.mbt/blob/main/AGENTS.md) +- [mizchi/luna.mbt](https://github.com/mizchi/luna.mbt/blob/main/CLAUDE.md) From 2ae0827256cb78b7ee826378fb414f547825b09b Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Sun, 8 Feb 2026 09:45:53 +0000 Subject: [PATCH 02/14] docs: Remove old instructions for copilot (#3143) --- .../authentication.instructions.md | 352 ++++++++++ .github/instructions/ci.instructions.md | 33 + .github/instructions/ci.md | 108 +++ .github/instructions/docs.instructions.md | 60 ++ .github/instructions/global.instructions.md | 163 +++++ .../performance-seo.instructions.md | 363 ++++++++++ .../instructions/source-code.instructions.md | 248 +++++++ .github/instructions/tests.instructions.md | 380 +++++++++++ .../ui-components.instructions.md | 634 ++++++++++++++++++ 9 files changed, 2341 insertions(+) create mode 100644 .github/instructions/authentication.instructions.md create mode 100644 .github/instructions/ci.instructions.md create mode 100644 .github/instructions/ci.md create mode 100644 .github/instructions/docs.instructions.md create mode 100644 .github/instructions/global.instructions.md create mode 100644 .github/instructions/performance-seo.instructions.md create mode 100644 .github/instructions/source-code.instructions.md create mode 100644 .github/instructions/tests.instructions.md create mode 100644 .github/instructions/ui-components.instructions.md diff --git a/.github/instructions/authentication.instructions.md b/.github/instructions/authentication.instructions.md new file mode 100644 index 000000000..43e8f2410 --- /dev/null +++ b/.github/instructions/authentication.instructions.md @@ -0,0 +1,352 @@ +# 認証・セキュリティガイド + +## 概要 + +AtCoder-NoviStepsプロジェクトの認証システム(Lucia Auth)とセキュリティ実装 + +## 認証アーキテクチャ + +### Lucia Auth v2 構成 + +- **Adapter**: `@lucia-auth/adapter-prisma` +- **Session管理**: PostgreSQL + Prisma +- **AtCoder連携**: AtCoderアカウント認証 + +### 認証フロー + +```mermaid +graph TD + A[ユーザー登録] --> B[AtCoder認証コード生成] + B --> C[AtCoder側でコード確認] + C --> D[認証ステータス更新] + D --> E[セッション作成] + E --> F[ログイン完了] +``` + +## Prismaスキーマ(認証関連) + +### User モデル + +```prisma +model User { + id String @id @unique + username String @unique + role Roles @default(USER) + atcoder_validation_code String @default("") + atcoder_username String @default("") + atcoder_validation_status Boolean? @default(false) + + auth_session Session[] + key Key[] +} + +enum Roles { + ADMIN + USER +} +``` + +### Session管理 + +```prisma +model Session { + id String @id @unique + user_id String + active_expires BigInt + idle_expires BigInt + + user User @relation(references: [id], fields: [user_id], onDelete: Cascade) +} +``` + +## 実装ガイド + +### Luciaセットアップ + +```typescript +// lib/server/auth/lucia.ts +import { lucia } from 'lucia'; +import { sveltekit } from 'lucia/middleware'; +import { prisma } from '@lucia-auth/adapter-prisma'; +import { PrismaClient } from '@prisma/client'; + +const client = new PrismaClient(); + +export const auth = lucia({ + env: process.env.NODE_ENV === 'development' ? 'DEV' : 'PROD', + middleware: sveltekit(), + adapter: prisma(client), + getUserAttributes: (data) => ({ + username: data.username, + role: data.role, + atcoderUsername: data.atcoder_username, + atcoderValidationStatus: data.atcoder_validation_status, + }), +}); + +export type Auth = typeof auth; +``` + +### AtCoder認証プロバイダー + +```typescript +// lib/server/auth/atcoder-provider.ts +export class AtCoderAuthProvider { + async generateValidationCode(username: string): Promise { + // 8桁のランダムコード生成 + const code = Math.random().toString(36).substring(2, 10).toUpperCase(); + + await prisma.user.update({ + where: { username }, + data: { + atcoder_validation_code: code, + atcoder_validation_status: false, + }, + }); + + return code; + } + + async validateAtCoderAccount(username: string): Promise { + const user = await prisma.user.findUnique({ + where: { username }, + select: { atcoder_validation_code: true, atcoder_username: true }, + }); + + if (!user?.atcoder_validation_code) return false; + + // AtCoder APIで自己紹介欄にコードが含まれているかチェック + const isValid = await this.checkCodeInProfile( + user.atcoder_username, + user.atcoder_validation_code, + ); + + if (isValid) { + await prisma.user.update({ + where: { username }, + data: { atcoder_validation_status: true }, + }); + } + + return isValid; + } + + private async checkCodeInProfile(atcoderUsername: string, code: string): Promise { + // AtCoder APIまたはスクレイピングでプロフィール確認 + // 実装詳細は外部API仕様に依存 + return false; // placeholder + } +} +``` + +## セキュリティ実装 + +### XSS対策 + +```typescript +// lib/server/security/xss.ts +import { filterXSS } from 'xss'; + +export function sanitizeHtml(input: string): string { + return filterXSS(input, { + whiteList: { + // 許可するHTMLタグを限定 + p: [], + br: [], + strong: [], + em: [], + }, + }); +} +``` + +### CSRF対策 + +```typescript +// src/hooks.server.ts +import type { Handle } from '@sveltejs/kit'; +import { auth } from '$lib/server/auth/lucia'; + +export const handle: Handle = async ({ event, resolve }) => { + // Lucia認証ハンドラー + event.locals.auth = auth.handleRequest(event); + + // CSRF保護(SvelteKit内蔵) + if (event.request.method === 'POST') { + const contentType = event.request.headers.get('content-type'); + if (contentType?.includes('application/x-www-form-urlencoded')) { + // Form actionでのCSRF自動チェック + } + } + + return await resolve(event); +}; +``` + +### セッション管理 + +```typescript +// lib/server/auth/session.ts +export class SessionManager { + static async createSession(userId: string): Promise { + return await auth.createSession({ + userId, + attributes: {}, + }); + } + + static async validateSession(sessionId: string): Promise<{ + session: Session | null; + user: User | null; + }> { + return await auth.validateSession(sessionId); + } + + static async invalidateSession(sessionId: string): Promise { + await auth.invalidateSession(sessionId); + } + + static async invalidateAllUserSessions(userId: string): Promise { + await auth.invalidateAllUserSessions(userId); + } +} +``` + +## 権限管理 + +### ロールベースアクセス制御 + +```typescript +// lib/server/auth/permissions.ts +export enum Permission { + READ_PROBLEMS = 'read:problems', + CREATE_WORKBOOK = 'create:workbook', + ADMIN_USERS = 'admin:users', + MODERATE_CONTENT = 'moderate:content', +} + +export const ROLE_PERMISSIONS: Record = { + USER: [Permission.READ_PROBLEMS, Permission.CREATE_WORKBOOK], + ADMIN: [ + Permission.READ_PROBLEMS, + Permission.CREATE_WORKBOOK, + Permission.ADMIN_USERS, + Permission.MODERATE_CONTENT, + ], +}; + +export function hasPermission(userRole: Roles, permission: Permission): boolean { + return ROLE_PERMISSIONS[userRole].includes(permission); +} +``` + +### ルートガード + +```typescript +// lib/server/auth/guards.ts +import { error } from '@sveltejs/kit'; +import type { RequestEvent } from '@sveltejs/kit'; + +export async function requireAuth(event: RequestEvent) { + const session = await event.locals.auth.validate(); + if (!session) { + throw error(401, 'Authentication required'); + } + return session; +} + +export async function requirePermission(event: RequestEvent, permission: Permission) { + const session = await requireAuth(event); + + if (!hasPermission(session.user.role, permission)) { + throw error(403, 'Insufficient permissions'); + } + + return session; +} + +export async function requireAtCoderVerification(event: RequestEvent) { + const session = await requireAuth(event); + + if (!session.user.atcoderValidationStatus) { + throw error(403, 'AtCoder account verification required'); + } + + return session; +} +``` + +## 環境変数 + +### 必須設定 + +```bash +# .env +LUCIA_SESSION_SECRET="your-super-secret-session-key-here" +DATABASE_URL="postgresql://user:pass@localhost:5432/atcoder_novisteps" + +# AtCoder連携(オプション) +ATCODER_API_BASE_URL="https://kenkoooo.com/atcoder/atcoder-api" +ATCODER_SCRAPING_USER_AGENT="AtCoderNoviSteps/1.0" +``` + +## テスト戦略 + +### 認証テスト + +```typescript +// tests/integration/auth.test.ts +import { test, expect } from 'vitest'; +import { auth } from '$lib/server/auth/lucia'; + +test('should create and validate session', async () => { + const user = await createTestUser(); + const session = await auth.createSession({ + userId: user.id, + attributes: {}, + }); + + expect(session.sessionId).toBeDefined(); + + const { session: validatedSession, user: validatedUser } = await auth.validateSession( + session.sessionId, + ); + + expect(validatedSession?.sessionId).toBe(session.sessionId); + expect(validatedUser?.id).toBe(user.id); +}); +``` + +### AtCoder認証テスト + +```typescript +// tests/unit/auth/atcoder-provider.test.ts +import { test, expect, vi } from 'vitest'; +import { AtCoderAuthProvider } from '$lib/server/auth/atcoder-provider'; + +test('should generate validation code', async () => { + const provider = new AtCoderAuthProvider(); + const code = await provider.generateValidationCode('testuser'); + + expect(code).toMatch(/^[A-Z0-9]{8}$/); +}); +``` + +## セキュリティチェックリスト + +### 本番環境 + +- [ ] セッションシークレットがランダム生成されている +- [ ] HTTPS強制設定 +- [ ] セキュリティヘッダー設定(CSP、HSTS等) +- [ ] レート制限実装 +- [ ] SQLインジェクション対策(Prisma使用) +- [ ] XSS対策(入力サニタイズ) +- [ ] CSRF対策(SvelteKit内蔵) + +### 開発環境 + +- [ ] `.env`ファイルの`.gitignore`設定 +- [ ] 開発用認証情報の分離 +- [ ] セキュリティテストの自動化 +- [ ] 依存関係の脆弱性スキャン diff --git a/.github/instructions/ci.instructions.md b/.github/instructions/ci.instructions.md new file mode 100644 index 000000000..4e53eed5e --- /dev/null +++ b/.github/instructions/ci.instructions.md @@ -0,0 +1,33 @@ +# CI/CD設定ガイド + +## 現在の設定ファイル + +- メインワークフロー: `.github/workflows/ci.yml` +- E2Eテスト: `.github/workflows/playwright.yml` +- パッケージ設定: `package.json` +- TypeScript設定: `tsconfig.json` + +## 推奨改善点 + +### package.jsonのscripts + +現在の`package.json`を確認して、以下のスクリプトが適切に設定されているか確認: + +```json +{ + "scripts": { + "build": "vite build", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "test:unit": "vitest", + "test:integration": "playwright test" + } +} +``` + +### CIワークフローの最適化 + +`.github/workflows/ci.yml`で以下の点を確認: + +- pnpmバージョンが`package.json`の`packageManager`と一致 +- Node.jsバージョンが`engines.node`と整合 +- キャッシュキーが適切に設定されている diff --git a/.github/instructions/ci.md b/.github/instructions/ci.md new file mode 100644 index 000000000..d4ff52d4f --- /dev/null +++ b/.github/instructions/ci.md @@ -0,0 +1,108 @@ +# CI / 自動化ガイド + +## 概要 + +AtCoder-NoviStepsプロジェクトのCI/CDパイプライン設定とベストプラクティス + +## 技術スタック + +- GitHub Actions +- Playwright (E2E) +- Vitest (Unit/Integration) +- pnpm +- Vercel (デプロイ) + +## ワークフロー構成 + +### 推奨ワークフロー + +| ファイル | トリガ | 内容 | +| ---------------- | ------------------------------ | --------------------------------------------- | +| `ci.yml` | PR / push | lint, typecheck, unit/integration test, build | +| `e2e.yml` | `workflow_run: ci.yml success` | Playwright E2E | +| `deploy.yml` | main ブランチ push | Vercel デプロイ | +| `codeql.yml` | スケジュール/PR | セキュリティ静的解析 | +| `dependabot.yml` | スケジュール | 依存更新 | + +### CI パイプライン詳細 + +```yaml +# .github/workflows/ci.yml の例 +name: CI +on: + pull_request: + push: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v3 + with: + version: 10.15.0 + - uses: actions/setup-node@v4 + with: + node-version: '>=18.16.0' + cache: 'pnpm' + - run: pnpm install + - run: pnpm check # svelte-check + - run: pnpm lint + - run: pnpm test:unit --coverage + - run: pnpm build +``` + +### E2E テスト + +```yaml +# .github/workflows/e2e.yml の例 +name: E2E Tests +on: + workflow_run: + workflows: ['CI'] + types: [completed] + +jobs: + e2e: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v3 + with: + version: 10.15.0 + - run: pnpm install + - run: pnpm build + - run: pnpm playwright install --with-deps + - run: pnpm test:integration + - uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: playwright-report +``` + +## キャッシュ戦略 + +- pnpm キャッシュキー: `pnpm-lock.yaml` ハッシュ +- Playwright ブラウザキャッシュ +- Prisma生成ファイルキャッシュ + +## パフォーマンス最適化 + +- 並列実行: lint/typecheck/test を並列化 +- matrix戦略: Node.js複数バージョン対応 +- 条件付き実行: 変更ファイルベースの最適化 + +## 環境変数・シークレット + +- DATABASE_URL +- SESSION_SECRET +- VERCEL_TOKEN (デプロイ用) +- GitHub Actions Secrets で管理 + +## カバレッジ + +- 目標: Lines 80%, Branches 70% +- Vitest coverage-v8 使用 +- アーティファクトでレポート保存 diff --git a/.github/instructions/docs.instructions.md b/.github/instructions/docs.instructions.md new file mode 100644 index 000000000..7ecaa36eb --- /dev/null +++ b/.github/instructions/docs.instructions.md @@ -0,0 +1,60 @@ +# ドキュメント運用ガイド + +## 概要 + +AtCoder-NoviStepsプロジェクトのドキュメント戦略とメンテナンス方針 + +## ドキュメント構成 + +### 種別と目的 + +| 種別 | 目的 | 場所 | 形式 | +| -------------- | ---------------- | ----------------------- | ---------------- | +| 開発ガイド | 開発者向け指針 | `.github/instructions/` | Markdown | +| ユーザーガイド | エンドユーザ向け | `/docs/` | Markdown | +| API仕様 | 外部連携 | 自動生成 | OpenAPI/Swagger | +| 変更履歴 | リリース追跡 | `CHANGELOG.md` | Keep a Changelog | +| README | プロジェクト概要 | `README.md` | GitHub標準 | + +### AtCoder固有ドキュメント + +- コンテスト問題データ構造 +- AtCoder API連携仕様 +- ユーザーレーティング算出ロジック +- 問題解説・ヒント執筆ガイド + +## 執筆・更新フロー + +### 原則 + +- 1 PR = 1 機能 + 関連ドキュメント更新 +- 破壊的変更は明確に "BREAKING CHANGE" 表記 +- スクリーンショットは圧縮済み画像使用 + +### レビュープロセス + +- 技術精度: エンジニアレビュー +- 日本語表現: 必要に応じてライティングレビュー +- ユーザビリティ: 実際の利用者フィードバック + +## Mermaid図活用 + +```mermaid +graph TD + A[ユーザー] --> B[問題一覧] + B --> C[問題詳細] + C --> D[解説表示] + D --> E[次の問題へ] +``` + +## メンテナンス + +- 月1回のドキュメント棚卸し +- 古いスクリーンショット更新 +- リンク切れチェック(CI組み込み推奨) + +## 多言語対応(将来) + +- 日本語メイン、英語サブ +- i18n用ディレクトリ構造準備 +- 翻訳キー管理ツール検討 diff --git a/.github/instructions/global.instructions.md b/.github/instructions/global.instructions.md new file mode 100644 index 000000000..22e364b94 --- /dev/null +++ b/.github/instructions/global.instructions.md @@ -0,0 +1,163 @@ +# グローバル設定ガイド + +## 概要 + +AtCoder-NoviStepsプロジェクト全体の統一設定とツールチェーン + +## 必須設定ファイル + +| ファイル | 目的 | 重要度 | +| ---------------------- | ---------------------- | ------ | +| `.editorconfig` | エディタ統一 | 必須 | +| `.eslintrc.cjs` | TypeScript/Svelte Lint | 必須 | +| `.prettierrc` | コードフォーマット | 必須 | +| `tsconfig.json` | TypeScript設定 | 必須 | +| `svelte.config.js` | Svelte/SvelteKit設定 | 必須 | +| `tailwind.config.js` | CSS Framework | 必須 | +| `playwright.config.ts` | E2Eテスト設定 | 必須 | +| `vitest.config.ts` | 単体テスト設定 | 必須 | +| `prisma/schema.prisma` | DB スキーマ | 必須 | + +## TypeScript設定 + +現在の `tsconfig.json` 基準: + +- `strict: true` 必須 +- `moduleResolution: "bundler"` +- SvelteKit連携 (`.svelte-kit/tsconfig.json` 拡張) + +## ESLint設定 + +```javascript +// .eslintrc.cjs 抜粋 +module.exports = { + extends: [ + 'eslint:recommended', + '@typescript-eslint/recommended', + 'plugin:svelte/recommended', + 'prettier', + ], + parser: '@typescript-eslint/parser', + parserOptions: { + project: './tsconfig.json', + extraFileExtensions: ['.svelte'], + }, +}; +``` + +## Prettier設定 + +```json +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] +} +``` + +## Git設定 + +### Husky + lint-staged + +```json +{ + "lint-staged": { + "**/*.{js,jsx,ts,tsx}": ["pnpm format", "pnpm lint"] + } +} +``` + +### .gitignore 重要項目 + +``` +# AtCoder固有 +/data/contests/ +/data/problems/cache/ + +# 標準 +node_modules/ +.env +.svelte-kit/ +build/ +.vercel/ +coverage/ +``` + +## 環境変数管理 + +### 命名規則 + +- `PUBLIC_`: クライアント側で使用可能 +- `DATABASE_URL`: Prisma接続文字列 +- `ATCODER_API_*`: AtCoder API関連 +- `LUCIA_SESSION_SECRET`: 認証シークレット + +### .env.example + +```bash +# Database +DATABASE_URL="postgresql://user:pass@localhost:5432/atcoder_novisteps" + +# Authentication +LUCIA_SESSION_SECRET="your-session-secret-here" + +# AtCoder API (if needed) +PUBLIC_ATCODER_API_BASE="https://kenkoooo.com/atcoder/atcoder-api" + +# Analytics (optional) +PUBLIC_GA_MEASUREMENT_ID="" +``` + +## パッケージマネージャ + +### pnpm設定 + +- バージョン: `10.15.0` 固定 +- `packageManager` フィールドで強制 +- `engines.node`: `>=18.16.0` + +## IDE設定 + +### VSCode推奨拡張 + +```json +{ + "recommendations": [ + "svelte.svelte-vscode", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "bradlc.vscode-tailwindcss", + "prisma.prisma" + ] +} +``` + +## セキュリティ設定 + +### CODEOWNERS + +``` +# Global +* @KATO-Hiro + +# Database migrations +/prisma/migrations/ @KATO-Hiro @database-reviewers + +# CI/CD +/.github/ @KATO-Hiro @devops-team +``` + +### dependabot.yml + +```yaml +version: 2 +updates: + - package-ecosystem: 'npm' + directory: '/' + schedule: + interval: 'weekly' + open-pull-requests-limit: 5 +``` diff --git a/.github/instructions/performance-seo.instructions.md b/.github/instructions/performance-seo.instructions.md new file mode 100644 index 000000000..2c8808258 --- /dev/null +++ b/.github/instructions/performance-seo.instructions.md @@ -0,0 +1,363 @@ +# パフォーマンス・SEO最適化ガイド + +## 概要 + +AtCoder-NoviStepsのパフォーマンス向上とSEO対策の包括的戦略 + +## パフォーマンス最適化 + +### Core Web Vitals目標 + +| 指標 | 目標値 | 測定方法 | +| ------------------------------ | ------- | ------------------------------ | +| LCP (Largest Contentful Paint) | < 2.5秒 | Lighthouse, PageSpeed Insights | +| FID (First Input Delay) | < 100ms | Real User Monitoring | +| CLS (Cumulative Layout Shift) | < 0.1 | Lighthouse | +| TTFB (Time to First Byte) | < 800ms | WebPageTest | + +### SvelteKit最適化設定 + +```javascript +// svelte.config.js +import adapter from '@sveltejs/adapter-vercel'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter({ + runtime: 'nodejs18.x', + regions: ['nrt1'], // 東京リージョン(日本ユーザー向け) + split: true, // ルート別コード分割 + }), + prerender: { + entries: [ + '*', + '/sitemap.xml', + '/robots.txt', + '/problems', // 静的プリレンダリング + '/contests', + ], + handleHttpError: 'warn', + handleMissingId: 'warn', + }, + csp: { + mode: 'auto', + directives: { + 'script-src': ['self', 'unsafe-inline', 'https://www.googletagmanager.com'], + 'style-src': ['self', 'unsafe-inline'], + 'img-src': ['self', 'data:', 'https:'], + 'font-src': ['self'], + }, + }, + }, +}; + +export default config; +``` + +### 画像最適化 + +```typescript +// lib/utils/imageOptimization.ts +export function getOptimizedImageUrl( + src: string, + width: number, + height?: number, + format: 'webp' | 'avif' | 'jpg' = 'webp', +): string { + // Vercel Image Optimization活用 + const params = new URLSearchParams({ + url: src, + w: width.toString(), + q: '75', // 品質75% + fm: format, + }); + + if (height) { + params.set('h', height.toString()); + } + + return `/_vercel/image?${params.toString()}`; +} + +// コンポーネントでの使用例 +export function createPictureElement( + src: string, + alt: string, + sizes: number[], +): HTMLPictureElement { + const picture = document.createElement('picture'); + + // WebP対応ブラウザ向け + const webpSource = document.createElement('source'); + webpSource.type = 'image/webp'; + webpSource.srcset = sizes + .map((size) => `${getOptimizedImageUrl(src, size, undefined, 'webp')} ${size}w`) + .join(', '); + + // フォールバック + const img = document.createElement('img'); + img.src = getOptimizedImageUrl(src, sizes[0]); + img.alt = alt; + img.loading = 'lazy'; + img.decoding = 'async'; + + picture.appendChild(webpSource); + picture.appendChild(img); + + return picture; +} +``` + +### データベース最適化 + +```typescript +// lib/server/repositories/optimized/ProblemRepository.ts +import { PrismaClient } from '@prisma/client'; + +export class OptimizedProblemRepository { + constructor(private prisma: PrismaClient) {} + + // インデックス活用クエリ + async findProblemsByDifficulty( + minDifficulty: number, + maxDifficulty: number, + limit: number = 50, + offset: number = 0, + ) { + return await this.prisma.task.findMany({ + where: { + atcoder_problems_difficulty: { + not: 'PENDING', + }, + // 難易度範囲での検索(インデックス必要) + atcoder_problems_difficulty_value: { + gte: minDifficulty, + lte: maxDifficulty, + }, + }, + select: { + // 必要フィールドのみ選択 + task_id: true, + title: true, + contest_id: true, + contest_type: true, + atcoder_problems_difficulty: true, + }, + orderBy: { + atcoder_problems_difficulty_value: 'asc', + }, + take: limit, + skip: offset, + }); + } + + // N+1問題解決(Prisma include) + async findProblemsWithTagsAndAnswers(userId: string) { + return await this.prisma.task.findMany({ + include: { + tags: { + include: { + tag: { + select: { name: true, is_official: true }, + }, + }, + }, + task_answers: { + where: { user_id: userId }, + include: { + status: { + select: { status_name: true, is_AC: true }, + }, + }, + }, + }, + }); + } +} +``` + +### キャッシュ戦略 + +```typescript +// lib/server/cache/CacheManager.ts +interface CacheEntry { + data: T; + timestamp: number; + ttl: number; +} + +export class CacheManager { + private cache = new Map>(); + + set(key: string, data: T, ttlSeconds: number = 3600): void { + this.cache.set(key, { + data, + timestamp: Date.now(), + ttl: ttlSeconds * 1000, + }); + } + + get(key: string): T | null { + const entry = this.cache.get(key); + + if (!entry) return null; + + if (Date.now() - entry.timestamp > entry.ttl) { + this.cache.delete(key); + return null; + } + + return entry.data; + } + + // プロブレム一覧のキャッシュ例 + async getCachedProblems(cacheKey: string, fetcher: () => Promise): Promise { + let problems = this.get(cacheKey); + + if (!problems) { + problems = await fetcher(); + this.set(cacheKey, problems, 1800); // 30分キャッシュ + } + + return problems; + } +} + +// 使用例 +const cacheManager = new CacheManager(); + +export async function getCachedProblemsByDifficulty( + minDiff: number, + maxDiff: number, +): Promise { + const cacheKey = `problems:difficulty:${minDiff}-${maxDiff}`; + + return await cacheManager.getCachedProblems(cacheKey, async () => { + return await problemRepository.findProblemsByDifficulty(minDiff, maxDiff); + }); +} +``` + +## SEO対策 + +### メタタグ管理(svelte-meta-tags) + +```typescript +// src/routes/+layout.svelte + + + +``` + +### 構造化データ + +```typescript +// lib/seo/structuredData.ts +export function createProblemStructuredData(problem: Problem) { + return { + '@context': 'https://schema.org', + '@type': 'LearningResource', + name: problem.title, + description: `AtCoder ${problem.contest_id} ${problem.task_table_index}問題`, + url: `https://atcoder-novisteps.vercel.app/problems/${problem.task_id}`, + learningResourceType: 'Problem', + educationalLevel: getDifficultyLevel(problem.atcoder_problems_difficulty), + about: { + '@type': 'Thing', + name: 'プログラミング競技', + sameAs: 'https://ja.wikipedia.org/wiki/プログラミングコンテスト', + }, + provider: { + '@type': 'Organization', + name: 'AtCoder NoviSteps', + url: 'https://atcoder-novisteps.vercel.app', + }, + }; +} + +export function createWorkbookStructuredData(workbook: WorkBook) { + return { + '@context': 'https://schema.org', + '@type': 'Course', + name: workbook.title, + description: workbook.description, + url: `https://atcoder-novisteps.vercel.app/workbooks/${workbook.urlSlug}`, + courseCode: workbook.urlSlug, + educationalCredentialAwarded: 'Certificate of Completion', + provider: { + '@type': 'Organization', + name: 'AtCoder NoviSteps', + }, + }; +} +``` + +### サイトマップ生成(super-sitemap) + +```typescript +// src/routes/sitemap.xml/+server.ts +import { createSitemap } from 'super-sitemap'; +import type { RequestHandler } from './$types'; +import { prisma } from '$lib/server/db'; + +export const GET: RequestHandler = async ({ url, setHeaders }) => { + // 静的ページ + const staticPages = [ + '', + 'problems', + 'workbooks', + 'about', + 'privacy', + 'terms' + ]; + + // 動的ページ(問題詳細) + const problems = await prisma.task.findMany({ + select: { + task_id: true, + updated_at: true + }, + where: { + atcoder_problems_difficulty: { not: +``` diff --git a/.github/instructions/source-code.instructions.md b/.github/instructions/source-code.instructions.md new file mode 100644 index 000000000..04d3b7d04 --- /dev/null +++ b/.github/instructions/source-code.instructions.md @@ -0,0 +1,248 @@ +# ソースコード構成ガイド + +## 概要 + +AtCoder-NoviStepsプロジェクトのソースコード構造とアーキテクチャ原則 + +## ディレクトリ構成 + +``` +src/ +├─ routes/ # SvelteKit ページルーティング +│ ├─ +layout.svelte # 全体レイアウト +│ ├─ +page.svelte # トップページ +│ ├─ problems/ # 問題関連ページ +│ ├─ contests/ # コンテスト関連ページ +│ ├─ users/ # ユーザー関連ページ +│ └─ api/ # API エンドポイント +├─ lib/ +│ ├─ components/ # 再利用UIコンポーネント +│ │ ├─ ui/ # 基本UIパーツ (Button, Input等) +│ │ ├─ problem/ # 問題表示コンポーネント +│ │ ├─ contest/ # コンテスト関連コンポーネント +│ │ └─ user/ # ユーザー関連コンポーネント +│ ├─ server/ # サーバーサイドロジック +│ │ ├─ auth/ # Lucia認証関連 +│ │ ├─ db/ # データベースアクセス +│ │ ├─ atcoder/ # AtCoder API連携 +│ │ └─ services/ # ビジネスロジック +│ ├─ stores/ # Svelte ストア +│ ├─ utils/ # 共通ユーティリティ +│ ├─ types/ # TypeScript型定義 +│ └─ config/ # 設定ファイル +├─ app.html # HTMLテンプレート +├─ app.d.ts # アプリ型拡張 +└─ hooks.server.ts # サーバーフック +``` + +## AtCoder特化アーキテクチャ + +### ドメインモデル + +- **Problem**: 問題データ (ID, タイトル, 難易度, URL等) +- **Contest**: コンテスト情報 (開催日, 問題リスト等) +- **User**: ユーザー情報 (AtCoder ID, レーティング等) +- **Submission**: 提出履歴 (問題ID, 結果, 提出日時等) + +### データフロー + +```mermaid +graph TD + A[AtCoder API] --> B[サービス層] + B --> C[Prisma Repository] + C --> D[PostgreSQL] + E[SvelteKit Routes] --> B + F[Svelte Components] --> E +``` + +## 命名規則 + +### ファイル・ディレクトリ + +- ページ: `kebab-case` (`problem-list.svelte`) +- コンポーネント: `PascalCase` (`ProblemCard.svelte`) +- ユーティリティ: `camelCase` (`formatDifficulty.ts`) + +### 変数・関数 + +- 変数: `camelCase` (`problemId`, `userRating`) +- 定数: `UPPER_SNAKE_CASE` (`ATCODER_BASE_URL`) +- 型: `PascalCase` (`Problem`, `ContestInfo`) +- インターフェース: `I` prefix (`IProblemRepository`) + +## コンポーネント設計 + +### Atoms (基本UI) + +```typescript +// lib/components/ui/Button.svelte +export interface ButtonProps { + variant?: 'primary' | 'secondary' | 'danger'; + size?: 'sm' | 'md' | 'lg'; + disabled?: boolean; +} +``` + +### Molecules (機能UI) + +```typescript +// lib/components/problem/ProblemCard.svelte +export interface ProblemCardProps { + problem: Problem; + showDifficulty?: boolean; + showTags?: boolean; +} +``` + +### Organisms (ページ要素) + +```typescript +// lib/components/problem/ProblemList.svelte +export interface ProblemListProps { + problems: Problem[]; + filters?: ProblemFilters; + pagination?: PaginationInfo; +} +``` + +## データアクセス層 + +### Repository パターン + +```typescript +// lib/server/repositories/ProblemRepository.ts +export interface IProblemRepository { + findById(id: string): Promise; + findByDifficulty(min: number, max: number): Promise; + search(query: string): Promise; +} +``` + +### Service層 + +```typescript +// lib/server/services/ProblemService.ts +export class ProblemService { + constructor( + private problemRepo: IProblemRepository, + private atcoderApi: IAtCoderApiClient, + ) {} + + async syncProblemsFromAtCoder(): Promise { + // AtCoder APIからデータ取得→DB更新 + } +} +``` + +## 状態管理 + +### Svelte Store活用 + +```typescript +// lib/stores/problemStore.ts +import { writable } from 'svelte/store'; + +export const selectedProblems = writable([]); +export const problemFilters = writable({ + difficulty: { min: 0, max: 4000 }, + tags: [], +}); +``` + +## API設計 + +### REST エンドポイント + +```typescript +// src/routes/api/problems/+server.ts +export async function GET({ url }) { + const difficulty = url.searchParams.get('difficulty'); + const tag = url.searchParams.get('tag'); + + const problems = await problemService.getProblems({ + difficulty: difficulty ? parseInt(difficulty) : undefined, + tag, + }); + + return json(problems); +} +``` + +## エラーハンドリング + +### カスタムエラー + +```typescript +// lib/types/errors.ts +export class AtCoderApiError extends Error { + constructor( + message: string, + public statusCode: number, + ) { + super(message); + this.name = 'AtCoderApiError'; + } +} +``` + +### エラー境界 + +```typescript +// src/routes/+error.svelte + + +{#if error.message.includes('AtCoder')} +

AtCoder APIに接続できませんでした。しばらく待ってから再試行してください。

+{:else} +

予期しないエラーが発生しました。

+{/if} +``` + +## パフォーマンス考慮 + +### レイジーローディング + +```typescript +// 重いコンポーネントの遅延読み込み +import { onMount } from 'svelte'; + +let ProblemVisualization; +onMount(async () => { + ProblemVisualization = (await import('./ProblemVisualization.svelte')).default; +}); +``` + +### データキャッシュ + +```typescript +// lib/server/cache/problemCache.ts +const CACHE_TTL = 60 * 60 * 1000; // 1時間 + +export class ProblemCache { + private cache = new Map(); + + async get(key: string): Promise { + const cached = this.cache.get(key); + if (cached && Date.now() - cached.timestamp < CACHE_TTL) { + return cached.data; + } + return null; + } +} +``` + +## AtCoder連携特記事項 + +### API制限対応 + +- レート制限: 1秒間に10リクエスト以下 +- キャッシュ必須: 問題データは1日1回更新で十分 +- 失敗時リトライ: 指数バックオフ + +### データ同期 + +- 増分更新: 新規問題のみ取得 +- バッチ処理: 定期的な全データ同期 +- 整合性チェック: データ欠損検出機能 diff --git a/.github/instructions/tests.instructions.md b/.github/instructions/tests.instructions.md new file mode 100644 index 000000000..f2a7e8558 --- /dev/null +++ b/.github/instructions/tests.instructions.md @@ -0,0 +1,380 @@ +# テスト戦略ガイド + +## 概要 + +AtCoder-NoviStepsプロジェクトの包括的テスト戦略とベストプラクティス + +## テスト構成 + +| テスト種別 | ツール | 目的 | 実行頻度 | +| ---------- | ----------- | -------------------- | ---------------- | +| 単体テスト | Vitest | 関数・コンポーネント | 常時 (commit/PR) | +| 統合テスト | Vitest + DB | サービス層・DB連携 | PR | +| E2Eテスト | Playwright | ユーザーシナリオ | main ブランチ | +| HTTPモック | Nock | 外部API安定化 | 全層 | + +## ディレクトリ構成 + +``` +tests/ +├─ unit/ # 単体テスト +│ ├─ utils/ # ユーティリティ関数 +│ ├─ components/ # Svelteコンポーネント +│ ├─ services/ # ビジネスロジック +│ └─ repositories/ # データアクセス層 +├─ integration/ # 統合テスト +│ ├─ api/ # APIエンドポイント +│ ├─ auth/ # 認証フロー +│ └─ atcoder/ # AtCoder連携 +├─ e2e/ # E2Eテスト +│ ├─ problem/ # 問題関連シナリオ +│ ├─ contest/ # コンテスト関連 +│ ├─ user/ # ユーザー機能 +│ └─ auth/ # 認証フロー +├─ fixtures/ # テストデータ +│ ├─ problems.json # 問題サンプルデータ +│ ├─ contests.json # コンテストデータ +│ └─ users.json # ユーザーデータ +├─ mocks/ +│ ├─ atcoder-api.ts # AtCoder API モック +│ └─ database.ts # DB モック +└─ utils/ + ├─ setup.ts # テスト環境セットアップ + ├─ factories.ts # テストデータファクトリ (@quramy/prisma-fabbrica) + └─ helpers.ts # テストヘルパー関数 +``` + +## 単体テスト(Vitest) + +> **バージョン情報**: Vitest v4.0.7 以上使用 +> +> **重要**: v3 → v4 への移行時は、coverage 設定に注意。詳細は `docs/dev-notes/2025-11-05/bump-vitest-from-v3.x-to-v4.x/plan.md` を参照 + +### 設定例 + +```typescript +// vite.config.ts +/// +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [sveltekit()], + test: { + include: ['src/**/*.{test,spec}.{js,ts}'], + environment: 'jsdom', + globals: true, + coverage: { + provider: 'v8', + // v4.0.7 以上: 明示的に include を指定(未読込ファイルは集計対象外) + // ⚠️ v3 の coverage.all, coverage.extensions は v4 で削除 + include: ['src/**/*.{ts,tsx,svelte}'], + exclude: ['node_modules/**', '.svelte-kit/**', 'build/**', 'dist/**'], + reporter: ['text', 'html', 'lcov'], + thresholds: { + lines: 80, + branches: 70, + functions: 80, + statements: 80, + }, + }, + }, +}); +``` + +### Svelteコンポーネントテスト + +```typescript +// tests/unit/components/ProblemCard.test.ts +import { render, screen } from '@testing-library/svelte'; +import { expect, test } from 'vitest'; +import ProblemCard from '$lib/components/problem/ProblemCard.svelte'; + +test('displays problem information correctly', () => { + const problem = { + id: 'abc001_a', + title: 'はじめてのあっとこーだー', + difficulty: 100, + contestId: 'abc001', + }; + + render(ProblemCard, { props: { problem } }); + + expect(screen.getByText('はじめてのあっとこーだー')).toBeInTheDocument(); + expect(screen.getByText('100')).toBeInTheDocument(); +}); +``` + +### サービス層テスト + +> **Vitest v4 注意**: `vi.restoreAllMocks()` は spy のみ復元します。モック全体をリセットする場合は、`vi.clearAllMocks()` / `vi.resetAllMocks()` / `.mockReset()` を明示的に使用してください。 + +```typescript +// tests/unit/services/ProblemService.test.ts +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { ProblemService } from '$lib/server/services/ProblemService'; + +describe('ProblemService', () => { + afterEach(() => { + // v4 以上: 明示的にモックをリセット + vi.clearAllMocks(); + }); + + it('should fetch problems by difficulty', async () => { + const mockRepo = { + findByDifficulty: vi.fn().mockResolvedValue([{ id: 'abc001_a', difficulty: 100 }]), + }; + + const service = new ProblemService(mockRepo); + const result = await service.getProblemsByDifficulty(0, 200); + + expect(result).toHaveLength(1); + expect(mockRepo.findByDifficulty).toHaveBeenCalledWith(0, 200); + }); +}); +``` + +## 統合テスト + +### データベーステスト + +```typescript +// tests/integration/repositories/ProblemRepository.test.ts +import { describe, it, beforeEach, afterEach } from 'vitest'; +import { PrismaClient } from '@prisma/client'; +import { ProblemRepository } from '$lib/server/repositories/ProblemRepository'; + +describe('ProblemRepository Integration', () => { + let prisma: PrismaClient; + let repository: ProblemRepository; + + beforeEach(async () => { + prisma = new PrismaClient({ + datasources: { db: { url: 'postgresql://test:test@localhost:5433/test' } }, + }); + await prisma.$connect(); + repository = new ProblemRepository(prisma); + }); + + afterEach(async () => { + await prisma.problem.deleteMany(); + await prisma.$disconnect(); + }); + + it('should create and find problem', async () => { + const problem = await repository.create({ + id: 'abc001_a', + title: 'Test Problem', + difficulty: 100, + }); + + const found = await repository.findById('abc001_a'); + expect(found?.title).toBe('Test Problem'); + }); +}); +``` + +## E2Eテスト(Playwright) + +### 設定例 + +```typescript +// playwright.config.ts +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests/e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: process.env.CI ? 'list' : 'html', + use: { + baseURL: 'http://localhost:4173', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + projects: [ + { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, + { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, + { name: 'webkit', use: { ...devices['Desktop Safari'] } }, + ], + webServer: { + command: 'pnpm build && pnpm preview', + port: 4173, + }, +}); +``` + +### シナリオテスト例 + +```typescript +// tests/e2e/problem/problem-search.spec.ts +import { test, expect } from '@playwright/test'; + +test.describe('Problem Search', () => { + test('should filter problems by difficulty', async ({ page }) => { + await page.goto('/problems'); + + // 難易度フィルター設定 + await page.fill('[data-testid="difficulty-min"]', '100'); + await page.fill('[data-testid="difficulty-max"]', '300'); + await page.click('[data-testid="filter-submit"]'); + + // 結果確認 + await expect(page.locator('[data-testid="problem-card"]')).toHaveCount(5); + + // 各問題の難易度が範囲内か確認 + const difficulties = await page.locator('[data-testid="problem-difficulty"]').allTextContents(); + for (const diff of difficulties) { + const value = parseInt(diff); + expect(value).toBeGreaterThanOrEqual(100); + expect(value).toBeLessThanOrEqual(300); + } + }); + + test('should navigate to problem detail', async ({ page }) => { + await page.goto('/problems'); + + await page.click('[data-testid="problem-card"]:first-child'); + await expect(page).toHaveURL(/\/problems\/[a-z0-9_]+/); + await expect(page.locator('h1')).toBeVisible(); + }); +}); +``` + +## モック・フィクスチャ + +### AtCoder API モック + +```typescript +// tests/mocks/atcoder-api.ts +import nock from 'nock'; + +export function mockAtCoderApi() { + nock('https://kenkoooo.com') + .get('/atcoder/atcoder-api/v3/problems') + .reply(200, [ + { + id: 'abc001_a', + title: 'はじめてのあっとこーだー', + contest_id: 'abc001', + }, + ]); +} + +// テストでの使用 +beforeEach(() => { + mockAtCoderApi(); +}); + +afterEach(() => { + nock.cleanAll(); +}); +``` + +### データファクトリ + +```typescript +// tests/utils/factories.ts +import { defineFactory } from '@quramy/prisma-fabbrica'; +import { faker } from '@faker-js/faker'; + +export const ProblemFactory = defineFactory({ + defaultData: () => ({ + id: faker.string.alphanumeric(10), + title: faker.lorem.words(3), + difficulty: faker.number.int({ min: 100, max: 3000 }), + contestId: faker.string.alphanumeric(6), + }), +}); + +// 使用例 +const problems = await ProblemFactory.createList(5); +``` + +## カバレッジ目標 + +> **Vitest v4.0.7 以上**: coverage 設定で `include` / `exclude` を明示指定してください。v3 の `coverage.all` / `coverage.extensions` は削除されました。 +> +> **ローカル開発**: `pnpm coverage` でエラーが出た場合は、`vite.config.ts` の coverage 設定を確認してください。 + +### 目標値 + +- **Lines**: 80% +- **Branches**: 70% +- **Functions**: 80% +- **Statements**: 80% + +### 重点テスト領域 + +1. **AtCoder API連携**: 外部依存の安定化 +2. **認証フロー**: セキュリティ重要機能 +3. **データ変換**: 問題データの正規化ロジック +4. **検索・フィルタ**: コア機能 + +## CI/CD統合 + +### GitHub Actionsでの実行 + +```yaml +# .github/workflows/test.yml +- name: Run unit tests + run: pnpm test:unit --coverage + +- name: Run integration tests + run: pnpm test:integration + env: + DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }} + +- name: Run E2E tests + run: pnpm test:e2e + +- name: Upload coverage + uses: codecov/codecov-action@v3 + with: + file: ./coverage/lcov.info +``` + +## パフォーマンステスト + +### ロードテスト例 + +```typescript +// tests/performance/problem-api.spec.ts +import { test } from '@playwright/test'; + +test('API should handle concurrent requests', async ({ page }) => { + const promises = Array.from({ length: 10 }, () => + page.request.get('/api/problems?difficulty=100'), + ); + + const responses = await Promise.all(promises); + + for (const response of responses) { + expect(response.status()).toBe(200); + } +}); +``` + +## デバッグ・トラブルシューティング + +### 一般的な問題 + +1. **Svelteコンポーネントレンダリング失敗**: jsdom環境設定確認 +2. **DB接続エラー**: テスト用DB起動確認 +3. **APIタイムアウト**: Nockモック設定確認 +4. **Playwright起動失敗**: ブラウザインストール確認 + +### デバッグコマンド + +```bash +# UIモードでVitest実行 +pnpm vitest --ui + +# PlaywrightのUIモード +pnpm playwright test --ui + +# カバレッジ詳細表示 +pnpm vitest --coverage --reporter=verbose +``` diff --git a/.github/instructions/ui-components.instructions.md b/.github/instructions/ui-components.instructions.md new file mode 100644 index 000000000..2945260ef --- /dev/null +++ b/.github/instructions/ui-components.instructions.md @@ -0,0 +1,634 @@ +# UI・コンポーネントガイド + +## 概要 + +AtCoder-NoviStepsのフロントエンド設計とSvelte 5 + Flowbite UIを活用したコンポーネント開発 + +## 技術スタック詳細 + +| 技術 | バージョン | 用途 | +| --------------- | ---------- | ------------------------------- | +| Svelte | 5.38.2 | フレームワーク(最新Runes API) | +| SvelteKit | 2.36.1 | フルスタックフレームワーク | +| svelte-5-ui-lib | 0.12.2 | UIコンポーネントライブラリ | +| Flowbite | 2.5.2 | デザインシステム | +| Tailwind CSS | 3.4.17 | CSSフレームワーク | +| tailwind-merge | 2.6.0 | クラス結合ユーティリティ | +| Lucide Svelte | 0.541.0 | アイコンライブラリ | + +## Svelte 5 Runes API活用 + +### 基本的なReactivity + +```typescript +// lib/components/problem/ProblemCard.svelte + + +
+
+

+ {problem.title} +

+ + {#if showDifficulty && problem.atcoder_problems_difficulty !== 'PENDING'} + + {problem.atcoder_problems_difficulty} + + {/if} +
+ +
+ {problem.task_id} + + {problem.contest_id.toUpperCase()} +
+ + {#if isAtCoderProblem} + + AtCoderで開く → + + {/if} +
+``` + +### Store with Runes + +```typescript +// lib/stores/problemStore.svelte.ts +import type { Problem, ProblemFilters } from '$lib/types/problem'; + +class ProblemStore { + private _problems = $state([]); + private _filters = $state({ + difficulty: { min: 0, max: 4000 }, + contestTypes: [], + searchQuery: '', + }); + + get problems() { + return this._problems; + } + + get filters() { + return this._filters; + } + + get filteredProblems() { + return $derived(() => { + return this._problems.filter((problem) => { + // 難易度フィルタ + const difficulty = problem.atcoder_problems_difficulty_value || 0; + if ( + difficulty < this._filters.difficulty.min || + difficulty > this._filters.difficulty.max + ) { + return false; + } + + // コンテストタイプフィルタ + if ( + this._filters.contestTypes.length > 0 && + !this._filters.contestTypes.includes(problem.contest_type) + ) { + return false; + } + + // 検索クエリフィルタ + if (this._filters.searchQuery) { + const query = this._filters.searchQuery.toLowerCase(); + return ( + problem.title.toLowerCase().includes(query) || + problem.task_id.toLowerCase().includes(query) + ); + } + + return true; + }); + }); + } + + setProblems(problems: Problem[]) { + this._problems = problems; + } + + updateFilters(newFilters: Partial) { + this._filters = { ...this._filters, ...newFilters }; + } + + resetFilters() { + this._filters = { + difficulty: { min: 0, max: 4000 }, + contestTypes: [], + searchQuery: '', + }; + } +} + +export const problemStore = new ProblemStore(); +``` + +## Flowbite UI統合 + +### コンポーネントベースの設計 + +```typescript +// lib/components/ui/Button.svelte + + + + {#if loading} + + + + + 処理中... + {:else} + {@render children?.()} + {/if} + +``` + +### フォームコンポーネント(Superforms統合) + +```typescript +// lib/components/forms/ProblemFilterForm.svelte + + +
+
+ +
+ + + {#if $errors.searchQuery} +

{$errors.searchQuery}

+ {/if} +
+ + +
+ +
+ + +
+
+ + +
+ + - {#if $errors.searchQuery} -

{$errors.searchQuery}

- {/if} -
- - -
- -
- - -
-
- - -
- -