Skip to content

Commit e178ed5

Browse files
Merge pull request #113 from kc3hack/feature/issue-103-highest-rank-display
feat: #103 /gradesページに最高ランク表示を追加
2 parents 2aac0a0 + 922efb1 commit e178ed5

File tree

6 files changed

+235
-1
lines changed

6 files changed

+235
-1
lines changed

backend/app/api/endpoints/users.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@
2121
from app.models.user import User
2222
from app.models.enums import SkillCategory
2323
from app.schemas.badge import Badge as BadgeSchema
24+
from app.schemas.grades import GradeStats as GradeStatsSchema
25+
from app.schemas.grades import HighestRank as HighestRankSchema
2426
from app.schemas.profile import Profile as ProfileSchema
2527
from app.schemas.profile import ProfileCreate, ProfileUpdate
2628
from app.schemas.quest_progress import QuestProgress as QuestProgressSchema
2729
from app.schemas.skill_tree import SkillTree as SkillTreeSchema
2830
from app.schemas.user import User as UserSchema
2931
from app.schemas.user import UserUpdate
32+
from app.services import grades_service
3033

3134
router = APIRouter()
3235

@@ -140,3 +143,34 @@ def get_my_quest_progress(
140143
) -> list[QuestProgressSchema]:
141144
"""認証済みユーザー自身のクエスト進捗一覧取得。"""
142145
return crud_quest_progress.get_quest_progress_by_user(db, current_user.id)
146+
147+
148+
@router.get("/me/grade-stats", response_model=GradeStatsSchema)
149+
def get_my_grade_stats(
150+
db: Session = Depends(get_db),
151+
current_user: User = Depends(get_current_user),
152+
) -> GradeStatsSchema:
153+
"""認証済みユーザー自身の成績統計情報を取得。"""
154+
# 連続記録日数を取得
155+
consecutive_days = grades_service.get_consecutive_days(db, current_user.id)
156+
157+
# 修了したクエスト数を取得
158+
completed_quests = grades_service.get_completed_quests_count(db, current_user.id)
159+
160+
# 最も進捗が高いカテゴリを取得
161+
category, _ = grades_service.get_highest_progress_category(db, current_user.id)
162+
163+
# 最高ランク情報を構築(現在の実装では全体で一つのrankを使用)
164+
highest_rank = HighestRankSchema(
165+
rank=current_user.rank,
166+
category=category,
167+
category_name=grades_service.CATEGORY_NAMES.get(category, "総合"),
168+
rank_name=grades_service.RANK_NAMES.get(current_user.rank, "種子"),
169+
color=grades_service.CATEGORY_COLORS.get(category, "#55aaff"),
170+
)
171+
172+
return GradeStatsSchema(
173+
consecutive_days=consecutive_days,
174+
completed_quests=completed_quests,
175+
highest_rank=highest_rank,
176+
)

backend/app/schemas/grades.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""Grades スキーマ - 成績・統計情報"""
2+
3+
from pydantic import BaseModel
4+
5+
6+
class HighestRank(BaseModel):
7+
"""最高ランク情報"""
8+
9+
rank: int # 0-9
10+
category: str # 'web' | 'ai' | 'security' | 'infrastructure' | 'game' | 'design'
11+
category_name: str # 表示用名称(例: "Web/App")
12+
rank_name: str # ランク名(例: "林", "森", "世界樹")
13+
color: str # 背景色(例: "#55aaff")
14+
15+
16+
class GradeStats(BaseModel):
17+
"""成績統計情報"""
18+
19+
consecutive_days: int # 連続記録日数
20+
completed_quests: int # 修了したクエスト数
21+
highest_rank: HighestRank # 最高ランク情報
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"""Grades サービス - 成績・統計情報の計算"""
2+
3+
from sqlalchemy.orm import Session
4+
5+
from app.crud import quest_progress as crud_quest_progress
6+
from app.crud import skill_tree as crud_skill_tree
7+
from app.models.enums import QuestStatus
8+
9+
10+
# カテゴリ色マッピング
11+
CATEGORY_COLORS = {
12+
"web": "#55aaff",
13+
"ai": "#e8b849",
14+
"security": "#e85555",
15+
"infrastructure": "#55cc55",
16+
"game": "#ff9955",
17+
"design": "#cc66dd",
18+
}
19+
20+
# カテゴリ名マッピング
21+
CATEGORY_NAMES = {
22+
"web": "Web/App",
23+
"ai": "AI",
24+
"security": "Security",
25+
"infrastructure": "Infra",
26+
"game": "Game",
27+
"design": "Design",
28+
}
29+
30+
# ランク名マッピング(0-9)
31+
RANK_NAMES = {
32+
0: "種子",
33+
1: "苗木",
34+
2: "若木",
35+
3: "巨木",
36+
4: "母樹",
37+
5: "林",
38+
6: "森",
39+
7: "霊樹",
40+
8: "古樹",
41+
9: "世界樹",
42+
}
43+
44+
45+
def calculate_skill_tree_progress(tree_data: dict) -> float:
46+
"""スキルツリーの進捗率を計算(0.0-1.0)
47+
48+
Args:
49+
tree_data: スキルツリーのJSONデータ
50+
51+
Returns:
52+
進捗率(0.0-1.0)
53+
"""
54+
if not tree_data or "nodes" not in tree_data:
55+
return 0.0
56+
57+
nodes = tree_data.get("nodes", [])
58+
if not nodes:
59+
return 0.0
60+
61+
completed_count = sum(1 for node in nodes if node.get("completed", False))
62+
total_count = len(nodes)
63+
64+
return completed_count / total_count if total_count > 0 else 0.0
65+
66+
67+
def get_highest_progress_category(db: Session, user_id: int) -> tuple[str, float]:
68+
"""最も進捗が高いカテゴリを取得
69+
70+
Args:
71+
db: データベースセッション
72+
user_id: ユーザーID
73+
74+
Returns:
75+
(カテゴリ名, 進捗率) のタプル
76+
"""
77+
skill_trees = crud_skill_tree.get_skill_trees_by_user(db, user_id)
78+
79+
if not skill_trees:
80+
return "web", 0.0 # デフォルトはweb
81+
82+
max_progress = 0.0
83+
max_category = "web"
84+
85+
for tree in skill_trees:
86+
progress = calculate_skill_tree_progress(tree.tree_data)
87+
if progress > max_progress:
88+
max_progress = progress
89+
max_category = tree.category
90+
91+
return max_category, max_progress
92+
93+
94+
def get_completed_quests_count(db: Session, user_id: int) -> int:
95+
"""修了したクエスト数を取得
96+
97+
Args:
98+
db: データベースセッション
99+
user_id: ユーザーID
100+
101+
Returns:
102+
修了したクエスト数
103+
"""
104+
quest_progress_list = crud_quest_progress.get_quest_progress_by_user(db, user_id)
105+
return sum(1 for qp in quest_progress_list if qp.status == QuestStatus.COMPLETED)
106+
107+
108+
def get_consecutive_days(db: Session, user_id: int) -> int:
109+
"""連続記録日数を取得(現時点では固定値を返す)
110+
111+
TODO: 将来的にログイン履歴などから算出
112+
113+
Args:
114+
db: データベースセッション
115+
user_id: ユーザーID
116+
117+
Returns:
118+
連続記録日数
119+
"""
120+
# 現時点では実装が複雑なため、仮の値を返す
121+
# 将来的にログイン履歴テーブルなどから算出
122+
return 0

frontend/src/features/grades/api/mock.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@ export const getGradeStats = async (): Promise<GradeStats> => {
44
// TODO: Replace with actual API call
55
return {
66
consecutiveDays: 365,
7-
completedQuests: 365
7+
completedQuests: 42,
8+
highestRank: {
9+
rank: 5,
10+
category: "web",
11+
categoryName: "Web/App",
12+
rankName: "林",
13+
color: "#55aaff"
14+
}
815
};
916
};
1017

frontend/src/features/grades/components/StatusCard.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,56 @@
11
'use client';
22

33
import React from 'react';
4+
import Image from 'next/image';
45
import { GradeStats } from '../types';
56

67
interface StatusCardProps {
78
stats: GradeStats;
89
}
910

11+
// ランク名マッピング(0-9)
12+
const RANK_NAMES: Record<number, string> = {
13+
0: "種子",
14+
1: "苗木",
15+
2: "若木",
16+
3: "巨木",
17+
4: "母樹",
18+
5: "林",
19+
6: "森",
20+
7: "霊樹",
21+
8: "古樹",
22+
9: "世界樹",
23+
};
24+
1025
export const StatusCard: React.FC<StatusCardProps> = ({ stats }) => {
26+
// ランク名を取得(rankNameがあればそれを使用、なければrankから取得)
27+
const displayRankName = stats.highestRank.rankName || RANK_NAMES[stats.highestRank.rank] || "種子";
28+
29+
console.log('Grade Stats:', stats); // デバッグ用
30+
console.log('Display Rank Name:', displayRankName); // デバッグ用
31+
1132
return (
1233
<div className="flex justify-around items-start w-full max-w-5xl mx-auto px-6 relative mt-16 pt-10">
34+
{/* Highest Rank */}
35+
<div className="flex flex-col items-left min-w-[200px] relative">
36+
<p className="text-[#006400] text-3xl font-medium absolute -top-16 -left-20">最高ランク</p>
37+
<div className="flex items-baseline mt-7">
38+
{/* ランク画像(背景透過) */}
39+
<div className="relative h-48 w-48 flex items-center justify-center">
40+
<Image
41+
src={`/images/ranks/rank_tree_${stats.highestRank.rank}.png`}
42+
alt={`Rank ${stats.highestRank.rank}`}
43+
width={180}
44+
height={180}
45+
className="object-contain"
46+
/>
47+
</div>
48+
<span className="text-3xl text-[#006400] font-medium ml-3 pb-2">
49+
{displayRankName}
50+
</span>
51+
</div>
52+
</div>
53+
1354
{/* Consecutive Days */}
1455
<div className="flex flex-col items-left min-w-[200px] relative">
1556
<p className="text-[#006400] text-3xl font-medium absolute -top-16 -left-20">連続記録</p>

frontend/src/features/grades/types/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1+
export interface HighestRank {
2+
rank: number; // 0-9
3+
category: string; // 'web' | 'ai' | 'security' | 'infrastructure' | 'game' | 'design'
4+
categoryName: string; // 表示用名称
5+
rankName: string; // ランク名(例: "林", "森", "世界樹")
6+
color: string; // 背景色
7+
}
8+
19
export interface GradeStats {
210
consecutiveDays: number;
311
completedQuests: number;
12+
highestRank: HighestRank;
413
}
514

615
export interface Badge {

0 commit comments

Comments
 (0)