diff --git a/package-lock.json b/package-lock.json index f1b10a9..cc3fb11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tooltip": "^1.2.7", + "@recharts/devtools": "^0.0.7", "@supabase/auth-helpers-nextjs": "^0.10.0", "@supabase/ssr": "^0.6.1", "@supabase/supabase-js": "^2.50.0", @@ -2115,6 +2116,17 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, + "node_modules/@recharts/devtools": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@recharts/devtools/-/devtools-0.0.7.tgz", + "integrity": "sha512-ud66rUf3FYf1yQLGSCowI50EQyC/rcZblvDgNvfUIVaEXyQtr5K2DFgwegziqbVclsVBQLTxyntVViJN5H4oWQ==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0", + "recharts": "^3.3.0" + } + }, "node_modules/@reduxjs/toolkit": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz", diff --git a/package.json b/package.json index 4a683c1..abdbbd7 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tooltip": "^1.2.7", + "@recharts/devtools": "^0.0.7", "@supabase/auth-helpers-nextjs": "^0.10.0", "@supabase/ssr": "^0.6.1", "@supabase/supabase-js": "^2.50.0", diff --git a/src/app/api/v2/quiz/history/route.ts b/src/app/api/v2/quiz/history/route.ts index fe5f9a5..572eb91 100644 --- a/src/app/api/v2/quiz/history/route.ts +++ b/src/app/api/v2/quiz/history/route.ts @@ -22,7 +22,8 @@ export async function GET(request: NextRequest) { try { const supabase = await createClient(); const { searchParams } = new URL(request.url); - const limit = parseInt(searchParams.get('limit') || '3'); + const limitParam = searchParams.get('limit'); + const limit = limitParam ? parseInt(limitParam) : null; const { data: { user }, error: userError, @@ -35,7 +36,7 @@ export async function GET(request: NextRequest) { ); } - const { data: quizHistoryData, error: quizHistoryError } = await supabase + let query = supabase .from('user_quiz_sessions') .select( ` @@ -50,8 +51,13 @@ export async function GET(request: NextRequest) { ) ` ) - .eq('user_id', user.id) - .limit(limit); + .eq('user_id', user.id); + + if (limit) { + query = query.limit(limit); + } + + const { data: quizHistoryData, error: quizHistoryError } = await query; if (quizHistoryError) { throw quizHistoryError; diff --git a/src/features/mypage/ClientMyPage.tsx b/src/features/mypage/ClientMyPage.tsx index a8f0cfd..8582043 100644 --- a/src/features/mypage/ClientMyPage.tsx +++ b/src/features/mypage/ClientMyPage.tsx @@ -1,28 +1,51 @@ 'use client'; +import { useQuery } from '@tanstack/react-query'; import { toast } from 'sonner'; import { useUser } from '@/hooks/useUser'; + import RequireLogin from '@/components/RequireLogin'; import Bookmark from './Bookmark'; import LearningProgress from './LearningProgress'; import QuizHistory from './QuizHistory'; -// import { Button } from '@/components/ui/button'; -import { - Card, - CardContent, - // CardDescription, - // CardHeader, - // CardTitle, -} from '@/components/ui/card'; -import { - // Settings, - Trophy, - BookOpen, - TrendingUp, - Star, -} from 'lucide-react'; +import { QuizType } from '@/types/quiz'; + +import { Card, CardContent } from '@/components/ui/card'; +import { Trophy, BookOpen, TrendingUp, Star } from 'lucide-react'; +import QuizScoreChart from './QuizScoreChart'; + +async function fetchQuizHistorys() { + const response = await fetch(`/api/v2/quiz/history`, { + method: 'GET', + }); + + if (!response.ok) { + const errorResponse = await response.json(); + throw new Error( + errorResponse?.error || '퀴즈 내역을 불러오는데 실패했습니다.' + ); + } + + const { quizHistory, totalCount } = await response.json(); + + return { quizHistory, totalCount } as { + quizHistory: QuizType[]; + totalCount: number; + }; +} + export default function ClientMyPage() { const { data: user, error: getUserError } = useUser(); + const { data, isLoading, error } = useQuery({ + queryKey: ['quizHistory'], + queryFn: () => fetchQuizHistorys(), + staleTime: 1000 * 60, + gcTime: 1000 * 60 * 5, + retry: 2, + refetchInterval: 1000 * 60, + }); + + const { quizHistory = [], totalCount = 0 } = data || {}; if (!user) { toast.error('로그인이 필요합니다'); @@ -36,7 +59,15 @@ export default function ClientMyPage() {
- + +
+
+
diff --git a/src/features/mypage/QuizHistory.tsx b/src/features/mypage/QuizHistory.tsx index aa3c832..3d56bf6 100644 --- a/src/features/mypage/QuizHistory.tsx +++ b/src/features/mypage/QuizHistory.tsx @@ -2,49 +2,20 @@ import { Trophy, Award, Star } from 'lucide-react'; import { formatDuration, formatDate } from '@/lib/utils'; -import DashboardCard, { DashboardCardItem } from './DashboardCard'; -import { useQuery } from '@tanstack/react-query'; - -interface QuizType extends DashboardCardItem { - id: string; - level: number; - total_questions: number; - correct_count: number; - duration: number; - created_at: string; -} - -async function fetchQuizHistorys(limit: number) { - const response = await fetch(`/api/v2/quiz/history?limit=${limit}`, { - method: 'GET', - }); - - if (!response.ok) { - const errorResponse = await response.json(); - throw new Error( - errorResponse?.error || '퀴즈 내역을 불러오는데 실패했습니다.' - ); - } - - const { quizHistory, totalCount } = await response.json(); - - return { quizHistory, totalCount } as { - quizHistory: QuizType[]; - totalCount: number; - }; -} -export default function QuizHistory({ limit = 3 }: { limit?: number }) { - const { data, isLoading, error } = useQuery({ - queryKey: ['quizHistory', limit], - queryFn: () => fetchQuizHistorys(limit), - staleTime: 1000 * 60, - gcTime: 1000 * 60 * 5, - retry: 2, - refetchInterval: 1000 * 60, - }); - - const { quizHistory = [], totalCount = 0 } = data || {}; +import { QuizType } from '@/types/quiz'; +import DashboardCard from './DashboardCard'; +export default function QuizHistory({ + quizHistory, + totalCount, + isLoading, + error, +}: { + quizHistory: QuizType[]; + totalCount: number; + isLoading: boolean; + error: Error | null; +}) { const renderQuizItem = (quiz: QuizType, index: number) => (
+ new Date(a.created_at).getTime() - new Date(b.created_at).getTime() + ) + .map((quiz) => ({ + name: new Date(quiz.created_at).toLocaleDateString('ko-KR', { + month: 'numeric', + day: 'numeric', + }), + score: Math.round((quiz.correct_count / quiz.total_questions) * 100), + level: quiz.level, + })); + + return ( +
+ 퀴즈 성적 분석 +
+ + + + + + [`${value}점`, `${props.payload.level}급`]} + labelStyle={{ color: '#333' }} + /> + + + + + +
+
+ ); +} diff --git a/src/features/word/ClientWordList.tsx b/src/features/word/ClientWordList.tsx index 096eacd..22fb031 100644 --- a/src/features/word/ClientWordList.tsx +++ b/src/features/word/ClientWordList.tsx @@ -4,8 +4,7 @@ import { useEffect, useState } from 'react'; import Link from 'next/link'; import { supabase } from '@/lib/supabase/client'; import { toast } from 'sonner'; -// import { Label } from '@/components/ui/label'; -// import { Switch } from '@/components/ui/switch'; + import { Card, CardContent, @@ -13,7 +12,6 @@ import { CardHeader, CardTitle, } from '@/components/ui/card'; -// import ErrorFallback from '@/components/ErrorFallback'; interface WordType { id: string; @@ -35,9 +33,6 @@ export default function ClientWordList({ level: number; }) { const [allWords, setAllWords] = useState([...wordList]); - // const [newWords, setNewWords] = useState([]); - // const [showNewOnly, setShowNewOnly] = useState(false); - // const [hasLoadedNewWords, setHasLoadedNewWords] = useState(false); // 누적된 급수의 단어 로드 useEffect(() => { @@ -59,46 +54,9 @@ export default function ClientWordList({ fetchAllWords(); }, [level]); - // TODO: 다른 방법 찾아보기 - // 신규 단어만 로드 (eq) - Switch ON 시에만 - // useEffect(() => { - // if (showNewOnly && !hasLoadedNewWords) { - // const fetchNewWords = async () => { - // const { data, error } = await supabase - // .from('words') - // .select('*') - // .eq('level', level) - // .range(0, 999); - // - // if (error) { - // console.error('신규 단어 조회 실패:', error); - // toast.error('신규 단어 조회 실패. 다시 시도해주세요.'); - // } else { - // setNewWords(data || []); - // setHasLoadedNewWords(true); - // } - // }; - // - // fetchNewWords(); - // } - // }, [showNewOnly, level, hasLoadedNewWords]); - return ( <> - {/* 데이터 핸들링이 힘들어서 삭제 필요 */} - {/*
- - -
*/}
- {/* {(showNewOnly ? newWords : allWords).map((word, index) => ( */} {allWords.map((word, index) => ( diff --git a/src/types/quiz.ts b/src/types/quiz.ts index d9f8b4b..9f76884 100644 --- a/src/types/quiz.ts +++ b/src/types/quiz.ts @@ -48,3 +48,13 @@ export interface QuestionData extends QuizData { question_text: QuizData['question']; meaning: QuizData['translation']; } + +export interface QuizType { + id: string; + level: number; + total_questions: number; + correct_count: number; + duration: number; + created_at: string; + [key: string]: unknown; +}