Skip to content

feat: #74 スキルツリーのバックエンドAPI統合#76

Merged
Inlet-back merged 5 commits intodevelopfrom
74-skill-tree-integration
Feb 21, 2026
Merged

feat: #74 スキルツリーのバックエンドAPI統合#76
Inlet-back merged 5 commits intodevelopfrom
74-skill-tree-integration

Conversation

@Inlet-back
Copy link
Contributor

@Inlet-back Inlet-back commented Feb 21, 2026

Issue: #74

実装の概要

スキルツリーのフロントエンド・バックエンドAPI統合を実装しました。

Phase 0(共通基盤):

  • API統合レイヤー (frontend/src/lib/api/skillTree.ts)
  • カテゴリ選択UI (CategorySelector.tsx)
  • ローディング・エラーコンポーネント
  • SkillTreeCanvasの動的ノード対応
  • 座標自動計算ユーティリティ

Phase 1(Dashboard統合):

  • バックエンドAPIからのスキルツリー取得
  • カテゴリ切り替え機能(6カテゴリ対応)
  • エラーハンドリング

バックエンド修正:

  • SQLAlchemyモデルのインポート順問題解決
  • テストユーザー作成スクリプト修正

変更ファイル:

更新: 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座標を自動生成
  • メリット:
    • カテゴリ追加時に座標データの手動作成が不要
    • バックエンドJSONに座標情報を持たせないため、データ構造がシンプル
    • 実装時間が短い(約1時間)
  • デメリット/リスク:
    • ノード配置が機械的でデザイン性に欠ける可能性
    • 複雑な依存関係で重なりが発生する可能性
    • 同一tier内のノード数が多い場合、横に広がりすぎる

2. 初回アクセス時の自動生成 (ADR-015参照)

  • 手法: GETで空配列が返った場合、フロントエンド側で自動的にPOSTエンドポイントを呼び出し
  • メリット:
    • シームレスなUX(ボタンクリック不要)
    • RESTful設計を維持(GET/POSTの責任分離)
    • アクセスしたカテゴリのみ生成されるため、API料金を最小化
  • デメリット/リスク:
    • 初回ロードが最大30秒遅くなる(LLM API呼び出し)
    • ユーザーが意図せずAPI料金を消費する可能性
    • 軽減策: バックエンドで10分間のキャッシュ実装済み、ローディングスピナー表示

却下したアプローチ(代替案)

1. 静的レイアウトJSON作成

  • 手法: 各カテゴリごとに手動で座標データを作成
  • 却下理由: 実装時間が長い(推定4-6時間)、メンテナンスコストが高い、ハッカソン期間の時間制約に合わない

2. バックエンドで初回アクセス時に自動生成

  • 手法: GETエンドポイントで空の場合はその場で生成してから返す
  • 却下理由: GETが副作用(DB書き込み)を持つことになりRESTful設計に反する、タイムアウトリスク

3. データベースシーディング時に全カテゴリを事前生成

  • 手法: ユーザー登録時に全6カテゴリのスキルツリーを生成
  • 却下理由: API料金が6倍かかる、ユーザーが使わないカテゴリも生成してしまう無駄

詳細は以下の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で200 OK確認、20ノード返却)
  • 正常系: カテゴリ切り替え動作確認 (6カテゴリすべて動作)
  • 異常系: バックエンド未起動時のエラー表示確認

テストしていないこと

  • E2Eテスト: バックエンド起動が前提のため、ハッカソンスコープ外
  • Canvas描画の視覚的テスト: レイアウト崩れや重なりの自動検証は未実施(手動確認のみ)
  • LLM APIなしの場合の挙動: APIキー未設定時はベースラインJSONが返る想定だが未検証
  • パフォーマンステスト: 大量ノード(100件以上)での描画速度やメモリ使用量
  • 複数ユーザーの同時アクセス: 並行処理時のキャッシュ一貫性
  • モバイルデバイスでのタッチ操作: ピンチズームやタップのテストは未実施
  • ネットワークエラー時のリトライ処理: API呼び出し失敗時の再試行ロジックは未実装

📸 動作確認証拠

前提条件

# バックエンド起動
cd backend
poetry run uvicorn app.main:app --reload --host 0.0.0.0 --port 8000

# テストユーザー作成
poetry run python scripts/seed_test_data.py

API動作確認(cURLコマンド)

# スキルツリー生成(初回)
curl -X POST http://localhost:8000/api/v1/analyze/skill-tree \
  -H "Content-Type: application/json" \
  -d '{"user_id": 1, "category": "web"}' | jq '.category, .tree_data.nodes | length'

# 期待される出力:
# "web"
# 20

フロントエンド動作確認

  1. Dashboard画面 (http://localhost:3000/dashboard) でカテゴリ選択UIが表示される
  2. 6カテゴリ(web/ai/security/infrastructure/game/design)を切り替え可能
  3. 初回アクセス時に自動的にスキルツリーを生成(ローディング表示)
  4. ピクセルアート風のスキルツリーCanvasが正常に描画される
  5. ノードクリックで詳細パネルが表示される
  6. ズーム操作(マウスホイール/ピンチ)が正常に動作する

修正前
スクリーンショット 2026-02-21 14 35 27

修正後
スクリーンショット 2026-02-21 15 55 11
スクリーンショット 2026-02-21 15 55 04
スクリーンショット 2026-02-21 15 54 56
スクリーンショット 2026-02-21 15 54 47
スクリーンショット 2026-02-21 15 54 39

Githubを使っての検証
スクリーンショット 2026-02-21 17 07 34


セキュリティに関する自己評価

  • 機密情報のハードコードはないか → すべて環境変数で管理、コード内にAPIキーやトークンなし
  • 入力値の検証(バリデーション)は行っているか → API呼び出し時にuser_id、categoryを検証
  • 既知の脆弱性パターンへの対策は考慮したか → XSS対策(Reactの自動エスケープ)、CSRF対策(FastAPIのCORS設定)

レビュワー(人間)への申し送り事項

  1. ADRの確認: 技術的意思決定の詳細は .github/decisions/014-*.md.github/decisions/015-*.md に記載しています
  2. Phase 2は未実装: /skillsページの統合は別PRで対応予定(ユーザーの判断により今回はスコープ外)
  3. バックエンド起動が必須: フロントエンドのテストにはバックエンドが起動している必要があります
  4. 初回ロードが遅い: LLM API呼び出しで最大30秒かかるため、デモ時は事前に生成済みのカテゴリを使用推奨
  5. 将来の改善案: ノード配置の最適化、プログレスバー表示、バックグラウンド生成、オフラインフォールバック

Closes #74

- 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
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • mounted state を setMounted(true) していますが、このファイル内で参照されていないため不要な状態更新になっています。SSR回避の目的なら mounted を実際に分岐に使うか、動的 import(ssr:false)に寄せて state 自体を削除してください。
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

Comment on lines +14 to +21
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: "🎨" },
];
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

カテゴリIDに infrastructure / game を含めていますが、現在のスキルツリー表示側のカテゴリキー(色・ラベル等)は infra など別名になっているため、選択カテゴリによっては色/ラベル参照が一致せず表示が崩れます。フロント側カテゴリ定義をバックエンドと同一の6種に統一するか、選択値を内部キーに変換する処理を追加してください。

Copilot uses AI. Check for mistakes.
Comment on lines 85 to 92
// ユーザー基本情報とスキルツリーを並行取得
const [statusData, treeData] = await Promise.all([
fetchUserDashboard(userId), // バッジ、ランク等(既存のmock API)
fetchSkillTree(numericUserId, category), // スキルツリー(バックエンドAPI)
]);

setUserStatus(statusData);

Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchUserDashboard の結果を userStatus に保存していますが、このコンポーネント内で userStatus を表示・子コンポーネントに渡しておらず、!userStatus ガード用途にしか見えません。不要ならフェッチ/状態を削除し、必要なら利用箇所(例: バッジ表示)へ明示的に props で渡す形にしてください。

Copilot uses AI. Check for mistakes.
category: string,
): Promise<SkillTreeData> {
const baseUrl = getApiBaseUrl();
const url = `${baseUrl}/api/v1/users/${userId}/skill-trees?category=${category}`;
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GET URL の category=${category} が URL エンコードされていないため、将来カテゴリ値に特殊文字が入った場合にクエリが壊れます。encodeURIComponent(category) を使って組み立ててください。

Suggested change
const url = `${baseUrl}/api/v1/users/${userId}/skill-trees?category=${category}`;
const url = `${baseUrl}/api/v1/users/${userId}/skill-trees?category=${encodeURIComponent(
category,
)}`;

Copilot uses AI. Check for mistakes.

# スキルツリー生成設定
SKIP_LLM_FOR_SKILL_TREE: bool = (
True # True: ベースラインJSONを直接返す(開発用), False: LLMを使用
Copy link

Copilot AI Feb 21, 2026

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 にするのが安全です。

Suggested change
True # True: ベースラインJSONを直接返す(開発用), False: LLMを使用
False # True: ベースラインJSONを直接返す(開発用), False: LLMを使用

Copilot uses AI. Check for mistakes.
Comment on lines +153 to +160
canvasNodes.push({
id: apiNode.id,
label: apiNode.name,
category: category as SkillCategory,
status,
x: xPos,
y: yPos,
tier,
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

categorycategory as SkillCategory で強制キャストしていますが、現状の SkillCategoryinfrainfrastructure/game を含まないため、CategorySelector で選択したカテゴリによっては Canvas 側の CAT[category] 参照が undefined になり描画/スタイルが崩れます。フロントの SkillCategory をバックエンドの enum(web/ai/security/infrastructure/game/design)に揃えるか、ここで APIカテゴリ→UIカテゴリの明示的マッピング+バリデーションを入れてください。

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +32
<label className="block text-sm font-medium text-[#2C5F2D] mb-2">
カテゴリを選択
</label>
<select
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

labelselect が関連付いていないため、スクリーンリーダー利用時にフォーム要素の説明が紐づきません。selectid を付け、label 側に htmlFor を設定してください。

Suggested change
<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"

Copilot uses AI. Check for mistakes.
{/* 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]">
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

className="... h-150 ..." は Tailwind のデフォルトスケールに存在せず(設定が無い場合)高さ指定が効かない可能性があります。以前の h-[600px] のような任意値クラスに戻すか、プロジェクトの theme に合わせた有効な高さクラスにしてください。

Suggested change
<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]">

Copilot uses AI. Check for mistakes.
Comment on lines +301 to +308
// 滑らかな枝を描画(ベジェ曲線風)
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;
}
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

activeNodes.find(...) を枝描画ループ内で毎フレーム実行し、さらに見つからない場合に console.warn を毎フレーム出力しているため、データ不整合時にログスパム+描画パフォーマンス劣化が発生します。id -> node のMapを useMemo で一度作り参照し、欠損childrenの警告は初回のみ(もしくは描画前のバリデーション)で出すようにしてください。

Copilot uses AI. Check for mistakes.
Comment on lines 53 to 77
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);
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchSkillTree 内の console.log は本番環境でユーザー操作のたびにログ出力されるためノイズになります。必要なら NODE_ENV !== 'production' 等でガードするか、アプリ側のロガーに寄せてデバッグ時のみ出力してください。

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +52
// 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 [];
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このファイルは converter.ts のロジックをコピペした手動検証用スクリプトで、今後の変更時に本実装と乖離しやすいです。リポジトリに残すなら frontend/scripts/ 等へ移し、実装を import して動かす(もしくはテスト基盤を導入して自動テスト化する)形にして重複を避けてください。

Copilot uses AI. Check for mistakes.
- フロントエンド: スキルツリーAPI統合、認証API、ログインページ
- バックエンド: 認証必須化、GitHub OAuth統合、開発モード
- テスト: settings.SKIP_LLM_FOR_SKILL_TREE モックパス修正
- 開発環境: テストデータ修正、環境変数追加

認証機能と統合することで、ユーザーごとのパーソナライズされた
スキルツリーを実装。GitHub OAuthでログイン後、実際のリポジトリを
分析したスキルツリーが表示される。

Closes #74
@Inlet-back Inlet-back force-pushed the 74-skill-tree-integration branch from 34e9dd2 to 4ad7153 Compare February 21, 2026 08:57
@Inlet-back Inlet-back merged commit 3b8c905 into develop Feb 21, 2026
5 checks passed
@Inlet-back Inlet-back deleted the 74-skill-tree-integration branch February 21, 2026 10:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: (Frontend) スキルツリーのバックエンドAPI統合

2 participants