-
Notifications
You must be signed in to change notification settings - Fork 0
feat: #103 /gradesページに最高ランク表示を追加 #113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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") | ||
|
|
||
|
|
||
| class GradeStats(BaseModel): | ||
| """成績統計情報""" | ||
|
|
||
| consecutive_days: int # 連続記録日数 | ||
| completed_quests: int # 修了したクエスト数 | ||
| highest_rank: HighestRank # 最高ランク情報 | ||
|
Comment on lines
+19
to
+21
|
||
| 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 |
| 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] || "種子"; | ||||||||
|
||||||||
|
|
||||||||
| console.log('Grade Stats:', stats); // デバッグ用 | ||||||||
| console.log('Display Rank Name:', displayRankName); // デバッグ用 | ||||||||
|
Comment on lines
+28
to
+30
|
||||||||
| console.log('Grade Stats:', stats); // デバッグ用 | |
| console.log('Display Rank Name:', displayRankName); // デバッグ用 |
Copilot
AI
Feb 21, 2026
There was a problem hiding this comment.
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
AI
Feb 21, 2026
There was a problem hiding this comment.
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 との整合性を確認することを推奨します。
There was a problem hiding this comment.
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 を設定してください。