diff --git a/components/features/my-page/MyPage.tsx b/components/features/my-page/MyPage.tsx index c70585b..cab0358 100644 --- a/components/features/my-page/MyPage.tsx +++ b/components/features/my-page/MyPage.tsx @@ -6,7 +6,7 @@ import { RecommendedSection } from "./recommend/RecommendedSection"; export default function MyPage() { return ( -
+

마이페이지 diff --git a/components/features/my-page/MyPagePagination.tsx b/components/features/my-page/MyPagePagination.tsx new file mode 100644 index 0000000..153fabf --- /dev/null +++ b/components/features/my-page/MyPagePagination.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface MyPagePaginationProps { + currentPage: number; + totalPages: number; + onPageChange: (page: number) => void; + className?: string; +} + +export function MyPagePagination({ + currentPage, + totalPages, + onPageChange, + className, +}: MyPagePaginationProps) { + if (totalPages <= 1) return null; + + const getPageNumbers = () => { + const pages: (number | "...")[] = []; + if (totalPages <= 7) { + return Array.from({ length: totalPages }, (_, i) => i + 1); + } + pages.push(1); + if (currentPage > 4) pages.push("..."); + const start = Math.max(2, currentPage - 2); + const end = Math.min(totalPages - 1, currentPage + 2); + for (let i = start; i <= end; i++) pages.push(i); + if (currentPage < totalPages - 3) pages.push("..."); + pages.push(totalPages); + return pages; + }; + + return ( +
+ + + {getPageNumbers().map((page, i) => + page === "..." ? ( + + … + + ) : ( + + ), + )} + + +
+ ); +} diff --git a/components/features/my-page/quizzes/WrongQuizList.tsx b/components/features/my-page/quizzes/WrongQuizList.tsx index 7729505..f2e5353 100644 --- a/components/features/my-page/quizzes/WrongQuizList.tsx +++ b/components/features/my-page/quizzes/WrongQuizList.tsx @@ -10,12 +10,16 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { WrongQuizListItem } from "./WrongQuizListItem"; +import { MyPagePagination } from "../MyPagePagination"; import type { MyPageQuizHistory } from "@/types/myPage"; +const PAGE_SIZE = 10; + type SortOrder = "newest" | "oldest"; export function WrongQuizList({ quizzes }: { quizzes: MyPageQuizHistory[] }) { const [sort, setSort] = useState("newest"); + const [currentPage, setCurrentPage] = useState(1); const sorted = useMemo(() => { return [...quizzes].sort((a, b) => { @@ -25,6 +29,12 @@ export function WrongQuizList({ quizzes }: { quizzes: MyPageQuizHistory[] }) { }); }, [quizzes, sort]); + const totalPages = Math.ceil(sorted.length / PAGE_SIZE); + const pagedItems = sorted.slice( + (currentPage - 1) * PAGE_SIZE, + currentPage * PAGE_SIZE, + ); + return (
@@ -37,7 +47,7 @@ export function WrongQuizList({ quizzes }: { quizzes: MyPageQuizHistory[] }) { setSort(v as SortOrder)} + onValueChange={(v) => { setSort(v as SortOrder); setCurrentPage(1); }} > 최신순 @@ -55,11 +65,19 @@ export function WrongQuizList({ quizzes }: { quizzes: MyPageQuizHistory[] }) { 틀린 퀴즈가 없습니다.

) : ( -
- {sorted.map((quiz) => ( - - ))} -
+ <> +
+ {pagedItems.map((quiz) => ( + + ))} +
+ + )}
); diff --git a/components/features/my-page/recommend/RecommendedBookList.tsx b/components/features/my-page/recommend/RecommendedBookList.tsx index 8b2a069..7751103 100644 --- a/components/features/my-page/recommend/RecommendedBookList.tsx +++ b/components/features/my-page/recommend/RecommendedBookList.tsx @@ -1,11 +1,14 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo } from "react"; import { Skeleton } from "@/components/ui/skeleton"; import { RecommendedBookListItem } from "./RecommendedBookListItem"; +import { MyPagePagination } from "../MyPagePagination"; import { fetchRecommendBooks } from "@/lib/mock/my-page-recommend-book"; import type { MyPageRecommendBook } from "@/types/myPage"; +const PAGE_SIZE = 10; + function ListItemSkeleton() { return (
@@ -40,6 +43,7 @@ export function RecommendedBookList() { const [books, setBooks] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isError, setIsError] = useState(false); + const [currentPage, setCurrentPage] = useState(1); useEffect(() => { fetchRecommendBooks() @@ -48,6 +52,12 @@ export function RecommendedBookList() { .finally(() => setIsLoading(false)); }, []); + const totalPages = Math.ceil(books.length / PAGE_SIZE); + const pagedItems = useMemo( + () => books.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE), + [books, currentPage], + ); + if (isError) { return (

@@ -75,10 +85,18 @@ export function RecommendedBookList() { } return ( -

- {books.map((book) => ( - - ))} -
+ <> +
+ {pagedItems.map((book) => ( + + ))} +
+ + ); } diff --git a/components/features/my-page/recommend/RecommendedHomePostList.tsx b/components/features/my-page/recommend/RecommendedHomePostList.tsx index e473e11..26ff353 100644 --- a/components/features/my-page/recommend/RecommendedHomePostList.tsx +++ b/components/features/my-page/recommend/RecommendedHomePostList.tsx @@ -1,11 +1,14 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo } from "react"; import { Skeleton } from "@/components/ui/skeleton"; import { RecommendedHomePostListItem } from "./RecommendedHomePostListItem"; +import { MyPagePagination } from "../MyPagePagination"; import { fetchRecommendHomePosts } from "@/lib/mock/my-page-recommend-home"; import type { MyPageRecommendHomePost } from "@/types/myPage"; +const PAGE_SIZE = 10; + function ListItemSkeleton() { return (
@@ -24,6 +27,7 @@ export function RecommendedHomePostList() { const [posts, setPosts] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isError, setIsError] = useState(false); + const [currentPage, setCurrentPage] = useState(1); useEffect(() => { fetchRecommendHomePosts() @@ -32,6 +36,12 @@ export function RecommendedHomePostList() { .finally(() => setIsLoading(false)); }, []); + const totalPages = Math.ceil(posts.length / PAGE_SIZE); + const pagedItems = useMemo( + () => posts.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE), + [posts, currentPage], + ); + if (isError) { return (

@@ -59,10 +69,18 @@ export function RecommendedHomePostList() { } return ( -

- {posts.map((post) => ( - - ))} -
+ <> +
+ {pagedItems.map((post) => ( + + ))} +
+ + ); } diff --git a/components/features/my-page/recommend/RecommendedVideoList.tsx b/components/features/my-page/recommend/RecommendedVideoList.tsx index 6aeb714..645db31 100644 --- a/components/features/my-page/recommend/RecommendedVideoList.tsx +++ b/components/features/my-page/recommend/RecommendedVideoList.tsx @@ -1,11 +1,14 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo } from "react"; import { Skeleton } from "@/components/ui/skeleton"; import { RecommendedVideoListItem } from "./RecommendedVideoListItem"; +import { MyPagePagination } from "../MyPagePagination"; import { fetchRecommendVideos } from "@/lib/mock/my-page-recommend-video"; import type { MyPageRecommendVideo } from "@/types/myPage"; +const PAGE_SIZE = 10; + function ListItemSkeleton() { return (
@@ -23,6 +26,7 @@ export function RecommendedVideoList() { const [videos, setVideos] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isError, setIsError] = useState(false); + const [currentPage, setCurrentPage] = useState(1); useEffect(() => { fetchRecommendVideos() @@ -31,6 +35,12 @@ export function RecommendedVideoList() { .finally(() => setIsLoading(false)); }, []); + const totalPages = Math.ceil(videos.length / PAGE_SIZE); + const pagedItems = useMemo( + () => videos.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE), + [videos, currentPage], + ); + if (isError) { return (

@@ -58,10 +68,18 @@ export function RecommendedVideoList() { } return ( -

- {videos.map((video) => ( - - ))} -
+ <> +
+ {pagedItems.map((video) => ( + + ))} +
+ + ); } diff --git a/components/features/my-page/scraps/ScrappedPostsList.tsx b/components/features/my-page/scraps/ScrappedPostsList.tsx index f309ea9..7f65ce9 100644 --- a/components/features/my-page/scraps/ScrappedPostsList.tsx +++ b/components/features/my-page/scraps/ScrappedPostsList.tsx @@ -11,9 +11,12 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { ScrappedPostListItem } from "./ScrappedPostListItem"; +import { MyPagePagination } from "../MyPagePagination"; import { fetchMyScraps } from "@/lib/mock/my-page-scraps"; import type { MyPageScrap } from "@/types/myPage"; +const PAGE_SIZE = 10; + type SortOrder = "newest" | "oldest"; function ListItemSkeleton() { @@ -36,6 +39,7 @@ export function ScrappedPostsList() { const [isError, setIsError] = useState(false); const [query, setQuery] = useState(""); const [sort, setSort] = useState("newest"); + const [currentPage, setCurrentPage] = useState(1); useEffect(() => { fetchMyScraps() @@ -62,6 +66,12 @@ export function ScrappedPostsList() { }); }, [scraps, query, sort]); + const totalPages = Math.ceil(processed.length / PAGE_SIZE); + const pagedItems = processed.slice( + (currentPage - 1) * PAGE_SIZE, + currentPage * PAGE_SIZE, + ); + if (isLoading) { return (
@@ -93,7 +103,7 @@ export function ScrappedPostsList() { type="text" placeholder="검색" value={query} - onChange={(e) => setQuery(e.target.value)} + onChange={(e) => { setQuery(e.target.value); setCurrentPage(1); }} className="h-8 w-40 rounded-md border border-border bg-background pl-7 pr-3 text-sm placeholder:text-muted-foreground focus:outline-none" />
@@ -105,7 +115,7 @@ export function ScrappedPostsList() { setSort(v as SortOrder)} + onValueChange={(v) => { setSort(v as SortOrder); setCurrentPage(1); }} > 최신순 @@ -124,11 +134,19 @@ export function ScrappedPostsList() { {query ? "검색 결과가 없습니다." : "스크랩한 글이 없습니다."}

) : ( -
- {processed.map((scrap) => ( - - ))} -
+ <> +
+ {pagedItems.map((scrap) => ( + + ))} +
+ + )}
); diff --git a/lib/mock/my-page-recommend-book.ts b/lib/mock/my-page-recommend-book.ts index 97d6afe..cb07514 100644 --- a/lib/mock/my-page-recommend-book.ts +++ b/lib/mock/my-page-recommend-book.ts @@ -49,11 +49,108 @@ export const MOCK_RECOMMEND_BOOKS: MyPageRecommendBook[] = [ publisher: "위키북스", publishedAt: "2018-12-04", }, + { + bookId: "book-005", + title: "리팩터링 2판", + authors: ["마틴 파울러"], + description: + "코드를 더 깔끔하게 만드는 리팩터링 기법을 JavaScript 예제와 함께 설명한다.", + cover: "https://picsum.photos/seed/book5/300/400", + url: "https://www.yes24.com/Product/Goods/89649360", + price: 38000, + publisher: "한빛미디어", + publishedAt: "2020-04-01", + }, + { + bookId: "book-006", + title: "HTTP 완벽 가이드", + authors: ["데이빗 고울리", "브라이언 토티"], + description: + "웹의 근간이 되는 HTTP 프로토콜의 동작 원리와 최신 스펙을 상세히 다룬다.", + cover: null, + url: "https://www.yes24.com/Product/Goods/15381085", + price: 49000, + publisher: "인사이트", + publishedAt: "2014-12-15", + }, + { + bookId: "book-007", + title: "자바스크립트 딥 다이브", + authors: ["이웅모"], + description: + "자바스크립트의 동작 원리를 기초부터 깊이 있게 이해할 수 있는 국내 저술서.", + cover: "https://picsum.photos/seed/book7/300/400", + url: "https://www.yes24.com/Product/Goods/92742567", + price: 45000, + publisher: "위키북스", + publishedAt: "2020-09-25", + }, + { + bookId: "book-008", + title: "쏙쏙 들어오는 함수형 코딩", + authors: ["에릭 노먼드"], + description: + "함수형 프로그래밍 패러다임을 실용적인 관점에서 쉽게 설명하는 입문서.", + cover: "https://picsum.photos/seed/book8/300/400", + url: "https://www.yes24.com/Product/Goods/108748841", + price: 33000, + publisher: "제이펍", + publishedAt: "2022-11-30", + }, + { + bookId: "book-009", + title: "실용주의 프로그래머", + authors: ["데이비드 토머스", "앤드류 헌트"], + description: + "더 나은 개발자가 되기 위한 실질적인 조언을 담은 개발 철학서의 고전.", + cover: null, + url: "https://www.yes24.com/Product/Goods/107077663", + price: 35000, + publisher: "인사이트", + publishedAt: "2022-02-18", + }, + { + bookId: "book-010", + title: "그림으로 배우는 네트워크 원리", + authors: ["Gene"], + description: + "네트워크의 기초 개념을 풍부한 그림과 함께 쉽게 이해할 수 있도록 설명한다.", + cover: "https://picsum.photos/seed/book10/300/400", + url: "https://www.yes24.com/Product/Goods/104369152", + price: 24000, + publisher: "영진닷컴", + publishedAt: "2022-05-20", + }, + { + bookId: "book-011", + title: "Growing Object-Oriented Software, Guided by Tests", + authors: ["Steve Freeman", "Nat Pryce"], + description: + "TDD를 실전에서 적용하는 방법과 객체지향 설계 원칙을 함께 배울 수 있는 명저.", + cover: "https://picsum.photos/seed/book11/300/400", + url: "https://www.yes24.com/Product/Goods/3551451", + publisher: "Addison-Wesley", + publishedAt: "2009-10-12", + }, + { + bookId: "book-012", + title: "컴퓨터 과학이 보이는 그림책", + authors: ["아다치 마사유키"], + description: + "컴퓨터 과학의 핵심 개념을 그림으로 직관적으로 이해할 수 있도록 구성한 입문서.", + cover: null, + url: "https://www.yes24.com/Product/Goods/66028583", + price: 16000, + publisher: "성안당", + publishedAt: "2019-02-15", + }, ]; export async function fetchRecommendBooks( - count = 4, + count?: number, ): Promise { await new Promise((resolve) => setTimeout(resolve, 400)); - return MOCK_RECOMMEND_BOOKS.slice(0, count); + return count !== undefined + ? MOCK_RECOMMEND_BOOKS.slice(0, count) + : MOCK_RECOMMEND_BOOKS; } diff --git a/lib/mock/my-page-recommend-home.ts b/lib/mock/my-page-recommend-home.ts index b1733cd..c444594 100644 --- a/lib/mock/my-page-recommend-home.ts +++ b/lib/mock/my-page-recommend-home.ts @@ -32,11 +32,74 @@ export const MOCK_RECOMMEND_HOME_POSTS: MyPageRecommendHomePost[] = [ summary: "레인지/리스트/해시 파티셔닝을 실제 서비스 케이스에 적용한 경험을 공유합니다.", date: "2026-04-13T11:00:00Z", }, + { + contentId: "content-305", + title: "Jest + React Testing Library로 컴포넌트 테스트 작성하기", + sourceName: "medium", + thumbnail: "https://picsum.photos/seed/rec5/400/240", + summary: "단위 테스트부터 인터랙션 테스트까지 실전 패턴을 다룹니다.", + date: "2026-04-11T09:00:00Z", + }, + { + contentId: "content-306", + title: "Spring Boot 3 마이그레이션 가이드 — Jakarta EE 전환과 주요 변경점", + sourceName: "kakao_tech", + thumbnail: null, + summary: "Spring Boot 2에서 3으로 업그레이드할 때 반드시 확인해야 할 사항들을 정리합니다.", + date: "2026-04-09T14:00:00Z", + }, + { + contentId: "content-307", + title: "AWS Lambda + API Gateway로 서버리스 백엔드 구축하기", + sourceName: "velog", + thumbnail: "https://picsum.photos/seed/rec7/400/240", + date: "2026-04-07T10:00:00Z", + }, + { + contentId: "content-308", + title: "Tailwind CSS v4 완전 가이드 — @theme 토큰과 CSS 변수 전환", + sourceName: "toss_tech", + thumbnail: "https://picsum.photos/seed/rec8/400/240", + summary: "v3에서 v4로 마이그레이션하는 방법과 새 토큰 시스템을 설명합니다.", + date: "2026-04-05T11:00:00Z", + }, + { + contentId: "content-309", + title: "CI/CD 파이프라인 구축 — GitHub Actions로 자동 배포 환경 만들기", + sourceName: "naver_d2", + thumbnail: null, + summary: "테스트, 빌드, 배포를 자동화하는 전체 워크플로우를 구성하는 방법을 소개합니다.", + date: "2026-04-03T09:00:00Z", + }, + { + contentId: "content-310", + title: "웹 접근성 A11y 실천 가이드 — WCAG 2.1 기준으로 코드 개선하기", + sourceName: "medium", + thumbnail: "https://picsum.photos/seed/rec10/400/240", + date: "2026-04-01T13:00:00Z", + }, + { + contentId: "content-311", + title: "Storybook 8 업데이트 — CSF3 형식과 Play function 실전 활용", + sourceName: "kakao_tech", + thumbnail: "https://picsum.photos/seed/rec11/400/240", + summary: "Storybook 최신 버전의 핵심 변경점과 인터랙션 테스트 작성법을 살펴봅니다.", + date: "2026-03-30T10:00:00Z", + }, + { + contentId: "content-312", + title: "OpenTelemetry로 분산 추적 구현하기 — 마이크로서비스 관찰 가능성 확보", + sourceName: "toss_tech", + thumbnail: null, + date: "2026-03-27T14:00:00Z", + }, ]; export async function fetchRecommendHomePosts( - count = 4, + count?: number, ): Promise { await new Promise((resolve) => setTimeout(resolve, 400)); - return MOCK_RECOMMEND_HOME_POSTS.slice(0, count); + return count !== undefined + ? MOCK_RECOMMEND_HOME_POSTS.slice(0, count) + : MOCK_RECOMMEND_HOME_POSTS; } diff --git a/lib/mock/my-page-recommend-video.ts b/lib/mock/my-page-recommend-video.ts index b74cbbb..fb7240a 100644 --- a/lib/mock/my-page-recommend-video.ts +++ b/lib/mock/my-page-recommend-video.ts @@ -41,11 +41,93 @@ export const MOCK_RECOMMEND_VIDEOS: MyPageRecommendVideo[] = [ views: 34800, uploadedAt: "2026-04-07T00:00:00Z", }, + { + videoId: "vid-005", + title: "TypeScript 제네릭 완전 정복 — 실전 패턴 7가지", + channelName: "코딩애플", + thumbnail: "https://picsum.photos/seed/vid5/400/240", + url: "https://www.youtube.com/watch?v=example5", + duration: "22:15", + views: 78400, + uploadedAt: "2026-04-04T00:00:00Z", + }, + { + videoId: "vid-006", + title: "Redis 기초부터 실전까지 — 캐싱 전략과 세션 관리", + channelName: "드림코딩", + thumbnail: null, + url: "https://www.youtube.com/watch?v=example6", + duration: "35:40", + views: 52100, + uploadedAt: "2026-04-01T00:00:00Z", + }, + { + videoId: "vid-007", + title: "CI/CD 파이프라인 구축 — GitHub Actions 실전편", + channelName: "Fireship", + thumbnail: "https://picsum.photos/seed/vid7/400/240", + url: "https://www.youtube.com/watch?v=example7", + duration: "14:32", + views: 120500, + uploadedAt: "2026-03-28T00:00:00Z", + }, + { + videoId: "vid-008", + title: "Spring Boot REST API 설계 — 실무 패턴과 예외 처리", + channelName: "우아한테크", + thumbnail: "https://picsum.photos/seed/vid8/400/240", + url: "https://www.youtube.com/watch?v=example8", + duration: "51:08", + views: 43700, + uploadedAt: "2026-03-25T00:00:00Z", + }, + { + videoId: "vid-009", + title: "SQL 쿼리 최적화 — 인덱스 설계와 실행 계획 분석", + channelName: "데이터리안", + thumbnail: null, + url: "https://www.youtube.com/watch?v=example9", + duration: "38:20", + views: 29300, + uploadedAt: "2026-03-22T00:00:00Z", + }, + { + videoId: "vid-010", + title: "Tailwind CSS v4 신기능 총정리 — 10분 핵심 정리", + channelName: "Traversy Media", + thumbnail: "https://picsum.photos/seed/vid10/400/240", + url: "https://www.youtube.com/watch?v=example10", + duration: "09:55", + views: 67800, + uploadedAt: "2026-03-18T00:00:00Z", + }, + { + videoId: "vid-011", + title: "Zustand로 전역 상태 관리하기 — Redux 없이 깔끔하게", + channelName: "코딩애플", + thumbnail: "https://picsum.photos/seed/vid11/400/240", + url: "https://www.youtube.com/watch?v=example11", + duration: "16:47", + views: 91200, + uploadedAt: "2026-03-14T00:00:00Z", + }, + { + videoId: "vid-012", + title: "웹 접근성 A11y 기초 — 스크린 리더 대응과 ARIA 사용법", + channelName: "드림코딩", + thumbnail: null, + url: "https://www.youtube.com/watch?v=example12", + duration: "24:11", + views: 18600, + uploadedAt: "2026-03-10T00:00:00Z", + }, ]; export async function fetchRecommendVideos( - count = 4, + count?: number, ): Promise { await new Promise((resolve) => setTimeout(resolve, 400)); - return MOCK_RECOMMEND_VIDEOS.slice(0, count); + return count !== undefined + ? MOCK_RECOMMEND_VIDEOS.slice(0, count) + : MOCK_RECOMMEND_VIDEOS; } diff --git a/lib/mock/my-page-scraps.ts b/lib/mock/my-page-scraps.ts index 49d2680..f322d01 100644 --- a/lib/mock/my-page-scraps.ts +++ b/lib/mock/my-page-scraps.ts @@ -74,6 +74,41 @@ export const MOCK_SCRAPS: MyPageScrap[] = [ summary: "Zustand v4에서 v5로 업그레이드할 때 꼭 확인해야 할 변경사항들을 정리했습니다.", }, + { + id: "9", + contentId: "content-109", + title: "Tailwind CSS v4 마이그레이션 — @theme 토큰과 CSS 변수 전환", + sourceName: "kakao_tech", + thumbnail: "https://picsum.photos/seed/scrap9/400/240", + createdAt: "2026-03-30T10:00:00Z", + summary: "v3에서 v4로 넘어갈 때 바뀐 설정 방식과 토큰 시스템을 정리합니다.", + }, + { + id: "10", + contentId: "content-110", + title: "Kubernetes 입문 — Pod, Service, Deployment 핵심 개념 정리", + sourceName: "naver_d2", + thumbnail: null, + createdAt: "2026-03-27T14:00:00Z", + summary: "컨테이너 오케스트레이션의 핵심 리소스를 예제와 함께 설명합니다.", + }, + { + id: "11", + contentId: "content-111", + title: "웹 성능 최적화 — Core Web Vitals 개선 실전 사례", + sourceName: "toss_tech", + thumbnail: "https://picsum.photos/seed/scrap11/400/240", + createdAt: "2026-03-24T09:00:00Z", + summary: "LCP, CLS, INP 지표를 실제 서비스에서 개선한 경험을 공유합니다.", + }, + { + id: "12", + contentId: "content-112", + title: "Git 브랜치 전략 — Trunk Based Development vs Git Flow 비교", + sourceName: "medium", + thumbnail: "https://picsum.photos/seed/scrap12/400/240", + createdAt: "2026-03-20T11:30:00Z", + }, ]; export async function fetchMyScraps(): Promise { diff --git a/lib/mock/my-page-wrong-quizzes.ts b/lib/mock/my-page-wrong-quizzes.ts index fc3f607..1cb290d 100644 --- a/lib/mock/my-page-wrong-quizzes.ts +++ b/lib/mock/my-page-wrong-quizzes.ts @@ -104,6 +104,42 @@ export const MOCK_QUIZ_HISTORIES: MyPageQuizHistory[] = [ passed: false, attemptedAt: "2026-04-01T15:00:00Z", }, + { + attemptId: "a10", + contentId: "content-210", + contentTitle: "Kubernetes Pod 스케줄링 전략 — Affinity와 Taint/Toleration", + thumbnail: "https://picsum.photos/seed/quiz10/400/240", + preview: "Kubernetes에서 Node Affinity와 Pod Affinity의 차이점은?", + level: "SENIOR", + score: 2, + totalQuestions: 5, + passed: false, + attemptedAt: "2026-03-29T11:00:00Z", + }, + { + attemptId: "a11", + contentId: "content-211", + contentTitle: "웹 성능 최적화 — Core Web Vitals 개선 실전 사례", + thumbnail: "https://picsum.photos/seed/quiz11/400/240", + preview: "LCP 지표를 개선하기 위한 가장 효과적인 방법은 무엇인가요?", + level: "MIDDLE", + score: 3, + totalQuestions: 5, + passed: false, + attemptedAt: "2026-03-26T09:30:00Z", + }, + { + attemptId: "a12", + contentId: "content-212", + contentTitle: "GraphQL vs REST — 실무에서 선택 기준과 트레이드오프", + thumbnail: null, + preview: "GraphQL의 N+1 문제를 해결하는 일반적인 방법은?", + level: "JUNIOR", + score: 1, + totalQuestions: 5, + passed: false, + attemptedAt: "2026-03-22T14:00:00Z", + }, ]; export async function fetchMyWrongQuizzes(): Promise {