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
23 changes: 23 additions & 0 deletions app/(main)/my-page/recommend/book/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Link from "next/link";
import { ArrowLeft } from "lucide-react";
import { RecommendedBookList } from "@/components/features/my-page/recommend/RecommendedBookList";

export default function RecommendBookPage() {
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>

<RecommendedBookList />
</div>
);
}
23 changes: 23 additions & 0 deletions app/(main)/my-page/recommend/home/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Link from "next/link";
import { ArrowLeft } from "lucide-react";
import { RecommendedHomePostList } from "@/components/features/my-page/recommend/RecommendedHomePostList";

export default function RecommendHomePage() {
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>

<RecommendedHomePostList />
</div>
);
}
23 changes: 23 additions & 0 deletions app/(main)/my-page/recommend/video/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Link from "next/link";
import { ArrowLeft } from "lucide-react";
import { RecommendedVideoList } from "@/components/features/my-page/recommend/RecommendedVideoList";

export default function RecommendVideoPage() {
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>

<RecommendedVideoList />
</div>
);
}
2 changes: 1 addition & 1 deletion components/features/my-page/quizzes/WrongQuizListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function WrongQuizListItem({ quiz }: { quiz: MyPageQuizHistory }) {
return (
<Link
href={`/home/${contentId}/quiz/result`}
className="-mx-2 flex gap-4 rounded-lg px-2 py-3 transition-colors"
className="-mx-2 flex gap-4 px-2 py-3 transition-colors"
>
<div className="relative aspect-[3/2] w-36 shrink-0 overflow-hidden rounded-sm bg-muted">
{thumbnail ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { MyPageRecommendBook } from "@/types/myPage";

export function RecommendedBookCard({ book }: { book: MyPageRecommendBook }) {
const { title, authors, cover, url, publisher, publishedAt } = book;
const year = publishedAt.slice(0, 4);
const year = publishedAt ? publishedAt.slice(0, 4) : "";

return (
<a href={url} target="_blank" rel="noopener noreferrer">
Expand Down
84 changes: 84 additions & 0 deletions components/features/my-page/recommend/RecommendedBookList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"use client";

import { useEffect, useState } from "react";
import { Skeleton } from "@/components/ui/skeleton";
import { RecommendedBookListItem } from "./RecommendedBookListItem";
import { fetchRecommendBooks } from "@/lib/mock/my-page-recommend-book";
import type { MyPageRecommendBook } from "@/types/myPage";

function ListItemSkeleton() {
return (
<div className="-mx-2 flex gap-4 px-2 py-3">
{/* 썸네일 */}
<Skeleton className="aspect-[2/3] w-24 shrink-0 rounded-sm" />

<div className="flex flex-1 flex-col gap-2 py-0.5">
{/* title */}
<Skeleton className="h-4 w-4/5 rounded" />

{/* description */}
<Skeleton className="h-3 w-full rounded" />
<Skeleton className="h-3 w-3/4 rounded" />

{/* authors */}
<Skeleton className="h-3 w-2/5 rounded" />

{/* bottom 영역 */}
<div className="mt-auto flex flex-col gap-1">
{/* publisher · year */}
<Skeleton className="h-3 w-24 rounded" />

{/* price */}
<Skeleton className="h-4 w-20 rounded" />
</div>
</div>
</div>
);
}

export function RecommendedBookList() {
const [books, setBooks] = useState<MyPageRecommendBook[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);

useEffect(() => {
fetchRecommendBooks()
.then((data) => setBooks(data))
.catch(() => setIsError(true))
.finally(() => setIsLoading(false));
}, []);

if (isError) {
return (
<p className="text-sm text-muted-foreground">
불러오는 중 오류가 발생했습니다.
</p>
);
}

if (isLoading) {
return (
<div className="divide-y divide-border">
{Array.from({ length: 4 }).map((_, i) => (
<ListItemSkeleton key={i} />
))}
</div>
);
}

if (books.length === 0) {
return (
<p className="py-10 text-center text-sm text-muted-foreground">
추천 서적이 없습니다.
</p>
);
}

return (
<div className="divide-y divide-border">
{books.map((book) => (
<RecommendedBookListItem key={book.bookId} book={book} />
))}
</div>
);
}
63 changes: 63 additions & 0 deletions components/features/my-page/recommend/RecommendedBookListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Image from "next/image";
import { BookOpen } from "lucide-react";
import type { MyPageRecommendBook } from "@/types/myPage";

export function RecommendedBookListItem({
book,
}: {
book: MyPageRecommendBook;
}) {
const {
title,
authors,
description,
cover,
url,
price,
publisher,
publishedAt,
} = book;
const year = publishedAt ? publishedAt.slice(0, 4) : "";

return (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="-mx-2 flex gap-4 px-2 py-3 transition-colors"
>
<div className="relative aspect-[2/3] w-24 shrink-0 overflow-hidden rounded-sm bg-muted">
{cover ? (
<Image fill src={cover} alt={title} 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="line-clamp-2 text-[15px] font-semibold leading-snug tracking-[-0.01em] text-foreground">
{title}
</p>

{description && (
<p className="line-clamp-1 text-xs text-muted-foreground font-medium">
{description}
</p>
)}
<p className="text-xs text-muted-foreground">{authors.join(", ")}</p>
<span className="mt-auto pt-1 text-xs text-muted-foreground">
{publisher}
<span className="mx-1">·</span>
{year}
</span>
{price && (
<span className="text-sm font-medium text-foreground">
{price.toLocaleString()}원
</span>
)}
</div>
</a>
);
}
68 changes: 68 additions & 0 deletions components/features/my-page/recommend/RecommendedHomePostList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"use client";

import { useEffect, useState } from "react";
import { Skeleton } from "@/components/ui/skeleton";
import { RecommendedHomePostListItem } from "./RecommendedHomePostListItem";
import { fetchRecommendHomePosts } from "@/lib/mock/my-page-recommend-home";
import type { MyPageRecommendHomePost } from "@/types/myPage";

function ListItemSkeleton() {
return (
<div className="-mx-2 flex gap-4 px-2 py-3">
<Skeleton className="aspect-[3/2] w-36 shrink-0 rounded-sm" />
<div className="flex flex-1 flex-col gap-2 py-0.5">
<Skeleton className="h-3 w-16 rounded" />
<Skeleton className="h-4 w-full rounded" />
<Skeleton className="h-4 w-4/5 rounded" />
<Skeleton className="mt-auto h-3 w-20 rounded" />
</div>
</div>
);
}

export function RecommendedHomePostList() {
const [posts, setPosts] = useState<MyPageRecommendHomePost[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);

useEffect(() => {
fetchRecommendHomePosts()
.then((data) => setPosts(data))
.catch(() => setIsError(true))
.finally(() => setIsLoading(false));
}, []);

if (isError) {
return (
<p className="text-sm text-muted-foreground">
불러오는 중 오류가 발생했습니다.
</p>
);
}

if (isLoading) {
return (
<div className="divide-y divide-border">
{Array.from({ length: 4 }).map((_, i) => (
<ListItemSkeleton key={i} />
))}
</div>
);
}

if (posts.length === 0) {
return (
<p className="py-10 text-center text-sm text-muted-foreground">
추천 글이 없습니다.
</p>
);
}

return (
<div className="divide-y divide-border">
{posts.map((post) => (
<RecommendedHomePostListItem key={post.contentId} post={post} />
))}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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 RecommendedHomePostListItem({
post,
}: {
post: MyPageRecommendHomePost;
}) {
const { contentId, title, sourceName, thumbnail, summary, date } = post;

return (
<Link
href={`/home/${contentId}`}
className="-mx-2 flex gap-4 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={title} className="object-cover" />
) : (
<div className="flex h-full w-full items-center justify-center">
<FileText className="h-5 w-5 text-muted-foreground/30" />
</div>
)}
</div>

<div className="flex min-w-0 flex-1 flex-col gap-1">
<div className="flex items-center gap-1.5">
<SourceLogo sourceName={sourceName} size={13} />
<span className="text-xs font-medium text-muted-foreground">
{sourceName}
</span>
</div>
<p className="line-clamp-2 text-[15px] font-semibold leading-snug tracking-[-0.01em] text-foreground">
{title}
</p>
{summary && (
<p className="line-clamp-1 text-xs text-muted-foreground">{summary}</p>
)}
<span className="mt-auto pt-1 text-xs text-muted-foreground">
{formatDate(date)}
</span>
</div>
</Link>
);
}
Loading
Loading