Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 10 additions & 4 deletions src/app/api/v2/quiz/history/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
`
Expand All @@ -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;
Expand Down
63 changes: 47 additions & 16 deletions src/features/mypage/ClientMyPage.tsx
Original file line number Diff line number Diff line change
@@ -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('로그인이 필요합니다');
Expand All @@ -36,7 +59,15 @@ export default function ClientMyPage() {
<LearningProgress />
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Bookmark />
<QuizHistory />
<QuizHistory
quizHistory={quizHistory}
totalCount={totalCount}
isLoading={isLoading}
error={error}
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<QuizScoreChart quizHistory={quizHistory} />
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="text-center">
Expand Down
55 changes: 13 additions & 42 deletions src/features/mypage/QuizHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => (
<div
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg h-[72px]"
Expand Down
73 changes: 73 additions & 0 deletions src/features/mypage/QuizScoreChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from 'recharts';
import { RechartsDevtools } from '@recharts/devtools';
import { QuizType } from '@/types/quiz';

export default function QuizScoreChart({
quizHistory,
}: {
quizHistory: QuizType[];
}) {
// 차트 데이터 변환: 날짜순 정렬 및 점수 계산
const chartData = [...quizHistory]
.sort(
(a, b) =>
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 (
<div className="w-full max-w-[700px] h-[70vh] max-h-[500px] flex flex-col gap-4">
<span className="text-lg font-bold">퀴즈 성적 분석</span>
<div className="w-full flex-1 min-h-0">
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={chartData}
margin={{
top: 5,
right: 20,
left: 0,
bottom: 5,
}}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis domain={[0, 100]} />
<Tooltip
formatter={(
value: number,
name: string,
props: { payload: { level: number } }
) => [`${value}점`, `${props.payload.level}급`]}
labelStyle={{ color: '#333' }}
/>
<Legend />
<Line
type="monotone"
dataKey="score"
name="점수"
stroke="#8884d8"
activeDot={{ r: 8 }}
/>
<RechartsDevtools />
</LineChart>
</ResponsiveContainer>
</div>
</div>
);
}
44 changes: 1 addition & 43 deletions src/features/word/ClientWordList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,14 @@ 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,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
// import ErrorFallback from '@/components/ErrorFallback';

interface WordType {
id: string;
Expand All @@ -35,9 +33,6 @@ export default function ClientWordList({
level: number;
}) {
const [allWords, setAllWords] = useState<WordType[]>([...wordList]);
// const [newWords, setNewWords] = useState<WordType[]>([]);
// const [showNewOnly, setShowNewOnly] = useState(false);
// const [hasLoadedNewWords, setHasLoadedNewWords] = useState(false);

// 누적된 급수의 단어 로드
useEffect(() => {
Expand All @@ -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 (
<>
{/* 데이터 핸들링이 힘들어서 삭제 필요 */}
{/* <div className="flex items-center space-x-2 mb-5">
<Switch
id="new-word-mode"
checked={showNewOnly}
onCheckedChange={setShowNewOnly}
/>
<Label className="text-lg" htmlFor="new-word-mode">
{level}급 신규 단어만 보기
{showNewOnly && `(${newWords.length}개)`}
</Label>
</div> */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5">
{/* {(showNewOnly ? newWords : allWords).map((word, index) => ( */}
{allWords.map((word, index) => (
<Link key={`${word.id}${index}`} href={`/word/${level}/${word.id}`}>
<Card className="hover:bg-sky-100">
Expand Down
10 changes: 10 additions & 0 deletions src/types/quiz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Loading