feat(frontend): 実習メニュー画面の更新とSkillTreeのロールバック#55
Conversation
…mages, revert skill tree
There was a problem hiding this comment.
Pull request overview
このPRは、実習メニュー画面の新規実装と、ダッシュボードのスキルロードマップコンポーネントの復元を行っています。実習メニューは8つのカテゴリ(Web、Mobile、Network、Game、Design、Infrastructure、AI、Security)を3x3グリッドで表示し、サイドバーにアクティブ状態のハイライト機能を追加しています。
Changes:
- 実習メニューコンポーネント(ExerciseMenu.tsx)を3x3グリッドレイアウトで新規作成
- タブナビゲーション機能を追加(通常演習、書籍、ターミナル)
- AppSidebarにアクティブパス検出機能を実装し、現在のページを視覚的にハイライト
- DashboardContainerにSkillRoadmapコンポーネントを復元(プレースホルダーから置き換え)
- /exercisesルート用のページとレイアウトを作成
Reviewed changes
Copilot reviewed 5 out of 13 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/src/features/exercise/components/ExerciseMenu.tsx | 演習メニューの新規実装。3x3グリッド、タブナビゲーション、8つの演習カテゴリを含む |
| frontend/src/features/dashboard/components/DashboardContainer.tsx | ツリー絵文字プレースホルダーをSkillRoadmapコンポーネントに置き換え |
| frontend/src/features/dashboard/components/AppSidebar.tsx | アクティブパス検出ロジックを追加し、現在のページをハイライト表示 |
| frontend/src/app/exercises/page.tsx | ExerciseMenuコンポーネントをレンダリングする演習ページを作成 |
| frontend/src/app/exercises/layout.tsx | ヘッダーとサイドバーを含む演習用レイアウトを作成 |
| // Dummy data for the cards (9 placeholders for 3x3 grid) | ||
| const items = Array.from({ length: 9 }).map((_, i) => ({ | ||
| id: i, | ||
| title: i === 0 ? 'Web' : i === 1 ? 'Mobile' : i === 2 ? 'Network' : i === 3 ? 'Game' : i === 4 ? 'Design' : i === 5 ? 'Infrastructure' : i === 6 ? 'AI' : i === 7 ? 'Security' : i === 8 ? 'Comming Soon...':`Exercise ${i + 1}`, |
There was a problem hiding this comment.
「Comming Soon...」のスペルミスがあります。「Coming Soon...」に修正してください。
| title: i === 0 ? 'Web' : i === 1 ? 'Mobile' : i === 2 ? 'Network' : i === 3 ? 'Game' : i === 4 ? 'Design' : i === 5 ? 'Infrastructure' : i === 6 ? 'AI' : i === 7 ? 'Security' : i === 8 ? 'Comming Soon...':`Exercise ${i + 1}`, | |
| title: i === 0 ? 'Web' : i === 1 ? 'Mobile' : i === 2 ? 'Network' : i === 3 ? 'Game' : i === 4 ? 'Design' : i === 5 ? 'Infrastructure' : i === 6 ? 'AI' : i === 7 ? 'Security' : i === 8 ? 'Coming Soon...':`Exercise ${i + 1}`, |
| // Dummy data for the cards (9 placeholders for 3x3 grid) | ||
| const items = Array.from({ length: 9 }).map((_, i) => ({ | ||
| id: i, | ||
| title: i === 0 ? 'Web' : i === 1 ? 'Mobile' : i === 2 ? 'Network' : i === 3 ? 'Game' : i === 4 ? 'Design' : i === 5 ? 'Infrastructure' : i === 6 ? 'AI' : i === 7 ? 'Security' : i === 8 ? 'Comming Soon...':`Exercise ${i + 1}`, | ||
| image: i === 0 ? '/images/exercises/Web.png' : i === 1 ? '/images/exercises/Mobile.png' : i === 2 ? '/images/exercises/Network.png' : i === 3 ? '/images/exercises/game.png' : i === 4 ? '/images/exercises/Design.png' : i === 5 ? '/images/exercises/Infr.png' : i === 6 ? '/images/exercises/ai.png' : i === 7 ? '/images/exercises/Security.png' : null, |
There was a problem hiding this comment.
この行の極めて長いネストされた三項演算子は、保守性とコードの可読性を大きく損ないます。代わりに、配列データとして定義し、マッピングする方が明確で保守しやすくなります。例えば、['Web', 'Mobile', 'Network', 'Game', 'Design', 'Infrastructure', 'AI', 'Security', 'Coming Soon...']という配列を定義し、items配列を生成する際にそのインデックスで参照する方法が推奨されます。
| // Dummy data for the cards (9 placeholders for 3x3 grid) | |
| const items = Array.from({ length: 9 }).map((_, i) => ({ | |
| id: i, | |
| title: i === 0 ? 'Web' : i === 1 ? 'Mobile' : i === 2 ? 'Network' : i === 3 ? 'Game' : i === 4 ? 'Design' : i === 5 ? 'Infrastructure' : i === 6 ? 'AI' : i === 7 ? 'Security' : i === 8 ? 'Comming Soon...':`Exercise ${i + 1}`, | |
| image: i === 0 ? '/images/exercises/Web.png' : i === 1 ? '/images/exercises/Mobile.png' : i === 2 ? '/images/exercises/Network.png' : i === 3 ? '/images/exercises/game.png' : i === 4 ? '/images/exercises/Design.png' : i === 5 ? '/images/exercises/Infr.png' : i === 6 ? '/images/exercises/ai.png' : i === 7 ? '/images/exercises/Security.png' : null, | |
| const EXERCISE_TITLES = [ | |
| 'Web', | |
| 'Mobile', | |
| 'Network', | |
| 'Game', | |
| 'Design', | |
| 'Infrastructure', | |
| 'AI', | |
| 'Security', | |
| 'Comming Soon...', | |
| ]; | |
| const EXERCISE_IMAGES = [ | |
| '/images/exercises/Web.png', | |
| '/images/exercises/Mobile.png', | |
| '/images/exercises/Network.png', | |
| '/images/exercises/game.png', | |
| '/images/exercises/Design.png', | |
| '/images/exercises/Infr.png', | |
| '/images/exercises/ai.png', | |
| '/images/exercises/Security.png', | |
| null, | |
| ]; | |
| // Dummy data for the cards (9 placeholders for 3x3 grid) | |
| const items = Array.from({ length: 9 }).map((_, i) => ({ | |
| id: i, | |
| title: EXERCISE_TITLES[i] ?? `Exercise ${i + 1}`, | |
| image: EXERCISE_IMAGES[i] ?? null, |
| const items = Array.from({ length: 9 }).map((_, i) => ({ | ||
| id: i, | ||
| title: i === 0 ? 'Web' : i === 1 ? 'Mobile' : i === 2 ? 'Network' : i === 3 ? 'Game' : i === 4 ? 'Design' : i === 5 ? 'Infrastructure' : i === 6 ? 'AI' : i === 7 ? 'Security' : i === 8 ? 'Comming Soon...':`Exercise ${i + 1}`, | ||
| image: i === 0 ? '/images/exercises/Web.png' : i === 1 ? '/images/exercises/Mobile.png' : i === 2 ? '/images/exercises/Network.png' : i === 3 ? '/images/exercises/game.png' : i === 4 ? '/images/exercises/Design.png' : i === 5 ? '/images/exercises/Infr.png' : i === 6 ? '/images/exercises/ai.png' : i === 7 ? '/images/exercises/Security.png' : null, |
There was a problem hiding this comment.
この行も極めて長いネストされた三項演算子で構成されており、保守性が非常に低くなっています。画像パスを配列として定義し、インデックスでアクセスする方法に変更することを強く推奨します。また、画像ファイル名の命名規則が一貫していません(Web.png、Mobile.png、Design.png、Security.pngは大文字始まり、game.png、ai.pngは小文字始まり、Infr.pngは省略形)。統一した命名規則に従うべきです。
| const items = Array.from({ length: 9 }).map((_, i) => ({ | |
| id: i, | |
| title: i === 0 ? 'Web' : i === 1 ? 'Mobile' : i === 2 ? 'Network' : i === 3 ? 'Game' : i === 4 ? 'Design' : i === 5 ? 'Infrastructure' : i === 6 ? 'AI' : i === 7 ? 'Security' : i === 8 ? 'Comming Soon...':`Exercise ${i + 1}`, | |
| image: i === 0 ? '/images/exercises/Web.png' : i === 1 ? '/images/exercises/Mobile.png' : i === 2 ? '/images/exercises/Network.png' : i === 3 ? '/images/exercises/game.png' : i === 4 ? '/images/exercises/Design.png' : i === 5 ? '/images/exercises/Infr.png' : i === 6 ? '/images/exercises/ai.png' : i === 7 ? '/images/exercises/Security.png' : null, | |
| const TITLES = [ | |
| 'Web', | |
| 'Mobile', | |
| 'Network', | |
| 'Game', | |
| 'Design', | |
| 'Infrastructure', | |
| 'AI', | |
| 'Security', | |
| 'Comming Soon...', | |
| ] as const; | |
| const IMAGES = [ | |
| '/images/exercises/Web.png', | |
| '/images/exercises/Mobile.png', | |
| '/images/exercises/Network.png', | |
| '/images/exercises/game.png', | |
| '/images/exercises/Design.png', | |
| '/images/exercises/Infr.png', | |
| '/images/exercises/ai.png', | |
| '/images/exercises/Security.png', | |
| null, | |
| ] as const; | |
| const items = Array.from({ length: 9 }).map((_, i) => ({ | |
| id: i, | |
| title: TITLES[i] ?? `Exercise ${i + 1}`, | |
| image: IMAGES[i] ?? null, |
| ]; | ||
|
|
||
| export function ExerciseMenu() { | ||
| const [activeTab, setActiveTab] = useState<TabType>('regular'); |
There was a problem hiding this comment.
activeTabステートが定義され、タブのクリックで更新されていますが、実際にコンテンツのフィルタリングや表示制御に使用されていません。このステートを使用してタブごとに異なるコンテンツを表示する実装が必要です。もし現時点でタブの機能が不要であれば、activeTabステートとsetActiveTabの定義を削除し、タブのボタンもクリック不可にするか、視覚的なフィードバックのみにすることを検討してください。
There was a problem hiding this comment.
ページ増やす時にいるのでignore
| import { AppSidebar } from '@/features/dashboard/components/AppSidebar'; | ||
|
|
||
| export const metadata = { | ||
| title: 'Exercises', | ||
| description: 'Your coding exercises', | ||
| }; | ||
|
|
||
| export default function ExerciseLayout({ | ||
| children, | ||
| }: { | ||
| children: React.ReactNode; | ||
| }) { | ||
| return ( | ||
| <div className="min-h-screen bg-[#FDFEF0]"> | ||
| {/* Top Header Bar - Full Width */} | ||
| <header className="sticky top-0 z-50 flex h-16 w-full items-center justify-end bg-[#559C71] px-6 text-white shadow-md"> | ||
| <div className="flex h-10 w-10 items-center justify-center rounded-full bg-white text-gray-600 shadow-sm cursor-pointer hover:bg-gray-100 transition-colors"> | ||
| {/* User Icon Placeholder */} | ||
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="w-6 h-6 text-gray-400"> | ||
| <path fillRule="evenodd" d="M7.5 6a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM3.751 20.105a8.25 8.25 0 0116.498 0 .75.75 0 01-.437.695A18.683 18.683 0 0112 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 01-.437-.695z" clipRule="evenodd" /> | ||
| </svg> | ||
| </div> | ||
| </header> | ||
|
|
||
| <div className="flex"> | ||
| <AppSidebar /> | ||
|
|
||
| {/* Main Content Area */} | ||
| <div className="flex-1 min-h-[calc(100vh-4rem)] bg-[#FDFEF0] sm:ml-64"> | ||
| {children} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
exercises/layout.tsxとdashboard/layout.tsxの間でレイアウトコードが重複しています。ヘッダーバー、サイドバー、メインコンテンツエリアの構造がほぼ同一です。共通のレイアウトコンポーネントを作成し、両方のレイアウトから再利用することで、保守性が向上し、将来的な変更が容易になります。例えば、AppLayoutコンポーネントを作成し、それを両方のレイアウトファイルから使用することを検討してください。
| <div | ||
| key={item.id} | ||
| className="flex aspect-square w-full flex-col items-center justify-center rounded-3xl border-2 border-[#3A7E56] bg-white p-4 shadow-sm transition-transform hover:scale-105 hover:shadow-md cursor-pointer overflow-hidden" |
There was a problem hiding this comment.
カードにcursor-pointerクラスが適用されていますが、クリック可能な要素としてdiv要素を使用しています。これはアクセシビリティの問題を引き起こします。キーボードナビゲーションやスクリーンリーダーのサポートが欠如しています。button要素またはLink要素を使用し、適切なrole、tabIndex、onKeyDownハンドラーを追加することを推奨します。また、クリックハンドラーが実装されていないため、現状では実際にクリックしても何も起こりません。
| return "ml-3 text-lg text-[#D16B36] font-bold"; | ||
| } | ||
| return "ml-3 text-lg"; | ||
| } |
There was a problem hiding this comment.
getTextClass関数の定義の最後にセミコロンがありません。コードの一貫性のため、セミコロンを追加するか、プロジェクト全体でセミコロンを省略する規則に従ってください。
| } | |
| }; |
| {/* Skill Tree Section */} | ||
| <div className="flex justify-center py-8"> | ||
| {/* Using a placeholder for the tree image based on the mockup description */} | ||
| <div className="relative h-64 w-64"> | ||
| {/* Fallback to a large tree emoji if no image provided */} | ||
| <div className="flex h-full w-full items-center justify-center text-[10rem]"> | ||
| 🌳 | ||
| </div> | ||
| </div> | ||
| <SkillRoadmap skills={userStatus.skillRoadmap} /> |
There was a problem hiding this comment.
PR説明では「SkillTree関連の変更をロールバック」と記載されていますが、実際のコード変更では、ツリー絵文字のプレースホルダーを削除してSkillRoadmapコンポーネントを追加しています。これは「ロールバック」ではなく、「SkillRoadmapコンポーネントの復元」または「SkillRoadmapコンポーネントの実装」と表現する方が正確です。説明と実装の不一致にご注意ください。

close #48
実装の概要
実習メニュー画面を更新し、SkillTree関連の変更をロールバックしました。
🔧 技術的な意思決定とトレードオフ (最重要)
採用したアプローチ
ExerciseMenu.tsx) を 3x3 のグリッドレイアウトに変更SkillTreeDisplayコンポーネントを削除し、DashboardContainerを元の状態に戻した却下したアプローチ(代替案)
🧪 テスト戦略と範囲
追加したテストケース
セキュリティに関する自己評価
レビュワー(人間)への申し送り事項
実習メニューの画像やラベルが正しいか確認してください。