Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions backend/app/api/endpoints/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@
from app.models.user import User
from app.models.enums import SkillCategory
from app.schemas.badge import Badge as BadgeSchema
from app.schemas.grades import GradeStats as GradeStatsSchema
from app.schemas.grades import HighestRank as HighestRankSchema
from app.schemas.profile import Profile as ProfileSchema
from app.schemas.profile import ProfileCreate, ProfileUpdate
from app.schemas.quest_progress import QuestProgress as QuestProgressSchema
from app.schemas.skill_tree import SkillTree as SkillTreeSchema
from app.schemas.user import User as UserSchema
from app.schemas.user import UserUpdate
from app.services import grades_service

router = APIRouter()

Expand Down Expand Up @@ -140,3 +143,34 @@ def get_my_quest_progress(
) -> list[QuestProgressSchema]:
"""認証済みユーザー自身のクエスト進捗一覧取得。"""
return crud_quest_progress.get_quest_progress_by_user(db, current_user.id)


@router.get("/me/grade-stats", response_model=GradeStatsSchema)
def get_my_grade_stats(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> GradeStatsSchema:
"""認証済みユーザー自身の成績統計情報を取得。"""
# 連続記録日数を取得
consecutive_days = grades_service.get_consecutive_days(db, current_user.id)

# 修了したクエスト数を取得
completed_quests = grades_service.get_completed_quests_count(db, current_user.id)

# 最も進捗が高いカテゴリを取得
category, _ = grades_service.get_highest_progress_category(db, current_user.id)

# 最高ランク情報を構築(現在の実装では全体で一つのrankを使用)
highest_rank = HighestRankSchema(
rank=current_user.rank,
category=category,
category_name=grades_service.CATEGORY_NAMES.get(category, "総合"),
rank_name=grades_service.RANK_NAMES.get(current_user.rank, "種子"),
color=grades_service.CATEGORY_COLORS.get(category, "#55aaff"),
)

return GradeStatsSchema(
consecutive_days=consecutive_days,
completed_quests=completed_quests,
highest_rank=highest_rank,
)
21 changes: 21 additions & 0 deletions backend/app/schemas/grades.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Grades スキーマ - 成績・統計情報"""

from pydantic import BaseModel


class HighestRank(BaseModel):
"""最高ランク情報"""

rank: int # 0-9
category: str # 'web' | 'ai' | 'security' | 'infrastructure' | 'game' | 'design'
category_name: str # 表示用名称(例: "Web/App")
rank_name: str # ランク名(例: "林", "森", "世界樹")
color: str # 背景色(例: "#55aaff")
Comment on lines +11 to +13
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.

バックエンドとフロントエンドでフィールド名の命名規則に不一致があります。バックエンドでは snake_case(category_name, rank_name)を使用していますが、フロントエンド(frontend/src/features/grades/types/index.ts)では camelCase(categoryName, rankName)を期待しています。このままでは API レスポンスをフロントエンドが正しく処理できません。

解決方法として、Pydantic の ConfigDict で alias_generator を設定し、自動的に camelCase に変換するか、フィールドに個別に alias を設定してください。

Copilot uses AI. Check for mistakes.


class GradeStats(BaseModel):
"""成績統計情報"""

consecutive_days: int # 連続記録日数
completed_quests: int # 修了したクエスト数
highest_rank: HighestRank # 最高ランク情報
Comment on lines +19 to +21
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.

バックエンドとフロントエンドでフィールド名の命名規則に不一致があります。バックエンドでは snake_case(consecutive_days, completed_quests, highest_rank)を使用していますが、フロントエンド(frontend/src/features/grades/types/index.ts)では camelCase(consecutiveDays, completedQuests, highestRank)を期待しています。このままでは API レスポンスをフロントエンドが正しく処理できません。

解決方法として、Pydantic の ConfigDict で alias_generator を設定し、自動的に camelCase に変換するか、フィールドに個別に alias を設定してください。

Copilot uses AI. Check for mistakes.
122 changes: 122 additions & 0 deletions backend/app/services/grades_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""Grades サービス - 成績・統計情報の計算"""

from sqlalchemy.orm import Session

from app.crud import quest_progress as crud_quest_progress
from app.crud import skill_tree as crud_skill_tree
from app.models.enums import QuestStatus


# カテゴリ色マッピング
CATEGORY_COLORS = {
"web": "#55aaff",
"ai": "#e8b849",
"security": "#e85555",
"infrastructure": "#55cc55",
"game": "#ff9955",
"design": "#cc66dd",
}

# カテゴリ名マッピング
CATEGORY_NAMES = {
"web": "Web/App",
"ai": "AI",
"security": "Security",
"infrastructure": "Infra",
"game": "Game",
"design": "Design",
}

# ランク名マッピング(0-9)
RANK_NAMES = {
0: "種子",
1: "苗木",
2: "若木",
3: "巨木",
4: "母樹",
5: "林",
6: "森",
7: "霊樹",
8: "古樹",
9: "世界樹",
}


def calculate_skill_tree_progress(tree_data: dict) -> float:
"""スキルツリーの進捗率を計算(0.0-1.0)

Args:
tree_data: スキルツリーのJSONデータ

Returns:
進捗率(0.0-1.0)
"""
if not tree_data or "nodes" not in tree_data:
return 0.0

nodes = tree_data.get("nodes", [])
if not nodes:
return 0.0

completed_count = sum(1 for node in nodes if node.get("completed", False))
total_count = len(nodes)

return completed_count / total_count if total_count > 0 else 0.0


def get_highest_progress_category(db: Session, user_id: int) -> tuple[str, float]:
"""最も進捗が高いカテゴリを取得

Args:
db: データベースセッション
user_id: ユーザーID

Returns:
(カテゴリ名, 進捗率) のタプル
"""
skill_trees = crud_skill_tree.get_skill_trees_by_user(db, user_id)

if not skill_trees:
return "web", 0.0 # デフォルトはweb

max_progress = 0.0
max_category = "web"

for tree in skill_trees:
progress = calculate_skill_tree_progress(tree.tree_data)
if progress > max_progress:
max_progress = progress
max_category = tree.category

return max_category, max_progress


def get_completed_quests_count(db: Session, user_id: int) -> int:
"""修了したクエスト数を取得

Args:
db: データベースセッション
user_id: ユーザーID

Returns:
修了したクエスト数
"""
quest_progress_list = crud_quest_progress.get_quest_progress_by_user(db, user_id)
return sum(1 for qp in quest_progress_list if qp.status == QuestStatus.COMPLETED)


def get_consecutive_days(db: Session, user_id: int) -> int:
"""連続記録日数を取得(現時点では固定値を返す)

TODO: 将来的にログイン履歴などから算出

Args:
db: データベースセッション
user_id: ユーザーID

Returns:
連続記録日数
"""
# 現時点では実装が複雑なため、仮の値を返す
# 将来的にログイン履歴テーブルなどから算出
return 0
9 changes: 8 additions & 1 deletion frontend/src/features/grades/api/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ export const getGradeStats = async (): Promise<GradeStats> => {
// TODO: Replace with actual API call
return {
consecutiveDays: 365,
completedQuests: 365
completedQuests: 42,
highestRank: {
rank: 5,
category: "web",
categoryName: "Web/App",
rankName: "林",
color: "#55aaff"
}
};
};

Expand Down
41 changes: 41 additions & 0 deletions frontend/src/features/grades/components/StatusCard.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,56 @@
'use client';

import React from 'react';
import Image from 'next/image';
import { GradeStats } from '../types';

interface StatusCardProps {
stats: GradeStats;
}

// ランク名マッピング(0-9)
const RANK_NAMES: Record<number, string> = {
0: "種子",
1: "苗木",
2: "若木",
3: "巨木",
4: "母樹",
5: "林",
6: "森",
7: "霊樹",
8: "古樹",
9: "世界樹",
};

export const StatusCard: React.FC<StatusCardProps> = ({ stats }) => {
// ランク名を取得(rankNameがあればそれを使用、なければrankから取得)
const displayRankName = stats.highestRank.rankName || RANK_NAMES[stats.highestRank.rank] || "種子";
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.

rankName が提供されない場合のフォールバック処理が実装されていますが、stats.highestRank 自体が null または undefined の場合にエラーが発生します。GradeStats インターフェースでは highestRank が必須プロパティとして定義されていますが、API からのレスポンスが期待通りでない場合に備えて、オプショナルチェーン(?.)を使用することを推奨します。

例: const displayRankName = stats.highestRank?.rankName || RANK_NAMES[stats.highestRank?.rank ?? 0] || "種子";

Copilot uses AI. Check for mistakes.

console.log('Grade Stats:', stats); // デバッグ用
console.log('Display Rank Name:', displayRankName); // デバッグ用
Comment on lines +28 to +30
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.

デバッグ用の console.log が本番コードに残っています。これらは開発時のみ有効にするか、削除してください。本番環境では不要なログ出力はパフォーマンスに影響を与える可能性があります。

Suggested change
console.log('Grade Stats:', stats); // デバッグ用
console.log('Display Rank Name:', displayRankName); // デバッグ用

Copilot uses AI. Check for mistakes.

return (
<div className="flex justify-around items-start w-full max-w-5xl mx-auto px-6 relative mt-16 pt-10">
{/* Highest Rank */}
<div className="flex flex-col items-left min-w-[200px] relative">
<p className="text-[#006400] text-3xl font-medium absolute -top-16 -left-20">最高ランク</p>
<div className="flex items-baseline mt-7">
{/* ランク画像(背景透過) */}
<div className="relative h-48 w-48 flex items-center justify-center">
<Image
src={`/images/ranks/rank_tree_${stats.highestRank.rank}.png`}
alt={`Rank ${stats.highestRank.rank}`}
width={180}
height={180}
className="object-contain"
/>
</div>
Comment on lines +39 to +47
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.

元の要件(Issue #103)では、ランク画像の背景にカテゴリ色を表示することが指定されていました(style={{ backgroundColor: categoryColor }})が、現在の実装ではカテゴリ色が使用されていません。要件との不一致が意図的なものか確認してください。

もし意図的な変更であれば問題ありませんが、カテゴリ色の背景を追加する場合は、以下のように実装できます:

<div className="relative h-48 w-48 flex items-center justify-center rounded-lg" 
     style={{ backgroundColor: stats.highestRank.color }}>

Copilot uses AI. Check for mistakes.
<span className="text-3xl text-[#006400] font-medium ml-3 pb-2">
{displayRankName}
</span>
Comment on lines +48 to +50
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.

元の要件(Issue #103)では、ランク画像の横にカテゴリ名("Web/App", "AI", など)を表示することが指定されていましたが、現在の実装ではランク名("種子", "苗木", "林", など)を表示しています。

要件との不一致が意図的なものか確認してください。PR説明には「カテゴリ名ではなく、ランク名(林、森など)を表示」と記載されているため、仕様変更の可能性がありますが、Issue との整合性を確認することを推奨します。

Copilot uses AI. Check for mistakes.
</div>
</div>

{/* Consecutive Days */}
<div className="flex flex-col items-left min-w-[200px] relative">
<p className="text-[#006400] text-3xl font-medium absolute -top-16 -left-20">連続記録</p>
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/features/grades/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
export interface HighestRank {
rank: number; // 0-9
category: string; // 'web' | 'ai' | 'security' | 'infrastructure' | 'game' | 'design'
categoryName: string; // 表示用名称
rankName: string; // ランク名(例: "林", "森", "世界樹")
color: string; // 背景色
}

export interface GradeStats {
consecutiveDays: number;
completedQuests: number;
highestRank: HighestRank;
}

export interface Badge {
Expand Down
Loading