diff --git a/public/ArrowLeft.svg b/public/ArrowLeft.svg new file mode 100644 index 0000000..b8c3685 --- /dev/null +++ b/public/ArrowLeft.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/ArrowRight.svg b/public/ArrowRight.svg new file mode 100644 index 0000000..50ac5bf --- /dev/null +++ b/public/ArrowRight.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/ArrowThickLeft.svg b/public/ArrowThickLeft.svg new file mode 100644 index 0000000..a262f49 --- /dev/null +++ b/public/ArrowThickLeft.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/ArrowThickRight.svg b/public/ArrowThickRight.svg new file mode 100644 index 0000000..b1e85a1 --- /dev/null +++ b/public/ArrowThickRight.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/BookImgSample.svg b/public/BookImgSample.svg new file mode 100644 index 0000000..06f9f4d --- /dev/null +++ b/public/BookImgSample.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/add_story.svg b/public/add_story.svg new file mode 100644 index 0000000..4b2a511 --- /dev/null +++ b/public/add_story.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/after_group.svg b/public/after_group.svg new file mode 100644 index 0000000..d5d74b6 --- /dev/null +++ b/public/after_group.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/after_home.svg b/public/after_home.svg new file mode 100644 index 0000000..92cce87 --- /dev/null +++ b/public/after_home.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/after_my.svg b/public/after_my.svg new file mode 100644 index 0000000..25a5bae --- /dev/null +++ b/public/after_my.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/after_news.svg b/public/after_news.svg new file mode 100644 index 0000000..54047b2 --- /dev/null +++ b/public/after_news.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/after_story.svg b/public/after_story.svg new file mode 100644 index 0000000..102cd20 --- /dev/null +++ b/public/after_story.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/before_group.svg b/public/before_group.svg new file mode 100644 index 0000000..91ae00a --- /dev/null +++ b/public/before_group.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/before_home.svg b/public/before_home.svg new file mode 100644 index 0000000..ab1fa4c --- /dev/null +++ b/public/before_home.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/before_my.svg b/public/before_my.svg new file mode 100644 index 0000000..beedc4c --- /dev/null +++ b/public/before_my.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/before_news.svg b/public/before_news.svg new file mode 100644 index 0000000..544b647 --- /dev/null +++ b/public/before_news.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/before_story.svg b/public/before_story.svg new file mode 100644 index 0000000..78fb9dd --- /dev/null +++ b/public/before_story.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/blank_heart.svg b/public/blank_heart.svg deleted file mode 100644 index a0275b9..0000000 --- a/public/blank_heart.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/public/gray_share.svg b/public/gray_share.svg new file mode 100644 index 0000000..e474b11 --- /dev/null +++ b/public/gray_share.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/inquiry.svg b/public/inquiry.svg new file mode 100644 index 0000000..c8edfc6 --- /dev/null +++ b/public/inquiry.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/menu_dots.svg b/public/menu_dots.svg new file mode 100644 index 0000000..b4ff2ed --- /dev/null +++ b/public/menu_dots.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/news_sample4.svg b/public/news_sample4.svg new file mode 100644 index 0000000..90da70d --- /dev/null +++ b/public/news_sample4.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/reply.svg b/public/reply.svg new file mode 100644 index 0000000..4004bf1 --- /dev/null +++ b/public/reply.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/reply2.svg b/public/reply2.svg new file mode 100644 index 0000000..9011ca5 --- /dev/null +++ b/public/reply2.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/report.svg b/public/report.svg new file mode 100644 index 0000000..41f235c --- /dev/null +++ b/public/report.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/share.svg b/public/share.svg new file mode 100644 index 0000000..5808465 --- /dev/null +++ b/public/share.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/triangle.svg b/public/triangle.svg new file mode 100644 index 0000000..0936c22 --- /dev/null +++ b/public/triangle.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/app/(main)/news/[id]/page.tsx b/src/app/(main)/news/[id]/page.tsx new file mode 100644 index 0000000..55f3cd6 --- /dev/null +++ b/src/app/(main)/news/[id]/page.tsx @@ -0,0 +1,179 @@ +import TodayRecommendedBooks from "@/components/base-ui/News/today_recommended_books"; +import Image from "next/image"; +import { notFound } from "next/navigation"; + +const DUMMY_NEWS = [ + { + id: 1, + imageUrl: "/news_sample4.svg", + title: "책 읽는 한강공원", + content: "소식내용소식내용소식내용소식내용소식내용소식내용소식내용소식내용소식내용소식내용", + date: "2025-10-09", + fullContent: `📚✨ 책읽는 한강공원이 📖 + +25년 하반기에 다시 돌아옵니다 🎶💃🏼🎺 +반짝이는 강물과 따스한 햇살 아래,특별한 프로그램들이 여러분을 기다립니다. + +자연 속에서 즐기는 여유, 모두가 함께 만드는 즐거움, 그리고 한강에서만 느낄 수 있는 특별한 순간까지! 한강에서 가족, 친구, 연인과 함께 소중한 추억을 만들어 보세요. 💐🌺🍀🌷 + + +특색 있는 공간조성과 콘텐츠로 업그레이드 되었습니다 ♥️ +기대하시라 🎺개봉박두~~~~~ + + +✨일정✨ + + +📅 9월 6일 부터 매주토요일~ + +⏰ 13:00~20:00 + + +📍여의도 한강공원 멀티프라자 + +하반기 : 2025.9.6..~10.25. 매주 토요일 + + +#캘박필수❤️ + + +다채로운 축제가 가득한 한강, 하반기에도 책읽는 한강공원에서 만나요 💖💗💝 + + +#서울 #한강 #축제 #한강공원 #한강데이트 #데이트 #서울 #한강 #책읽는한강공원 #여의도한강공원 #책 + +#서울핫플 #위대한가이드 #잠원한강공원 #여의도한강공원 #광나루 #서울핫플추천 #서울팝업 #팝업스토어추천 #무료공연 #서울무료공연`, + }, + { + id: 2, + imageUrl: "/news_sample4.svg", + title: "책 읽는 한강공원", + content: "소식내용소식내용소식내용소식내용소식내용소식내용소식내용소식내용소식내용소식내용", + date: "2025-10-09", + fullContent: "소식 상세 내용.", + }, + { + id: 3, + imageUrl: "/news_sample4.svg", + title: "책 읽는 한강공원", + content: "소식내용소식내용소식내용소식내용소식내용소식내용소식내용소식내용소식내용소식내용", + date: "2025-10-09", + fullContent: "소식 상세 내용.", + }, + { + id: 4, + imageUrl: "/news_sample4.svg", + title: "책 읽는 한강공원", + content: "소식내용소식내용소식내용소식내용소식내용소식내용소식내용소식내용소식내용소식내용", + date: "2025-10-09", + fullContent: "소식 상세 내용.", + }, +]; + +const DUMMY_BOOKS = [ + { + id: 1, + imgUrl: "/booksample.svg", + title: "책 제목", + author: "작가작가작가", + }, + { + id: 2, + imgUrl: "/booksample.svg", + title: "책 제목", + author: "작가작가작가", + }, + { + id: 3, + imgUrl: "/booksample.svg", + title: "책 제목", + author: "작가작가작가", + }, + { + id: 4, + imgUrl: "/booksample.svg", + title: "책 제목", + author: "작가작가작가", + }, +]; + +function getNewsById(id: number) { + return DUMMY_NEWS.find((news) => news.id === id); +} + +type Props = { + params: Promise<{ id: string }>; +}; + +export default async function NewsDetailPage({ params }: Props) { + const { id } = await params; + const news = getNewsById(Number(id)); + + if (!news) { + notFound(); + } + + return ( + <> +
+ {news.title} + + +
+
+
전체
+
+ next +
+
글 상세보기
+
+
+
+ + {/* 메인 */} +
+
+

{news.title}

+

{news.date}

+
+ + {/* 본문 */} +
+

+ {news.fullContent || news.content} +

+
+
+
+ + + {/* 문의하기 */} + + + ); +} diff --git a/src/app/(main)/news/page.tsx b/src/app/(main)/news/page.tsx new file mode 100644 index 0000000..d6aa075 --- /dev/null +++ b/src/app/(main)/news/page.tsx @@ -0,0 +1,117 @@ +"use client"; + +import Image from "next/image"; +import NewsList from "@/components/base-ui/News/news_list"; +import TodayRecommendedBooks from "@/components/base-ui/News/today_recommended_books"; + +const DUMMY_NEWS = [ + { + id: 1, + imageUrl: "/news_sample.svg", + title: "책 읽는 한강공원", + content: "소식내용소식내용소식내용소식내용소식내용소식내용소식내용소식내용소식내용소식내용", + date: "2025-10-09", + }, + { + id: 2, + imageUrl: "/news_sample.svg", + title: "책 읽는 한강공원", + content: "소식내용소식내용소식내용소식내용소식내용소식내용소식내용소식내용소식내용소식내용", + date: "2025-10-09", + }, + { + id: 3, + imageUrl: "/news_sample.svg", + title: "책 읽는 한강공원", + content: "소식내용소식내용소식내용소식내용소식내용소식내용소식내용소식내용소식내용소식내용", + date: "2025-10-09", + }, + { + id: 4, + imageUrl: "/news_sample.svg", + title: "책 읽는 한강공원", + content: "소식내용소식내용소식내용소식내용소식내용소식내용소식내용소식내용소식내용소식내용", + date: "2025-10-09", + }, +]; + +const DUMMY_BOOKS = [ + { + id: 1, + imgUrl: "/booksample.svg", + title: "책 제목", + author: "작가작가작가", + }, + { + id: 2, + imgUrl: "/booksample.svg", + title: "책 제목", + author: "작가작가작가", + }, + { + id: 3, + imgUrl: "/booksample.svg", + title: "책 제목", + author: "작가작가작가", + }, + { + id: 4, + imgUrl: "/booksample.svg", + title: "책 제목", + author: "작가작가작가", + }, +]; + +export default function NewsPage() { + return ( +
+
+
+ 소식 배너 +
+
+ + {/* 뉴스 리스트 */} +
+ {DUMMY_NEWS.map((news) => ( + + ))} +
+ +
+ + {/* 오늘의 추천 */} + + + {/* 문의하기 */} + +
+ ); +} \ No newline at end of file diff --git a/src/app/(main)/stories/[id]/page.tsx b/src/app/(main)/stories/[id]/page.tsx new file mode 100644 index 0000000..a3eaa88 --- /dev/null +++ b/src/app/(main)/stories/[id]/page.tsx @@ -0,0 +1,87 @@ +import BookstoryDetail from "@/components/base-ui/BookStory/bookstory_detail"; +import StoryNavigation from "@/components/base-ui/BookStory/story_navigation"; +import CommentSection from "@/components/base-ui/Comment/comment_section"; +import Image from "next/image"; +import { getStoryById, getAdjacentStoryIds } from "@/data/dummyStories"; +import { notFound } from "next/navigation"; + +type Props = { + params: Promise<{ id: string }>; +}; + +export default async function StoryDetailPage({ params }: Props) { + const { id } = await params; + + // id로 스토리 데이터 가져오기 + const story = getStoryById(Number(id)); + + // 스토리가 없으면 404 + if (!story) { + notFound(); + } + + // 이전/다음 스토리 ID + const { prevId, nextId } = getAdjacentStoryIds(Number(id)); + + return ( +
+ {/* 책이야기 > 상세보기 */} + {/* 모바일: 전체 너비 선 */} +
+
+
책이야기
+
+ next +
+
상세보기
+
+
+ {/* 태블릿/데스크탑: max-w 안에서 선 */} +
+
책이야기
+
+ next +
+
상세보기
+
+ {/* 메인 콘텐츠 영역 */} +
+ + + + {/* 책이야기 글 본문 */} +
+

{story.title}

+

+ {story.content} +

+
+ {/* 댓글 */} +
+ +
+
+
+ ); +} diff --git a/src/app/(main)/stories/new/page.tsx b/src/app/(main)/stories/new/page.tsx new file mode 100644 index 0000000..53410a6 --- /dev/null +++ b/src/app/(main)/stories/new/page.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import Image from "next/image"; +import BookstoryText from "@/components/base-ui/BookStory/bookstory_text"; + +export default function NewStoryPage() { + const router = useRouter(); + const [title, setTitle] = useState(""); + const [detail, setDetail] = useState(""); + + const handleCancel = () => { + router.back(); + }; + + const handleSubmit = () => { + // TODO: 실제 저장 로직 구현 + console.log("저장:", { title, detail }); + router.push("/stories"); + }; + + return ( +
+ {/* 책이야기 > 글 작성하기 */} + {/* 모바일: 전체 너비 선 */} +
+
+
전체
+
+ next +
+
글 작성하기
+
+
+ {/* 태블릿/데스크탑: max-w 안에서 선 */} +
+
전체
+
+ next +
+
글 작성하기
+
+ {/* 메인 콘텐츠 영역 */} +
+ {/* 책 선택하기 박스 */} +
+
+ +
+
+ + {/* 글 작성 영역 */} +
+ +
+ + {/* 하단 버튼 */} +
+
+ + +
+
+
+
+ ); +} diff --git a/src/app/(main)/stories/page.tsx b/src/app/(main)/stories/page.tsx new file mode 100644 index 0000000..eb35a35 --- /dev/null +++ b/src/app/(main)/stories/page.tsx @@ -0,0 +1,93 @@ +"use client"; +import BookStoryCard from "@/components/base-ui/BookStory/bookstory_card"; +import ListSubscribe from "@/components/base-ui/home/list_subscribe"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import { DUMMY_STORIES } from "@/data/dummyStories"; + +// TODO: 실제 로그인 상태 여부는 나중에 +const isLoggedIn = false; // true: 로그인, false: 로그인X + +export default function StoriesPage() { + const router = useRouter(); + + const handleCardClick = (id: number) => { + router.push(`/stories/${id}`); + }; + + return ( +
+
+
+ 전체 +
+
+ 구독중 +
+
+ 긁적긁적 +
+
+ 북적북적 +
+
+ + {/* 메인 콘텐츠 영역 */} +
+
+ {/* 첫 번째 줄 */} + {DUMMY_STORIES.slice(0, 4).map((story) => ( +
handleCardClick(story.id)} + className="cursor-pointer" + > + +
+ ))} + + {/* 두 번째 줄: 비로그인 시 사용자 추천 + 카드 3개, 로그인 시 카드 4개 */} + {!isLoggedIn && } + {DUMMY_STORIES.slice(4, isLoggedIn ? 8 : 7).map((story) => ( +
handleCardClick(story.id)} + className="cursor-pointer" + > + +
+ ))} +
+ + {/* 글쓰기 버튼 */} +
+ +
+
+
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index 35a21c0..f60370e 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -31,6 +31,9 @@ } @theme inline { + --breakpoint-t: 768px; + --breakpoint-d: 1440px; + --color-background: var(--background); --color-foreground: var(--foreground); --font-sans: "Pretendard Variable", Pretendard, system-ui, sans-serif; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 219383c..e361923 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; +import BottomNav from "@/components/layout/BottomNav"; import { Toaster } from "react-hot-toast"; import { AuthProvider } from "@/components/auth/AuthProvider"; import "@/app/globals.css"; @@ -27,10 +28,11 @@ export default function RootLayout({ return ( {children} + diff --git a/src/components/base-ui/BookStory/bookstory_card.tsx b/src/components/base-ui/BookStory/bookstory_card.tsx index 615489d..17715f7 100644 --- a/src/components/base-ui/BookStory/bookstory_card.tsx +++ b/src/components/base-ui/BookStory/bookstory_card.tsx @@ -1,6 +1,6 @@ -'use client'; +"use client"; -import Image from 'next/image'; +import Image from "next/image"; type Props = { authorName: string; @@ -19,7 +19,7 @@ function timeAgo(iso: string) { const diff = Date.now() - new Date(iso).getTime(); const minutes = Math.floor(diff / 60000); - if (minutes < 1) return '방금'; + if (minutes < 1) return "방금"; if (minutes < 60) return `${minutes}분 전`; const hours = Math.floor(minutes / 60); @@ -31,19 +31,19 @@ function timeAgo(iso: string) { export default function BookStoryCard({ authorName, - profileImgSrc = '/profile2.svg', + profileImgSrc = "/profile2.svg", createdAt, viewCount, - coverImgSrc = '/bookstorycard.svg', + coverImgSrc = "/bookstorycard.svg", title, content, likeCount = 1, commentCount = 1, onSubscribeClick, - subscribeText = '구독', + subscribeText = "구독", }: Props) { return ( -
+
{/* 상단 프로필 */}
{/* 프로필 */} @@ -90,15 +90,17 @@ export default function BookStoryCard({ {/* 제목 + 내용 */}

{title}

-

{content}

+

+ {content} +

{/* 좋아요 + 댓글 */}
{/* 좋아요 */} -
+
좋아요 아이콘 {/* 구분선 */} -
+
{/* 댓글 */} -
+
댓글 아이콘 댓글 {commentCount}
diff --git a/src/components/base-ui/BookStory/bookstory_detail.tsx b/src/components/base-ui/BookStory/bookstory_detail.tsx index ec96de0..888a811 100644 --- a/src/components/base-ui/BookStory/bookstory_detail.tsx +++ b/src/components/base-ui/BookStory/bookstory_detail.tsx @@ -1,8 +1,8 @@ -'use client'; +"use client"; -import React from 'react'; -import Image from 'next/image'; -import Link from 'next/link'; +import { useState, useRef, useEffect } from "react"; +import Image from "next/image"; +import Link from "next/link"; type BookstoryDetailProps = { imageUrl?: string; @@ -17,33 +17,172 @@ type BookstoryDetailProps = { bookTitle: string; bookAuthor: string; - bookDetail: string; + bookDetail?: string; + + createdAt: string; + viewCount: number; + likeCount?: number; authorHref?: string; // 기본: `/profile/${authorId}` className?: string; }; +function timeAgo(iso: string) { + const diff = Date.now() - new Date(iso).getTime(); + const minutes = Math.floor(diff / 60000); + + if (minutes < 1) return "방금"; + if (minutes < 60) return `${minutes}분 전`; + + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}시간 전`; + + const days = Math.floor(hours / 24); + return `${days}일 전`; +} + export default function BookstoryDetail({ - imageUrl = '/bookstory_example.svg', + imageUrl = "/bookstory_example.svg", authorName, authorNickname, authorId, - profileImgSrc = '/profile2.svg', - subscribeText = '구독', + profileImgSrc = "/profile2.svg", + subscribeText = "구독", onSubscribeClick, bookTitle, bookAuthor, bookDetail, + createdAt, + viewCount, + likeCount = 1, authorHref, - className = '', + className = "", }: BookstoryDetailProps) { const href = authorHref ?? `/profile/${authorId}`; + const [menuOpen, setMenuOpen] = useState(false); + const menuRef = useRef(null); + + // 바깥 클릭 시 메뉴 닫기 + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(e.target as Node)) { + setMenuOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); return (
-
+ {/* 모바일: 시간/조회수 */} +
+ {timeAgo(createdAt)} + 조회수 {viewCount} +
+ + {/* 모바일: 프로필 + 구독 */} +
+ {/* 프로필 */} + +
+ {authorName} +
+

{authorNickname}

+ + + {/* 구독 */} + +
+ + {/* 모바일: 책 제목 + 햄버거 */} +
+
+

{bookTitle}

+

{bookAuthor}

+
+ + {/* 햄버거 */} +
+ + + {menuOpen && ( +
+ +
+ +
+ )} +
+
+ + {/* 모바일: 이미지 + 좋아요/공유 */} +
+ {/* 이미지 */} +
+ {bookTitle} +
+ + {/* 좋아요/공유 */} +
+
+ heart + 좋아요 {likeCount} +
+
+ share + 공유하기 +
+
+
+ + {/* 태블릿부터 이미지 */} +
{bookTitle}
-
-
- -
+ {/* 태블릿부터: 오른쪽 정보 영역 */} +
+ {/* 닉네임 + 제목/저자 + 좋아요/공유 */} +
+
+ {/* 닉네임 */} + +
+ {authorName} +
+

+ {authorNickname} +

+ + + {/* 제목/저자 */} +
+

{bookTitle}

+

{bookAuthor}

+
+
+ + {/* 좋아요/공유 + 시간/조회수 */} +
+
+ heart + + 좋아요 {likeCount} + {authorName} + 공유하기
- -
-

{authorName}

-

{authorNickname}

+
+ {timeAgo(createdAt)} + 조회수 {viewCount}
- +
+
+ {/* 구독 + 햄버거 */} +
-
-
+ {/* 햄버거 */} +
+ -
-

{bookTitle}

-

{bookAuthor}

-

{bookDetail}

+ {/* 드롭다운 메뉴 */} + {menuOpen && ( +
+ +
+ +
+ )} +
diff --git a/src/components/base-ui/BookStory/bookstory_text.tsx b/src/components/base-ui/BookStory/bookstory_text.tsx index 0da91a2..79feae4 100644 --- a/src/components/base-ui/BookStory/bookstory_text.tsx +++ b/src/components/base-ui/BookStory/bookstory_text.tsx @@ -1,6 +1,6 @@ -'use client'; +"use client"; -import React, { useCallback, useLayoutEffect, useRef } from 'react'; +import React, { useCallback, useLayoutEffect, useRef } from "react"; type BookstoryTextProps = { title: string; @@ -22,14 +22,14 @@ export default function BookstoryText({ const el = textareaRef.current; if (!el) return; - el.style.height = '0px'; // 먼저 줄여서 scrollHeight 정확히 계산 + el.style.height = "0px"; // 먼저 줄여서 scrollHeight 정확히 계산 el.style.height = `${el.scrollHeight}px`; }, [detail]); // textarea에서 Tab을 "들여쓰기"로 처리 const handleDetailKeyDown = useCallback( (e: React.KeyboardEvent) => { - if (e.key !== 'Tab') return; + if (e.key !== "Tab") return; e.preventDefault(); @@ -37,7 +37,7 @@ export default function BookstoryText({ const start = el.selectionStart ?? 0; const end = el.selectionEnd ?? 0; - const insert = ' '; + const insert = " "; const next = detail.slice(0, start) + insert + detail.slice(end); onChangeDetail(next); @@ -46,7 +46,7 @@ export default function BookstoryText({ el.selectionStart = el.selectionEnd = start + insert.length; }); }, - [detail, onChangeDetail], + [detail, onChangeDetail] ); return ( @@ -65,7 +65,7 @@ export default function BookstoryText({ placeholder="제목을 입력해주세요" className=" w-full bg-transparent outline-none - text-Gray-7 subhead_2 + text-Gray-7 subhead_3 t:subhead_2 placeholder:text-Gray-3 " /> @@ -78,11 +78,11 @@ export default function BookstoryText({ value={detail} onChange={(e) => onChangeDetail(e.target.value)} onKeyDown={handleDetailKeyDown} - placeholder="내용을 자유롭게 입력해주세요." + placeholder="자신의 책이야기를 들려주세요. (최대 5000자)" rows={6} className=" w-full resize-none bg-transparent outline-none - text-Gray-7 subhead_4_1 + text-Gray-7 body_1 t:subhead_4_1 placeholder:text-Gray-3 whitespace-pre-wrap " diff --git a/src/components/base-ui/BookStory/story_navigation.tsx b/src/components/base-ui/BookStory/story_navigation.tsx new file mode 100644 index 0000000..10801e5 --- /dev/null +++ b/src/components/base-ui/BookStory/story_navigation.tsx @@ -0,0 +1,102 @@ +"use client"; + +import Image from "next/image"; +import { useRouter } from "next/navigation"; + +type StoryNavigationProps = { + currentId: number; + prevId: number | null; + nextId: number | null; + children: React.ReactNode; +}; + +export default function StoryNavigation({ + currentId, + prevId, + nextId, + children, +}: StoryNavigationProps) { + const router = useRouter(); + + const handlePrev = () => { + if (prevId) { + router.push(`/stories/${prevId}`); + } + }; + + const handleNext = () => { + if (nextId) { + router.push(`/stories/${nextId}`); + } + }; + + return ( +
+ {/* 이전 버튼 - 태블릿/데스크탑 */} + + + {/* 모바일: 컨텐츠 + 오버레이 화살표 */} +
+ {children} + + {/* 이전 버튼 - 이미지 위 오버레이 */} + + + {/* 다음 버튼 - 이미지 위 오버레이 */} + +
+ + {/* 태블릿/데스크탑: 컨텐츠 */} +
{children}
+ + {/* 다음 버튼 - 태블릿/데스크탑 */} + +
+ ); +} diff --git a/src/components/base-ui/Comment/comment_input.tsx b/src/components/base-ui/Comment/comment_input.tsx new file mode 100644 index 0000000..d162832 --- /dev/null +++ b/src/components/base-ui/Comment/comment_input.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { useState } from "react"; + +//댓글 입력창 컴포넌트 +type CommentInputProps = { + onSubmit: (content: string) => void; + placeholder?: string; +}; + +export default function CommentInput({ + onSubmit, + placeholder = "댓글 내용", +}: CommentInputProps) { + const [content, setContent] = useState(""); + + const handleSubmit = () => { + if (!content.trim()) return; + onSubmit(content); + setContent(""); + }; + + return ( +
+ setContent(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleSubmit()} + placeholder={placeholder} + className="flex-1 w-[240px] h-[36px] t:w-[850px] t:h-[56px] px-4 py-3 rounded-lg border border-Subbrown-4 bg-White Body_1_2 text-Gray-7 placeholder:text-Gray-3 outline-none" + /> + +
+ ); +} diff --git a/src/components/base-ui/Comment/comment_item.tsx b/src/components/base-ui/Comment/comment_item.tsx new file mode 100644 index 0000000..88df2cf --- /dev/null +++ b/src/components/base-ui/Comment/comment_item.tsx @@ -0,0 +1,196 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import Image from "next/image"; + +// 댓글 1개 컴포넌트 +type CommentItemProps = { + id: number; + authorName: string; + profileImgSrc?: string; + content: string; + createdAt: string; + isAuthor?: boolean; // 글 작성자 여부 (작성자 뱃지용) + isMine?: boolean; // 내가 쓴 댓글인지 여부 (메뉴 분기용) + isReply?: boolean; // 대댓글 여부 + onReply?: (id: number) => void; + onEdit?: (id: number, content: string) => void; + onDelete?: (id: number) => void; + onReport?: (id: number) => void; +}; + +function formatDate(iso: string) { + const date = new Date(iso); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}.${month}.${day}`; +} + +export default function CommentItem({ + id, + authorName, + profileImgSrc = "/profile2.svg", + content, + createdAt, + isAuthor = false, + isMine = false, + isReply = false, + onReply, + onEdit, + onDelete, + onReport, +}: CommentItemProps) { + const [menuOpen, setMenuOpen] = useState(false); + const menuRef = useRef(null); + + // 바깥 클릭 시 메뉴 닫기 + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(e.target as Node)) { + setMenuOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + // 댓글 본체 (프로필 + 이름 + 내용 + 날짜 + 메뉴) + const commentBody = ( +
+ {/* 상단: 프로필 + 이름 + 작성자 + 날짜 */} +
+
+ {/* 프로필 이미지 */} +
+ {authorName} +
+ {authorName} + {isAuthor && ( + 작성자 + )} +
+ {formatDate(createdAt)} +
+ + {/* 댓글 내용 + 메뉴 (같은 줄) */} +
+

{content}

+ {/* 메뉴 버튼 */} +
+ + + {/* 드롭다운 메뉴 */} + {menuOpen && ( +
+ {/* 답글달기 */} + {!isReply && onReply && ( + <> + +
+ + )} + {/* 내 댓글일 때: 수정, 삭제 */} + {isMine ? ( + <> + {onEdit && ( + + )} + {onEdit && onDelete && ( +
+ )} + {onDelete && ( + + )} + + ) : ( + /* 남의 댓글일 때: 신고하기 */ + + )} +
+ )} +
+
+
+ ); + + // 대댓글이면 reply 아이콘 + 댓글 본체 + if (isReply) { + return ( +
+ 대댓글{commentBody} +
+ ); + } + + // 일반 댓글 + return
{commentBody}
; +} diff --git a/src/components/base-ui/Comment/comment_list.tsx b/src/components/base-ui/Comment/comment_list.tsx new file mode 100644 index 0000000..1da98b1 --- /dev/null +++ b/src/components/base-ui/Comment/comment_list.tsx @@ -0,0 +1,174 @@ +"use client"; + +import { useState } from "react"; +import CommentInput from "./comment_input"; +import CommentItem from "./comment_item"; +import Image from "next/image"; + +// 댓글 목록 (댓글 입력창 + 댓글 + 대댓글) 컴포넌트 +export type Comment = { + id: number; + authorName: string; + profileImgSrc?: string; + content: string; + createdAt: string; + isAuthor?: boolean; // 글 작성자인지 (뱃지용) + isMine?: boolean; // 내가 쓴 댓글인지 + replies?: Comment[]; +}; + +type CommentListProps = { + comments: Comment[]; + onAddComment: (content: string) => void; + onAddReply?: (parentId: number, content: string) => void; + onEditComment?: (id: number, content: string) => void; + onDeleteComment?: (id: number) => void; + onReportComment?: (id: number) => void; +}; + +export default function CommentList({ + comments, + onAddComment, + onAddReply, + onEditComment, + onDeleteComment, + onReportComment, +}: CommentListProps) { + // 각 댓글의 답글 입력창 표시 여부 + const [replyInputOpen, setReplyInputOpen] = useState>({}); + // 각 댓글의 답글 입력 내용을 관리 + const [replyContents, setReplyContents] = useState>({}); + + const handleReplyClick = (commentId: number) => { + setReplyInputOpen((prev) => ({ + ...prev, + [commentId]: !prev[commentId], + })); + if (!replyInputOpen[commentId]) { + setReplyContents((prev) => ({ + ...prev, + [commentId]: "", + })); + } + }; + + const handleReplySubmit = (commentId: number) => { + const replyContent = replyContents[commentId]?.trim(); + if (!replyContent) return; + + // 답글 추가 + if (onAddReply) { + onAddReply(commentId, replyContent); + } + + // 답글 입력창 닫기 및 내용 초기화 + setReplyInputOpen((prev) => ({ + ...prev, + [commentId]: false, + })); + setReplyContents((prev) => { + const newContents = { ...prev }; + delete newContents[commentId]; + return newContents; + }); + }; + + const handleReplyContentChange = (commentId: number, content: string) => { + setReplyContents((prev) => ({ + ...prev, + [commentId]: content, + })); + }; + + return ( +
+ {/* 댓글 헤더 */} +

댓글

+ + {/* 댓글 입력 */} + + + {/* 댓글 목록 */} +
+ {comments.map((comment) => { + const isReplyInputVisible = replyInputOpen[comment.id]; + const replyContent = replyContents[comment.id] || ""; + + return ( +
+ + + {/* 답글 입력창 */} + {isReplyInputVisible && ( +
+ 대댓글 +
+ + handleReplyContentChange(comment.id, e.target.value) + } + onKeyDown={(e) => { + if (e.key === "Enter") { + handleReplySubmit(comment.id); + } + }} + placeholder="답글 달기" + className="flex-1 w-[240px] h-[36px] t:w-[850px] t:h-[56px] px-4 py-3 rounded-lg border border-Subbrown-4 bg-White Body_1_2 text-Gray-7 placeholder:text-Gray-3 outline-none" + autoFocus + /> + +
+
+ )} + + {/* 대댓글 */} + {comment.replies?.map((reply) => ( +
+ +
+ ))} +
+ ); + })} +
+
+ ); +} diff --git a/src/components/base-ui/Comment/comment_section.tsx b/src/components/base-ui/Comment/comment_section.tsx new file mode 100644 index 0000000..b2dd4dd --- /dev/null +++ b/src/components/base-ui/Comment/comment_section.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { useState } from "react"; +import CommentList, { Comment } from "./comment_list"; + +// 더미 댓글 데이터 +const DUMMY_COMMENTS: Comment[] = [ + { + id: 1, + authorName: "hy_1234", + profileImgSrc: "/profile2.svg", + content: "인정합니다.", + createdAt: "2025-09-22T10:00:00", + isAuthor: true, // 글 작성자 + isMine: true, // 내가 쓴 댓글 + }, + { + id: 3, + authorName: "hy-123456", + profileImgSrc: "/profile4.svg", + content: "인정합니다.", + createdAt: "2025-09-22T12:00:00", + isAuthor: false, + isMine: false, + }, +]; + +// 어떤 글의 댓글인지 구분(나중에 api 연동 시 사용) +type CommentSectionProps = { + storyId: number; +}; + +export default function CommentSection({ storyId }: CommentSectionProps) { + const [comments, setComments] = useState(DUMMY_COMMENTS); + + const handleAddComment = (content: string) => { + const newComment: Comment = { + id: Date.now(), + authorName: "유빈", // TODO: 실제 로그인 유저 정보 + profileImgSrc: "/profile2.svg", + content, + createdAt: new Date().toISOString(), + isAuthor: false, + isMine: true, // 내가 쓴 댓글 + }; + setComments([...comments, newComment]); + }; + + const handleAddReply = (parentId: number, content: string) => { + // 새 답글 생성 + const newReply: Comment = { + id: Date.now(), + authorName: "유빈", + profileImgSrc: "/profile2.svg", + content, + createdAt: new Date().toISOString(), + isAuthor: false, + isMine: true, + }; + + // 원래 댓글 찾아서 replies 배열에 답글 추가 + setComments((prevComments) => { + return prevComments.map((comment) => { + if (comment.id === parentId) { + // 원래 댓글의 replies 배열에 새 답글 추가 + return { + ...comment, + replies: [...(comment.replies || []), newReply], + }; + } + return comment; + }); + }); + }; + + const handleEditComment = (id: number, content: string) => { + + }; + + const handleDeleteComment = (id: number) => { + setComments(comments.filter((c) => c.id !== id)); + }; + + const handleReportComment = (id: number) => { + // 신고 API 연동 + console.log("댓글 신고:", id); + alert("신고가 접수되었습니다."); + }; + + return ( + + ); +} + diff --git a/src/components/base-ui/News/news_list.tsx b/src/components/base-ui/News/news_list.tsx index 9cfce0c..9cfe20e 100644 --- a/src/components/base-ui/News/news_list.tsx +++ b/src/components/base-ui/News/news_list.tsx @@ -1,7 +1,9 @@ 'use client'; import Image from 'next/image'; +import { useRouter } from 'next/navigation'; type NewsListProps = { + id?: number; imageUrl: string; title: string; content: string; @@ -10,17 +12,28 @@ type NewsListProps = { }; export default function NewsList({ + id, imageUrl, title, content, date, className = '', }: NewsListProps) { + const router = useRouter(); + + const handleClick = () => { + if (id) { + router.push(`/news/${id}`); + } + }; + return (
@@ -35,9 +48,7 @@ export default function NewsList({ />
- {/* middle + right */} -
- {/* middle text */} +

{title}

@@ -45,8 +56,8 @@ export default function NewsList({

- {/* 최소 120px 확보 + 날짜 오른쪽 고정 */} -
+ {/* 날짜 - 모바일: 아래 오른쪽, 태블릿 이상: 오른쪽 */} +

{date}

diff --git a/src/components/base-ui/News/today_recommended_books.tsx b/src/components/base-ui/News/today_recommended_books.tsx new file mode 100644 index 0000000..3159521 --- /dev/null +++ b/src/components/base-ui/News/today_recommended_books.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { useState } from "react"; +import BookCoverCard from "./recommendbook_element"; + +type Book = { + id: number; + imgUrl: string; + title: string; + author: string; +}; + +type TodayRecommendedBooksProps = { + books: Book[]; + className?: string; +}; + +export default function TodayRecommendedBooks({ + books, + className = "", +}: TodayRecommendedBooksProps) { + const [likedBooks, setLikedBooks] = useState>({}); + + const handleLikeChange = (bookId: number, liked: boolean) => { + setLikedBooks((prev) => ({ + ...prev, + [bookId]: liked, + })); + }; + + return ( +
+
+

오늘의 추천 책

+
+ {books.map((book) => ( + handleLikeChange(book.id, liked)} + /> + ))} +
+
+
+ ); +} diff --git a/src/components/layout/BottomNav.tsx b/src/components/layout/BottomNav.tsx new file mode 100644 index 0000000..ad6856c --- /dev/null +++ b/src/components/layout/BottomNav.tsx @@ -0,0 +1,72 @@ +"use client"; + +import Link from "next/link"; +import Image from "next/image"; +import { usePathname } from "next/navigation"; + +const NAV_ITEMS = [ + { + label: "책모 홈", + href: "/", + iconBefore: "/before_home.svg", + iconAfter: "/after_home.svg", + }, + { + label: "모임", + href: "/groups", + iconBefore: "/before_group.svg", + iconAfter: "/after_group.svg", + }, + { + label: "책 이야기", + href: "/stories", + iconBefore: "/before_story.svg", + iconAfter: "/after_story.svg", + }, + { + label: "소식", + href: "/news", + iconBefore: "/before_news.svg", + iconAfter: "/after_news.svg", + }, + { + label: "마이페이지", + href: "/mypage", + iconBefore: "/before_my.svg", + iconAfter: "/after_my.svg", + }, +]; + +export default function BottomNav() { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 06b8614..553f0ae 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -12,16 +12,29 @@ const NAV = [ { label: "소식", href: "/news" }, ]; +// 현재 경로에 맞는 페이지 타이틀 반환 +const getPageTitle = (pathname: string) => { + if (pathname === "/") return "책모 홈"; + if (pathname.startsWith("/groups")) return "모임"; + if (pathname.startsWith("/stories")) return "책 이야기"; + if (pathname.startsWith("/news")) return "소식"; + return "책모 홈"; +}; + export default function Header() { const pathname = usePathname(); + const pageTitle = getPageTitle(pathname); return ( -
-
-
+
+
+
{/*로고 + 메뉴*/} -
- +
+ 책모 로고 -
+ {/* 모바일: 중앙 타이틀 표시 */} + + {pageTitle} + + {/*아이콘*/} -
+
+ {/* 태블릿부터 프로필 표시 */} diff --git a/src/data/dummyStories.ts b/src/data/dummyStories.ts new file mode 100644 index 0000000..959b5c0 --- /dev/null +++ b/src/data/dummyStories.ts @@ -0,0 +1,150 @@ +// 더미 데이터 (API 연동 시 삭제) +export const DUMMY_STORIES = [ + { + id: 1, + authorName: "hy_0716", + authorNickname: "hy_0716", + createdAt: "2026-01-03T10:00:00", + viewCount: 302, + title: "나는 나이든 왕자다", + content: + "나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸 깨달은지 얼마 안되었다...나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸 깨달은지 얼마 안되었다...나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸 깨달은지 얼마 안되었다...나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸 깨달은지 얼마 안되었다...나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸 깨달은지 얼마 안되었다...나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸 깨달은지 얼마 안되었다...나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸 깨달은지 얼마 안되었다...나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸 깨달은지 얼마 안되었다...나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸 깨달은지 얼마 안되었다...나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸 깨달은지 얼마 안되었다...나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸 깨달은지 얼마 안되었다...나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸 깨달은지 얼마 안되었다...나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸 깨달은지 얼마 안되었다...나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸 깨달은지 얼마 안되었다...나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸 깨달은지 얼마 안되었다...나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸 깨달은지 얼마 안되었다...나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸 깨달은지 얼마 안되었다...나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸 깨달은지 얼마 안되었다...나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸 깨달은지 얼마 안되었다...나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸 깨달은지 얼마 안되었다...나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바", + likeCount: 1, + commentCount: 1, + bookTitle: "어린 왕자", + bookAuthor: "생텍쥐페리", + bookDetail: "사막에 불시착한 비행기 조종사가 어린 왕자를 만나 겪는 이야기...", + bookImageUrl: "/BookImgSample.svg", + }, + { + id: 2, + authorName: "hy_0716", + authorNickname: "hy_0716", + createdAt: "2026-01-03T09:00:00", + viewCount: 302, + title: "나는 나이든 왕자다", + content: + "나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸...", + likeCount: 1, + commentCount: 1, + bookTitle: "어린 왕자", + bookAuthor: "생텍쥐페리", + bookDetail: "사막에 불시착한 비행기 조종사가 어린 왕자를 만나 겪는 이야기...", + bookImageUrl: "/BookImgSample.svg", + }, + { + id: 3, + authorName: "hy_0716", + authorNickname: "hy_0716", + createdAt: "2026-01-02T15:00:00", + viewCount: 302, + title: "나는 나이든 왕자다", + content: + "나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸...", + likeCount: 1, + commentCount: 1, + bookTitle: "어린 왕자", + bookAuthor: "생텍쥐페리", + bookDetail: "사막에 불시착한 비행기 조종사가 어린 왕자를 만나 겪는 이야기...", + bookImageUrl: "/BookImgSample.svg", + }, + { + id: 4, + authorName: "hy_0716", + authorNickname: "hy_0716", + createdAt: "2026-01-02T12:00:00", + viewCount: 302, + title: "나는 나이든 왕자다", + content: + "나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸...", + likeCount: 1, + commentCount: 1, + bookTitle: "어린 왕자", + bookAuthor: "생텍쥐페리", + bookDetail: "사막에 불시착한 비행기 조종사가 어린 왕자를 만나 겪는 이야기...", + bookImageUrl: "/BookImgSample.svg", + }, + { + id: 5, + authorName: "hy_0716", + authorNickname: "hy_0716", + createdAt: "2026-01-01T10:00:00", + viewCount: 302, + title: "나는 나이든 왕자다", + content: + "나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸...", + likeCount: 1, + commentCount: 1, + bookTitle: "어린 왕자", + bookAuthor: "생텍쥐페리", + bookDetail: "사막에 불시착한 비행기 조종사가 어린 왕자를 만나 겪는 이야기...", + bookImageUrl: "/BookImgSample.svg", + }, + { + id: 6, + authorName: "hy_0716", + authorNickname: "hy_0716", + createdAt: "2025-12-31T10:00:00", + viewCount: 302, + title: "나는 나이든 왕자다", + content: + "나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸...", + likeCount: 1, + commentCount: 1, + bookTitle: "어린 왕자", + bookAuthor: "생텍쥐페리", + bookDetail: "사막에 불시착한 비행기 조종사가 어린 왕자를 만나 겪는 이야기...", + bookImageUrl: "/BookImgSample.svg", + }, + { + id: 7, + authorName: "hy_0716", + authorNickname: "hy_0716", + createdAt: "2025-12-30T10:00:00", + viewCount: 302, + title: "나는 나이든 왕자다", + content: + "나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸...", + likeCount: 1, + commentCount: 1, + bookTitle: "어린 왕자", + bookAuthor: "생텍쥐페리", + bookDetail: "사막에 불시착한 비행기 조종사가 어린 왕자를 만나 겪는 이야기...", + bookImageUrl: "/BookImgSample.svg", + }, + { + id: 8, + authorName: "hy_0716", + authorNickname: "hy_0716", + createdAt: "2025-12-29T10:00:00", + viewCount: 302, + title: "나는 나이든 왕자다", + content: + "나는 나이든 왕자다. 그 누가 숫자가 중요하다가 했던가. 세고 또 세는 그런 마법같은 경험을 한사람은 놀랍도록 이세상에 얼마 안된다! 나는 숲이 아닌 바다란걸...", + likeCount: 1, + commentCount: 1, + bookTitle: "어린 왕자", + bookAuthor: "생텍쥐페리", + bookDetail: "사막에 불시착한 비행기 조종사가 어린 왕자를 만나 겪는 이야기...", + bookImageUrl: "/BookImgSample.svg", + }, +]; + +export function getStoryById(id: number) { + return DUMMY_STORIES.find((story) => story.id === id); +} + +// 이전/다음 스토리 ID 가져오기 +export function getAdjacentStoryIds(id: number) { + const currentIndex = DUMMY_STORIES.findIndex((story) => story.id === id); + + if (currentIndex === -1) { + return { prevId: null, nextId: null }; + } + + const prevId = currentIndex > 0 ? DUMMY_STORIES[currentIndex - 1].id : null; + const nextId = currentIndex < DUMMY_STORIES.length - 1 ? DUMMY_STORIES[currentIndex + 1].id : null; + + return { prevId, nextId }; +} +