-
Notifications
You must be signed in to change notification settings - Fork 0
feat: #74 スキルツリーのバックエンドAPI統合 #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| # 014 スキルツリーの座標自動レイアウト生成 | ||
|
|
||
| ## ステータス | ||
|
|
||
| - [x] **決定** | ||
|
|
||
| ## コンテキスト (課題と背景) | ||
|
|
||
| Issue #74でスキルツリーのフロントエンド・バックエンドAPI統合を行う際、バックエンドから返されるノードデータをCanvas上にどう配置するかを決定する必要があった。 | ||
|
|
||
| ### 背景 | ||
| - バックエンドのJSONには`prerequisites`(前提スキル)の依存関係のみが存在し、X/Y座標情報は存在しない | ||
| - 6カテゴリ(web/ai/security/infrastructure/game/design)それぞれに対応する必要がある | ||
| - 将来的にカテゴリやスキルノード数が増減する可能性がある | ||
| - ハッカソン期間のため、実装時間を最小化したい | ||
|
|
||
| ## 決定 (Decision) | ||
|
|
||
| **APIデータから`prerequisites`の深さに基づいてtier(階層)を計算し、X/Y座標を自動生成する方式を採用** | ||
|
|
||
| ### 実装方針 | ||
| - 前提スキルを持たないノードを tier 0 とする | ||
| - 各ノードのtierは、その前提スキルの最大tier + 1 で計算 | ||
| - Y座標: `tier * 120 + 60` (tier間距離120px) | ||
| - X座標: 同一tier内のノードを等間隔に配置 (横幅800pxを基準に分散) | ||
|
|
||
| ## 代替案との比較 (Options) | ||
|
|
||
| ### 1. 静的レイアウトJSONを作成(カテゴリ別) | ||
|
|
||
| - **Good**: | ||
| - デザイナーが最適な配置を手動で決定できる | ||
| - 視覚的に美しいレイアウトが実現可能 | ||
| - **Bad**: | ||
| - 各カテゴリごとに座標データを手動作成する必要がある(6カテゴリ × 平均20ノード = 120座標) | ||
| - カテゴリ追加やノード変更のたびに座標データのメンテナンスが必要 | ||
| - 実装時間が長い(推定4-6時間) | ||
| - **却下理由**: ハッカソン期間の時間制約に合わない | ||
|
|
||
| ### 2. 全カテゴリ共通の静的座標プリセット | ||
|
|
||
| - **Good**: | ||
| - 実装が比較的簡単 | ||
| - 全カテゴリで一貫したレイアウト | ||
| - **Bad**: | ||
| - カテゴリごとにノード数が異なるため、スペースの無駄や重なりが発生 | ||
| - スケーラビリティがない(10ノードと30ノードで同じレイアウトは不適切) | ||
| - **却下理由**: 柔軟性に欠け、ユーザー体験を損なう可能性 | ||
|
|
||
| ### 3. 力学モデル(Force-Directed Graph) | ||
|
|
||
| - **Good**: | ||
| - グラフ理論に基づく自然な配置 | ||
| - ライブラリ(D3.js等)が存在する | ||
| - **Bad**: | ||
| - ピクセルアート風デザインと相性が悪い(動的なアニメーションが発生) | ||
| - 初期表示でノードが動くためUXが低下 | ||
| - 実装時間が長い | ||
| - **却下理由**: プロジェクトのデザイン方針に合わない | ||
|
|
||
| ## 結果 (Consequences) | ||
|
|
||
| ### Positive | ||
|
|
||
| - ✅ カテゴリ追加時に座標データを手動作成する必要がない | ||
| - ✅ バックエンドJSONに座標情報を持たせないため、データ構造がシンプル | ||
| - ✅ prerequisites依存関係のみでレイアウトが決定されるため、保守性が高い | ||
| - ✅ 実装時間が短い(約1時間) | ||
|
|
||
| ### Negative | ||
|
|
||
| - ❌ ノード配置が機械的で、デザイン性に欠ける可能性 | ||
| - ❌ 複雑な依存関係グラフの場合、重なりが発生する可能性 | ||
| - ❌ 同一tier内のノード数が多い場合、横に広がりすぎる(現状は考慮していない) | ||
|
|
||
| ### 将来の改善案 | ||
|
|
||
| - デザイナーによるレイアウト最適化が必要になった場合、座標オーバーライド機能を追加可能 | ||
| - 横幅の自動調整ロジック追加(tier内ノード数に応じて最大幅を制限) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| # 015 初回アクセス時のスキルツリー自動生成 | ||
|
|
||
| ## ステータス | ||
|
|
||
| - [x] **決定** | ||
|
|
||
| ## コンテキスト (課題と背景) | ||
|
|
||
| Issue #74でスキルツリーのフロントエンド・バックエンドAPI統合を行う際、ユーザーが初めて特定のカテゴリのスキルツリーにアクセスした場合の挙動を決定する必要があった。 | ||
|
|
||
| ### 背景 | ||
| - バックエンドの`GET /api/v1/users/{id}/skill-trees?category={cat}`は、データベースに該当データがない場合は空配列`[]`を返す | ||
| - スキルツリーの生成には`POST /api/v1/analyze/skill-tree`を呼び出す必要がある | ||
| - LLM API呼び出しが含まれるため、生成には最大30秒かかる可能性がある | ||
| - ユーザーに生成ボタンを押させる手間を減らしたい | ||
|
|
||
| ## 決定 (Decision) | ||
|
|
||
| **GETで空配列が返った場合、フロントエンド側で自動的にPOSTエンドポイントを呼び出し、スキルツリーを生成する** | ||
|
|
||
| ### 実装方針 | ||
| ```typescript | ||
| export async function fetchSkillTree( | ||
| userId: number, | ||
| category: string | ||
| ): Promise<SkillTreeData | null> { | ||
| const response = await fetch( | ||
| `${API_BASE_URL}/users/${userId}/skill-trees?category=${category}` | ||
| ); | ||
| const data = await response.json(); | ||
|
|
||
| // 空配列の場合は自動生成 | ||
| if (data.length === 0) { | ||
| return await generateSkillTree(userId, category); | ||
| } | ||
| return data[0]; | ||
| } | ||
| ``` | ||
|
|
||
| ## 代替案との比較 (Options) | ||
|
|
||
| ### 1. ユーザーに「生成」ボタンをクリックさせる | ||
|
|
||
| - **Good**: | ||
| - ユーザーが明示的に生成を開始するため、意図しないAPI料金消費が発生しない | ||
| - 生成が必要かどうかをユーザーが判断できる | ||
| - **Bad**: | ||
| - UXが悪い(空の画面→ボタンクリック→待機のステップが発生) | ||
| - 初回アクセス時に追加操作が必要になる | ||
| - **却下理由**: ハッカソンのデモとしてシームレスなUXを優先したい | ||
|
|
||
| ### 2. バックエンドで初回アクセス時に自動生成 | ||
|
|
||
| - **Good**: | ||
| - フロントエンドのロジックがシンプル | ||
| - バックエンドで生成タイミングを一元管理 | ||
| - **Bad**: | ||
| - GETエンドポイントが副作用(DB書き込み)を持つことになり、RESTful設計に反する | ||
| - 初回アクセスの場合、レスポンスが遅くなる(最大30秒)→タイムアウトリスク | ||
| - **却下理由**: RESTful設計の原則を守り、GET/POSTの責任分離を維持したい | ||
|
|
||
| ### 3. データベースシーディング時に全ユーザー・全カテゴリのスキルツリーを事前生成 | ||
|
|
||
| - **Good**: | ||
| - 初回アクセスが常に高速 | ||
| - フロントエンドの実装が単純 | ||
| - **Bad**: | ||
| - ユーザー登録時に6カテゴリ全てを生成するため、LLM API料金が6倍かかる | ||
| - ユーザーが使わないカテゴリも生成してしまう(無駄なコスト) | ||
| - **却下理由**: API料金の無駄遣いとスケーラビリティの問題 | ||
|
|
||
| ## 結果 (Consequences) | ||
|
|
||
| ### Positive | ||
|
|
||
| - ✅ ユーザーが手動で「生成」ボタンを押す必要がない | ||
| - ✅ シームレスなUX(初回アクセスでもスキルツリーが自動的に表示される) | ||
| - ✅ RESTful設計を維持(GET/POSTの責任分離) | ||
| - ✅ ユーザーがアクセスしたカテゴリのみ生成されるため、API料金を最小化 | ||
|
|
||
| ### Negative | ||
|
|
||
| - ❌ 初回ロードが遅くなる(LLM API呼び出しで最大30秒) | ||
| - ❌ ユーザーが意図せずAPI料金を消費する可能性がある(カテゴリ切り替え時に自動生成が走る) | ||
| - ❌ ローディング中のユーザー体験がやや低下(待機時間の発生) | ||
|
|
||
| ### 軽減策 | ||
|
|
||
| - **10分間のキャッシュ**: バックエンドで生成結果を10分間キャッシュすることで、同じカテゴリへの再アクセスは高速 | ||
| - **ローディングスピナー**: 待機状態を視覚的に明示し、ユーザーに進行状況を伝える | ||
| - **エラーハンドリング**: 生成失敗時は明確なエラーメッセージを表示し、再試行を促す | ||
|
|
||
| ### 将来の改善案 | ||
|
|
||
| - バックエンドで生成状況をWebSocket等でリアルタイムに通知(プログレスバー表示) | ||
| - ユーザー設定で「自動生成」のオン/オフを切り替え可能にする | ||
| - 生成処理をバックグラウンドジョブ化し、完了後に通知(非同期処理) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,45 @@ | ||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * CategorySelector Component | ||||||||||||||||||||||||||
| * スキルツリーのカテゴリ選択UI | ||||||||||||||||||||||||||
| * Dashboard と /skills ページで共用可能な設計 | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import React from "react"; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| export interface CategorySelectorProps { | ||||||||||||||||||||||||||
| currentCategory: string; | ||||||||||||||||||||||||||
| onCategoryChange: (category: string) => void; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const CATEGORIES = [ | ||||||||||||||||||||||||||
| { id: "web", name: "Web開発", icon: "🌐" }, | ||||||||||||||||||||||||||
| { id: "ai", name: "AI/ML", icon: "🤖" }, | ||||||||||||||||||||||||||
| { id: "security", name: "セキュリティ", icon: "🔒" }, | ||||||||||||||||||||||||||
| { id: "infrastructure", name: "インフラ", icon: "☁️" }, | ||||||||||||||||||||||||||
| { id: "game", name: "ゲーム", icon: "🎮" }, | ||||||||||||||||||||||||||
| { id: "design", name: "デザイン", icon: "🎨" }, | ||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||
|
Comment on lines
+14
to
+21
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| export const CategorySelector: React.FC<CategorySelectorProps> = ({ | ||||||||||||||||||||||||||
| currentCategory, | ||||||||||||||||||||||||||
| onCategoryChange, | ||||||||||||||||||||||||||
| }) => { | ||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||
| <div className="mb-4"> | ||||||||||||||||||||||||||
| <label className="block text-sm font-medium text-[#2C5F2D] mb-2"> | ||||||||||||||||||||||||||
| カテゴリを選択 | ||||||||||||||||||||||||||
| </label> | ||||||||||||||||||||||||||
| <select | ||||||||||||||||||||||||||
|
Comment on lines
+29
to
+32
|
||||||||||||||||||||||||||
| <label className="block text-sm font-medium text-[#2C5F2D] mb-2"> | |
| カテゴリを選択 | |
| </label> | |
| <select | |
| <label | |
| htmlFor="category-selector" | |
| className="block text-sm font-medium text-[#2C5F2D] mb-2" | |
| > | |
| カテゴリを選択 | |
| </label> | |
| <select | |
| id="category-selector" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SKIP_LLM_FOR_SKILL_TREEのデフォルトがTrueになっているため、環境変数未設定の環境(本番/CI含む)で常に LLM をスキップしてベースライン固定になります。開発用フラグならデフォルトはFalseにして.env(または dev 用設定)でのみTrueにするのが安全です。