diff --git a/components/features/my-page/MyPage.tsx b/components/features/my-page/MyPage.tsx
index b69e580..1ee78df 100644
--- a/components/features/my-page/MyPage.tsx
+++ b/components/features/my-page/MyPage.tsx
@@ -3,6 +3,7 @@
import Link from "next/link";
import { ChevronRight } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton";
+import { ScrappedPostsSection } from "./ScrappedPostsSection";
function SectionHeader({ title, href }: { title: string; href?: string }) {
return (
@@ -43,25 +44,16 @@ export default function MyPage() {
- {/* 1. 로드맵 */}
-
-
- {/* 2. 스크랩한 글들 */}
-
+ {/* 1. 스크랩한 글들 */}
+
- {/* 3. 틀린 퀴즈들 */}
+ {/* 2. 틀린 퀴즈들 */}
- {/* 4. 추천 */}
+ {/* 3. 추천 */}
diff --git a/components/features/my-page/ScrappedPostCard.tsx b/components/features/my-page/ScrappedPostCard.tsx
new file mode 100644
index 0000000..c10219f
--- /dev/null
+++ b/components/features/my-page/ScrappedPostCard.tsx
@@ -0,0 +1,51 @@
+"use client";
+
+import Link from "next/link";
+import Image from "next/image";
+import { Bookmark } from "lucide-react";
+import { SourceLogo } from "@/components/features/home/SourceLogo";
+import { formatDate } from "@/lib/utils";
+import type { MyPageScrap } from "@/types/myPage";
+
+export function ScrappedPostCard({ scrap }: { scrap: MyPageScrap }) {
+ const { contentId, title, sourceName, thumbnail, createdAt } = scrap;
+
+ return (
+
+ {thumbnail ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+
+ {sourceName}
+
+
+
+
+ {title}
+
+
+
+ {formatDate(createdAt)}
+
+
+
+ );
+}
diff --git a/components/features/my-page/ScrappedPostListItem.tsx b/components/features/my-page/ScrappedPostListItem.tsx
new file mode 100644
index 0000000..0e7bc20
--- /dev/null
+++ b/components/features/my-page/ScrappedPostListItem.tsx
@@ -0,0 +1,49 @@
+"use client";
+
+import Link from "next/link";
+import Image from "next/image";
+import { Bookmark } from "lucide-react";
+import { SourceLogo } from "@/components/features/home/SourceLogo";
+import { formatDate } from "@/lib/utils";
+import type { MyPageScrap } from "@/types/myPage";
+
+export function ScrappedPostListItem({ scrap }: { scrap: MyPageScrap }) {
+ const { contentId, title, sourceName, thumbnail, createdAt, summary } = scrap;
+
+ return (
+
+
+ {thumbnail ? (
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+
+
+ {sourceName}
+
+
+
+ {title}
+
+ {summary && (
+
+ {summary}
+
+ )}
+
+ {formatDate(createdAt)}
+
+
+
+ );
+}
diff --git a/components/features/my-page/ScrappedPostsList.tsx b/components/features/my-page/ScrappedPostsList.tsx
new file mode 100644
index 0000000..f309ea9
--- /dev/null
+++ b/components/features/my-page/ScrappedPostsList.tsx
@@ -0,0 +1,135 @@
+"use client";
+
+import { useEffect, useState, useMemo } from "react";
+import { ChevronDown, Search } from "lucide-react";
+import { Skeleton } from "@/components/ui/skeleton";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { ScrappedPostListItem } from "./ScrappedPostListItem";
+import { fetchMyScraps } from "@/lib/mock/my-page-scraps";
+import type { MyPageScrap } from "@/types/myPage";
+
+type SortOrder = "newest" | "oldest";
+
+function ListItemSkeleton() {
+ return (
+
+ );
+}
+
+export function ScrappedPostsList() {
+ const [scraps, setScraps] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isError, setIsError] = useState(false);
+ const [query, setQuery] = useState("");
+ const [sort, setSort] = useState("newest");
+
+ useEffect(() => {
+ fetchMyScraps()
+ .then((data) => setScraps(data))
+ .catch(() => setIsError(true))
+ .finally(() => setIsLoading(false));
+ }, []);
+
+ const processed = useMemo(() => {
+ const q = query.trim().toLowerCase();
+ const filtered = q
+ ? scraps.filter(
+ (s) =>
+ s.title.toLowerCase().includes(q) ||
+ s.sourceName.toLowerCase().includes(q) ||
+ (s.summary?.toLowerCase().includes(q) ?? false),
+ )
+ : scraps;
+
+ return [...filtered].sort((a, b) => {
+ const diff =
+ new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
+ return sort === "newest" ? -diff : diff;
+ });
+ }, [scraps, query, sort]);
+
+ if (isLoading) {
+ return (
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ))}
+
+ );
+ }
+
+ if (isError) {
+ return (
+
+ 불러오는 중 오류가 발생했습니다.
+
+ );
+ }
+
+ return (
+
+
+
+ {processed.length}개
+
+
+
+
+ setQuery(e.target.value)}
+ 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"
+ />
+
+
+
+ {sort === "newest" ? "최신순" : "오래된순"}
+
+
+
+ setSort(v as SortOrder)}
+ >
+
+ 최신순
+
+
+ 오래된순
+
+
+
+
+
+
+
+ {processed.length === 0 ? (
+
+ {query ? "검색 결과가 없습니다." : "스크랩한 글이 없습니다."}
+
+ ) : (
+
+ {processed.map((scrap) => (
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/components/features/my-page/ScrappedPostsSection.tsx b/components/features/my-page/ScrappedPostsSection.tsx
new file mode 100644
index 0000000..b3c40bf
--- /dev/null
+++ b/components/features/my-page/ScrappedPostsSection.tsx
@@ -0,0 +1,65 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import Link from "next/link";
+import { ArrowRight } from "lucide-react";
+import { Skeleton } from "@/components/ui/skeleton";
+import { ScrappedPostCard } from "./ScrappedPostCard";
+import { fetchMyScrapsPreview } from "@/lib/mock/my-page-scraps";
+import type { MyPageScrap } from "@/types/myPage";
+
+export function ScrappedPostsSection() {
+ const [scraps, setScraps] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ fetchMyScrapsPreview(4).then((data) => {
+ setScraps(data);
+ setIsLoading(false);
+ });
+ }, []);
+
+ return (
+
+
+
+ 스크랩한 글들
+
+
+
+ 전체 보기
+
+
+
+ {isLoading ? (
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+ ) : scraps.length === 0 ? (
+ 스크랩한 글이 없습니다.
+ ) : (
+
+ {scraps.map((scrap) => (
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/lib/mock/my-page-scraps.ts b/lib/mock/my-page-scraps.ts
new file mode 100644
index 0000000..49d2680
--- /dev/null
+++ b/lib/mock/my-page-scraps.ts
@@ -0,0 +1,87 @@
+import type { MyPageScrap } from "@/types/myPage";
+
+export const MOCK_SCRAPS: MyPageScrap[] = [
+ {
+ id: "1",
+ contentId: "content-101",
+ title: "React 19의 새로운 기능들: useActionState, useFormStatus 완벽 정리",
+ sourceName: "medium",
+ thumbnail: "https://picsum.photos/seed/scrap1/400/240",
+ createdAt: "2026-04-20T09:15:00Z",
+ summary:
+ "React 19에서 도입된 훅들과 Server Actions 통합 방식을 예제 코드와 함께 살펴봅니다.",
+ },
+ {
+ id: "2",
+ contentId: "content-102",
+ title: "TypeScript 5.5 주요 변경사항 — infer 키워드 개선과 strictness 강화",
+ sourceName: "naver_d2",
+ thumbnail: "https://picsum.photos/seed/scrap2/400/240",
+ createdAt: "2026-04-18T14:30:00Z",
+ summary:
+ "TypeScript 5.5의 핵심 업데이트를 마이그레이션 관점에서 정리한 글입니다.",
+ },
+ {
+ id: "3",
+ contentId: "content-103",
+ title: "Next.js App Router에서 Server Component와 Client Component 경계 설계하기",
+ sourceName: "kakao_tech",
+ thumbnail: "https://picsum.photos/seed/scrap3/400/240",
+ createdAt: "2026-04-15T11:00:00Z",
+ },
+ {
+ id: "4",
+ contentId: "content-104",
+ title: "PostgreSQL EXPLAIN ANALYZE 읽는 법 — 쿼리 최적화 실전 가이드",
+ sourceName: "stack overflow",
+ thumbnail: null,
+ createdAt: "2026-04-13T08:45:00Z",
+ summary: "슬로우 쿼리를 분석하고 인덱스 전략을 수립하는 방법을 다룹니다.",
+ },
+ {
+ id: "5",
+ contentId: "content-105",
+ title: "Docker Compose로 로컬 개발 환경 구성하기 — 실전 템플릿 공개",
+ sourceName: "velog",
+ thumbnail: "https://picsum.photos/seed/scrap5/400/240",
+ createdAt: "2026-04-10T16:20:00Z",
+ },
+ {
+ id: "6",
+ contentId: "content-106",
+ title: "REST API 설계 원칙 — 버저닝 전략과 에러 응답 포맷 표준화",
+ sourceName: "medium",
+ thumbnail: "https://picsum.photos/seed/scrap6/400/240",
+ createdAt: "2026-04-08T10:00:00Z",
+ summary:
+ "실무에서 자주 마주치는 API 설계 결정들을 사례 중심으로 설명합니다.",
+ },
+ {
+ id: "7",
+ contentId: "content-107",
+ title: "Redis 캐싱 전략 비교 — Cache-Aside, Write-Through, Write-Behind",
+ sourceName: "toss_tech",
+ thumbnail: null,
+ createdAt: "2026-04-05T13:10:00Z",
+ },
+ {
+ id: "8",
+ contentId: "content-108",
+ title: "Zustand v5 마이그레이션 가이드 — 스토어 구조와 미들웨어 변경점",
+ sourceName: "velog",
+ thumbnail: "https://picsum.photos/seed/scrap8/400/240",
+ createdAt: "2026-04-02T09:30:00Z",
+ summary:
+ "Zustand v4에서 v5로 업그레이드할 때 꼭 확인해야 할 변경사항들을 정리했습니다.",
+ },
+];
+
+export async function fetchMyScraps(): Promise {
+ await new Promise((resolve) => setTimeout(resolve, 400));
+ return MOCK_SCRAPS;
+}
+
+export async function fetchMyScrapsPreview(count = 4): Promise {
+ await new Promise((resolve) => setTimeout(resolve, 400));
+ return MOCK_SCRAPS.slice(0, count);
+}
diff --git a/my-page-detail.jpg b/my-page-detail.jpg
new file mode 100644
index 0000000..b6a37e9
Binary files /dev/null and b/my-page-detail.jpg differ
diff --git a/types/myPage.ts b/types/myPage.ts
new file mode 100644
index 0000000..c0cfaa8
--- /dev/null
+++ b/types/myPage.ts
@@ -0,0 +1,9 @@
+export interface MyPageScrap {
+ id: string;
+ contentId: string;
+ title: string;
+ sourceName: string;
+ thumbnail: string | null;
+ createdAt: string;
+ summary?: string;
+}