-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat/#86] 퀴즈 페이지 구현 #88
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
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📋 Walkthrough새로운 퀴즈 페이지 기능을 구현합니다. 세 단계 흐름(카테고리 선택 → 퀴즈 세션 → 결과)을 지원하며, 카테고리별 질문 생성, 사용자 답변 추적, 결과 계산 로직을 포함합니다. 기존 내비게이션에 퀴즈 메뉴를 추가하고 재사용 가능한 GradientButton 컴포넌트를 도입합니다. 📝 Changes
🔄 Sequence Diagram(s)sequenceDiagram
participant User
participant QuizPage as Quiz Page<br/>(Main)
participant CategorySel as CategorySelection<br/>Component
participant QuizSes as QuizSession<br/>Component
participant QuizRes as QuizResult<br/>Component
participant QuizLib as Quiz Library<br/>(generateQuizQuestions,<br/>calculateQuizResult)
User->>QuizPage: 퀴즈 페이지 접속
QuizPage->>CategorySel: stage = "category" 렌더링
User->>CategorySel: 카테고리 선택 및 "퀴즈 시작하기" 클릭
CategorySel->>QuizLib: generateQuizQuestions(category, count)
QuizLib-->>CategorySel: QuizQuestion[] 반환
CategorySel->>QuizPage: onCategorySelect(category, questions) 호출
QuizPage->>QuizPage: state 업데이트<br/>(selectedCategory, questions)
QuizPage->>QuizSes: stage = "quiz" 렌더링
Note over QuizPage,QuizSes: questions & category 전달
User->>QuizSes: 문제 풀이 진행 (선택지 클릭 및 다음)
QuizSes->>QuizSes: 현재 답변 저장<br/>userAnswers 배열 추적
User->>QuizSes: 마지막 문제에서 "다음" 클릭
QuizSes->>QuizLib: calculateQuizResult(questions, userAnswers)
QuizLib-->>QuizSes: QuizResult 반환
QuizSes->>QuizPage: onComplete(result) 호출
QuizPage->>QuizPage: state 업데이트<br/>(result, stage = "result")
QuizPage->>QuizRes: stage = "result" 렌더링
Note over QuizPage,QuizRes: result & category 전달
User->>QuizRes: 결과 확인 및 오답 노트 조회
User->>QuizRes: "다시 풀기" 클릭
QuizRes->>QuizLib: generateQuizQuestions(category, count)
QuizLib-->>QuizRes: 새로운 QuizQuestion[] 반환
QuizRes->>QuizPage: onRetry(newQuestions) 호출
QuizPage->>QuizPage: state 업데이트<br/>(questions, stage = "quiz")
QuizPage->>QuizSes: 새 문제로 다시 렌더링
User->>QuizRes: "처음으로" 클릭
QuizRes->>QuizPage: onRestart() 호출
QuizPage->>QuizPage: 모든 state 리셋
QuizPage->>CategorySel: stage = "category" 렌더링
🎯 Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~35 minutes 특히 주의가 필요한 영역:
🐰 Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
Actionable comments posted: 1
♻️ Duplicate comments (3)
src/contexts/ToastContext.tsx (1)
78-78: 동일한 커스텀 클래스 사용이 파일에서도
bg-linear-to-r커스텀 클래스를 사용하고 있습니다. SearchBar.tsx에서 언급한 것과 동일하게, Tailwind 설정에서 이 클래스가 제대로 정의되어 있는지 확인이 필요합니다.src/components/ui/buttons/GradientButton.tsx (1)
1-32: GradientButton 컴포넌트 구현 검토컴포넌트 구조가 잘 설계되어 있습니다. Props 타입이 명확하고, disabled 상태 처리도 적절합니다. 다만 Line 26의
bg-linear-to-r커스텀 클래스가 Tailwind 설정에 정의되어 있는지 확인이 필요합니다.src/components/quiz/CategorySelection.tsx (1)
123-123: 커스텀 Tailwind 클래스 확인 필요SearchBar.tsx에서 언급한 것과 동일하게,
bg-linear-to-r커스텀 클래스가 Tailwind 설정에 정의되어 있는지 확인이 필요합니다.
🧹 Nitpick comments (8)
src/app/quiz/page.tsx (1)
53-59: 빈 문제 배열에 대한 방어 로직 추가 권장
selectedCategory는 확인하지만questions배열이 비어있을 가능성을 검증하지 않습니다.generateQuizQuestions가 예외적으로 빈 배열을 반환하는 경우QuizSession컴포넌트에서 오류가 발생할 수 있습니다.다음과 같이 수정하여 방어 로직을 추가하세요:
-{stage === "quiz" && selectedCategory && ( +{stage === "quiz" && selectedCategory && questions.length > 0 && ( <QuizSession questions={questions} category={selectedCategory} onComplete={handleQuizComplete} /> )}src/components/quiz/CategorySelection.tsx (1)
15-28: 카테고리 배열 중앙화 검토카테고리 배열이 컴포넌트 내에 하드코딩되어 있습니다. 이미
@/components/ui/category/config에서 카테고리 설정을 가져오고 있으므로, 카테고리 목록도 해당 설정 파일에서 export하여 사용하는 것을 고려해보세요. 이렇게 하면 카테고리가 변경될 때 한 곳에서만 수정하면 됩니다.src/components/quiz/QuizResult.tsx (3)
32-35: 오답 인덱스 계산 시indexOf의존 대신 인덱스 보존 구조로 단순화 제안
wrongQuestions를result.questions.filter(...)로 만든 뒤, 렌더링 시에 다시result.questions.indexOf(question)로 원래 인덱스를 찾는 구조는:
- 매 오답마다
indexOf를 돌려 O(n²) 패턴이고- 객체 동일성에 의존해 약간 취약합니다. (현재 구현에서는 동작하지만, 이후 질문 배열이 가공되면 깨질 수 있음)
처음부터 인덱스를 함께 들고 다니면 더 단순하고 안전할 것 같습니다. 예시는 아래처럼 정리할 수 있습니다.
- const wrongQuestions = result.questions.filter( - (q, idx) => result.userAnswers[idx] !== q.correctAnswer - ); + const wrongQuestions = result.questions + .map((q, idx) => ({ question: q, index: idx })) + .filter( + ({ question, index }) => + result.userAnswers[index] !== question.correctAnswer + ); ... - {wrongQuestions.map((question, idx) => { - const originalIdx = result.questions.indexOf(question); - const userAnswer = result.userAnswers[originalIdx]; + {wrongQuestions.map(({ question, index: originalIdx }) => { + const userAnswer = result.userAnswers[originalIdx]; ... - <div - key={idx} + <div + key={question.term.id}이렇게 하면 인덱스 재계산이 필요 없고, key도 term id 기준으로 더 안정적으로 줄 수 있습니다.
Also applies to: 130-137
36-60: 틀린 문제 스크랩 시 진행 상태 표현 및 중복 클릭 방지 고려
handleScrapWrongTerms가 비동기 루프를 돌지만, 별도의 로딩 상태 없이 버튼이 항상 활성화되어 있어 빠른 연속 클릭 시 불필요한 중복 호출이 발생할 수 있습니다(내부isScraped체크로 실제 중복 스크랩은 막히더라도 호출 수는 늘어남).UX 관점에서:
- 로딩 중에는 버튼을 disable하거나
- "스크랩 중..." 등의 상태 텍스트를 보여주는
간단한 플래그(
isScrapping)를 두는 것도 고려해 볼 만합니다. 필수는 아니지만, 사용성 측면에서 개선 여지가 있습니다.Also applies to: 119-126
62-74:handleRetryand similar async handlers should resetisRetryingstate in afinallyblockCurrently,
setIsRetrying(false)is only called in the catch block. While the parent component (QuizPage) does unmount QuizResult immediately afteronRetrysucceeds, this creates a subtle dependency on parent behavior. Usingfinallyensures the loading state is always cleared regardless of success or failure, making the component more robust and preventing potential issues if the parent component behavior changes.const handleRetry = async () => { setIsRetrying(true); try { const newQuestions = await generateQuizQuestions( category, result.totalQuestions ); onRetry(newQuestions); - } catch { - showToast("퀴즈 생성에 실패했습니다.", "error"); - setIsRetrying(false); - } + } catch { + showToast("퀴즈 생성에 실패했습니다.", "error"); + } finally { + setIsRetrying(false); + } };This pattern appears in multiple locations (lines 62-74 and 189-195).
src/components/quiz/QuizSession.tsx (2)
84-88: '답변한 문제' 카운트에 현재 선택된 답변이 즉시 반영되지 않음
답변한 문제카운트는userAnswers.filter((a) => a !== null).length기준이라:
- 현재 문제에서 선택만 해둔 상태(아직 다음/이전으로 이동 전)에서는
- 사용자가 보기에는 답변을 했는데 카운트에는 반영되지 않는 시점이 생깁니다.
의도된 UX라면 그대로 두셔도 되지만, 선택 순간에 바로 반영되길 원한다면 예를 들어:
const baseAnswered = userAnswers.filter((a) => a !== null).length; const answered = baseAnswered + (selectedAnswer !== null && userAnswers[currentQuestionIndex] === null ? 1 : 0);처럼 현재 문항의 임시 선택을 포함해서 카운트를 계산하는 것도 한 가지 옵션입니다.
Also applies to: 36-45
141-175: 선택지 개수 4개 가정(A~D)에 대한 방어 코드 또는 주석 추가 제안
choiceLabel을["A", "B", "C", "D"][index]로 설정하고 있어, 현재generateQuizQuestions가 항상 4지선다를 반환한다는 전제를 두고 있습니다.지금 구현 상태에서는 전제가 맞지만:
- 향후 문제 타입을 늘려 선택지 수가 바뀌거나
- 데이터 쪽에서 잘못된 choices 길이를 넘겨줄 경우
레이블이
undefined로 표시될 수 있습니다. 아주 간단하게는:
choices.length !== 4일 때 개발용 콘솔 경고를 남기거나- 주석으로 "항상 4지선다를 기대한다"는 전제를 적어 두면
추후 유지보수 시 혼동을 줄일 수 있습니다.
src/lib/quiz.ts (1)
112-135:calculateQuizResult의 0문항 입력 방어 로직 추가 제안현재 구현은
score = Math.round((correctCount / questions.length) * 100);이라, 이 함수가 실수로 빈 배열(questions.length === 0)과 함께 호출되면0으로 나누기로NaN/Infinity가 나올 수 있습니다.일반 플로우상
generateQuizQuestions가 최소 4문항을 보장하므로 실제로는 잘 안 맞을 수 있지만, 유틸 함수 특성상 방어 로직을 한 줄 넣어두면 재사용성이 좋아집니다. 예를 들어:export function calculateQuizResult( questions: QuizQuestion[], userAnswers: (string | null)[] ): QuizResult { - let correctCount = 0; + if (questions.length === 0) { + return { + totalQuestions: 0, + correctAnswers: 0, + wrongAnswers: 0, + score: 0, + questions, + userAnswers, + }; + } + + let correctCount = 0;처럼 0문항일 때는 0점/0개로 명시적으로 처리해 두면, 향후 다른 곳에서 재사용하더라도 안전합니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
src/app/quiz/page.tsx(1 hunks)src/components/landing/CTASection.tsx(3 hunks)src/components/quiz/CategorySelection.tsx(1 hunks)src/components/quiz/QuizResult.tsx(1 hunks)src/components/quiz/QuizSession.tsx(1 hunks)src/components/search/SearchBar.tsx(1 hunks)src/components/term-detail/TabSection.tsx(1 hunks)src/components/ui/buttons/GradientButton.tsx(1 hunks)src/constants/navigation.ts(1 hunks)src/contexts/ToastContext.tsx(1 hunks)src/lib/quiz.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
src/components/term-detail/TabSection.tsx (1)
src/components/term-detail/tabs/RelatedTab.tsx (1)
RelatedTab(20-39)
src/components/quiz/QuizResult.tsx (4)
src/lib/quiz.ts (3)
QuizQuestion(11-16)QuizResult(18-25)generateQuizQuestions(42-107)src/contexts/AuthContext.tsx (1)
useAuth(269-275)src/contexts/ToastContext.tsx (1)
useToast(99-105)src/components/ui/buttons/GradientButton.tsx (1)
GradientButton(9-32)
src/app/quiz/page.tsx (4)
src/lib/quiz.ts (2)
QuizQuestion(11-16)QuizResult(18-25)src/components/quiz/CategorySelection.tsx (1)
CategorySelection(30-150)src/components/quiz/QuizSession.tsx (1)
QuizSession(22-203)src/components/quiz/QuizResult.tsx (1)
QuizResult(22-199)
src/components/quiz/QuizSession.tsx (2)
src/lib/quiz.ts (3)
QuizQuestion(11-16)QuizResult(18-25)calculateQuizResult(112-135)src/components/ui/buttons/GradientButton.tsx (1)
GradientButton(9-32)
src/components/quiz/CategorySelection.tsx (5)
src/lib/quiz.ts (2)
QuizQuestion(11-16)generateQuizQuestions(42-107)src/contexts/ToastContext.tsx (1)
useToast(99-105)src/app/onboarding/components/categoruButton.tsx (1)
CategoryButton(18-42)src/components/ui/buttons/GradientButton.tsx (1)
GradientButton(9-32)src/components/icons/ic_arrow_right.tsx (1)
ArrowRightIcon(3-25)
src/lib/quiz.ts (1)
src/lib/terms.ts (3)
TermIndexItem(6-16)getTermsIndex(90-105)getTermsByTag(140-143)
src/components/landing/CTASection.tsx (1)
src/components/ui/buttons/GradientButton.tsx (1)
GradientButton(9-32)
🔇 Additional comments (8)
src/components/term-detail/TabSection.tsx (1)
28-28: 코드 변경사항 확인 완료조건부 렌더링을 단일 라인으로 정리한 것은 적절합니다. 기능상 변경사항이 없으며 가독성도 유지됩니다.
src/constants/navigation.ts (1)
3-3: 퀴즈 네비게이션 항목 추가 확인새로운 퀴즈 메뉴 항목이 올바르게 추가되었습니다. 기존 패턴을 따르고 있으며 타입 안정성도 유지됩니다.
src/components/landing/CTASection.tsx (1)
31-43: GradientButton 컴포넌트 전환 완료네이티브 버튼을 새로운
GradientButton컴포넌트로 성공적으로 교체했습니다. 기존 기능이 유지되며 컴포넌트 재사용성이 향상되었습니다.src/app/quiz/page.tsx (1)
12-44: 퀴즈 페이지 상태 관리 로직 확인3단계 퀴즈 플로우의 상태 관리 로직이 명확하고 잘 구조화되어 있습니다. 각 핸들러가 적절히 상태를 업데이트하고 있습니다.
src/components/quiz/CategorySelection.tsx (1)
44-65: 퀴즈 시작 로직 및 에러 핸들링 확인카테고리 선택 검증과 에러 처리가 잘 구현되어 있습니다. 사용자에게 적절한 피드백을 제공하며, 로딩 상태도 올바르게 관리되고 있습니다.
src/lib/quiz.ts (2)
11-25: 퀴즈 도메인 모델링(QuizQuestion / QuizResult) 타입 정의가 명확합니다
QuizQuestion과QuizResult가 UI에서 필요한 필드를 딱 맞게 담고 있어서:
- 세션 진행(questions + 정답 문자열)과
- 결과 화면(total/correct/wrong/score + userAnswers)
양쪽에서 재사용하기 좋게 잘 설계된 것 같습니다. 이후 다른 퀴즈 화면이 생겨도 이 두 타입을 그대로 가져다 쓰기 좋아 보입니다.
42-64: Error handling for insufficient terms is already consistently implemented at both quiz creation points.The concern raised about potential missing error handling at the initial quiz start has been addressed:
CategorySelection.tsxalready wraps thegenerateQuizQuestionscall in a try/catch block (lines 52-64) that displays user-friendly error messages via toast, identical to the error handling inQuizResult.handleRetry. Both call sites showerr.messageor fallback text to the user, preventing console-only errors.Likely an incorrect or invalid review comment.
src/components/quiz/QuizSession.tsx (1)
16-20: This concern does not apply due to the conditional rendering architecture.QuizSession is unmounted whenever the stage leaves "quiz" (via conditional rendering in QuizPage). When retrying a quiz, the component is completely unmounted first (stage changes to "result"), then remounted (stage changes back to "quiz") with fresh props. The state cannot persist across prop changes because the component instance is destroyed and recreated.
The mounting lifecycle is: category selection → quiz (mount) → result (unmount) → retry (remount with new questions). No useEffect is needed for this pattern.
Likely an incorrect or invalid review comment.
| <div className="relative h-20 w-full"> | ||
| {/* 1. 아우라 레이어 (opacity 30 + blur + gradient border) */} | ||
| <div className="absolute top-0 left-0 h-20 w-[864px] rounded-[20px] border border-violet-700 bg-gradient-to-r from-violet-700 to-red-400 opacity-30 blur-sm"></div> | ||
| <div className="absolute top-0 left-0 h-20 w-[864px] rounded-[20px] border border-violet-700 bg-linear-to-r from-violet-700 to-red-400 opacity-30 blur-sm"></div> |
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.
🧩 Analysis chain
🏁 Script executed:
find . -name "tailwind.config.*" -o -name "tailwind.config.*"Repository: ButterWaffle-GotIT/GotIT
Length of output: 88
🏁 Script executed:
fd "tailwind" -e js -e ts -e jsonRepository: ButterWaffle-GotIT/GotIT
Length of output: 123
🏁 Script executed:
cat src/components/search/SearchBar.tsx | head -50Repository: ButterWaffle-GotIT/GotIT
Length of output: 1731
🏁 Script executed:
cat tailwind.config.jsRepository: ButterWaffle-GotIT/GotIT
Length of output: 979
bg-linear-to-r is not defined and will not work
bg-linear-to-r is neither a standard Tailwind utility nor defined in tailwind.config.js. This class name appears to be a typo. Change it to bg-gradient-to-r (the standard Tailwind gradient utility) or define bg-linear-to-r as a custom utility if intentionally different styling is needed.
🤖 Prompt for AI Agents
In src/components/search/SearchBar.tsx around line 23, the class
"bg-linear-to-r" is invalid (typo) and won't apply a Tailwind gradient; replace
it with the standard "bg-gradient-to-r" class (or, if the different name was
intentional, add a custom utility in tailwind.config.js). Update the className
string accordingly so the element uses the correct Tailwind gradient utility.
✨ 작업 개요
퀴즈 페이지 구현
📌 관련 이슈
✅ 작업 내용
퀴즈 페이지 구현
📷 UI 스크린샷 (해당 시)
💬 기타 사항
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선사항
✏️ Tip: You can customize this high-level summary in your review settings.