Skip to content
Merged
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
2 changes: 1 addition & 1 deletion app/(main)/my-page/scraps/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Link from "next/link";
import { ArrowLeft } from "lucide-react";
import { ScrappedPostsList } from "@/components/features/my-page/ScrappedPostsList";
import { ScrappedPostsList } from "@/components/features/my-page/scraps/ScrappedPostsList";

export default function ScrapsPage() {
return (
Expand Down
26 changes: 26 additions & 0 deletions app/(main)/my-page/wrong-quizzes/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Link from "next/link";
import { ArrowLeft } from "lucide-react";
import { WrongQuizList } from "@/components/features/my-page/quizzes/WrongQuizList";
import { fetchMyWrongQuizzes } from "@/lib/mock/my-page-wrong-quizzes";

export default async function WrongQuizzesPage() {
const quizzes = await fetchMyWrongQuizzes();

return (
<div className="mx-auto max-w-5xl space-y-6 px-4 py-8 lg:px-8">
<Link
href="/my-page"
className="group/back inline-flex w-fit items-center gap-1.5 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground"
>
<ArrowLeft className="h-4 w-4 transition-transform group-hover/back:-translate-x-0.5" />
마이페이지
</Link>

<h1 className="text-xl font-bold tracking-[-0.01em] text-foreground md:text-2xl">
틀린 퀴즈들
</h1>

<WrongQuizList quizzes={quizzes} />
</div>
);
}
8 changes: 3 additions & 5 deletions components/features/my-page/MyPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import Link from "next/link";
import { ChevronRight } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton";
import { ScrappedPostsSection } from "./ScrappedPostsSection";
import { ScrappedPostsSection } from "./scraps/ScrappedPostsSection";
import { WrongQuizSection } from "./quizzes/WrongQuizSection";

function SectionHeader({ title, href }: { title: string; href?: string }) {
return (
Expand Down Expand Up @@ -48,10 +49,7 @@ export default function MyPage() {
<ScrappedPostsSection />

{/* 2. 틀린 퀴즈들 */}
<section>
<SectionHeader title="틀린 퀴즈들" />
<SkeletonCards />
</section>
<WrongQuizSection />

{/* 3. 추천 */}
<section className="space-y-8">
Expand Down
45 changes: 45 additions & 0 deletions components/features/my-page/quizzes/WrongQuizCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Link from "next/link";
import { formatDate } from "@/lib/utils";
import type { MyPageQuizHistory } from "@/types/myPage";
import type { QuizLevel } from "@/types/quiz";

const LEVEL_LABEL: Record<QuizLevel, string> = {
BEGINNER: "입문",
JUNIOR: "초급",
MIDDLE: "중급",
SENIOR: "고급",
};

export function LevelBadge({ level }: { level: QuizLevel }) {
return (
<span className="rounded-[5px] bg-muted px-1.5 py-0.5 text-[11px] text-muted-foreground font-medium">
{LEVEL_LABEL[level]}
</span>
);
}

export function WrongQuizCard({ quiz }: { quiz: MyPageQuizHistory }) {
const { contentId, contentTitle, level, score, totalQuestions, attemptedAt } =
quiz;

return (
<Link href={`/home/${contentId}/quiz/result`}>
<div className="group flex h-full flex-col rounded-xl border border-border bg-card p-3">
<p className="line-clamp-2 flex-1 text-sm font-semibold leading-snug tracking-[-0.01em] text-foreground">
{contentTitle}
</p>

<div className="mt-3 flex items-center justify-between text-xs">
<span className="text-muted-foreground">
{LEVEL_LABEL[level]}
<span className="mx-1">·</span>
<span className="font-medium text-red-500">
{totalQuestions - score}개 틀림
</span>
</span>
<span className="text-muted-foreground">{formatDate(attemptedAt)}</span>
</div>
</div>
</Link>
);
}
66 changes: 66 additions & 0 deletions components/features/my-page/quizzes/WrongQuizList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"use client";

import { useState, useMemo } from "react";
import { ChevronDown } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { WrongQuizListItem } from "./WrongQuizListItem";
import type { MyPageQuizHistory } from "@/types/myPage";

type SortOrder = "newest" | "oldest";

export function WrongQuizList({ quizzes }: { quizzes: MyPageQuizHistory[] }) {
const [sort, setSort] = useState<SortOrder>("newest");

const sorted = useMemo(() => {
return [...quizzes].sort((a, b) => {
const diff =
new Date(a.attemptedAt).getTime() - new Date(b.attemptedAt).getTime();
return sort === "newest" ? -diff : diff;
});
}, [quizzes, sort]);

return (
<div>
<div className="mb-4 flex items-center justify-between">
<span className="text-sm text-muted-foreground">{sorted.length}개</span>
<DropdownMenu modal={false}>
<DropdownMenuTrigger className="flex h-8 w-24 cursor-pointer items-center justify-between rounded-md border border-border bg-background px-3 text-sm text-foreground focus:outline-none">
{sort === "newest" ? "최신순" : "오래된순"}
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-[6rem] p-1">
<DropdownMenuRadioGroup
value={sort}
onValueChange={(v) => setSort(v as SortOrder)}
>
<DropdownMenuRadioItem className="cursor-pointer" value="newest">
최신순
</DropdownMenuRadioItem>
<DropdownMenuRadioItem className="cursor-pointer" value="oldest">
오래된순
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>

{sorted.length === 0 ? (
<p className="py-10 text-center text-sm text-muted-foreground">
틀린 퀴즈가 없습니다.
</p>
) : (
<div className="divide-y divide-border">
{sorted.map((quiz) => (
<WrongQuizListItem key={quiz.attemptId} quiz={quiz} />
))}
</div>
)}
</div>
);
}
55 changes: 55 additions & 0 deletions components/features/my-page/quizzes/WrongQuizListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Link from "next/link";
import Image from "next/image";
import { BookOpen } from "lucide-react";
import { formatDate } from "@/lib/utils";
import type { MyPageQuizHistory } from "@/types/myPage";
import type { QuizLevel } from "@/types/quiz";

const LEVEL_LABEL: Record<QuizLevel, string> = {
BEGINNER: "입문",
JUNIOR: "초급",
MIDDLE: "중급",
SENIOR: "고급",
};

export function WrongQuizListItem({ quiz }: { quiz: MyPageQuizHistory }) {
const { contentId, contentTitle, thumbnail, preview, level, score, totalQuestions, attemptedAt } =
quiz;

return (
<Link
href={`/home/${contentId}/quiz/result`}
className="-mx-2 flex gap-4 rounded-lg px-2 py-3 transition-colors"
>
<div className="relative aspect-[3/2] w-36 shrink-0 overflow-hidden rounded-sm bg-muted">
{thumbnail ? (
<Image fill src={thumbnail} alt={contentTitle} className="object-cover" />
) : (
<div className="flex h-full w-full items-center justify-center">
<BookOpen className="h-5 w-5 text-muted-foreground/30" />
</div>
)}
</div>

<div className="flex min-w-0 flex-1 flex-col gap-1">
<p className="text-[15px] font-semibold leading-snug tracking-[-0.01em] text-foreground">
{contentTitle}
</p>

{preview && (
<p className="line-clamp-1 text-xs text-muted-foreground">{preview}</p>
)}

<span className="mt-auto text-xs text-muted-foreground">
{LEVEL_LABEL[level]}
<span className="mx-1">·</span>
<span className="font-medium text-red-500">
{totalQuestions - score}개 틀림
</span>
<span className="mx-1">·</span>
{formatDate(attemptedAt)}
</span>
</div>
</Link>
);
}
62 changes: 62 additions & 0 deletions components/features/my-page/quizzes/WrongQuizSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"use client";

import { useEffect, useState } from "react";
import Link from "next/link";
import { ArrowRight } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton";
import { WrongQuizCard } from "./WrongQuizCard";
import { fetchMyWrongQuizzesPreview } from "@/lib/mock/my-page-wrong-quizzes";
import type { MyPageQuizHistory } from "@/types/myPage";

export function WrongQuizSection() {
const [quizzes, setQuizzes] = useState<MyPageQuizHistory[]>([]);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
fetchMyWrongQuizzesPreview(4).then((data) => {
setQuizzes(data);
setIsLoading(false);
});
}, []);

return (
<section>
<div className="mb-4 flex items-center justify-between">
<h2 className="text-base font-semibold text-foreground">틀린 퀴즈들</h2>
<Link
href="/my-page/wrong-quizzes"
className="flex items-center gap-0.5 text-sm text-muted-foreground transition-colors hover:text-foreground"
>
<ArrowRight className="h-4 w-4" />
전체 보기
</Link>
</div>

{isLoading ? (
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<div
key={i}
className="flex flex-col rounded-xl border border-border bg-card p-3"
>
<Skeleton className="mb-1 h-3.5 w-full rounded" />
<Skeleton className="h-3.5 w-4/5 rounded" />
<div className="mt-3 flex items-center justify-between">
<Skeleton className="h-3 w-20 rounded" />
<Skeleton className="h-3 w-14 rounded" />
</div>
</div>
))}
</div>
) : quizzes.length === 0 ? (
<p className="text-sm text-muted-foreground">틀린 퀴즈가 없습니다.</p>
) : (
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
{quizzes.map((quiz) => (
<WrongQuizCard key={quiz.attemptId} quiz={quiz} />
))}
</div>
)}
</section>
);
}
Loading
Loading