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/scraps/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 { ScrappedPostsList } from "@/components/features/my-page/ScrappedPostsList";

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

<ScrappedPostsList />
</div>
);
}
4 changes: 2 additions & 2 deletions components/features/home/search/HomeTopPostsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function ChangeRate({ rate }: { rate: number }) {

function TopPostCard({ post }: { post: TrendTopPost }) {
return (
<div className="w-44 shrink-0 overflow-hidden rounded-xl border border-border bg-card">
<div className="w-44 shrink-0 overflow-hidden rounded-md border border-border bg-card">
{/* 썸네일 영역 */}
<div className="relative aspect-video w-full overflow-hidden">
{post.thumbnailUrl ? (
Expand Down Expand Up @@ -66,7 +66,7 @@ function TopPostCard({ post }: { post: TrendTopPost }) {

function TopPostCardSkeleton() {
return (
<div className="w-44 shrink-0 overflow-hidden rounded-xl border border-border bg-card">
<div className="w-44 shrink-0 overflow-hidden rounded-md border border-border bg-card">
<Skeleton className="aspect-video w-full" />
<div className="flex flex-col gap-2 p-3">
<Skeleton className="h-3 w-14 rounded" />
Expand Down
18 changes: 5 additions & 13 deletions components/features/my-page/MyPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -43,25 +44,16 @@ export default function MyPage() {
</p>
</div>

{/* 1. 로드맵 */}
<section>
<SectionHeader title="로드맵" />
<Skeleton className="h-44 rounded-xl" />
</section>

{/* 2. 스크랩한 글들 */}
<section>
<SectionHeader title="스크랩한 글들" />
<SkeletonCards />
</section>
{/* 1. 스크랩한 글들 */}
<ScrappedPostsSection />

{/* 3. 틀린 퀴즈들 */}
{/* 2. 틀린 퀴즈들 */}
<section>
<SectionHeader title="틀린 퀴즈들" />
<SkeletonCards />
</section>

{/* 4. 추천 */}
{/* 3. 추천 */}
<section className="space-y-8">
<SectionHeader title="추천" />

Expand Down
51 changes: 51 additions & 0 deletions components/features/my-page/ScrappedPostCard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Link
href={`/home/${contentId}`}
className="group flex h-full flex-col overflow-hidden rounded-md border border-border bg-card"
>
{thumbnail ? (
<div className="relative aspect-video w-full overflow-hidden bg-muted">
<Image
fill
src={thumbnail}
alt={title}
className="object-cover transition-transform duration-200"
/>
</div>
) : (
<div className="flex aspect-video w-full items-center justify-center bg-muted/50">
<Bookmark className="h-6 w-6 text-muted-foreground/30" />
</div>
)}

<div className="flex flex-1 flex-col gap-1.5 p-3">
<div className="flex items-center gap-1.5">
<SourceLogo sourceName={sourceName} size={14} />
<span className="text-xs text-muted-foreground font-medium">
{sourceName}
</span>
</div>

<p className="line-clamp-2 flex-1 text-sm font-medium leading-snug tracking-[-0.01em] text-foreground">
{title}
</p>

<span className="text-xs text-muted-foreground">
{formatDate(createdAt)}
</span>
</div>
</Link>
);
}
49 changes: 49 additions & 0 deletions components/features/my-page/ScrappedPostListItem.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Link
href={`/home/${contentId}`}
className="-mx-2 flex gap-4 rounded-lg 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">
<Bookmark 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 text-muted-foreground font-medium">
{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(createdAt)}
</span>
</div>
</Link>
);
}
135 changes: 135 additions & 0 deletions components/features/my-page/ScrappedPostsList.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<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 ScrappedPostsList() {
const [scraps, setScraps] = useState<MyPageScrap[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
const [query, setQuery] = useState("");
const [sort, setSort] = useState<SortOrder>("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 (
<div className="divide-y divide-border">
{Array.from({ length: 5 }).map((_, i) => (
<ListItemSkeleton key={i} />
))}
</div>
);
}

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

return (
<div>
<div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<span className="text-sm text-muted-foreground">
{processed.length}개
</span>
<div className="flex items-center gap-2 sm:justify-end">
<div className="relative">
<Search className="absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
<input
type="text"
placeholder="검색"
value={query}
onChange={(e) => 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"
/>
</div>
<DropdownMenu modal={false}>
<DropdownMenuTrigger className="flex h-8 w-24 cursor-pointer items-center justify-between rounded-md border border-border bg-background px-3 text-sm text-foreground focus:outline-none">
{sort === "newest" ? "최신순" : "오래된순"}
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-[6rem] p-1">
<DropdownMenuRadioGroup
value={sort}
onValueChange={(v) => setSort(v as SortOrder)}
>
<DropdownMenuRadioItem className="cursor-pointer" value="newest">
최신순
</DropdownMenuRadioItem>
<DropdownMenuRadioItem className="cursor-pointer" value="oldest">
오래된순
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>

{processed.length === 0 ? (
<p className="py-10 text-center text-sm text-muted-foreground">
{query ? "검색 결과가 없습니다." : "스크랩한 글이 없습니다."}
</p>
) : (
<div className="divide-y divide-border">
{processed.map((scrap) => (
<ScrappedPostListItem key={scrap.id} scrap={scrap} />
))}
</div>
)}
</div>
);
}
65 changes: 65 additions & 0 deletions components/features/my-page/ScrappedPostsSection.tsx
Original file line number Diff line number Diff line change
@@ -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<MyPageScrap[]>([]);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
fetchMyScrapsPreview(4).then((data) => {
setScraps(data);
setIsLoading(false);
});
}, []);

return (
<section>
<div className="mb-4 flex items-center justify-between">
<h2 className="text-base font-semibold text-foreground">
스크랩한 글들
</h2>
<Link
href="/my-page/scraps"
className="flex items-center gap-0.5 text-sm text-muted-foreground transition-colors hover:text-foreground"
>
<ArrowRight className="h-4 w-4" />
전체 보기
</Link>
</div>

{isLoading ? (
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<div
key={i}
className="flex flex-col overflow-hidden rounded-xl border border-border bg-card"
>
<Skeleton className="aspect-video w-full rounded-none" />
<div className="flex flex-col gap-2 p-3">
<Skeleton className="h-3 w-16 rounded" />
<Skeleton className="h-3.5 w-full rounded" />
<Skeleton className="h-3.5 w-4/5 rounded" />
<Skeleton className="h-3 w-20 rounded" />
</div>
</div>
))}
</div>
) : scraps.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">
{scraps.map((scrap) => (
<ScrappedPostCard key={scrap.id} scrap={scrap} />
))}
</div>
)}
</section>
);
}
Loading
Loading