+
+
+ {cover ? (
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+ {title}
+
+
+ {authors.join(", ")}
+
+
+ {publisher}
+ ·
+ {year}
+
+
+
+
+ );
+}
diff --git a/components/features/my-page/recommend/RecommendedHomePostCard.tsx b/components/features/my-page/recommend/RecommendedHomePostCard.tsx
new file mode 100644
index 0000000..3294510
--- /dev/null
+++ b/components/features/my-page/recommend/RecommendedHomePostCard.tsx
@@ -0,0 +1,48 @@
+import Link from "next/link";
+import Image from "next/image";
+import { FileText } from "lucide-react";
+import { SourceLogo } from "@/components/features/home/SourceLogo";
+import { formatDate } from "@/lib/utils";
+import type { MyPageRecommendHomePost } from "@/types/myPage";
+
+export function RecommendedHomePostCard({
+ post,
+}: {
+ post: MyPageRecommendHomePost;
+}) {
+ const { contentId, title, sourceName, thumbnail, date } = post;
+
+ return (
+
+
+
+ {thumbnail ? (
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+
+ {sourceName}
+
+
+ {title}
+
+
+ {formatDate(date)}
+
+
+
+
+ );
+}
diff --git a/components/features/my-page/recommend/RecommendedSection.tsx b/components/features/my-page/recommend/RecommendedSection.tsx
new file mode 100644
index 0000000..02f1db3
--- /dev/null
+++ b/components/features/my-page/recommend/RecommendedSection.tsx
@@ -0,0 +1,147 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import Link from "next/link";
+import { ArrowRight } from "lucide-react";
+import { Skeleton } from "@/components/ui/skeleton";
+import { RecommendedHomePostCard } from "./RecommendedHomePostCard";
+import { RecommendedVideoCard } from "./RecommendedVideoCard";
+import { RecommendedBookCard } from "./RecommendedBookCard";
+import { fetchRecommendHomePosts } from "@/lib/mock/my-page-recommend-home";
+import { fetchRecommendVideos } from "@/lib/mock/my-page-recommend-video";
+import { fetchRecommendBooks } from "@/lib/mock/my-page-recommend-book";
+import type {
+ MyPageRecommendHomePost,
+ MyPageRecommendVideo,
+ MyPageRecommendBook,
+} from "@/types/myPage";
+
+function SubSectionHeader({
+ title,
+ href,
+}: {
+ title: string;
+ href: string;
+}) {
+ return (
+
+
{title}
+
+
+ 전체 보기
+
+
+ );
+}
+
+function CardSkeleton() {
+ return (
+
+ );
+}
+
+function BookCardSkeleton() {
+ return (
+
+ );
+}
+
+export function RecommendedSection() {
+ const [homePosts, setHomePosts] = useState
([]);
+ const [videos, setVideos] = useState([]);
+ const [books, setBooks] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ Promise.all([
+ fetchRecommendHomePosts(4),
+ fetchRecommendVideos(4),
+ fetchRecommendBooks(4),
+ ]).then(([posts, vids, bks]) => {
+ setHomePosts(posts);
+ setVideos(vids);
+ setBooks(bks);
+ setIsLoading(false);
+ });
+ }, []);
+
+ return (
+
+ 추천
+
+
+
+ {isLoading ? (
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+ ) : homePosts.length === 0 ? (
+
추천 글이 없습니다.
+ ) : (
+
+ {homePosts.map((post) => (
+
+ ))}
+
+ )}
+
+
+
+
+ {isLoading ? (
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+ ) : videos.length === 0 ? (
+
추천 영상이 없습니다.
+ ) : (
+
+ {videos.map((video) => (
+
+ ))}
+
+ )}
+
+
+
+
+ {isLoading ? (
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+ ) : books.length === 0 ? (
+
추천 서적이 없습니다.
+ ) : (
+
+ {books.map((book) => (
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/components/features/my-page/recommend/RecommendedVideoCard.tsx b/components/features/my-page/recommend/RecommendedVideoCard.tsx
new file mode 100644
index 0000000..4b00d7a
--- /dev/null
+++ b/components/features/my-page/recommend/RecommendedVideoCard.tsx
@@ -0,0 +1,51 @@
+import Image from "next/image";
+import { Youtube } from "lucide-react";
+import { formatDate } from "@/lib/utils";
+import type { MyPageRecommendVideo } from "@/types/myPage";
+
+function formatViews(views: number): string {
+ if (views >= 10000) return `${Math.floor(views / 10000)}만`;
+ if (views >= 1000) return `${Math.floor(views / 1000)}천`;
+ return String(views);
+}
+
+export function RecommendedVideoCard({ video }: { video: MyPageRecommendVideo }) {
+ const { title, channelName, thumbnail, url, duration, views, uploadedAt } = video;
+
+ return (
+
+
+
+ {thumbnail ? (
+
+ ) : (
+
+
+
+ )}
+
+ {duration}
+
+
+
+
+
+ {title}
+
+
+ {channelName}
+ ·
+ 조회 {formatViews(views)}
+ ·
+ {formatDate(uploadedAt)}
+
+
+
+
+ );
+}
diff --git a/lib/mock/my-page-recommend-book.ts b/lib/mock/my-page-recommend-book.ts
new file mode 100644
index 0000000..b63e4ad
--- /dev/null
+++ b/lib/mock/my-page-recommend-book.ts
@@ -0,0 +1,47 @@
+import type { MyPageRecommendBook } from "@/types/myPage";
+
+export const MOCK_RECOMMEND_BOOKS: MyPageRecommendBook[] = [
+ {
+ bookId: "book-001",
+ title: "클린 아키텍처",
+ authors: ["로버트 C. 마틴"],
+ cover: "https://picsum.photos/seed/book1/300/400",
+ url: "https://www.yes24.com/Product/Goods/77283734",
+ publisher: "인사이트",
+ publishedAt: "2019-08-20",
+ },
+ {
+ bookId: "book-002",
+ title: "가상 면접 사례로 배우는 대규모 시스템 설계 기초",
+ authors: ["알렉스 쉬"],
+ cover: "https://picsum.photos/seed/book2/300/400",
+ url: "https://www.yes24.com/Product/Goods/102819435",
+ publisher: "인사이트",
+ publishedAt: "2021-11-24",
+ },
+ {
+ bookId: "book-003",
+ title: "이펙티브 타입스크립트",
+ authors: ["댄 밴더캄"],
+ cover: null,
+ url: "https://www.yes24.com/Product/Goods/102124327",
+ publisher: "인사이트",
+ publishedAt: "2021-11-12",
+ },
+ {
+ bookId: "book-004",
+ title: "데이터 중심 애플리케이션 설계",
+ authors: ["마틴 클레퍼만"],
+ cover: "https://picsum.photos/seed/book4/300/400",
+ url: "https://www.yes24.com/Product/Goods/59566585",
+ publisher: "위키북스",
+ publishedAt: "2018-12-04",
+ },
+];
+
+export async function fetchRecommendBooks(
+ count = 4,
+): Promise {
+ await new Promise((resolve) => setTimeout(resolve, 400));
+ return MOCK_RECOMMEND_BOOKS.slice(0, count);
+}
diff --git a/lib/mock/my-page-recommend-home.ts b/lib/mock/my-page-recommend-home.ts
new file mode 100644
index 0000000..b1733cd
--- /dev/null
+++ b/lib/mock/my-page-recommend-home.ts
@@ -0,0 +1,42 @@
+import type { MyPageRecommendHomePost } from "@/types/myPage";
+
+export const MOCK_RECOMMEND_HOME_POSTS: MyPageRecommendHomePost[] = [
+ {
+ contentId: "content-301",
+ title: "React 서버 컴포넌트 완전 정복 — 렌더링 모델과 캐싱 전략",
+ sourceName: "velog",
+ thumbnail: "https://picsum.photos/seed/rec1/400/240",
+ summary: "RSC의 렌더링 흐름과 fetch 캐싱 옵션을 심층 분석합니다.",
+ date: "2026-04-20T10:00:00Z",
+ },
+ {
+ contentId: "content-302",
+ title: "TypeScript satisfies 연산자 — 타입 추론을 유지하면서 검증하기",
+ sourceName: "kakao_tech",
+ thumbnail: "https://picsum.photos/seed/rec2/400/240",
+ summary: "as와 satisfies의 차이점과 실전 활용 패턴을 정리합니다.",
+ date: "2026-04-18T09:00:00Z",
+ },
+ {
+ contentId: "content-303",
+ title: "모노레포 환경에서 Turborepo로 빌드 캐시 최적화하기",
+ sourceName: "toss_tech",
+ thumbnail: null,
+ date: "2026-04-15T14:00:00Z",
+ },
+ {
+ contentId: "content-304",
+ title: "PostgreSQL 파티셔닝 전략 — 대용량 테이블 성능 개선 실전 사례",
+ sourceName: "naver_d2",
+ thumbnail: "https://picsum.photos/seed/rec4/400/240",
+ summary: "레인지/리스트/해시 파티셔닝을 실제 서비스 케이스에 적용한 경험을 공유합니다.",
+ date: "2026-04-13T11:00:00Z",
+ },
+];
+
+export async function fetchRecommendHomePosts(
+ count = 4,
+): Promise {
+ await new Promise((resolve) => setTimeout(resolve, 400));
+ return MOCK_RECOMMEND_HOME_POSTS.slice(0, count);
+}
diff --git a/lib/mock/my-page-recommend-video.ts b/lib/mock/my-page-recommend-video.ts
new file mode 100644
index 0000000..b74cbbb
--- /dev/null
+++ b/lib/mock/my-page-recommend-video.ts
@@ -0,0 +1,51 @@
+import type { MyPageRecommendVideo } from "@/types/myPage";
+
+export const MOCK_RECOMMEND_VIDEOS: MyPageRecommendVideo[] = [
+ {
+ videoId: "vid-001",
+ title: "10분 만에 이해하는 React 19 Actions — useActionState 실전편",
+ channelName: "코딩애플",
+ thumbnail: "https://picsum.photos/seed/vid1/400/240",
+ url: "https://www.youtube.com/watch?v=example1",
+ duration: "10:24",
+ views: 142300,
+ uploadedAt: "2026-04-19T00:00:00Z",
+ },
+ {
+ videoId: "vid-002",
+ title: "Next.js App Router 완전 정복 — 캐싱부터 스트리밍까지",
+ channelName: "Traversy Media",
+ thumbnail: "https://picsum.photos/seed/vid2/400/240",
+ url: "https://www.youtube.com/watch?v=example2",
+ duration: "42:17",
+ views: 89500,
+ uploadedAt: "2026-04-14T00:00:00Z",
+ },
+ {
+ videoId: "vid-003",
+ title: "Docker & Kubernetes 입문 — 컨테이너 오케스트레이션 기초",
+ channelName: "드림코딩",
+ thumbnail: null,
+ url: "https://www.youtube.com/watch?v=example3",
+ duration: "28:50",
+ views: 61200,
+ uploadedAt: "2026-04-10T00:00:00Z",
+ },
+ {
+ videoId: "vid-004",
+ title: "TanStack Query v5 마이그레이션 완벽 가이드",
+ channelName: "Jack Herrington",
+ thumbnail: "https://picsum.photos/seed/vid4/400/240",
+ url: "https://www.youtube.com/watch?v=example4",
+ duration: "18:03",
+ views: 34800,
+ uploadedAt: "2026-04-07T00:00:00Z",
+ },
+];
+
+export async function fetchRecommendVideos(
+ count = 4,
+): Promise {
+ await new Promise((resolve) => setTimeout(resolve, 400));
+ return MOCK_RECOMMEND_VIDEOS.slice(0, count);
+}
diff --git a/types/myPage.ts b/types/myPage.ts
index 8d193dc..b33b804 100644
--- a/types/myPage.ts
+++ b/types/myPage.ts
@@ -22,3 +22,33 @@ export interface MyPageScrap {
createdAt: string;
summary?: string;
}
+
+export interface MyPageRecommendHomePost {
+ contentId: string;
+ title: string;
+ sourceName: string;
+ thumbnail: string | null;
+ summary?: string;
+ date: string;
+}
+
+export interface MyPageRecommendVideo {
+ videoId: string;
+ title: string;
+ channelName: string;
+ thumbnail: string | null;
+ url: string;
+ duration: string;
+ views: number;
+ uploadedAt: string;
+}
+
+export interface MyPageRecommendBook {
+ bookId: string;
+ title: string;
+ authors: string[];
+ cover: string | null;
+ url: string;
+ publisher: string;
+ publishedAt: string;
+}