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
29 changes: 20 additions & 9 deletions components/features/my-page/recommend/RecommendedBookCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@ import { BookOpen } from "lucide-react";
import type { MyPageRecommendBook } from "@/types/myPage";

export function RecommendedBookCard({ book }: { book: MyPageRecommendBook }) {
const { title, authors, cover, url, publisher, publishedAt } = book;
const year = publishedAt ? publishedAt.slice(0, 4) : "";
const { title, authors, thumbnail, url, publisher, price, salePrice } = book;
const hasDiscount = salePrice !== -1;
const hasThumbnail = thumbnail && thumbnail.trim() !== "";

return (
<a href={url} target="_blank" rel="noopener noreferrer">
<div className="group flex h-full flex-col overflow-hidden rounded-md border border-border bg-card">
<div className="relative aspect-[3/4] w-full overflow-hidden bg-muted">
{cover ? (
{hasThumbnail ? (
<Image
fill
src={cover}
src={thumbnail!}
alt={title}
className="object-cover transition-transform duration-200"
/>
Expand All @@ -31,11 +32,21 @@ export function RecommendedBookCard({ book }: { book: MyPageRecommendBook }) {
<span className="text-xs text-muted-foreground">
{authors.join(", ")}
</span>
<span className="text-xs text-muted-foreground">
{publisher}
<span className="mx-1">·</span>
{year}
</span>
<span className="text-xs text-muted-foreground">{publisher}</span>
{hasDiscount ? (
<div className="flex flex-col">
<span className="text-[11px] text-muted-foreground line-through">
{price.toLocaleString()}원
</span>
<span className="text-sm font-semibold text-foreground">
{salePrice.toLocaleString()}원
</span>
</div>
) : (
<span className="text-sm font-semibold text-foreground">
{price.toLocaleString()}원
</span>
)}
</div>
</div>
</a>
Expand Down
59 changes: 20 additions & 39 deletions components/features/my-page/recommend/RecommendedBookList.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,22 @@
"use client";

import { useEffect, useState, useMemo } from "react";
import { useEffect, useState } 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;
import type { MyPageRecommendBooksResponse } 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>
Expand All @@ -40,24 +25,18 @@ function ListItemSkeleton() {
}

export function RecommendedBookList() {
const [books, setBooks] = useState<MyPageRecommendBook[]>([]);
const [booksData, setBooksData] =
useState<MyPageRecommendBooksResponse | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
const [currentPage, setCurrentPage] = useState(1);

useEffect(() => {
fetchRecommendBooks()
.then((data) => setBooks(data))
.then((data) => setBooksData(data))
.catch(() => setIsError(true))
.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 (
<p className="text-sm text-muted-foreground">
Expand All @@ -76,6 +55,16 @@ export function RecommendedBookList() {
);
}

if (!booksData?.isPersonalized) {
return (
<p className="py-10 text-center text-sm text-muted-foreground">
{booksData?.message ?? "아직 추천할 도서가 부족해요. 더 많은 글을 읽어보세요!"}
</p>
);
}

const books = booksData.books;

if (books.length === 0) {
return (
<p className="py-10 text-center text-sm text-muted-foreground">
Expand All @@ -85,18 +74,10 @@ export function RecommendedBookList() {
}

return (
<>
<div className="divide-y divide-border">
{pagedItems.map((book) => (
<RecommendedBookListItem key={book.bookId} book={book} />
))}
</div>
<MyPagePagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
className="mt-8 mb-12"
/>
</>
<div className="divide-y divide-border">
{books.map((book) => (
<RecommendedBookListItem key={book.url} book={book} />
))}
</div>
);
}
52 changes: 25 additions & 27 deletions components/features/my-page/recommend/RecommendedBookListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,9 @@ export function RecommendedBookListItem({
}: {
book: MyPageRecommendBook;
}) {
const {
title,
authors,
description,
cover,
url,
price,
publisher,
publishedAt,
} = book;
const year = publishedAt ? publishedAt.slice(0, 4) : "";
const { title, authors, contents, thumbnail, url, price, salePrice, publisher } = book;
const hasDiscount = salePrice !== -1;
const hasThumbnail = thumbnail && thumbnail.trim() !== "";

return (
<a
Expand All @@ -27,8 +19,8 @@ export function RecommendedBookListItem({
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" />
{hasThumbnail ? (
<Image fill src={thumbnail!} 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" />
Expand All @@ -40,23 +32,29 @@ export function RecommendedBookListItem({
<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}
{contents && (
<p className="line-clamp-1 text-xs font-medium text-muted-foreground">
{contents}
</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>
)}
<span className="text-xs text-muted-foreground">{publisher}</span>
<div className="mt-auto pt-1">
{hasDiscount ? (
<div className="flex items-baseline gap-1.5">
<span className="text-[11px] text-muted-foreground line-through">
{price.toLocaleString()}원
</span>
<span className="text-sm font-semibold text-foreground">
{salePrice.toLocaleString()}원
</span>
</div>
) : (
<span className="text-sm font-semibold text-foreground">
{price.toLocaleString()}원
</span>
)}
</div>
</div>
</a>
);
Expand Down
14 changes: 10 additions & 4 deletions components/features/my-page/recommend/RecommendedSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { fetchRecommendBooks } from "@/lib/mock/my-page-recommend-book";
import type {
MyPageRecommendContentsResponse,
MyPageRecommendYoutubeResponse,
MyPageRecommendBook,
MyPageRecommendBooksResponse,
} from "@/types/myPage";

function SubSectionHeader({ title, href }: { title: string; href: string }) {
Expand Down Expand Up @@ -62,7 +62,8 @@ export function RecommendedSection() {
useState<MyPageRecommendContentsResponse | null>(null);
const [videosData, setVideosData] =
useState<MyPageRecommendYoutubeResponse | null>(null);
const [books, setBooks] = useState<MyPageRecommendBook[]>([]);
const [booksData, setBooksData] =
useState<MyPageRecommendBooksResponse | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);

Expand All @@ -75,14 +76,15 @@ export function RecommendedSection() {
.then(([postsData, vidsData, bks]) => {
setHomePostsData(postsData);
setVideosData(vidsData);
setBooks(bks);
setBooksData(bks);
})
.catch(() => setIsError(true))
.finally(() => setIsLoading(false));
}, []);

const homePosts = homePostsData?.contents ?? [];
const videos = videosData?.videos ?? [];
const books = booksData?.books ?? [];

if (isError) {
return (
Expand Down Expand Up @@ -164,14 +166,18 @@ export function RecommendedSection() {
<BookCardSkeleton key={i} />
))}
</div>
) : !booksData?.isPersonalized ? (
<p className="text-sm text-muted-foreground">
{booksData?.message ?? "아직 추천할 도서가 부족해요. 더 많은 글을 읽어보세요!"}
</p>
) : books.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">
{books.map((book) => (
<RecommendedBookCard key={book.bookId} book={book} />
<RecommendedBookCard key={book.url} book={book} />
))}
</div>
)}
Expand Down
Loading
Loading