diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx new file mode 100644 index 0000000..55c5cb8 --- /dev/null +++ b/src/app/dashboard/page.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { IndexedDbProgressRepository } from "@/repositories/ProgressRepository"; +import type { DashboardSummary } from "@/types/progress"; + +const progressRepository = new IndexedDbProgressRepository(); + +function formatPercent(ratio: number): string { + return `${Math.round(ratio * 100)}%`; +} + +export default function DashboardPage() { + const [summary, setSummary] = useState(null); + const [error, setError] = useState(false); + + useEffect(() => { + progressRepository.getDashboardSummary().then( + (result) => setSummary(result), + (err) => { + console.error("getDashboardSummary failed:", err); + setError(true); + } + ); + }, []); + + if (error) { + return ( +

+ 학습 기록을 불러오지 못했다. 다시 시도해달라. +

+ ); + } + + if (!summary) { + return

불러오는 중...

; + } + + return ( +
+

대시보드

+
+
+ 오늘 풀이수 + {summary.todayCount} +
+
+ 오늘 정답률 + {formatPercent(summary.todayAccuracy)} +
+
+ 전체 풀이수 + {summary.totalCount} +
+
+ 전체 정답률 + {formatPercent(summary.totalAccuracy)} +
+
+
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 976eb90..1419d46 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; +import { NavBar } from "@/features/nav/NavBar"; import "./globals.css"; const geistSans = Geist({ @@ -13,8 +14,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "PassFlow", + description: "정보처리기사 필기 CBT 웹앱", }; export default function RootLayout({ @@ -24,10 +25,13 @@ export default function RootLayout({ }>) { return ( - {children} + + + {children} + ); } diff --git a/src/app/review/page.tsx b/src/app/review/page.tsx index ac840d9..be9ff10 100644 --- a/src/app/review/page.tsx +++ b/src/app/review/page.tsx @@ -125,10 +125,11 @@ export default function ReviewPage() { setQuestions((prev) => prev.filter((q) => q.questionId !== questionId)); } - async function handleRetryAll() { + async function handleRetry(selectedQuestions: Question[]) { + if (selectedQuestions.length === 0) return; try { const theoryMap = await questionRepository.getTheoryMap(); - setPhase({ kind: "active", questions, theoryMap }); + setPhase({ kind: "active", questions: selectedQuestions, theoryMap }); } catch { setPhase({ kind: "error", message: "관련 이론 데이터를 불러오지 못했다. 다시 시도해달라." }); } @@ -202,7 +203,7 @@ export default function ReviewPage() { questions={questions} emptyMessage={EMPTY_MESSAGE[tab]} onRemove={tab === "recent" ? undefined : handleRemove} - onRetryAll={handleRetryAll} + onRetry={handleRetry} /> )} diff --git a/src/features/nav/NavBar.tsx b/src/features/nav/NavBar.tsx new file mode 100644 index 0000000..a6db5b8 --- /dev/null +++ b/src/features/nav/NavBar.tsx @@ -0,0 +1,30 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +const LINKS = [ + { href: "/", label: "홈" }, + { href: "/practice", label: "문제풀이" }, + { href: "/review", label: "복습" }, + { href: "/dashboard", label: "대시보드" }, +] as const; + +export function NavBar() { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/src/features/practice/PracticeSession.tsx b/src/features/practice/PracticeSession.tsx index fd93c73..ee40639 100644 --- a/src/features/practice/PracticeSession.tsx +++ b/src/features/practice/PracticeSession.tsx @@ -83,6 +83,8 @@ export function PracticeSession({ questions, theoryMap, onFinish }: PracticeSess goTo(current + 1); } else if (e.key === "ArrowLeft") { goTo(current - 1); + } else if (e.key === "f" || e.key === "F") { + toggleFavorite(); } } window.addEventListener("keydown", handleKeyDown); @@ -119,7 +121,7 @@ export function PracticeSession({ questions, theoryMap, onFinish }: PracticeSess > ← 이전 - Space: 다음 · 1~4: 답 선택 + Space: 다음 · 1~4: 답 선택 · F: 즐겨찾기 {current === questions.length - 1 ? ( +
+ + +