Skip to content

Commit fed6335

Browse files
committed
feat: #112 GitHub連携とAI解析によるランク判定機能の実装
- ログイン画面のGitHubボタンを直接OAuthフローに接続 - OAuth完了時にバックエンドがGitHub統計を取得してLLMでランク判定 - ダッシュボード初回アクセス時にランク測定UIをモーダル表示 - RankMeasurementが既に判定されたランク情報を取得して豪華演出で表示 - getCurrentUser() APIを追加してユーザー情報取得 - localStorageでユーザーごとの初回表示フラグを管理
1 parent 33a4422 commit fed6335

File tree

4 files changed

+118
-29
lines changed

4 files changed

+118
-29
lines changed

frontend/src/app/login/page.tsx

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import { useState } from "react";
44
import { useRouter } from "next/navigation";
55
import { login, register } from "@/lib/api/auth";
6-
import { RankMeasurement } from "@/features/dashboard/components/RankMeasurement";
76

87
export default function LoginPage() {
98
const router = useRouter();
@@ -12,7 +11,6 @@ export default function LoginPage() {
1211
const [isRegister, setIsRegister] = useState(false);
1312
const [error, setError] = useState<string | null>(null);
1413
const [loading, setLoading] = useState(false);
15-
const [showRankMeasurement, setShowRankMeasurement] = useState(false);
1614

1715
const handleSubmit = async (e: React.FormEvent) => {
1816
e.preventDefault();
@@ -35,12 +33,7 @@ export default function LoginPage() {
3533
};
3634

3735
const handleGitHubLogin = () => {
38-
// ランク測定UIを表示
39-
setShowRankMeasurement(true);
40-
};
41-
42-
const handleRankMeasurementComplete = () => {
43-
// ランク測定完了後、GitHub OAuth フローを開始
36+
// GitHub OAuth フローを直接開始(ADR 018に基づく)
4437
const apiBaseUrl =
4538
process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
4639
window.location.href = `${apiBaseUrl}/api/v1/auth/github/login`;
@@ -191,17 +184,6 @@ export default function LoginPage() {
191184
}
192185
}
193186
`}</style>
194-
195-
{/* ランク測定モーダル */}
196-
{showRankMeasurement && (
197-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm animate-fadeIn">
198-
<div className="w-full h-full overflow-auto">
199-
<RankMeasurement
200-
onComplete={handleRankMeasurementComplete}
201-
/>
202-
</div>
203-
</div>
204-
)}
205187
</div>
206188
);
207189
}

frontend/src/features/dashboard/components/DashboardContainer.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import { useEffect, useState, useCallback } from "react";
99
import dynamic from "next/dynamic";
1010
import type { UserStatus } from "../types";
1111
import { fetchUserDashboard } from "../api/mock";
12+
import { getCurrentUser } from "@/lib/api/auth";
1213
import { AcquiredBadges } from "./AcquiredBadges";
1314
import { CategorySelector } from "./CategorySelector";
15+
import { RankMeasurement } from "./RankMeasurement";
1416
import { SkillNodePanel } from "../../skill-tree/components/SkillNodePanel";
1517
import { RankBar } from "../../skill-tree/components/RankBar";
1618
import { SkillLegend } from "../../skill-tree/components/SkillLegend";
@@ -56,6 +58,9 @@ export function DashboardContainer() {
5658
const [streamProgress, setStreamProgress] = useState(0);
5759
const [error, setError] = useState<string | null>(null);
5860

61+
// ランク測定モーダル表示フラグ
62+
const [showRankMeasurement, setShowRankMeasurement] = useState(false);
63+
5964
// Skill Tree States
6065
const [selectedNode, setSelectedNode] = useState<TreeSkillNode | null>(null);
6166
const [zoomAction, setZoomAction] = useState<{
@@ -84,6 +89,22 @@ export function DashboardContainer() {
8489
if (!isMounted) return; // アンマウント済みなら中断
8590

8691
setUserStatus(statusData);
92+
93+
// 初回アクセス判定(localStorageを使用)
94+
// ユーザーIDを取得してユーザーごとに判定
95+
try {
96+
const userInfo = await getCurrentUser();
97+
const storageKey = `rank_animation_viewed_${userInfo.id}`;
98+
const hasViewed = localStorage.getItem(storageKey);
99+
100+
if (!hasViewed) {
101+
// 初回アクセスの場合、ランク測定モーダルを表示
102+
setShowRankMeasurement(true);
103+
}
104+
} catch (err) {
105+
console.error("ユーザー情報取得エラー:", err);
106+
}
107+
87108
setLoading(false);
88109

89110
// スキルツリーはストリーミングで取得
@@ -183,6 +204,19 @@ export function DashboardContainer() {
183204
};
184205
}, [category]);
185206

207+
// ランク測定完了時のハンドラー
208+
const handleRankMeasurementComplete = async () => {
209+
try {
210+
const userInfo = await getCurrentUser();
211+
const storageKey = `rank_animation_viewed_${userInfo.id}`;
212+
localStorage.setItem(storageKey, "true");
213+
setShowRankMeasurement(false);
214+
} catch (err) {
215+
console.error("ランク測定完了処理エラー:", err);
216+
setShowRankMeasurement(false);
217+
}
218+
};
219+
186220
if (loading) {
187221
return (
188222
<div className="flex min-h-screen items-center justify-center bg-[#FDFEF0]">
@@ -325,6 +359,15 @@ export function DashboardContainer() {
325359
</div>
326360
</section>
327361
</div>
362+
363+
{/* ランク測定モーダル(初回アクセス時のみ表示) */}
364+
{showRankMeasurement && (
365+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm animate-fadeIn">
366+
<div className="w-full h-full overflow-auto">
367+
<RankMeasurement onComplete={handleRankMeasurementComplete} />
368+
</div>
369+
</div>
370+
)}
328371
</main>
329372
);
330373
}

frontend/src/features/dashboard/components/RankMeasurement.tsx

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,39 @@
11
'use client';
22

33
import { useState, useRef, useEffect } from 'react';
4-
import { analyzeRank, RankAnalysisResponse } from '../api/rankApi';
4+
import { getCurrentUser } from '@/lib/api/auth';
55
import { generateSkillTree } from '@/lib/api/skillTree';
66

7+
// ランク名マッピング(rank番号からランク名を取得)
8+
const RANK_NAMES = [
9+
'種子', // rank 0
10+
'芽', // rank 1
11+
'若木', // rank 2
12+
'樹', // rank 3
13+
'母樹', // rank 4
14+
'賢樹', // rank 5
15+
'神樹', // rank 6
16+
'世界樹', // rank 7
17+
];
18+
19+
// UserInfo型定義(getCurrentUserの戻り値に合わせる)
20+
interface UserInfo {
21+
id: number;
22+
username: string;
23+
rank: number;
24+
exp: number;
25+
created_at: string;
26+
updated_at: string;
27+
}
28+
29+
// RankAnalysisResponse互換の型(既存コードとの互換性のため)
30+
export interface RankAnalysisResponse {
31+
percentile: number;
32+
rank: number;
33+
rank_name: string;
34+
reasoning: string;
35+
}
36+
737
type MeasurementState =
838
| 'idle'
939
| 'charging'
@@ -16,11 +46,10 @@ type MeasurementState =
1646
| 'error';
1747

1848
interface RankMeasurementProps {
19-
githubUsername?: string;
2049
onComplete?: (rank: RankAnalysisResponse) => void;
2150
}
2251

23-
export function RankMeasurement({ githubUsername, onComplete }: RankMeasurementProps) {
52+
export function RankMeasurement({ onComplete }: RankMeasurementProps) {
2453
const [state, setState] = useState<MeasurementState>('idle');
2554
const [chargeProgress, setChargeProgress] = useState(0);
2655
const [rankResult, setRankResult] = useState<RankAnalysisResponse | null>(null);
@@ -162,13 +191,16 @@ export function RankMeasurement({ githubUsername, onComplete }: RankMeasurementP
162191
await new Promise(resolve => setTimeout(resolve, FINALIZING_DURATION));
163192

164193
try {
165-
// 実際のランク判定API呼び出し
166-
const result = await analyzeRank({
167-
github_username: githubUsername || 'default-user',
168-
portfolio_text: 'ポートフォリオ情報',
169-
qiita_id: '',
170-
other_info: 'コミュニティ活動',
171-
});
194+
// OAuth完了時にバックエンドで既に判定されたランク情報を取得
195+
const userInfo = await getCurrentUser();
196+
197+
// RankAnalysisResponse形式に変換
198+
const result: RankAnalysisResponse = {
199+
rank: userInfo.rank,
200+
rank_name: RANK_NAMES[userInfo.rank] || '不明',
201+
percentile: (userInfo.rank / 7) * 100, // 暫定: rank0-7を0-100%に変換
202+
reasoning: `GitHub統計情報に基づいて自動判定されました。あなたのランクは「${RANK_NAMES[userInfo.rank] || '不明'}」です。`,
203+
};
172204

173205
setRankResult(result);
174206
setState('revealing');

frontend/src/lib/api/auth.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,35 @@ export async function logout(): Promise<{ message: string }> {
9898

9999
return response.json();
100100
}
101+
102+
/**
103+
* 現在のユーザー情報を取得
104+
*
105+
* @returns ユーザー情報(id, username, rank, exp等)
106+
* @throws 認証失敗時のエラー
107+
*/
108+
export async function getCurrentUser(): Promise<{
109+
id: number;
110+
username: string;
111+
rank: number;
112+
exp: number;
113+
created_at: string;
114+
updated_at: string;
115+
}> {
116+
const baseUrl = getApiBaseUrl();
117+
const url = `${baseUrl}/api/v1/users/me`;
118+
119+
const response = await fetch(url, {
120+
method: "GET",
121+
credentials: "include",
122+
});
123+
124+
if (!response.ok) {
125+
const errorText = await response.text();
126+
throw new Error(
127+
`ユーザー情報の取得に失敗しました: ${response.status} ${errorText}`,
128+
);
129+
}
130+
131+
return response.json();
132+
}

0 commit comments

Comments
 (0)