Conversation
- Phase 0: API layer, CategorySelector, converter utility実装 - Phase 1: Dashboard統合、動的ノード表示 - 自動生成機能: GET→POST fallback実装 - レイアウト最適化: Y座標調整(BASE_Y=-400)、密度ベース配置 - パフォーマンス改善: Canvas SSR無効化、デバッグログ削減 - UI強化: 葉っぱ・枝の滑らかな描画、グリッド背景 - 認証機能統合: develop mergeで取り込み (#59) - ADR追加: 014-coordinate-auto-layout, 015-auto-generation Addresses: #74
4f05d2c to
03b2885
Compare
There was a problem hiding this comment.
Pull request overview
スキルツリー機能について、フロントエンドからバックエンドAPIを呼び出して取得/初回生成し、Canvas描画を動的ノードに対応させる統合作業を行うPRです(Issue #74 / ADR-014,015に沿った実装)。
Changes:
- フロントエンドにスキルツリーAPI統合レイヤーと、カテゴリ切替UI/ローディング・エラー表示を追加
- APIノードをCanvas用ノードへ変換する自動レイアウト(tier計算+配置)ユーティリティを追加し、Canvas側を動的ノード対応
- バックエンド側でスキルツリー生成の開発用フラグ導入、seedスクリプトのモデル登録改善、プロンプト要件調整、ADR追加
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/src/lib/api/skillTree.ts | スキルツリーGET/POSTの統合レイヤー(空配列時の自動生成含む)を追加 |
| frontend/src/features/dashboard/components/CategorySelector.tsx | 6カテゴリの選択UIを追加 |
| frontend/src/features/dashboard/components/DashboardContainer.tsx | カテゴリ状態+API取得+Canvasへ動的ノード注入に対応 |
| frontend/src/features/skill-tree/utils/converter.ts | APIノード→Canvasノードへの変換+自動レイアウト生成を追加 |
| frontend/src/features/skill-tree/components/SkillTreeCanvas.tsx | 描画データを props で差し替え可能にし、枝/葉の描画ロジックを更新 |
| frontend/src/features/skill-tree/components/LoadingSpinner.tsx | ローディングUIを追加 |
| frontend/src/features/skill-tree/components/ErrorMessage.tsx | エラー表示UIを追加 |
| frontend/test-converter.js | 変換ロジックの手動テスト用スクリプトを追加 |
| backend/app/core/config.py | スキルツリー生成の開発用フラグを追加 |
| backend/app/services/skill_tree_service.py | 開発モード時にLLMをスキップしてベースライン返却する分岐を追加 |
| backend/app/core/prompts.py | 生成要件(ベースライン全ノード維持等)を更新 |
| backend/scripts/seed_test_data.py | モデル登録のための base import を追加 |
| .gitignore | frontend/src/lib を誤って無視しないよう例外ルール追加 |
| .github/decisions/014-.md / 015-.md | レイアウト自動生成・初回自動生成のADRを追加 |
Comments suppressed due to low confidence (2)
frontend/src/features/dashboard/components/DashboardContainer.tsx:140
- 上部で
if (loading) return .../if (error || !userStatus) return ...と早期 return しているため、Skill Tree セクション内の{loading ? <LoadingSpinner/> : error ? <ErrorMessage/> : ...}分岐が実質到達不能になっています。ローディング/エラーUIを一箇所に統一し、カテゴリ切替時にページ全体が消えないよう skill-tree 用の loading state を分離する等に整理してください。
if (loading) {
return (
<div className="flex min-h-screen items-center justify-center bg-transparent">
<div className="text-center">
<div className="mb-4 inline-flex h-12 w-12 animate-spin rounded-full border-4 border-gray-300 border-t-[#559C71]" />
<p className="text-[#559C71] tracking-widest">LOADING...</p>
</div>
</div>
);
}
if (error || !userStatus) {
return (
<div className="flex min-h-screen items-center justify-center bg-transparent">
<div className="rounded-none border-2 border-red-500 bg-white p-8 text-center shadow-[4px_4px_0px_0px_rgba(239,68,68,1)]">
<p className="text-red-500">{error || "Error loading dashboard"}</p>
</div>
</div>
);
}
frontend/src/features/dashboard/components/DashboardContainer.tsx:70
mountedstate をsetMounted(true)していますが、このファイル内で参照されていないため不要な状態更新になっています。SSR回避の目的ならmountedを実際に分岐に使うか、動的 import(ssr:false)に寄せて state 自体を削除してください。
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
| 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: "🎨" }, | ||
| ]; |
There was a problem hiding this comment.
カテゴリIDに infrastructure / game を含めていますが、現在のスキルツリー表示側のカテゴリキー(色・ラベル等)は infra など別名になっているため、選択カテゴリによっては色/ラベル参照が一致せず表示が崩れます。フロント側カテゴリ定義をバックエンドと同一の6種に統一するか、選択値を内部キーに変換する処理を追加してください。
| // ユーザー基本情報とスキルツリーを並行取得 | ||
| const [statusData, treeData] = await Promise.all([ | ||
| fetchUserDashboard(userId), // バッジ、ランク等(既存のmock API) | ||
| fetchSkillTree(numericUserId, category), // スキルツリー(バックエンドAPI) | ||
| ]); | ||
|
|
||
| setUserStatus(statusData); | ||
|
|
There was a problem hiding this comment.
fetchUserDashboard の結果を userStatus に保存していますが、このコンポーネント内で userStatus を表示・子コンポーネントに渡しておらず、!userStatus ガード用途にしか見えません。不要ならフェッチ/状態を削除し、必要なら利用箇所(例: バッジ表示)へ明示的に props で渡す形にしてください。
frontend/src/lib/api/skillTree.ts
Outdated
| category: string, | ||
| ): Promise<SkillTreeData> { | ||
| const baseUrl = getApiBaseUrl(); | ||
| const url = `${baseUrl}/api/v1/users/${userId}/skill-trees?category=${category}`; |
There was a problem hiding this comment.
GET URL の category=${category} が URL エンコードされていないため、将来カテゴリ値に特殊文字が入った場合にクエリが壊れます。encodeURIComponent(category) を使って組み立ててください。
| const url = `${baseUrl}/api/v1/users/${userId}/skill-trees?category=${category}`; | |
| const url = `${baseUrl}/api/v1/users/${userId}/skill-trees?category=${encodeURIComponent( | |
| category, | |
| )}`; |
|
|
||
| # スキルツリー生成設定 | ||
| SKIP_LLM_FOR_SKILL_TREE: bool = ( | ||
| True # True: ベースラインJSONを直接返す(開発用), False: LLMを使用 |
There was a problem hiding this comment.
SKIP_LLM_FOR_SKILL_TREE のデフォルトが True になっているため、環境変数未設定の環境(本番/CI含む)で常に LLM をスキップしてベースライン固定になります。開発用フラグならデフォルトは False にして .env(または dev 用設定)でのみ True にするのが安全です。
| True # True: ベースラインJSONを直接返す(開発用), False: LLMを使用 | |
| False # True: ベースラインJSONを直接返す(開発用), False: LLMを使用 |
| canvasNodes.push({ | ||
| id: apiNode.id, | ||
| label: apiNode.name, | ||
| category: category as SkillCategory, | ||
| status, | ||
| x: xPos, | ||
| y: yPos, | ||
| tier, |
There was a problem hiding this comment.
category を category as SkillCategory で強制キャストしていますが、現状の SkillCategory は infra で infrastructure/game を含まないため、CategorySelector で選択したカテゴリによっては Canvas 側の CAT[category] 参照が undefined になり描画/スタイルが崩れます。フロントの SkillCategory をバックエンドの enum(web/ai/security/infrastructure/game/design)に揃えるか、ここで APIカテゴリ→UIカテゴリの明示的マッピング+バリデーションを入れてください。
| <label className="block text-sm font-medium text-[#2C5F2D] mb-2"> | ||
| カテゴリを選択 | ||
| </label> | ||
| <select |
There was a problem hiding this comment.
label と select が関連付いていないため、スクリーンリーダー利用時にフォーム要素の説明が紐づきません。select に id を付け、label 側に htmlFor を設定してください。
| <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" |
| {/* Skill Tree Section */} | ||
| <section className="relative z-0"> | ||
| <div className="relative w-full h-[600px] overflow-hidden border-4 border-[#2C5F2D] bg-[#0a0f08] shadow-[8px_8px_0_0_#2C5F2D]"> | ||
| <div className="relative w-full h-150 overflow-hidden border-4 border-[#2C5F2D] bg-[#0a0f08] shadow-[8px_8px_0_0_#2C5F2D]"> |
There was a problem hiding this comment.
className="... h-150 ..." は Tailwind のデフォルトスケールに存在せず(設定が無い場合)高さ指定が効かない可能性があります。以前の h-[600px] のような任意値クラスに戻すか、プロジェクトの theme に合わせた有効な高さクラスにしてください。
| <div className="relative w-full h-150 overflow-hidden border-4 border-[#2C5F2D] bg-[#0a0f08] shadow-[8px_8px_0_0_#2C5F2D]"> | |
| <div className="relative w-full h-[600px] overflow-hidden border-4 border-[#2C5F2D] bg-[#0a0f08] shadow-[8px_8px_0_0_#2C5F2D]"> |
| // 滑らかな枝を描画(ベジェ曲線風) | ||
| for (const nd of activeNodes) { | ||
| for (const cid of nd.children) { | ||
| const ch = getNodeById(cid); | ||
| if (!ch) continue; | ||
| const ch = activeNodes.find((n) => n.id === cid); | ||
| if (!ch) { | ||
| console.warn(`Child node not found: ${cid} for parent ${nd.id}`); | ||
| continue; | ||
| } |
There was a problem hiding this comment.
activeNodes.find(...) を枝描画ループ内で毎フレーム実行し、さらに見つからない場合に console.warn を毎フレーム出力しているため、データ不整合時にログスパム+描画パフォーマンス劣化が発生します。id -> node のMapを useMemo で一度作り参照し、欠損childrenの警告は初回のみ(もしくは描画前のバリデーション)で出すようにしてください。
frontend/src/lib/api/skillTree.ts
Outdated
| console.log(`Fetching skill tree: userId=${userId}, category=${category}`); | ||
|
|
||
| const response = await fetch(url, { | ||
| credentials: "include", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| cache: "no-store", // カテゴリ切り替え時のキャッシュ問題を防ぐ | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| const errorText = await response.text(); | ||
| throw new Error( | ||
| `スキルツリーの取得に失敗しました: ${response.status} ${errorText}`, | ||
| ); | ||
| } | ||
|
|
||
| const data = await response.json(); | ||
|
|
||
| // 空配列が返ってきた場合(初回アクセス)は自動生成 | ||
| if (Array.isArray(data) && data.length === 0) { | ||
| console.log( | ||
| `カテゴリ '${category}' のスキルツリーが存在しないため、生成します...`, | ||
| ); | ||
| return await generateSkillTree(userId, category); |
There was a problem hiding this comment.
fetchSkillTree 内の console.log は本番環境でユーザー操作のたびにログ出力されるためノイズになります。必要なら NODE_ENV !== 'production' 等でガードするか、アプリ側のロガーに寄せてデバッグ時のみ出力してください。
| // Test converter logic with actual API data | ||
|
|
||
| const testData = { | ||
| nodes: [ | ||
| { | ||
| id: "web_html_css", | ||
| name: "HTML/CSS基礎", | ||
| completed: false, | ||
| description: "セマンティックなマークアップと基本的なスタイリング", | ||
| prerequisites: [], | ||
| estimated_hours: 20, | ||
| }, | ||
| { | ||
| id: "web_http_basics", | ||
| name: "HTTPプロトコル基礎", | ||
| completed: false, | ||
| description: "リクエスト/レスポンス、メソッド、ステータスコードの理解", | ||
| prerequisites: [], | ||
| estimated_hours: 20, | ||
| }, | ||
| { | ||
| id: "web_js_basics", | ||
| name: "JavaScript基礎", | ||
| completed: false, | ||
| description: "変数、制御構文、基本的なDOM操作", | ||
| prerequisites: ["web_html_css"], | ||
| estimated_hours: 30, | ||
| }, | ||
| { | ||
| id: "web_css_fw", | ||
| name: "CSSフレームワーク", | ||
| completed: false, | ||
| description: "Tailwind CSSやBootstrap等を用いた効率的なスタイリング", | ||
| prerequisites: ["web_html_css"], | ||
| estimated_hours: 30, | ||
| }, | ||
| { | ||
| id: "web_a11y_basics", | ||
| name: "Webアクセシビリティ基礎", | ||
| completed: false, | ||
| description: "WAI-ARIAやコントラスト比など、基本的なアクセシビリティ対応", | ||
| prerequisites: ["web_html_css"], | ||
| estimated_hours: 30, | ||
| }, | ||
| ], | ||
| }; | ||
|
|
||
| // Converter logic (copied from converter.ts) | ||
| function convertNodes(apiNodes, category = "web") { | ||
| if (!apiNodes || apiNodes.length === 0) { | ||
| console.log("No nodes provided!"); | ||
| return []; |
There was a problem hiding this comment.
このファイルは converter.ts のロジックをコピペした手動検証用スクリプトで、今後の変更時に本実装と乖離しやすいです。リポジトリに残すなら frontend/scripts/ 等へ移し、実装を import して動かす(もしくはテスト基盤を導入して自動テスト化する)形にして重複を避けてください。
- フロントエンド: スキルツリーAPI統合、認証API、ログインページ - バックエンド: 認証必須化、GitHub OAuth統合、開発モード - テスト: settings.SKIP_LLM_FOR_SKILL_TREE モックパス修正 - 開発環境: テストデータ修正、環境変数追加 認証機能と統合することで、ユーザーごとのパーソナライズされた スキルツリーを実装。GitHub OAuthでログイン後、実際のリポジトリを 分析したスキルツリーが表示される。 Closes #74
34e9dd2 to
4ad7153
Compare
Issue: #74
実装の概要
スキルツリーのフロントエンド・バックエンドAPI統合を実装しました。
Phase 0(共通基盤):
frontend/src/lib/api/skillTree.ts)CategorySelector.tsx)Phase 1(Dashboard統合):
バックエンド修正:
変更ファイル:
更新: 9ファイル
フロントエンド (4ファイル)
3. skillTree.ts - 認証対応(userId削除、/users/me使用)
4. DashboardContainer.tsx - 認証済みユーザーAPI統合
5. page.tsx - ログインリンク追加
6. SkillTreeCanvas.tsx - Lint修正(未使用変数削除)
バックエンド (5ファイル)
7. analyze.py - 認証必須化(current_user依存)、/dashboardリダイレクト
8. analyze.py - SkillTreeRequestからuser_id削除
9. test_skill_tree_service.py - settings.SKIP_LLM_FOR_SKILL_TREE モックパス修正
10. seed_test_data.py - パスワードハッシュ化対応
11. .env - 認証・開発モード環境変数追加
🔧 技術的な意思決定とトレードオフ (最重要)
採用したアプローチ
1. 座標の自動レイアウト生成 (ADR-014参照)
prerequisitesの深さに基づいてtier(階層)を計算し、X/Y座標を自動生成2. 初回アクセス時の自動生成 (ADR-015参照)
却下したアプローチ(代替案)
1. 静的レイアウトJSON作成
2. バックエンドで初回アクセス時に自動生成
3. データベースシーディング時に全カテゴリを事前生成
詳細は以下のADRを参照:
.github/decisions/014-skill-tree-coordinate-auto-layout.md.github/decisions/015-skill-tree-auto-generation-on-first-access.md🧪 テスト戦略と範囲
追加したテストケース
npm run build成功)seed_test_data.py成功)テストしていないこと
📸 動作確認証拠
前提条件
API動作確認(cURLコマンド)
フロントエンド動作確認
http://localhost:3000/dashboard) でカテゴリ選択UIが表示される修正前

修正後





Githubを使っての検証

セキュリティに関する自己評価
レビュワー(人間)への申し送り事項
.github/decisions/014-*.mdと.github/decisions/015-*.mdに記載しています/skillsページの統合は別PRで対応予定(ユーザーの判断により今回はスコープ外)Closes #74