From ade08e1a3669cc3b22fc8dbb5c215f8cfd2a80ab Mon Sep 17 00:00:00 2001 From: heeyongKim <166043860+heeeeyong@users.noreply.github.com> Date: Mon, 13 Oct 2025 18:21:15 +0900 Subject: [PATCH 01/34] =?UTF-8?q?fix:=20=EC=9C=A0=EC=A0=80=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EC=BD=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/feed/people.svg | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/assets/feed/people.svg b/src/assets/feed/people.svg index b207e318..16ffaaa1 100644 --- a/src/assets/feed/people.svg +++ b/src/assets/feed/people.svg @@ -1,5 +1,6 @@ - - - - - + + + + + + \ No newline at end of file From 062120ee6070d947080703289ecb72deaec36f67 Mon Sep 17 00:00:00 2001 From: heeyongKim <166043860+heeeeyong@users.noreply.github.com> Date: Mon, 13 Oct 2025 18:30:17 +0900 Subject: [PATCH 02/34] =?UTF-8?q?fix:=20=ED=88=AC=ED=91=9C=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=8B=9C=20=EC=A0=9C=EB=AA=A9=20=EA=B0=80=EC=9D=B4?= =?UTF-8?q?=EB=93=9C=20=EB=A9=98=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/pollwrite/PollCreationSection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pollwrite/PollCreationSection.tsx b/src/components/pollwrite/PollCreationSection.tsx index 92b5dca1..0f1eb489 100644 --- a/src/components/pollwrite/PollCreationSection.tsx +++ b/src/components/pollwrite/PollCreationSection.tsx @@ -108,7 +108,7 @@ const PollCreationSection = ({ Date: Mon, 27 Oct 2025 14:48:02 +0900 Subject: [PATCH 03/34] =?UTF-8?q?feat:=20=ED=94=BC=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=20=EC=98=81=EC=97=AD=20=EA=B5=AC=ED=98=84=20-=20?= =?UTF-8?q?=EC=A7=80=EA=B8=88=20=EB=9C=A8=EB=8A=94=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=EA=B8=80=20=EC=84=B9=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/feed/RecommendedFeedCard.tsx | 30 +++ .../feed/RecommendedFeedSection.tsx | 173 ++++++++++++++++++ src/components/feed/TotalFeed.tsx | 27 ++- 3 files changed, 220 insertions(+), 10 deletions(-) create mode 100644 src/components/feed/RecommendedFeedCard.tsx create mode 100644 src/components/feed/RecommendedFeedSection.tsx diff --git a/src/components/feed/RecommendedFeedCard.tsx b/src/components/feed/RecommendedFeedCard.tsx new file mode 100644 index 00000000..0f39b0c3 --- /dev/null +++ b/src/components/feed/RecommendedFeedCard.tsx @@ -0,0 +1,30 @@ +import styled from '@emotion/styled'; +import PostBody from '../common/Post/PostBody'; +import PostFooter from '../common/Post/PostFooter'; +import PostHeader from '../common/Post/PostHeader'; +import type { PostData } from '../../types/post'; +import { colors } from '@/styles/global/global'; + +const RecommendedFeedCard = (postData: PostData) => { + return ( + + + + + + ); +}; + +const CardContainer = styled.div` + display: flex; + flex-direction: column; + gap: 16px; + padding: 20px; + background-color: ${colors.darkgrey.dark}; + border-radius: 12px; + min-width: 280px; + width: 280px; + flex-shrink: 0; +`; + +export default RecommendedFeedCard; diff --git a/src/components/feed/RecommendedFeedSection.tsx b/src/components/feed/RecommendedFeedSection.tsx new file mode 100644 index 00000000..da1758be --- /dev/null +++ b/src/components/feed/RecommendedFeedSection.tsx @@ -0,0 +1,173 @@ +import styled from '@emotion/styled'; +import RecommendedFeedCard from './RecommendedFeedCard'; +import { colors, typography } from '@/styles/global/global'; +import type { PostData } from '@/types/post'; + +// 목업 데이터 +const mockRecommendedFeeds: PostData[] = [ + { + feedId: 101, + creatorId: 1, + creatorNickname: 'user.01', + creatorProfileImageUrl: 'https://via.placeholder.com/36', + alias: '공식 인플루언서', + postDate: '12시간 전', + isbn: '9788936434267', + bookTitle: '책이름을입력해주세요...', + bookAuthor: '한강', + contentBody: + '세줄까지만 입력 가능합니다.ㄴ ㅇㄹㄴㄴ ㅇㅎㄴ녀;ㅇㄹ만; ↑ㅇㅎㅇ ↓ ↑ㅇ앙; ↓ ↑ㅇ악; ↓ ↑ ㅁ보름ㅇㄹ과; ㅁ먼엄안ㅇ만;ㅇㄹ라; ㅁ ㅁ엄만; 입력...', + contentUrls: [], + likeCount: 123, + commentCount: 123, + isSaved: false, + isLiked: false, + isPublic: true, + isWriter: false, + }, + { + feedId: 102, + creatorId: 1, + creatorNickname: 'user.01', + creatorProfileImageUrl: 'https://via.placeholder.com/36', + alias: '공식 인플루언서', + postDate: '12시간 전', + isbn: '9788936434267', + bookTitle: '책이름을입력해주세요...', + bookAuthor: '한강', + contentBody: '한 줄이어도 카드 영역은 그대로 유지', + contentUrls: [], + likeCount: 123, + commentCount: 123, + isSaved: false, + isLiked: false, + isPublic: true, + isWriter: false, + }, + { + feedId: 103, + creatorId: 1, + creatorNickname: 'user.01', + creatorProfileImageUrl: 'https://via.placeholder.com/36', + alias: '공식 인플루언서', + postDate: '12시간 전', + isbn: '9788936434267', + bookTitle: '책이름을입력해주세요...', + bookAuthor: '한강', + contentBody: '안 줄이어도 카드 영역은 그대로 유지해야 합니다.', + contentUrls: [], + likeCount: 123, + commentCount: 123, + isSaved: false, + isLiked: false, + isPublic: true, + isWriter: false, + }, + { + feedId: 104, + creatorId: 1, + creatorNickname: 'user.01', + creatorProfileImageUrl: 'https://via.placeholder.com/36', + alias: '공식 인플루언서', + postDate: '12시간 전', + isbn: '9788936434267', + bookTitle: '책이름을입력해주세요...', + bookAuthor: '한강', + contentBody: + '세줄까지만 입력 가능합니다.ㄴ ㅇㄹㄴㄴ ㅇㅎㄴ녀;ㅇㄹ만; ↑ㅇㅎㅇ ↓ ↑ㅇ앙; ↓ ↑ㅇ악; ↓ ↑ ㅁ보름ㅇㄹ과; ㅁ먼엄안ㅇ만;ㅇㄹ라; ㅁ ㅁ엄만; 입력...', + contentUrls: [], + likeCount: 123, + commentCount: 123, + isSaved: false, + isLiked: false, + isPublic: true, + isWriter: false, + }, + { + feedId: 105, + creatorId: 1, + creatorNickname: 'user.01', + creatorProfileImageUrl: 'https://via.placeholder.com/36', + alias: '공식 인플루언서', + postDate: '12시간 전', + isbn: '9788936434267', + bookTitle: '책이름을입력해주세요...', + bookAuthor: '한강', + contentBody: + '세줄까지만 입력 가능합니다.ㄴ ㅇㄹㄴㄴ ㅇㅎㄴ녀;ㅇㄹ만; ↑ㅇㅎㅇ ↓ ↑ㅇ앙; ↓ ↑ㅇ악; ↓ ↑ ㅁ보름ㅇㄹ과; ㅁ먼엄안ㅇ만;ㅇㄹ라; ㅁ ㅁ엄만; 입력...', + contentUrls: [], + likeCount: 123, + commentCount: 123, + isSaved: false, + isLiked: false, + isPublic: true, + isWriter: false, + }, +]; + +const RecommendedFeedSection = () => { + return ( + + + 지금 뜨는 추천 글 + + + + {mockRecommendedFeeds.map(feed => ( + + ))} + + + + + ); +}; + +const SectionContainer = styled.div` + display: flex; + flex-direction: column; + width: 100%; + background-color: ${colors.black.main}; +`; + +const SectionHeader = styled.div` + padding: 28px 20px 16px; +`; + +const HeaderText = styled.h2` + color: ${colors.white}; + font-size: ${typography.fontSize.lg}; + font-weight: ${typography.fontWeight.bold}; + line-height: normal; + margin: 0; +`; + +const CarouselContainer = styled.div` + width: 100%; + overflow-x: auto; + overflow-y: hidden; + + /* 스크롤바 숨기기 */ + &::-webkit-scrollbar { + display: none; + } + -ms-overflow-style: none; + scrollbar-width: none; +`; + +const CardList = styled.div` + display: flex; + gap: 10px; + padding: 0 20px 28px; + width: fit-content; +`; + +const BorderBottom = styled.div` + width: 94.8%; + margin: 0 auto; + padding: 0 20px; + height: 6px; + background: #1c1c1c; +`; + +export default RecommendedFeedSection; diff --git a/src/components/feed/TotalFeed.tsx b/src/components/feed/TotalFeed.tsx index ddb533c0..270a4008 100644 --- a/src/components/feed/TotalFeed.tsx +++ b/src/components/feed/TotalFeed.tsx @@ -1,25 +1,32 @@ import styled from '@emotion/styled'; import FollowList from './FollowList'; import FeedPost from './FeedPost'; +import RecommendedFeedSection from './RecommendedFeedSection'; import type { FeedListProps } from '../../types/post'; import { colors, typography } from '@/styles/global/global'; -const TotalFeed = ({ showHeader, posts = [], isTotalFeed, isLast = false }: FeedListProps) => { +const TotalFeed = ({ showHeader, posts = [], isTotalFeed }: FeedListProps) => { const hasPosts = posts.length > 0; return ( {hasPosts ? ( - posts.map((post, index) => ( - - )) + <> + {posts.map((post, index) => ( + <> + + {/* 10번째 피드 이후에 추천 섹션 한 번만 표시 */} + {index === 9 && } + + ))} + ) : (
피드에 작성된 글이 없어요
From 917e1b6c7ab123f6d03b47fac656246171ebf4b9 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Mon, 27 Oct 2025 14:56:24 +0900 Subject: [PATCH 04/34] =?UTF-8?q?style:=20=ED=97=A4=EB=8D=94=20=EC=84=B9?= =?UTF-8?q?=EC=85=98=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/feed/FeedPost.tsx | 2 +- .../feed/RecommendedFeedSection.tsx | 53 ++++++++++++------- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/components/feed/FeedPost.tsx b/src/components/feed/FeedPost.tsx index 28d3105a..574a90c2 100644 --- a/src/components/feed/FeedPost.tsx +++ b/src/components/feed/FeedPost.tsx @@ -17,7 +17,7 @@ const Container = styled.div` `; const BorderBottom = styled.div` - width: 94.8%; + width: 100%; /* min-width: 280px; max-width: 500px; */ margin: 0 auto; diff --git a/src/components/feed/RecommendedFeedSection.tsx b/src/components/feed/RecommendedFeedSection.tsx index da1758be..03deb9cc 100644 --- a/src/components/feed/RecommendedFeedSection.tsx +++ b/src/components/feed/RecommendedFeedSection.tsx @@ -105,24 +105,6 @@ const mockRecommendedFeeds: PostData[] = [ }, ]; -const RecommendedFeedSection = () => { - return ( - - - 지금 뜨는 추천 글 - - - - {mockRecommendedFeeds.map(feed => ( - - ))} - - - - - ); -}; - const SectionContainer = styled.div` display: flex; flex-direction: column; @@ -131,14 +113,24 @@ const SectionContainer = styled.div` `; const SectionHeader = styled.div` - padding: 28px 20px 16px; + padding: 40px 20px 20px; + display: flex; + flex-direction: column; `; const HeaderText = styled.h2` color: ${colors.white}; - font-size: ${typography.fontSize.lg}; + font-size: ${typography.fontSize.xl}; font-weight: ${typography.fontWeight.bold}; line-height: normal; + margin-bottom: 8px; +`; + +const SubText = styled.p` + color: ${colors.grey[100]}; + font-size: ${typography.fontSize.sm}; + font-weight: ${typography.fontWeight.medium}; + line-height: 20px; margin: 0; `; @@ -170,4 +162,25 @@ const BorderBottom = styled.div` background: #1c1c1c; `; +// Component +const RecommendedFeedSection = () => { + return ( + + + 지금 뜨는 추천 글 + 비슷한 취향의 인플루언서, 작가가 + 추천하는 도서를 만나보세요. + + + + {mockRecommendedFeeds.map(feed => ( + + ))} + + + + + ); +}; + export default RecommendedFeedSection; From a73b81dce55cd1ee8bbd1bd4e813f95384f59100 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Mon, 27 Oct 2025 15:00:12 +0900 Subject: [PATCH 05/34] =?UTF-8?q?feat:=20Embla=20Carousel=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 ++ pnpm-lock.yaml | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/package.json b/package.json index da28e12c..ea7b87df 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "@emotion/styled": "^11.14.0", "@types/react-datepicker": "^7.0.0", "axios": "^1.11.0", + "embla-carousel": "^8.6.0", + "embla-carousel-react": "^8.6.0", "react": "^19.1.0", "react-cookie": "^8.0.1", "react-datepicker": "^8.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fae23ebd..2f86b85f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,12 @@ importers: axios: specifier: ^1.11.0 version: 1.11.0 + embla-carousel: + specifier: ^8.6.0 + version: 8.6.0 + embla-carousel-react: + specifier: ^8.6.0 + version: 8.6.0(react@19.1.0) react: specifier: ^19.1.0 version: 19.1.0 @@ -866,6 +872,19 @@ packages: electron-to-chromium@1.5.151: resolution: {integrity: sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA==} + embla-carousel-react@8.6.0: + resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==} + peerDependencies: + react: ^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + embla-carousel-reactive-utils@8.6.0: + resolution: {integrity: sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==} + peerDependencies: + embla-carousel: 8.6.0 + + embla-carousel@8.6.0: + resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -2487,6 +2506,18 @@ snapshots: electron-to-chromium@1.5.151: {} + embla-carousel-react@8.6.0(react@19.1.0): + dependencies: + embla-carousel: 8.6.0 + embla-carousel-reactive-utils: 8.6.0(embla-carousel@8.6.0) + react: 19.1.0 + + embla-carousel-reactive-utils@8.6.0(embla-carousel@8.6.0): + dependencies: + embla-carousel: 8.6.0 + + embla-carousel@8.6.0: {} + encodeurl@2.0.0: {} error-ex@1.3.2: From 0aa0b568706dfee461962f77d5368329cf11edf1 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Mon, 27 Oct 2025 15:14:04 +0900 Subject: [PATCH 06/34] =?UTF-8?q?feat:=20Embla=20Carousel=EB=A1=9C=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EA=B0=80=EB=A1=9C=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A1=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/feed/RecommendedFeedCard.tsx | 5 +- .../feed/RecommendedFeedSection.tsx | 52 +++++++++++-------- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/components/feed/RecommendedFeedCard.tsx b/src/components/feed/RecommendedFeedCard.tsx index 0f39b0c3..6543c66a 100644 --- a/src/components/feed/RecommendedFeedCard.tsx +++ b/src/components/feed/RecommendedFeedCard.tsx @@ -22,9 +22,8 @@ const CardContainer = styled.div` padding: 20px; background-color: ${colors.darkgrey.dark}; border-radius: 12px; - min-width: 280px; - width: 280px; - flex-shrink: 0; + width: 100%; + height: 100%; `; export default RecommendedFeedCard; diff --git a/src/components/feed/RecommendedFeedSection.tsx b/src/components/feed/RecommendedFeedSection.tsx index 03deb9cc..e38473ae 100644 --- a/src/components/feed/RecommendedFeedSection.tsx +++ b/src/components/feed/RecommendedFeedSection.tsx @@ -2,6 +2,8 @@ import styled from '@emotion/styled'; import RecommendedFeedCard from './RecommendedFeedCard'; import { colors, typography } from '@/styles/global/global'; import type { PostData } from '@/types/post'; +import useEmblaCarousel from 'embla-carousel-react'; +import type { EmblaOptionsType } from 'embla-carousel'; // 목업 데이터 const mockRecommendedFeeds: PostData[] = [ @@ -134,28 +136,26 @@ const SubText = styled.p` margin: 0; `; -const CarouselContainer = styled.div` - width: 100%; - overflow-x: auto; - overflow-y: hidden; - - /* 스크롤바 숨기기 */ - &::-webkit-scrollbar { - display: none; - } - -ms-overflow-style: none; - scrollbar-width: none; +const EmblaViewport = styled.div` + overflow: hidden; `; -const CardList = styled.div` +const EmblaContainer = styled.div` display: flex; - gap: 10px; - padding: 0 20px 28px; - width: fit-content; + touch-action: pan-y pinch-zoom; + gap: 12px; + padding: 0 20px; +`; + +const EmblaSlide = styled.div` + transform: translate3d(0, 0, 0); + flex: 0 0 calc(100% - 10px); /* 좌우 패딩 제외한 전체 너비 */ + min-width: 0; + padding-bottom: 28px; `; const BorderBottom = styled.div` - width: 94.8%; + width: 100%; margin: 0 auto; padding: 0 20px; height: 6px; @@ -164,6 +164,14 @@ const BorderBottom = styled.div` // Component const RecommendedFeedSection = () => { + const options: EmblaOptionsType = { + align: 'center', + slidesToScroll: 1, + containScroll: 'trimSnaps', + }; + + const [emblaRef] = useEmblaCarousel(options); + return ( @@ -171,13 +179,15 @@ const RecommendedFeedSection = () => { 비슷한 취향의 인플루언서, 작가가 추천하는 도서를 만나보세요. - - + + {mockRecommendedFeeds.map(feed => ( - + + + ))} - - + + ); From fdff2363aebec87210a6166aeb8b7de229c7914d Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Mon, 27 Oct 2025 15:27:26 +0900 Subject: [PATCH 07/34] =?UTF-8?q?style:=20=EA=B3=B5=EC=8B=9D=20=EC=9D=B8?= =?UTF-8?q?=ED=94=8C=EB=A3=A8=EC=96=B8=EC=84=9C=20=EC=83=89=EC=83=81=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/feed/RecommendedFeedCard.tsx | 8 ++++++-- src/components/feed/RecommendedFeedSection.tsx | 15 ++++++++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/components/feed/RecommendedFeedCard.tsx b/src/components/feed/RecommendedFeedCard.tsx index 6543c66a..3e0ea3d2 100644 --- a/src/components/feed/RecommendedFeedCard.tsx +++ b/src/components/feed/RecommendedFeedCard.tsx @@ -5,10 +5,14 @@ import PostHeader from '../common/Post/PostHeader'; import type { PostData } from '../../types/post'; import { colors } from '@/styles/global/global'; -const RecommendedFeedCard = (postData: PostData) => { +interface RecommendedFeedCardProps extends PostData { + aliasColor?: string; +} + +const RecommendedFeedCard = (postData: RecommendedFeedCardProps) => { return ( - + diff --git a/src/components/feed/RecommendedFeedSection.tsx b/src/components/feed/RecommendedFeedSection.tsx index e38473ae..90b4ee09 100644 --- a/src/components/feed/RecommendedFeedSection.tsx +++ b/src/components/feed/RecommendedFeedSection.tsx @@ -5,14 +5,19 @@ import type { PostData } from '@/types/post'; import useEmblaCarousel from 'embla-carousel-react'; import type { EmblaOptionsType } from 'embla-carousel'; -// 목업 데이터 -const mockRecommendedFeeds: PostData[] = [ +// 목업 데이터 (aliasColor 추가 필요) +interface MockPostData extends PostData { + aliasColor?: string; +} + +const mockRecommendedFeeds: MockPostData[] = [ { feedId: 101, creatorId: 1, creatorNickname: 'user.01', creatorProfileImageUrl: 'https://via.placeholder.com/36', alias: '공식 인플루언서', + aliasColor: colors.neongreen, postDate: '12시간 전', isbn: '9788936434267', bookTitle: '책이름을입력해주세요...', @@ -33,6 +38,7 @@ const mockRecommendedFeeds: PostData[] = [ creatorNickname: 'user.01', creatorProfileImageUrl: 'https://via.placeholder.com/36', alias: '공식 인플루언서', + aliasColor: colors.neongreen, postDate: '12시간 전', isbn: '9788936434267', bookTitle: '책이름을입력해주세요...', @@ -52,6 +58,7 @@ const mockRecommendedFeeds: PostData[] = [ creatorNickname: 'user.01', creatorProfileImageUrl: 'https://via.placeholder.com/36', alias: '공식 인플루언서', + aliasColor: colors.neongreen, postDate: '12시간 전', isbn: '9788936434267', bookTitle: '책이름을입력해주세요...', @@ -71,6 +78,7 @@ const mockRecommendedFeeds: PostData[] = [ creatorNickname: 'user.01', creatorProfileImageUrl: 'https://via.placeholder.com/36', alias: '공식 인플루언서', + aliasColor: colors.neongreen, postDate: '12시간 전', isbn: '9788936434267', bookTitle: '책이름을입력해주세요...', @@ -91,6 +99,7 @@ const mockRecommendedFeeds: PostData[] = [ creatorNickname: 'user.01', creatorProfileImageUrl: 'https://via.placeholder.com/36', alias: '공식 인플루언서', + aliasColor: colors.neongreen, postDate: '12시간 전', isbn: '9788936434267', bookTitle: '책이름을입력해주세요...', @@ -151,7 +160,7 @@ const EmblaSlide = styled.div` transform: translate3d(0, 0, 0); flex: 0 0 calc(100% - 10px); /* 좌우 패딩 제외한 전체 너비 */ min-width: 0; - padding-bottom: 28px; + padding-bottom: 40px; `; const BorderBottom = styled.div` From c36bb560ac72aa0eb59b23afd861ed3b29ac5fc7 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Mon, 27 Oct 2025 15:31:20 +0900 Subject: [PATCH 08/34] =?UTF-8?q?refactor:=20PostFooter=EB=A5=BC=20?= =?UTF-8?q?=ED=95=98=EB=8B=A8=EC=97=90=20=EA=B3=A0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/feed/RecommendedFeedCard.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/feed/RecommendedFeedCard.tsx b/src/components/feed/RecommendedFeedCard.tsx index 3e0ea3d2..2cf479f9 100644 --- a/src/components/feed/RecommendedFeedCard.tsx +++ b/src/components/feed/RecommendedFeedCard.tsx @@ -13,7 +13,9 @@ const RecommendedFeedCard = (postData: RecommendedFeedCardProps) => { return ( - + + + ); @@ -28,6 +30,17 @@ const CardContainer = styled.div` border-radius: 12px; width: 100%; height: 100%; + + > *:last-child { + margin-top: auto; + } +`; + +const PostBodyWrapper = styled.div` + .content { + -webkit-line-clamp: 3 !important; + min-height: 60px; + } `; export default RecommendedFeedCard; From 928fd6d13b1b54670d8ef472641c43ba57b56b10 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Mon, 27 Oct 2025 16:17:14 +0900 Subject: [PATCH 09/34] =?UTF-8?q?refactor:=20=EB=B6=81=EB=A7=88=ED=81=AC?= =?UTF-8?q?=20=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=82=AD=EC=A0=9C=20=EB=B0=8F?= =?UTF-8?q?=20=EC=9D=BC=EB=B6=80=20=EC=8A=A4=ED=83=80=EC=9D=BC=EB=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/feed/RecommendedFeedCard.tsx | 7 ++++++- src/components/feed/RecommendedFeedSection.tsx | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/feed/RecommendedFeedCard.tsx b/src/components/feed/RecommendedFeedCard.tsx index 2cf479f9..6d3022d2 100644 --- a/src/components/feed/RecommendedFeedCard.tsx +++ b/src/components/feed/RecommendedFeedCard.tsx @@ -15,8 +15,8 @@ const RecommendedFeedCard = (postData: RecommendedFeedCardProps) => { + - ); }; @@ -34,6 +34,11 @@ const CardContainer = styled.div` > *:last-child { margin-top: auto; } + + /* 북마크 아이콘 숨기기 */ + .right { + display: none; + } `; const PostBodyWrapper = styled.div` diff --git a/src/components/feed/RecommendedFeedSection.tsx b/src/components/feed/RecommendedFeedSection.tsx index 90b4ee09..dd024662 100644 --- a/src/components/feed/RecommendedFeedSection.tsx +++ b/src/components/feed/RecommendedFeedSection.tsx @@ -147,18 +147,18 @@ const SubText = styled.p` const EmblaViewport = styled.div` overflow: hidden; + padding: 0 20px; `; const EmblaContainer = styled.div` display: flex; touch-action: pan-y pinch-zoom; gap: 12px; - padding: 0 20px; `; const EmblaSlide = styled.div` transform: translate3d(0, 0, 0); - flex: 0 0 calc(100% - 10px); /* 좌우 패딩 제외한 전체 너비 */ + flex: 0 0 calc(100% - 10px); min-width: 0; padding-bottom: 40px; `; From 4eb60f42017bec68c6978b0af0aa6668d219a667 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Mon, 27 Oct 2025 17:18:00 +0900 Subject: [PATCH 10/34] =?UTF-8?q?refactor:=20=EB=8D=94=EB=B3=B4=EA=B8=B0?= =?UTF-8?q?=20=EC=95=84=EC=9D=B4=EC=BD=98=20=EA=B5=90=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/feed/lookmore-influencer.svg | 11 +++++++++++ src/components/feed/RecommendedFeedCard.tsx | 6 ++++++ src/components/feed/RecommendedFeedSection.tsx | 2 +- 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 src/assets/feed/lookmore-influencer.svg diff --git a/src/assets/feed/lookmore-influencer.svg b/src/assets/feed/lookmore-influencer.svg new file mode 100644 index 00000000..754f0fd5 --- /dev/null +++ b/src/assets/feed/lookmore-influencer.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/components/feed/RecommendedFeedCard.tsx b/src/components/feed/RecommendedFeedCard.tsx index 6d3022d2..b1824ba1 100644 --- a/src/components/feed/RecommendedFeedCard.tsx +++ b/src/components/feed/RecommendedFeedCard.tsx @@ -4,6 +4,7 @@ import PostFooter from '../common/Post/PostFooter'; import PostHeader from '../common/Post/PostHeader'; import type { PostData } from '../../types/post'; import { colors } from '@/styles/global/global'; +import lookmoreInfluencer from '@/assets/feed/lookmore-influencer.svg'; interface RecommendedFeedCardProps extends PostData { aliasColor?: string; @@ -46,6 +47,11 @@ const PostBodyWrapper = styled.div` -webkit-line-clamp: 3 !important; min-height: 60px; } + + && img.lookmore-icon, + && .lookmore-icon { + content: url("${lookmoreInfluencer}") !important; + } `; export default RecommendedFeedCard; diff --git a/src/components/feed/RecommendedFeedSection.tsx b/src/components/feed/RecommendedFeedSection.tsx index dd024662..59f6a2f9 100644 --- a/src/components/feed/RecommendedFeedSection.tsx +++ b/src/components/feed/RecommendedFeedSection.tsx @@ -23,7 +23,7 @@ const mockRecommendedFeeds: MockPostData[] = [ bookTitle: '책이름을입력해주세요...', bookAuthor: '한강', contentBody: - '세줄까지만 입력 가능합니다.ㄴ ㅇㄹㄴㄴ ㅇㅎㄴ녀;ㅇㄹ만; ↑ㅇㅎㅇ ↓ ↑ㅇ앙; ↓ ↑ㅇ악; ↓ ↑ ㅁ보름ㅇㄹ과; ㅁ먼엄안ㅇ만;ㅇㄹ라; ㅁ ㅁ엄만; 입력...', + '세줄까지만 입력 가능합니다.ㄴ ㅇㄹㄴㄴ ㅇㅎㄴ녀;ㅇㄹ만; ↑ㅇㅎㅇ ↓ ↑ㅇ앙; ↓ ↑ㅇ악; ↓ ↑ ㅁ보름ㅇㄹ과; ㅁ먼엄안ㅇ만;ㅇㄹ라; ㅁ ㅁ엄만; 입력..ㄴㅇㅁㄴㅇㅁㄴㅇㅁㄴㅇㅁㅇㅁㅇㅁㄴㅇㅁㅇㅁㅇs.', contentUrls: [], likeCount: 123, commentCount: 123, From 82319c054ed7140adc5810549a8ce96b151c0652 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Mon, 27 Oct 2025 17:31:12 +0900 Subject: [PATCH 11/34] =?UTF-8?q?feat:=20=EB=AA=A9=EC=97=85=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=B1=85?= =?UTF-8?q?=20=EC=B9=B4=EB=93=9C=20=ED=81=B4=EB=A6=AD=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=AC=B4=ED=9A=A8=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/feed/RecommendedFeedCard.tsx | 14 +++- .../feed/RecommendedFeedSection.tsx | 83 ++++++++++--------- 2 files changed, 54 insertions(+), 43 deletions(-) diff --git a/src/components/feed/RecommendedFeedCard.tsx b/src/components/feed/RecommendedFeedCard.tsx index b1824ba1..e1044937 100644 --- a/src/components/feed/RecommendedFeedCard.tsx +++ b/src/components/feed/RecommendedFeedCard.tsx @@ -11,8 +11,12 @@ interface RecommendedFeedCardProps extends PostData { } const RecommendedFeedCard = (postData: RecommendedFeedCardProps) => { + const handleCardClick = () => { + window.open(`/feed/${postData.feedId}`, '_blank'); + }; + return ( - + @@ -26,7 +30,7 @@ const CardContainer = styled.div` display: flex; flex-direction: column; gap: 16px; - padding: 20px; + padding: 12px; background-color: ${colors.darkgrey.dark}; border-radius: 12px; width: 100%; @@ -48,10 +52,16 @@ const PostBodyWrapper = styled.div` min-height: 60px; } + /* prettier-ignore */ && img.lookmore-icon, && .lookmore-icon { content: url("${lookmoreInfluencer}") !important; } + + /* 책 카드 클릭 무효화 - 카드 전체 클릭만 작동하도록 */ + > div > div:first-of-type { + pointer-events: none; + } `; export default RecommendedFeedCard; diff --git a/src/components/feed/RecommendedFeedSection.tsx b/src/components/feed/RecommendedFeedSection.tsx index 59f6a2f9..6229e74b 100644 --- a/src/components/feed/RecommendedFeedSection.tsx +++ b/src/components/feed/RecommendedFeedSection.tsx @@ -14,19 +14,19 @@ const mockRecommendedFeeds: MockPostData[] = [ { feedId: 101, creatorId: 1, - creatorNickname: 'user.01', + creatorNickname: '책덕후민지', creatorProfileImageUrl: 'https://via.placeholder.com/36', alias: '공식 인플루언서', aliasColor: colors.neongreen, - postDate: '12시간 전', + postDate: '2시간 전', isbn: '9788936434267', - bookTitle: '책이름을입력해주세요...', + bookTitle: '채식주의자', bookAuthor: '한강', contentBody: - '세줄까지만 입력 가능합니다.ㄴ ㅇㄹㄴㄴ ㅇㅎㄴ녀;ㅇㄹ만; ↑ㅇㅎㅇ ↓ ↑ㅇ앙; ↓ ↑ㅇ악; ↓ ↑ ㅁ보름ㅇㄹ과; ㅁ먼엄안ㅇ만;ㅇㄹ라; ㅁ ㅁ엄만; 입력..ㄴㅇㅁㄴㅇㅁㄴㅇㅁㄴㅇㅁㅇㅁㅇㅁㄴㅇㅁㅇㅁㅇs.', + '한강 작가님의 문장은 정말 날카롭고도 아름다워요. 문장 하나하나가 단순히 글이 아니라, 인간의 본성과 욕망, 그리고 사회의 폭력성을 정교하게 해부하는 칼날처럼 느껴졌어요. 읽는 내내 숨이 막히는 긴장감과 함께, 인간이라는 존재에 대한 불편한 진실을 마주해야 했습니다. 특히 마지막 장면은 잊을 수 없을 만큼 강렬했어요. 그 장면이 주는 여운이 너무 커서 책을 덮은 후에도 한동안 아무 말도 할 수 없었어요. 한강 작가님이 표현한 고통과 침묵, 그리고 인간의 내면은 오랜 시간 제 마음속에 남아 계속 생각나게 합니다.', contentUrls: [], - likeCount: 123, - commentCount: 123, + likeCount: 342, + commentCount: 28, isSaved: false, isLiked: false, isPublic: true, @@ -34,19 +34,19 @@ const mockRecommendedFeeds: MockPostData[] = [ }, { feedId: 102, - creatorId: 1, - creatorNickname: 'user.01', + creatorId: 2, + creatorNickname: '북튜버지수', creatorProfileImageUrl: 'https://via.placeholder.com/36', alias: '공식 인플루언서', aliasColor: colors.neongreen, - postDate: '12시간 전', - isbn: '9788936434267', - bookTitle: '책이름을입력해주세요...', - bookAuthor: '한강', - contentBody: '한 줄이어도 카드 영역은 그대로 유지', + postDate: '5시간 전', + isbn: '9788954676540', + bookTitle: '달러구트 꿈 백화점', + bookAuthor: '이미예', + contentBody: '힐링이 필요할 때 읽기 좋은 책! 따뜻한 이야기가 마음을 녹여줍니다.', contentUrls: [], - likeCount: 123, - commentCount: 123, + likeCount: 287, + commentCount: 19, isSaved: false, isLiked: false, isPublic: true, @@ -54,19 +54,20 @@ const mockRecommendedFeeds: MockPostData[] = [ }, { feedId: 103, - creatorId: 1, - creatorNickname: 'user.01', + creatorId: 3, + creatorNickname: '문학소녀윤아', creatorProfileImageUrl: 'https://via.placeholder.com/36', alias: '공식 인플루언서', aliasColor: colors.neongreen, - postDate: '12시간 전', - isbn: '9788936434267', - bookTitle: '책이름을입력해주세요...', - bookAuthor: '한강', - contentBody: '안 줄이어도 카드 영역은 그대로 유지해야 합니다.', + postDate: '8시간 전', + isbn: '9788937460449', + bookTitle: '1984', + bookAuthor: '조지 오웰', + contentBody: + '지금 읽어도 너무나 현대적인 고전. 빅브라더의 감시 사회가 현실이 되어가는 것 같아 무섭기도 하네요. 모든 사람이 꼭 읽어야 할 필독서입니다.', contentUrls: [], - likeCount: 123, - commentCount: 123, + likeCount: 521, + commentCount: 45, isSaved: false, isLiked: false, isPublic: true, @@ -74,20 +75,20 @@ const mockRecommendedFeeds: MockPostData[] = [ }, { feedId: 104, - creatorId: 1, - creatorNickname: 'user.01', + creatorId: 4, + creatorNickname: '작가지망생', creatorProfileImageUrl: 'https://via.placeholder.com/36', alias: '공식 인플루언서', aliasColor: colors.neongreen, - postDate: '12시간 전', - isbn: '9788936434267', - bookTitle: '책이름을입력해주세요...', - bookAuthor: '한강', + postDate: '1일 전', + isbn: '9788932917245', + bookTitle: '불편한 편의점', + bookAuthor: '김호연', contentBody: - '세줄까지만 입력 가능합니다.ㄴ ㅇㄹㄴㄴ ㅇㅎㄴ녀;ㅇㄹ만; ↑ㅇㅎㅇ ↓ ↑ㅇ앙; ↓ ↑ㅇ악; ↓ ↑ ㅁ보름ㅇㄹ과; ㅁ먼엄안ㅇ만;ㅇㄹ라; ㅁ ㅁ엄만; 입력...', + '올해 읽은 책 중 최고예요! 독고 씨와 염 여사의 이야기가 너무 따뜻하고 감동적이었어요. 읽으면서 계속 울었던 것 같아요. 모두에게 추천합니다!', contentUrls: [], - likeCount: 123, - commentCount: 123, + likeCount: 456, + commentCount: 38, isSaved: false, isLiked: false, isPublic: true, @@ -95,20 +96,20 @@ const mockRecommendedFeeds: MockPostData[] = [ }, { feedId: 105, - creatorId: 1, - creatorNickname: 'user.01', + creatorId: 5, + creatorNickname: '책읽는개발자', creatorProfileImageUrl: 'https://via.placeholder.com/36', alias: '공식 인플루언서', aliasColor: colors.neongreen, - postDate: '12시간 전', - isbn: '9788936434267', - bookTitle: '책이름을입력해주세요...', + postDate: '2일 전', + isbn: '9788936434298', + bookTitle: '작별하지 않는다', bookAuthor: '한강', contentBody: - '세줄까지만 입력 가능합니다.ㄴ ㅇㄹㄴㄴ ㅇㅎㄴ녀;ㅇㄹ만; ↑ㅇㅎㅇ ↓ ↑ㅇ앙; ↓ ↑ㅇ악; ↓ ↑ ㅁ보름ㅇㄹ과; ㅁ먼엄안ㅇ만;ㅇㄹ라; ㅁ ㅁ엄만; 입력...', + '한강 작가의 섬세한 문장들이 가슴을 울립니다. 상실과 기억, 그리고 삶에 대한 깊은 성찰을 담은 작품이에요. 천천히 음미하며 읽었습니다.', contentUrls: [], - likeCount: 123, - commentCount: 123, + likeCount: 398, + commentCount: 31, isSaved: false, isLiked: false, isPublic: true, From 5cbfd8216d90df7d91943457b0e72085d095c8f6 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Mon, 27 Oct 2025 17:33:31 +0900 Subject: [PATCH 12/34] =?UTF-8?q?fix:=20=ED=94=BC=EB=93=9C=2010=EA=B0=9C?= =?UTF-8?q?=EB=A7=88=EB=8B=A4=20=EC=B6=94=EC=B2=9C=20=EA=B8=80=20=EC=84=B9?= =?UTF-8?q?=EC=85=98=EC=9D=B4=20=EB=B0=98=EB=B3=B5=EB=90=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/feed/TotalFeed.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/feed/TotalFeed.tsx b/src/components/feed/TotalFeed.tsx index 270a4008..aef010ed 100644 --- a/src/components/feed/TotalFeed.tsx +++ b/src/components/feed/TotalFeed.tsx @@ -19,11 +19,11 @@ const TotalFeed = ({ showHeader, posts = [], isTotalFeed }: FeedListProps) => { key={`${post.feedId}-${index}`} showHeader={showHeader} isMyFeed={isTotalFeed} - isLast={false} // 추천 섹션이 있으므로 마지막 구분선 유지 + isLast={false} {...post} /> - {/* 10번째 피드 이후에 추천 섹션 한 번만 표시 */} - {index === 9 && } + {/* 10개마다 추천 섹션 반복 표시 */} + {(index + 1) % 10 === 0 && } ))} From 194c2e8b07f099697fb10719a8c0b52e89625679 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Tue, 28 Oct 2025 09:09:19 +0900 Subject: [PATCH 13/34] =?UTF-8?q?Refactor:=20=EC=84=B9=EC=85=98=EB=A7=88?= =?UTF-8?q?=EB=8B=A4=20=EB=8B=A4=EB=A5=B8=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=20=EB=B0=8F=20=EB=AA=A9=EC=97=85=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feed/RecommendedFeedSection.tsx | 128 +------ src/components/feed/TotalFeed.tsx | 4 +- src/mocks/recommendedFeeds.mock.ts | 328 ++++++++++++++++++ 3 files changed, 345 insertions(+), 115 deletions(-) create mode 100644 src/mocks/recommendedFeeds.mock.ts diff --git a/src/components/feed/RecommendedFeedSection.tsx b/src/components/feed/RecommendedFeedSection.tsx index 6229e74b..594cf40f 100644 --- a/src/components/feed/RecommendedFeedSection.tsx +++ b/src/components/feed/RecommendedFeedSection.tsx @@ -1,121 +1,10 @@ import styled from '@emotion/styled'; import RecommendedFeedCard from './RecommendedFeedCard'; import { colors, typography } from '@/styles/global/global'; -import type { PostData } from '@/types/post'; import useEmblaCarousel from 'embla-carousel-react'; import type { EmblaOptionsType } from 'embla-carousel'; +import { allMockRecommendedFeeds } from '@/mocks/recommendedFeeds.mock'; -// 목업 데이터 (aliasColor 추가 필요) -interface MockPostData extends PostData { - aliasColor?: string; -} - -const mockRecommendedFeeds: MockPostData[] = [ - { - feedId: 101, - creatorId: 1, - creatorNickname: '책덕후민지', - creatorProfileImageUrl: 'https://via.placeholder.com/36', - alias: '공식 인플루언서', - aliasColor: colors.neongreen, - postDate: '2시간 전', - isbn: '9788936434267', - bookTitle: '채식주의자', - bookAuthor: '한강', - contentBody: - '한강 작가님의 문장은 정말 날카롭고도 아름다워요. 문장 하나하나가 단순히 글이 아니라, 인간의 본성과 욕망, 그리고 사회의 폭력성을 정교하게 해부하는 칼날처럼 느껴졌어요. 읽는 내내 숨이 막히는 긴장감과 함께, 인간이라는 존재에 대한 불편한 진실을 마주해야 했습니다. 특히 마지막 장면은 잊을 수 없을 만큼 강렬했어요. 그 장면이 주는 여운이 너무 커서 책을 덮은 후에도 한동안 아무 말도 할 수 없었어요. 한강 작가님이 표현한 고통과 침묵, 그리고 인간의 내면은 오랜 시간 제 마음속에 남아 계속 생각나게 합니다.', - contentUrls: [], - likeCount: 342, - commentCount: 28, - isSaved: false, - isLiked: false, - isPublic: true, - isWriter: false, - }, - { - feedId: 102, - creatorId: 2, - creatorNickname: '북튜버지수', - creatorProfileImageUrl: 'https://via.placeholder.com/36', - alias: '공식 인플루언서', - aliasColor: colors.neongreen, - postDate: '5시간 전', - isbn: '9788954676540', - bookTitle: '달러구트 꿈 백화점', - bookAuthor: '이미예', - contentBody: '힐링이 필요할 때 읽기 좋은 책! 따뜻한 이야기가 마음을 녹여줍니다.', - contentUrls: [], - likeCount: 287, - commentCount: 19, - isSaved: false, - isLiked: false, - isPublic: true, - isWriter: false, - }, - { - feedId: 103, - creatorId: 3, - creatorNickname: '문학소녀윤아', - creatorProfileImageUrl: 'https://via.placeholder.com/36', - alias: '공식 인플루언서', - aliasColor: colors.neongreen, - postDate: '8시간 전', - isbn: '9788937460449', - bookTitle: '1984', - bookAuthor: '조지 오웰', - contentBody: - '지금 읽어도 너무나 현대적인 고전. 빅브라더의 감시 사회가 현실이 되어가는 것 같아 무섭기도 하네요. 모든 사람이 꼭 읽어야 할 필독서입니다.', - contentUrls: [], - likeCount: 521, - commentCount: 45, - isSaved: false, - isLiked: false, - isPublic: true, - isWriter: false, - }, - { - feedId: 104, - creatorId: 4, - creatorNickname: '작가지망생', - creatorProfileImageUrl: 'https://via.placeholder.com/36', - alias: '공식 인플루언서', - aliasColor: colors.neongreen, - postDate: '1일 전', - isbn: '9788932917245', - bookTitle: '불편한 편의점', - bookAuthor: '김호연', - contentBody: - '올해 읽은 책 중 최고예요! 독고 씨와 염 여사의 이야기가 너무 따뜻하고 감동적이었어요. 읽으면서 계속 울었던 것 같아요. 모두에게 추천합니다!', - contentUrls: [], - likeCount: 456, - commentCount: 38, - isSaved: false, - isLiked: false, - isPublic: true, - isWriter: false, - }, - { - feedId: 105, - creatorId: 5, - creatorNickname: '책읽는개발자', - creatorProfileImageUrl: 'https://via.placeholder.com/36', - alias: '공식 인플루언서', - aliasColor: colors.neongreen, - postDate: '2일 전', - isbn: '9788936434298', - bookTitle: '작별하지 않는다', - bookAuthor: '한강', - contentBody: - '한강 작가의 섬세한 문장들이 가슴을 울립니다. 상실과 기억, 그리고 삶에 대한 깊은 성찰을 담은 작품이에요. 천천히 음미하며 읽었습니다.', - contentUrls: [], - likeCount: 398, - commentCount: 31, - isSaved: false, - isLiked: false, - isPublic: true, - isWriter: false, - }, -]; const SectionContainer = styled.div` display: flex; @@ -173,7 +62,11 @@ const BorderBottom = styled.div` `; // Component -const RecommendedFeedSection = () => { +interface RecommendedFeedSectionProps { + sectionIndex?: number; +} + +const RecommendedFeedSection = ({ sectionIndex = 0 }: RecommendedFeedSectionProps) => { const options: EmblaOptionsType = { align: 'center', slidesToScroll: 1, @@ -182,6 +75,13 @@ const RecommendedFeedSection = () => { const [emblaRef] = useEmblaCarousel(options); + // 섹션 인덱스에 따라 다른 5개 게시글 선택 + const startIndex = (sectionIndex * 5) % allMockRecommendedFeeds.length; + const selectedFeeds = [ + ...allMockRecommendedFeeds.slice(startIndex, startIndex + 5), + ...allMockRecommendedFeeds.slice(0, Math.max(0, startIndex + 5 - allMockRecommendedFeeds.length)), + ].slice(0, 5); + return ( @@ -191,7 +91,7 @@ const RecommendedFeedSection = () => { - {mockRecommendedFeeds.map(feed => ( + {selectedFeeds.map(feed => ( diff --git a/src/components/feed/TotalFeed.tsx b/src/components/feed/TotalFeed.tsx index aef010ed..b183a129 100644 --- a/src/components/feed/TotalFeed.tsx +++ b/src/components/feed/TotalFeed.tsx @@ -23,7 +23,9 @@ const TotalFeed = ({ showHeader, posts = [], isTotalFeed }: FeedListProps) => { {...post} /> {/* 10개마다 추천 섹션 반복 표시 */} - {(index + 1) % 10 === 0 && } + {(index + 1) % 10 === 0 && ( + + )} ))} diff --git a/src/mocks/recommendedFeeds.mock.ts b/src/mocks/recommendedFeeds.mock.ts new file mode 100644 index 00000000..42d50ef2 --- /dev/null +++ b/src/mocks/recommendedFeeds.mock.ts @@ -0,0 +1,328 @@ +import { colors } from '@/styles/global/global'; +import type { PostData } from '@/types/post'; + +// 목업 데이터 (aliasColor 추가 필요) +interface MockPostData extends PostData { + aliasColor?: string; +} + +export const allMockRecommendedFeeds: MockPostData[] = [ + // 첫 번째 세트 (1-5) + { + feedId: 101, + creatorId: 1, + creatorNickname: '책덕후민지', + creatorProfileImageUrl: 'https://via.placeholder.com/36', + alias: '공식 인플루언서', + aliasColor: colors.neongreen, + postDate: '2시간 전', + isbn: '9788936434267', + bookTitle: '채식주의자', + bookAuthor: '한강', + contentBody: + '한강 작가님의 문장은 정말 날카롭고도 아름다워요. 문장 하나하나가 단순히 글이 아니라, 인간의 본성과 욕망, 그리고 사회의 폭력성을 정교하게 해부하는 칼날처럼 느껴졌어요. 읽는 내내 숨이 막히는 긴장감과 함께, 인간이라는 존재에 대한 불편한 진실을 마주해야 했습니다.', + contentUrls: [], + likeCount: 342, + commentCount: 28, + isSaved: false, + isLiked: false, + isPublic: true, + isWriter: false, + }, + { + feedId: 102, + creatorId: 2, + creatorNickname: '북튜버지수', + creatorProfileImageUrl: 'https://via.placeholder.com/36', + alias: '공식 인플루언서', + aliasColor: colors.neongreen, + postDate: '5시간 전', + isbn: '9788954676540', + bookTitle: '달러구트 꿈 백화점', + bookAuthor: '이미예', + contentBody: + '힐링이 필요할 때 읽기 좋은 책! 따뜻한 이야기가 마음을 녹여줍니다. 꿈을 판다는 독특한 설정이 매력적이고, 각 에피소드마다 담긴 메시지가 깊어요.', + contentUrls: [], + likeCount: 287, + commentCount: 19, + isSaved: false, + isLiked: false, + isPublic: true, + isWriter: false, + }, + { + feedId: 103, + creatorId: 3, + creatorNickname: '문학소녀윤아', + creatorProfileImageUrl: 'https://via.placeholder.com/36', + alias: '공식 인플루언서', + aliasColor: colors.neongreen, + postDate: '8시간 전', + isbn: '9788937460449', + bookTitle: '1984', + bookAuthor: '조지 오웰', + contentBody: + '지금 읽어도 너무나 현대적인 고전. 빅브라더의 감시 사회가 현실이 되어가는 것 같아 무섭기도 하네요. 모든 사람이 꼭 읽어야 할 필독서입니다.', + contentUrls: [], + likeCount: 521, + commentCount: 45, + isSaved: false, + isLiked: false, + isPublic: true, + isWriter: false, + }, + { + feedId: 104, + creatorId: 4, + creatorNickname: '작가지망생', + creatorProfileImageUrl: 'https://via.placeholder.com/36', + alias: '공식 인플루언서', + aliasColor: colors.neongreen, + postDate: '1일 전', + isbn: '9788932917245', + bookTitle: '불편한 편의점', + bookAuthor: '김호연', + contentBody: + '올해 읽은 책 중 최고예요! 독고 씨와 염 여사의 이야기가 너무 따뜻하고 감동적이었어요. 읽으면서 계속 울었던 것 같아요. 모두에게 추천합니다!', + contentUrls: [], + likeCount: 456, + commentCount: 38, + isSaved: false, + isLiked: false, + isPublic: true, + isWriter: false, + }, + { + feedId: 105, + creatorId: 5, + creatorNickname: '책읽는개발자', + creatorProfileImageUrl: 'https://via.placeholder.com/36', + alias: '공식 인플루언서', + aliasColor: colors.neongreen, + postDate: '2일 전', + isbn: '9788936434298', + bookTitle: '작별하지 않는다', + bookAuthor: '한강', + contentBody: + '한강 작가의 섬세한 문장들이 가슴을 울립니다. 상실과 기억, 그리고 삶에 대한 깊은 성찰을 담은 작품이에요. 천천히 음미하며 읽었습니다.', + contentUrls: [], + likeCount: 398, + commentCount: 31, + isSaved: false, + isLiked: false, + isPublic: true, + isWriter: false, + }, + // 두 번째 세트 (6-10) + { + feedId: 106, + creatorId: 6, + creatorNickname: '심야독서러버', + creatorProfileImageUrl: 'https://via.placeholder.com/36', + alias: '공식 인플루언서', + aliasColor: colors.neongreen, + postDate: '3일 전', + isbn: '9788954676533', + bookTitle: '미드나잇 라이브러리', + bookAuthor: '매트 헤이그', + contentBody: + '삶의 선택에 대해 다시 생각하게 되는 책이에요. 주인공 노라가 다양한 삶을 경험하는 과정이 너무 인상깊었어요. 지금 내 삶도 소중하다는 걸 깨닫게 해주는 작품입니다.', + contentUrls: [], + likeCount: 612, + commentCount: 52, + isSaved: false, + isLiked: false, + isPublic: true, + isWriter: false, + }, + { + feedId: 107, + creatorId: 7, + creatorNickname: '추리소설매니아', + creatorProfileImageUrl: 'https://via.placeholder.com/36', + alias: '공식 인플루언서', + aliasColor: colors.neongreen, + postDate: '4일 전', + isbn: '9788932917238', + bookTitle: '살인자의 기억법', + bookAuthor: '김영하', + contentBody: + '반전에 반전을 거듭하는 스토리가 정말 압권이에요! 김영하 작가님의 필력이 빛나는 작품입니다. 마지막 장면에서 소름이 쫙 돋았어요.', + contentUrls: [], + likeCount: 445, + commentCount: 37, + isSaved: false, + isLiked: false, + isPublic: true, + isWriter: false, + }, + { + feedId: 108, + creatorId: 8, + creatorNickname: '에세이추천러', + creatorProfileImageUrl: 'https://via.placeholder.com/36', + alias: '공식 인플루언서', + aliasColor: colors.neongreen, + postDate: '5일 전', + isbn: '9788936434274', + bookTitle: '여행의 이유', + bookAuthor: '김영하', + contentBody: + '여행에 대한 새로운 시각을 갖게 해주는 에세이입니다. 김영하 작가님의 통찰력 있는 글이 여행의 의미를 되새기게 만들어요. 여행을 좋아하는 분들께 강추합니다!', + contentUrls: [], + likeCount: 378, + commentCount: 29, + isSaved: false, + isLiked: false, + isPublic: true, + isWriter: false, + }, + { + feedId: 109, + creatorId: 9, + creatorNickname: '판타지덕후', + creatorProfileImageUrl: 'https://via.placeholder.com/36', + alias: '공식 인플루언서', + aliasColor: colors.neongreen, + postDate: '6일 전', + isbn: '9788983920966', + bookTitle: '해리포터와 마법사의 돌', + bookAuthor: 'J.K. 롤링', + contentBody: + '어렸을 때 읽고 다시 읽어봤는데, 여전히 마법 같은 책이에요. 호그와트에 입학하고 싶다는 꿈을 다시 꾸게 만드는 작품. 세대를 넘어 사랑받는 이유를 알 것 같아요.', + contentUrls: [], + likeCount: 589, + commentCount: 67, + isSaved: false, + isLiked: false, + isPublic: true, + isWriter: false, + }, + { + feedId: 110, + creatorId: 10, + creatorNickname: '자기계발러버', + creatorProfileImageUrl: 'https://via.placeholder.com/36', + alias: '공식 인플루언서', + aliasColor: colors.neongreen, + postDate: '1주 전', + isbn: '9788934986867', + bookTitle: '아주 작은 습관의 힘', + bookAuthor: '제임스 클리어', + contentBody: + '습관을 바꾸고 싶은 분들께 정말 추천해요. 1%의 개선이 쌓여 큰 변화를 만든다는 메시지가 와닿았어요. 실천 가능한 구체적인 방법들이 가득한 책입니다.', + contentUrls: [], + likeCount: 723, + commentCount: 81, + isSaved: false, + isLiked: false, + isPublic: true, + isWriter: false, + }, + // 세 번째 세트 (11-15) + { + feedId: 111, + creatorId: 11, + creatorNickname: '로맨스소설러버', + creatorProfileImageUrl: 'https://via.placeholder.com/36', + alias: '공식 인플루언서', + aliasColor: colors.neongreen, + postDate: '1주 전', + isbn: '9788952779847', + bookTitle: '82년생 김지영', + bookAuthor: '조남주', + contentBody: + '우리 시대 여성들의 이야기가 고스란히 담긴 책이에요. 공감되는 부분이 너무 많아서 읽으면서 여러 감정이 교차했습니다. 모든 세대가 읽어봤으면 하는 작품이에요.', + contentUrls: [], + likeCount: 567, + commentCount: 94, + isSaved: false, + isLiked: false, + isPublic: true, + isWriter: false, + }, + { + feedId: 112, + creatorId: 12, + creatorNickname: '과학책마니아', + creatorProfileImageUrl: 'https://via.placeholder.com/36', + alias: '공식 인플루언서', + aliasColor: colors.neongreen, + postDate: '1주 전', + isbn: '9788934942467', + bookTitle: '코스모스', + bookAuthor: '칼 세이건', + contentBody: + '우주에 대한 경외감을 느끼게 해주는 명저입니다. 과학책이지만 시적인 문장들이 가득해서 읽는 즐거움이 있어요. 우주의 광대함 앞에서 겸손해지게 만드는 책입니다.', + contentUrls: [], + likeCount: 412, + commentCount: 33, + isSaved: false, + isLiked: false, + isPublic: true, + isWriter: false, + }, + { + feedId: 113, + creatorId: 13, + creatorNickname: '역사덕후', + creatorProfileImageUrl: 'https://via.placeholder.com/36', + alias: '공식 인플루언서', + aliasColor: colors.neongreen, + postDate: '1주 전', + isbn: '9788934942696', + bookTitle: '사피엔스', + bookAuthor: '유발 하라리', + contentBody: + '인류의 역사를 완전히 새로운 시각으로 바라보게 되는 책이에요. 방대한 내용을 흥미진진하게 풀어냈어요. 읽으면서 계속 생각할 거리를 던져주는 작품입니다.', + contentUrls: [], + likeCount: 891, + commentCount: 103, + isSaved: false, + isLiked: false, + isPublic: true, + isWriter: false, + }, + { + feedId: 114, + creatorId: 14, + creatorNickname: '시집러버', + creatorProfileImageUrl: 'https://via.placeholder.com/36', + alias: '공식 인플루언서', + aliasColor: colors.neongreen, + postDate: '2주 전', + isbn: '9788936434021', + bookTitle: '저녁의 게임', + bookAuthor: '박상영', + contentBody: + '사랑과 관계에 대한 솔직한 이야기들이 담긴 소설이에요. 감정선이 세밀하게 그려져 있어서 읽는 내내 빠져들었습니다. 현대인의 고독과 연결에 대한 깊은 성찰이 담겨있어요.', + contentUrls: [], + likeCount: 334, + commentCount: 27, + isSaved: false, + isLiked: false, + isPublic: true, + isWriter: false, + }, + { + feedId: 115, + creatorId: 15, + creatorNickname: '고전문학러버', + creatorProfileImageUrl: 'https://via.placeholder.com/36', + alias: '공식 인플루언서', + aliasColor: colors.neongreen, + postDate: '2주 전', + isbn: '9788937462917', + bookTitle: '죄와 벌', + bookAuthor: '표도르 도스토옙스키', + contentBody: + '인간의 심리를 이토록 깊이있게 다룬 작품이 또 있을까요. 라스콜리니코프의 내면 묘사가 정말 압권입니다. 읽는 동안 철학적 질문들에 대해 끊임없이 고민하게 되는 걸작이에요.', + contentUrls: [], + likeCount: 478, + commentCount: 41, + isSaved: false, + isLiked: false, + isPublic: true, + isWriter: false, + }, +]; From e67e20f8f6bee4b4dc2fcaab35a1ea6b0a17b0f9 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Tue, 28 Oct 2025 09:12:47 +0900 Subject: [PATCH 14/34] =?UTF-8?q?chore:=20=EC=9D=BC=EB=B6=80=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/feed/RecommendedFeedCard.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/feed/RecommendedFeedCard.tsx b/src/components/feed/RecommendedFeedCard.tsx index e1044937..1d087389 100644 --- a/src/components/feed/RecommendedFeedCard.tsx +++ b/src/components/feed/RecommendedFeedCard.tsx @@ -40,7 +40,6 @@ const CardContainer = styled.div` margin-top: auto; } - /* 북마크 아이콘 숨기기 */ .right { display: none; } @@ -58,7 +57,6 @@ const PostBodyWrapper = styled.div` content: url("${lookmoreInfluencer}") !important; } - /* 책 카드 클릭 무효화 - 카드 전체 클릭만 작동하도록 */ > div > div:first-of-type { pointer-events: none; } From 7b28ca442a8112deddaebe0f5fd44f022e47ba00 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Tue, 28 Oct 2025 20:36:44 +0900 Subject: [PATCH 15/34] =?UTF-8?q?feat:=20AI=20=EB=8F=85=EC=84=9C=EA=B0=90?= =?UTF-8?q?=EC=83=81=EB=AC=B8=20=EC=83=9D=EC=84=B1=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/memory/ai.svg | 4 ++++ .../MemoryAddButton/MemoryAddButton.tsx | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 src/assets/memory/ai.svg diff --git a/src/assets/memory/ai.svg b/src/assets/memory/ai.svg new file mode 100644 index 00000000..ffef4a08 --- /dev/null +++ b/src/assets/memory/ai.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/memory/MemoryAddButton/MemoryAddButton.tsx b/src/components/memory/MemoryAddButton/MemoryAddButton.tsx index dbdc0eaf..a4fbfc1b 100644 --- a/src/components/memory/MemoryAddButton/MemoryAddButton.tsx +++ b/src/components/memory/MemoryAddButton/MemoryAddButton.tsx @@ -1,13 +1,16 @@ import { useState, useRef, useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; +import { usePopupActions } from '@/hooks/usePopupActions'; import plusIcon from '../../../assets/memory/plus.svg'; import penIcon from '../../../assets/memory/pen.svg'; import voteIcon from '../../../assets/memory/vote.svg'; +import aiIcon from '../../../assets/memory/ai.svg'; import { AddButton, DropdownContainer, DropdownItem } from './MemoryAddButton.styled'; const MemoryAddButton = () => { const navigate = useNavigate(); const { roomId } = useParams<{ roomId: string }>(); // useParams 추가 + const { openConfirm } = usePopupActions(); const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); @@ -52,6 +55,18 @@ const MemoryAddButton = () => { console.log('투표 생성하기 - roomId:', currentRoomId); }; + const handleAIWrite = () => { + setIsOpen(false); + openConfirm({ + title: 'AI 독서감상문 생성 (Beta)', + disc: '기록장에서 작성한 기록을 기반으로
독서감상문을 생성하시겠어요?
(서비스 내 잔여 이용횟수 : n/5)', + onConfirm: () => { + console.log('AI 독서 감상문 생성 시작'); + // TODO: AI 생성 API 호출 + }, + }); + }; + return (
@@ -68,6 +83,10 @@ const MemoryAddButton = () => { 투표 생성 투표 생성 + + AI 독서 감상문 생성 + AI 독서 감상문 생성 + )}
From e97d8a9c69a1af9903867d559a3c7e17bf222804 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Tue, 28 Oct 2025 20:38:29 +0900 Subject: [PATCH 16/34] =?UTF-8?q?feat:=20ConfirmModal=20=EC=99=B8=EB=B6=80?= =?UTF-8?q?=20=EC=98=81=EC=97=AD=20=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EB=8B=AB=ED=9E=88=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/Modal/ConfirmModal.tsx | 6 +++++- src/components/common/Modal/PopupContainer.tsx | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/common/Modal/ConfirmModal.tsx b/src/components/common/Modal/ConfirmModal.tsx index 2cfe5577..7e6748aa 100644 --- a/src/components/common/Modal/ConfirmModal.tsx +++ b/src/components/common/Modal/ConfirmModal.tsx @@ -3,8 +3,12 @@ import { colors, typography } from '@/styles/global/global'; import type { ConfirmModalProps } from '@/stores/usePopupStore'; const ConfirmModal = ({ title, disc, onConfirm, onClose }: ConfirmModalProps) => { + const handleContainerClick = (e: React.MouseEvent) => { + e.stopPropagation(); + }; + return ( - +
{title}
diff --git a/src/components/common/Modal/PopupContainer.tsx b/src/components/common/Modal/PopupContainer.tsx index b978b525..3c35d617 100644 --- a/src/components/common/Modal/PopupContainer.tsx +++ b/src/components/common/Modal/PopupContainer.tsx @@ -41,7 +41,7 @@ const PopupContainer = () => { switch (popupType) { case 'confirm-modal': return ( - + ); From 2990f4863b30d1f47a6f8abc4eca6b1050e49bb5 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Tue, 28 Oct 2025 20:41:02 +0900 Subject: [PATCH 17/34] =?UTF-8?q?refactor:=20AI=20=EB=8F=85=EC=84=9C=20?= =?UTF-8?q?=EA=B0=90=EC=83=81=EB=AC=B8=20=EC=83=9D=EC=84=B1=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=EC=97=90=EC=84=9C=20=EC=B7=A8=EC=86=8C/=ED=99=95?= =?UTF-8?q?=EC=9D=B8=EC=9C=BC=EB=A1=9C=20=EB=B0=9B=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=8D=BC=ED=8B=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/Modal/ConfirmModal.tsx | 13 ++++++++++--- .../memory/MemoryAddButton/MemoryAddButton.tsx | 2 ++ src/stores/usePopupStore.ts | 2 ++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/components/common/Modal/ConfirmModal.tsx b/src/components/common/Modal/ConfirmModal.tsx index 7e6748aa..a874f9b1 100644 --- a/src/components/common/Modal/ConfirmModal.tsx +++ b/src/components/common/Modal/ConfirmModal.tsx @@ -2,7 +2,14 @@ import styled from '@emotion/styled'; import { colors, typography } from '@/styles/global/global'; import type { ConfirmModalProps } from '@/stores/usePopupStore'; -const ConfirmModal = ({ title, disc, onConfirm, onClose }: ConfirmModalProps) => { +const ConfirmModal = ({ + title, + disc, + onConfirm, + onClose, + confirmText = '예', + cancelText = '아니요', +}: ConfirmModalProps) => { const handleContainerClick = (e: React.MouseEvent) => { e.stopPropagation(); }; @@ -13,10 +20,10 @@ const ConfirmModal = ({ title, disc, onConfirm, onClose }: ConfirmModalProps) =>
diff --git a/src/components/memory/MemoryAddButton/MemoryAddButton.tsx b/src/components/memory/MemoryAddButton/MemoryAddButton.tsx index a4fbfc1b..4aea9070 100644 --- a/src/components/memory/MemoryAddButton/MemoryAddButton.tsx +++ b/src/components/memory/MemoryAddButton/MemoryAddButton.tsx @@ -60,6 +60,8 @@ const MemoryAddButton = () => { openConfirm({ title: 'AI 독서감상문 생성 (Beta)', disc: '기록장에서 작성한 기록을 기반으로
독서감상문을 생성하시겠어요?
(서비스 내 잔여 이용횟수 : n/5)', + confirmText: '확인', + cancelText: '취소', onConfirm: () => { console.log('AI 독서 감상문 생성 시작'); // TODO: AI 생성 API 호출 diff --git a/src/stores/usePopupStore.ts b/src/stores/usePopupStore.ts index acd104e6..0e03e11f 100644 --- a/src/stores/usePopupStore.ts +++ b/src/stores/usePopupStore.ts @@ -14,6 +14,8 @@ export interface ConfirmModalProps { disc: string; onConfirm?: () => void; onClose?: () => void; + confirmText?: string; + cancelText?: string; } export interface MoreMenuProps { From aadd7d8dc15fcbe3c092a4996fd6b085a6ed5ae4 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Tue, 28 Oct 2025 20:53:48 +0900 Subject: [PATCH 18/34] =?UTF-8?q?feat:=20AI=20=EB=8F=85=EC=84=9C=EA=B0=90?= =?UTF-8?q?=EC=83=81=EB=AC=B8=20=EB=A1=9C=EB=94=A9=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MemoryAddButton/MemoryAddButton.tsx | 8 +- src/pages/aiwrite/AIWriteLoading.tsx | 76 +++++++++++++++++++ src/pages/index.tsx | 2 + 3 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 src/pages/aiwrite/AIWriteLoading.tsx diff --git a/src/components/memory/MemoryAddButton/MemoryAddButton.tsx b/src/components/memory/MemoryAddButton/MemoryAddButton.tsx index 4aea9070..2dbfd372 100644 --- a/src/components/memory/MemoryAddButton/MemoryAddButton.tsx +++ b/src/components/memory/MemoryAddButton/MemoryAddButton.tsx @@ -10,7 +10,7 @@ import { AddButton, DropdownContainer, DropdownItem } from './MemoryAddButton.st const MemoryAddButton = () => { const navigate = useNavigate(); const { roomId } = useParams<{ roomId: string }>(); // useParams 추가 - const { openConfirm } = usePopupActions(); + const { openConfirm, closePopup } = usePopupActions(); const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); @@ -63,8 +63,10 @@ const MemoryAddButton = () => { confirmText: '확인', cancelText: '취소', onConfirm: () => { - console.log('AI 독서 감상문 생성 시작'); - // TODO: AI 생성 API 호출 + closePopup(); + const currentRoomId = roomId || '1'; + navigate(`/aiwrite/${currentRoomId}`); + console.log('AI 독서 감상문 생성 시작 - roomId:', currentRoomId); }, }); }; diff --git a/src/pages/aiwrite/AIWriteLoading.tsx b/src/pages/aiwrite/AIWriteLoading.tsx new file mode 100644 index 00000000..c0caaf5b --- /dev/null +++ b/src/pages/aiwrite/AIWriteLoading.tsx @@ -0,0 +1,76 @@ +import { useNavigate, useParams } from 'react-router-dom'; +import styled from '@emotion/styled'; +import { colors, typography } from '@/styles/global/global'; +import LoadingSpinner from '@/components/common/LoadingSpinner'; +import TitleHeader from '@/components/common/TitleHeader'; +import leftArrow from '@/assets/common/leftArrow.svg'; + +const AIWriteLoading = () => { + const navigate = useNavigate(); + const { roomId } = useParams<{ roomId: string }>(); + + const handleBackClick = () => { + navigate(`/rooms/${roomId}/memory`); + }; + + return ( + + } + onLeftClick={handleBackClick} + /> + + + + + 독서 감상문을 생성중이에요! + 조금만 기다려주세요 + + + + ); +}; + +const Container = styled.div` + display: flex; + flex-direction: column; + width: 100%; + min-height: 100vh; + background-color: ${colors.black.main}; + padding-top: 56px; +`; + +const Content = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex: 1; + gap: 20px; +`; + +const MessageContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +`; + +const Message = styled.div` + color: ${colors.white}; + font-size: ${typography.fontSize.lg}; + font-weight: ${typography.fontWeight.semibold}; + line-height: 24px; + text-align: center; +`; + +const SubMessage = styled.div` + color: ${colors.grey[100]}; + font-size: ${typography.fontSize.sm}; + font-weight: ${typography.fontWeight.regular}; + line-height: 20px; + text-align: center; +`; + +export default AIWriteLoading; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index ff870356..42b85e62 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -40,6 +40,7 @@ import Notice from './notice/Notice'; import ParticipatedGroupDetail from './groupDetail/ParticipatedGroupDetail'; import GroupMembers from './groupMembers/GroupMembers'; import Guide from './Guide'; +import AIWriteLoading from './aiwrite/AIWriteLoading'; const Router = () => { const router = createBrowserRouter( @@ -64,6 +65,7 @@ const Router = () => { } /> } /> } /> + } /> } /> } /> } /> From 694abe703121f2268b7187c5c6e78e74da7a0693 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Tue, 28 Oct 2025 21:09:14 +0900 Subject: [PATCH 19/34] =?UTF-8?q?feat:=20AI=20=EB=8F=85=EC=84=9C=EA=B0=90?= =?UTF-8?q?=EC=83=81=EB=AC=B8=20=EA=B2=B0=EA=B3=BC=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/common/infoIcon_white.svg | 5 ++ src/mocks/aiwrite.mock.ts | 19 ++++++ src/pages/aiwrite/AIWriteLoading.tsx | 96 +++++++++++++++++++++++++--- 3 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 src/assets/common/infoIcon_white.svg create mode 100644 src/mocks/aiwrite.mock.ts diff --git a/src/assets/common/infoIcon_white.svg b/src/assets/common/infoIcon_white.svg new file mode 100644 index 00000000..6e86bad6 --- /dev/null +++ b/src/assets/common/infoIcon_white.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/mocks/aiwrite.mock.ts b/src/mocks/aiwrite.mock.ts new file mode 100644 index 00000000..a6b5f780 --- /dev/null +++ b/src/mocks/aiwrite.mock.ts @@ -0,0 +1,19 @@ +export const MOCK_AI_WRITING = `레이 커즈와일의 마침내 특이점이 시작된다는 읽는 내내 머릿속이 폭발하는 느낌이었다. 인공지능, 나노기술, 생명공학이 동시에 발전해서 결국 인간의 지능과 기계를 융합하는 시대가 온다는 주장인데, 솔직히 처음엔 SF소설 같은 이야기로 느껴졌다. + +하지만 커즈와일이 데이터와 과학적 근거를 차근차근 쌓아가며 기술 발전이 기하급수적이라는 걸 보여줄 때는 설득력이 꽤 컸다. 특히 인간 수명 연장과 의식 업로드에 대한 부분은 조금 무섭기도 하고 설레기도 했다. + +이 책의 단숨히 기술 낙관주의가 아니라, 우리가 맞이할 변화에 대해 어떤 윤리적 기준과 사회적 합의가 필요한지 고민하게 만든 점이 좋았다. 읽고 나니 '미래는 멀리 있지 않다'는 말이 실감난다. 당장 내가 AI를 어떻게 활용하고, 기술과 함께 어떻게 성장할지 스스로 계획을 세우고 싶어졌다. 딥에서 다른 사람들은 이 책을 읽고 어떤 생각을 했을지 궁금하다. + +레이 커즈와일의 마침내 특이점이 시작된다는 읽는 내내 머릿속이 폭발하는 느낌이었다. 인공지능, 나노기술, 생명공학이 동시에 발전해서 결국 인간의 지능과 기계를 융합하는 시대가 온다는 주장인데, 솔직히 처음엔 SF소설 같은 이야기로 느껴졌다. + +하지만 커즈와일이 데이터와 과학적 근거를 차근차근 쌓아가며 기술 발전이 기하급수적이라는 걸 보여줄 때는 설득력이 꽤 컸다. 특히 인간 수명 연장과 의식 업로드에 대한 부분은 조금 무섭기도 하고 설레기도 했다. 이 책의 단숨히 기술 낙관주의가 아니라, 우리가 맞이할 변화에 대해 어떤 윤리적 기준과 사회적 합의가 필요한지 고민하게 만든 점이 좋았다. + +책을 읽으면서 가장 인상 깊었던 부분은 기술 발전의 속도가 우리의 예상을 훨씬 뛰어넘는다는 점이었다. 커즈와일은 기술이 선형적으로 발전하는 것이 아니라 기하급수적으로 발전한다고 주장하는데, 과거의 데이터를 보면 실제로 그렇다는 것을 알 수 있었다. 컴퓨터의 성능이 18개월마다 두 배로 증가한다는 무어의 법칙을 넘어서, 이제는 AI가 스스로 학습하고 발전하는 단계에 이르렀다. + +특히 인상 깊었던 것은 뇌의 역설계에 대한 부분이었다. 인간의 뇌를 완전히 이해하고 그것을 컴퓨터로 재현할 수 있다면, 우리는 진정한 의미의 인공지능을 만들 수 있을 것이다. 하지만 그것이 과연 윤리적으로 올바른 일인지, 인간의 정체성은 무엇인지에 대한 깊은 고민이 필요하다고 느껴졌다. + +또한 나노기술의 발전이 의학 분야에 미칠 영향도 놀라웠다. 혈관 속을 돌아다니며 암세포를 찾아내고 제거하는 나노로봇, DNA를 수정하여 유전병을 치료하는 기술 등은 마치 공상과학 영화 속 이야기 같았지만, 커즈와일은 이것이 곧 현실이 될 것이라고 확신에 차서 말한다. + +하지만 이 모든 기술 발전이 과연 인류에게 축복일까? 책을 읽으면서 계속해서 이런 의문이 들었다. 기술이 발전할수록 인간의 일자리는 줄어들 것이고, 빈부 격차는 더욱 심해질 수 있다. 또한 AI가 인간보다 뛰어난 지능을 갖게 되면, 과연 인간의 존재 의미는 무엇인지, 우리는 어떻게 살아가야 하는지에 대한 근본적인 질문에 직면하게 될 것이다. + +읽고 나니 '미래는 멀리 있지 않다'는 말이 실감난다. 당장 내가 AI를 어떻게 활용하고, 기술과 함께 어떻게 성장할지 스스로 계획을 세우고 싶어졌다. 딥에서 다른 사람들은 이 책을 읽고 어떤 생각을 했을지 궁금하다. 함께 이야기를 나누며 미래에 대한 통찰을 얻고 싶다.`; diff --git a/src/pages/aiwrite/AIWriteLoading.tsx b/src/pages/aiwrite/AIWriteLoading.tsx index c0caaf5b..909c9ef9 100644 --- a/src/pages/aiwrite/AIWriteLoading.tsx +++ b/src/pages/aiwrite/AIWriteLoading.tsx @@ -1,18 +1,35 @@ +import { useState, useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import styled from '@emotion/styled'; import { colors, typography } from '@/styles/global/global'; import LoadingSpinner from '@/components/common/LoadingSpinner'; import TitleHeader from '@/components/common/TitleHeader'; import leftArrow from '@/assets/common/leftArrow.svg'; +import infoIcon from '@/assets/common/infoIcon_white.svg'; +import { MOCK_AI_WRITING } from '@/mocks/aiwrite.mock'; const AIWriteLoading = () => { const navigate = useNavigate(); const { roomId } = useParams<{ roomId: string }>(); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const timer = setTimeout(() => { + setIsLoading(false); + }, 5000); + + return () => clearTimeout(timer); + }, []); const handleBackClick = () => { navigate(`/rooms/${roomId}/memory`); }; + const handleCopyToClipboard = () => { + // TODO: 클립보드 복사 기능 구현 + console.log('클립보드에 복사'); + }; + return ( { onLeftClick={handleBackClick} /> - - - - 독서 감상문을 생성중이에요! - 조금만 기다려주세요 - - + {isLoading ? ( + + + + 독서 감상문을 생성중이에요! + 조금만 기다려주세요 + + + ) : ( + + + 정보 + 내 기록과 총평을 바탕으로 생성된 감상문입니다. + + {MOCK_AI_WRITING} + 클립보드에 복사 + + )} ); }; @@ -41,7 +69,7 @@ const Container = styled.div` padding-top: 56px; `; -const Content = styled.div` +const LoadingContent = styled.div` display: flex; flex-direction: column; align-items: center; @@ -73,4 +101,56 @@ const SubMessage = styled.div` text-align: center; `; +const ResultContent = styled.div` + display: flex; + flex-direction: column; + width: 100%; + height: calc(100vh - 56px); + overflow-y: auto; +`; + +const InfoBanner = styled.div` + display: flex; + align-items: center; + gap: 4px; + padding: 13px 26px; + + img { + width: 20px; + height: 20px; + } + + span { + color: ${colors.grey[200]}; + font-size: ${typography.fontSize.xs}; + font-weight: ${typography.fontWeight.regular}; + line-height: auto; + } +`; + +const ContentText = styled.div` + flex: 1; + padding: 0 26px 74px; + color: ${colors.white}; + font-size: ${typography.fontSize.sm}; + font-weight: ${typography.fontWeight.regular}; + line-height: 20px; + white-space: pre-wrap; +`; + +const CopyButton = styled.button` + position: fixed; + bottom: 0; + width: 100%; + height: 50px; + background-color: ${colors.purple.main}; + color: ${colors.white}; + font-size: ${typography.fontSize.lg}; + font-weight: ${typography.fontWeight.semibold}; + border: none; + cursor: pointer; + text-align: center; + line-height: 50px; +`; + export default AIWriteLoading; From b26fef79a859005721338d2030e6511f5de72e03 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Tue, 28 Oct 2025 21:11:06 +0900 Subject: [PATCH 20/34] =?UTF-8?q?chore:=20=ED=8C=8C=EC=9D=BC=EB=AA=85?= =?UTF-8?q?=EC=9D=84=20AiWrite=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/aiwrite/{AIWriteLoading.tsx => AIWrite.tsx} | 4 ++-- src/pages/index.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename src/pages/aiwrite/{AIWriteLoading.tsx => AIWrite.tsx} (98%) diff --git a/src/pages/aiwrite/AIWriteLoading.tsx b/src/pages/aiwrite/AIWrite.tsx similarity index 98% rename from src/pages/aiwrite/AIWriteLoading.tsx rename to src/pages/aiwrite/AIWrite.tsx index 909c9ef9..da67e819 100644 --- a/src/pages/aiwrite/AIWriteLoading.tsx +++ b/src/pages/aiwrite/AIWrite.tsx @@ -8,7 +8,7 @@ import leftArrow from '@/assets/common/leftArrow.svg'; import infoIcon from '@/assets/common/infoIcon_white.svg'; import { MOCK_AI_WRITING } from '@/mocks/aiwrite.mock'; -const AIWriteLoading = () => { +const AIWrite = () => { const navigate = useNavigate(); const { roomId } = useParams<{ roomId: string }>(); const [isLoading, setIsLoading] = useState(true); @@ -153,4 +153,4 @@ const CopyButton = styled.button` line-height: 50px; `; -export default AIWriteLoading; +export default AIWrite; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 42b85e62..d3fbb0e1 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -40,7 +40,7 @@ import Notice from './notice/Notice'; import ParticipatedGroupDetail from './groupDetail/ParticipatedGroupDetail'; import GroupMembers from './groupMembers/GroupMembers'; import Guide from './Guide'; -import AIWriteLoading from './aiwrite/AIWriteLoading'; +import AIWrite from './aiwrite/AIWrite'; const Router = () => { const router = createBrowserRouter( @@ -65,7 +65,7 @@ const Router = () => { } /> } /> } /> - } /> + } /> } /> } /> } /> From 9e779c946efddce3fd6f97cbc8e057c69180ceb2 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Tue, 28 Oct 2025 21:16:56 +0900 Subject: [PATCH 21/34] =?UTF-8?q?feat:=20=ED=81=B4=EB=A6=BD=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20=EB=B3=B5=EC=82=AC=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/aiwrite/AIWrite.tsx | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/pages/aiwrite/AIWrite.tsx b/src/pages/aiwrite/AIWrite.tsx index da67e819..659e310c 100644 --- a/src/pages/aiwrite/AIWrite.tsx +++ b/src/pages/aiwrite/AIWrite.tsx @@ -4,6 +4,7 @@ import styled from '@emotion/styled'; import { colors, typography } from '@/styles/global/global'; import LoadingSpinner from '@/components/common/LoadingSpinner'; import TitleHeader from '@/components/common/TitleHeader'; +import { usePopupActions } from '@/hooks/usePopupActions'; import leftArrow from '@/assets/common/leftArrow.svg'; import infoIcon from '@/assets/common/infoIcon_white.svg'; import { MOCK_AI_WRITING } from '@/mocks/aiwrite.mock'; @@ -11,6 +12,7 @@ import { MOCK_AI_WRITING } from '@/mocks/aiwrite.mock'; const AIWrite = () => { const navigate = useNavigate(); const { roomId } = useParams<{ roomId: string }>(); + const { openSnackbar } = usePopupActions(); const [isLoading, setIsLoading] = useState(true); useEffect(() => { @@ -25,9 +27,23 @@ const AIWrite = () => { navigate(`/rooms/${roomId}/memory`); }; - const handleCopyToClipboard = () => { - // TODO: 클립보드 복사 기능 구현 - console.log('클립보드에 복사'); + const handleCopyToClipboard = async () => { + try { + await navigator.clipboard.writeText(MOCK_AI_WRITING); + openSnackbar({ + message: '클립보드에 복사가 완료되었어요', + variant: 'top', + onClose: () => {}, + }); + } catch (error) { + console.error('클립보드 복사 실패:', error); + openSnackbar({ + message: '복사에 실패했습니다', + variant: 'bottom', + isError: true, + onClose: () => {}, + }); + } }; return ( From 58352ae5818addfd96bcfd7860c04851d270e973 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Tue, 28 Oct 2025 21:20:06 +0900 Subject: [PATCH 22/34] =?UTF-8?q?feat:=20=EB=92=A4=EB=A1=9C=EA=B0=80?= =?UTF-8?q?=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20=ED=99=95=EC=9D=B8=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/aiwrite/AIWrite.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/pages/aiwrite/AIWrite.tsx b/src/pages/aiwrite/AIWrite.tsx index 659e310c..4ad27adf 100644 --- a/src/pages/aiwrite/AIWrite.tsx +++ b/src/pages/aiwrite/AIWrite.tsx @@ -12,7 +12,7 @@ import { MOCK_AI_WRITING } from '@/mocks/aiwrite.mock'; const AIWrite = () => { const navigate = useNavigate(); const { roomId } = useParams<{ roomId: string }>(); - const { openSnackbar } = usePopupActions(); + const { openSnackbar, openConfirm, closePopup } = usePopupActions(); const [isLoading, setIsLoading] = useState(true); useEffect(() => { @@ -24,7 +24,16 @@ const AIWrite = () => { }, []); const handleBackClick = () => { - navigate(`/rooms/${roomId}/memory`); + openConfirm({ + title: 'AI 독서감상문 생성 (Beta)', + disc: '생성된 감상문은 다시 볼 수 없으며, 잔여 이용횟수는 차감돼요. 계속하시겠어요?', + confirmText: '확인', + cancelText: '취소', + onConfirm: () => { + closePopup(); + navigate(`/rooms/${roomId}/memory`); + }, + }); }; const handleCopyToClipboard = async () => { From c29e140c37c3b267c43fd66a5ab06e82028017f8 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Wed, 29 Oct 2025 13:05:19 +0900 Subject: [PATCH 23/34] =?UTF-8?q?feat:=20AI=20=EB=8F=85=EC=84=9C=20?= =?UTF-8?q?=EA=B0=90=EC=83=81=EB=AC=B8=20=EC=83=9D=EC=84=B1=20API=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/record/createAiReview.ts | 31 +++++++++++++++++ src/api/record/index.ts | 3 +- src/pages/aiwrite/AIWrite.tsx | 60 +++++++++++++++++++++++++++----- src/types/record.ts | 6 ++++ 4 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 src/api/record/createAiReview.ts diff --git a/src/api/record/createAiReview.ts b/src/api/record/createAiReview.ts new file mode 100644 index 00000000..72d37c5d --- /dev/null +++ b/src/api/record/createAiReview.ts @@ -0,0 +1,31 @@ +import { apiClient } from '../index'; +import type { CreateAiReviewData, ApiResponse } from '@/types/record'; + +// API 응답 타입 +export type CreateAiReviewResponse = ApiResponse; + +// AI 독서감상문 생성 API 함수 +export const createAiReview = async (roomId: number) => { + const response = await apiClient.post( + `/rooms/${roomId}/record/ai-review`, + ); + return response.data; +}; + +/* +사용 예시: +try { + const result = await createAiReview(1); + if (result.isSuccess) { + console.log("생성된 독서감상문:", result.data.content); + console.log("잔여 이용 횟수:", result.data.count); + // 성공 처리 로직 + } else { + console.error("AI 독서감상문 생성 실패:", result.message); + // 실패 처리 로직 + } +} catch (error) { + console.error("API 호출 오류:", error); + // 에러 처리 로직 +} +*/ diff --git a/src/api/record/index.ts b/src/api/record/index.ts index 8a096063..d69fbc18 100644 --- a/src/api/record/index.ts +++ b/src/api/record/index.ts @@ -3,4 +3,5 @@ export * from './createVote'; export * from './deleteRecord'; export * from './deleteVote'; export * from './postVote'; -export * from './pinRecordToFeed'; \ No newline at end of file +export * from './pinRecordToFeed'; +export * from './createAiReview'; \ No newline at end of file diff --git a/src/pages/aiwrite/AIWrite.tsx b/src/pages/aiwrite/AIWrite.tsx index 4ad27adf..6beb0fb7 100644 --- a/src/pages/aiwrite/AIWrite.tsx +++ b/src/pages/aiwrite/AIWrite.tsx @@ -7,21 +7,65 @@ import TitleHeader from '@/components/common/TitleHeader'; import { usePopupActions } from '@/hooks/usePopupActions'; import leftArrow from '@/assets/common/leftArrow.svg'; import infoIcon from '@/assets/common/infoIcon_white.svg'; -import { MOCK_AI_WRITING } from '@/mocks/aiwrite.mock'; +import { createAiReview } from '@/api/record'; const AIWrite = () => { const navigate = useNavigate(); const { roomId } = useParams<{ roomId: string }>(); const { openSnackbar, openConfirm, closePopup } = usePopupActions(); const [isLoading, setIsLoading] = useState(true); + const [aiContent, setAiContent] = useState(''); useEffect(() => { - const timer = setTimeout(() => { - setIsLoading(false); - }, 5000); + const fetchAiReview = async () => { + if (!roomId) return; + + try { + const result = await createAiReview(Number(roomId)); + + if (result.isSuccess) { + setAiContent(result.data.content); + } else { + openSnackbar({ + message: result.message, + variant: 'top', + isError: true, + onClose: () => {}, + }); + navigate(`/rooms/${roomId}/memory`); + } + } catch (error) { + console.error('AI 독서감상문 생성 실패:', error); + let errorMessage = 'AI 독서감상문 생성에 실패했습니다'; + + if (error && typeof error === 'object' && 'response' in error) { + const axiosError = error as { + response?: { + data?: { + message?: string; + }; + }; + }; + if (axiosError.response?.data?.message) { + errorMessage = axiosError.response.data.message; + } + } + + openSnackbar({ + message: errorMessage, + variant: 'top', + isError: true, + onClose: () => {}, + }); + navigate(`/rooms/${roomId}/memory`); + } finally { + setIsLoading(false); + } + }; - return () => clearTimeout(timer); - }, []); + fetchAiReview(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [roomId]); const handleBackClick = () => { openConfirm({ @@ -38,7 +82,7 @@ const AIWrite = () => { const handleCopyToClipboard = async () => { try { - await navigator.clipboard.writeText(MOCK_AI_WRITING); + await navigator.clipboard.writeText(aiContent); openSnackbar({ message: '클립보드에 복사가 완료되었어요', variant: 'top', @@ -77,7 +121,7 @@ const AIWrite = () => { 정보 내 기록과 총평을 바탕으로 생성된 감상문입니다. - {MOCK_AI_WRITING} + {aiContent} 클립보드에 복사 )} diff --git a/src/types/record.ts b/src/types/record.ts index db0c0995..38c0e867 100644 --- a/src/types/record.ts +++ b/src/types/record.ts @@ -72,6 +72,12 @@ export interface UpdateVoteData { roomId: number; // 방 ID } +// AI 독서감상문 생성 응답 데이터 타입 +export interface CreateAiReviewData { + content: string; // 생성된 독서감상문 내용 + count: number; // 잔여 이용 횟수 +} + // 공통 API 응답 타입 export interface ApiResponse { isSuccess: boolean; From 70a645fd55f2bea127e176e0aa00ee2285d2bdef Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Wed, 29 Oct 2025 13:27:26 +0900 Subject: [PATCH 24/34] =?UTF-8?q?feat:=20AI=20=EC=9D=B4=EC=9A=A9=20?= =?UTF-8?q?=ED=9A=9F=EC=88=98=20=EC=A1=B0=ED=9A=8C=20API=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20=EB=B0=8F=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/record/getAiUsage.ts | 31 ++++++++ src/api/record/index.ts | 3 +- .../MemoryAddButton/MemoryAddButton.tsx | 76 +++++++++++++++---- src/types/record.ts | 6 ++ 4 files changed, 101 insertions(+), 15 deletions(-) create mode 100644 src/api/record/getAiUsage.ts diff --git a/src/api/record/getAiUsage.ts b/src/api/record/getAiUsage.ts new file mode 100644 index 00000000..9360b5d9 --- /dev/null +++ b/src/api/record/getAiUsage.ts @@ -0,0 +1,31 @@ +import { apiClient } from '../index'; +import type { AiUsageData, ApiResponse } from '@/types/record'; + +// API 응답 타입 +export type GetAiUsageResponse = ApiResponse; + +// AI 이용 횟수 조회 API 함수 +export const getAiUsage = async (roomId: number) => { + const response = await apiClient.get( + `/rooms/${roomId}/users/ai-usage`, + ); + return response.data; +}; + +/* +사용 예시: +try { + const result = await getAiUsage(1); + if (result.isSuccess) { + console.log("AI 독서감상문 작성 가능 횟수:", result.data.recordReviewCount); + console.log("기록 작성 횟수:", result.data.recordCount); + // 성공 처리 로직 + } else { + console.error("AI 이용 횟수 조회 실패:", result.message); + // 실패 처리 로직 + } +} catch (error) { + console.error("API 호출 오류:", error); + // 에러 처리 로직 +} +*/ diff --git a/src/api/record/index.ts b/src/api/record/index.ts index d69fbc18..db7c2243 100644 --- a/src/api/record/index.ts +++ b/src/api/record/index.ts @@ -4,4 +4,5 @@ export * from './deleteRecord'; export * from './deleteVote'; export * from './postVote'; export * from './pinRecordToFeed'; -export * from './createAiReview'; \ No newline at end of file +export * from './createAiReview'; +export * from './getAiUsage'; \ No newline at end of file diff --git a/src/components/memory/MemoryAddButton/MemoryAddButton.tsx b/src/components/memory/MemoryAddButton/MemoryAddButton.tsx index 2dbfd372..dd8cdad3 100644 --- a/src/components/memory/MemoryAddButton/MemoryAddButton.tsx +++ b/src/components/memory/MemoryAddButton/MemoryAddButton.tsx @@ -1,6 +1,7 @@ import { useState, useRef, useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { usePopupActions } from '@/hooks/usePopupActions'; +import { getAiUsage } from '@/api/record'; import plusIcon from '../../../assets/memory/plus.svg'; import penIcon from '../../../assets/memory/pen.svg'; import voteIcon from '../../../assets/memory/vote.svg'; @@ -10,7 +11,7 @@ import { AddButton, DropdownContainer, DropdownItem } from './MemoryAddButton.st const MemoryAddButton = () => { const navigate = useNavigate(); const { roomId } = useParams<{ roomId: string }>(); // useParams 추가 - const { openConfirm, closePopup } = usePopupActions(); + const { openConfirm, closePopup, openSnackbar } = usePopupActions(); const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); @@ -55,20 +56,67 @@ const MemoryAddButton = () => { console.log('투표 생성하기 - roomId:', currentRoomId); }; - const handleAIWrite = () => { + const handleAIWrite = async () => { setIsOpen(false); - openConfirm({ - title: 'AI 독서감상문 생성 (Beta)', - disc: '기록장에서 작성한 기록을 기반으로
독서감상문을 생성하시겠어요?
(서비스 내 잔여 이용횟수 : n/5)', - confirmText: '확인', - cancelText: '취소', - onConfirm: () => { - closePopup(); - const currentRoomId = roomId || '1'; - navigate(`/aiwrite/${currentRoomId}`); - console.log('AI 독서 감상문 생성 시작 - roomId:', currentRoomId); - }, - }); + const currentRoomId = roomId || '1'; + + try { + const result = await getAiUsage(Number(currentRoomId)); + + if (result.isSuccess) { + const { recordCount, recordReviewCount } = result.data; + + // 기록이 2개 미만인 경우 에러 표시 + if (recordCount < 2) { + openSnackbar({ + message: `독후감 생성을 위해서는 최소 2개의 기록이 필요합니다. 현재 기록 개수: ${recordCount}`, + variant: 'top', + isError: true, + onClose: () => {}, + }); + return; + } + + // 잔여 횟수가 0인 경우 + if (recordReviewCount <= 0) { + openSnackbar({ + message: '사용자의 독후감 작성 수가 5회를 초과했습니다.', + variant: 'top', + isError: true, + onClose: () => {}, + }); + return; + } + + // 모달 표시 + openConfirm({ + title: 'AI 독서감상문 생성 (Beta)', + disc: `기록장에서 작성한 기록을 기반으로
독서감상문을 생성하시겠어요?
(서비스 내 잔여 이용횟수 : ${recordReviewCount}/5)`, + confirmText: '확인', + cancelText: '취소', + onConfirm: () => { + closePopup(); + navigate(`/aiwrite/${currentRoomId}`); + console.log('AI 독서 감상문 생성 시작 - roomId:', currentRoomId); + }, + }); + } else { + openSnackbar({ + message: result.message, + variant: 'top', + isError: true, + onClose: () => {}, + }); + } + } catch (error) { + console.error('AI 이용 횟수 조회 실패:', error); + openSnackbar({ + message: 'AI 이용 횟수 조회에 실패했습니다', + variant: 'top', + isError: true, + onClose: () => {}, + }); + } }; return ( diff --git a/src/types/record.ts b/src/types/record.ts index 38c0e867..a7295a4d 100644 --- a/src/types/record.ts +++ b/src/types/record.ts @@ -78,6 +78,12 @@ export interface CreateAiReviewData { count: number; // 잔여 이용 횟수 } +// AI 이용 횟수 조회 응답 데이터 타입 +export interface AiUsageData { + recordReviewCount: number; // AI 독서감상문 작성 가능 횟수 + recordCount: number; // 기록 작성 횟수 +} + // 공통 API 응답 타입 export interface ApiResponse { isSuccess: boolean; From 9558164351d49f46dd2051df26e8529c2f030f70 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Wed, 29 Oct 2025 22:43:45 +0900 Subject: [PATCH 25/34] =?UTF-8?q?fix:=20AI=20=EC=9D=B4=EC=9A=A9=20?= =?UTF-8?q?=ED=9A=9F=EC=88=98=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/memory/MemoryAddButton/MemoryAddButton.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/memory/MemoryAddButton/MemoryAddButton.tsx b/src/components/memory/MemoryAddButton/MemoryAddButton.tsx index dd8cdad3..9354d3c2 100644 --- a/src/components/memory/MemoryAddButton/MemoryAddButton.tsx +++ b/src/components/memory/MemoryAddButton/MemoryAddButton.tsx @@ -77,8 +77,8 @@ const MemoryAddButton = () => { return; } - // 잔여 횟수가 0인 경우 - if (recordReviewCount <= 0) { + // 잔여 횟수가 5회 이상인 경우 (이미 5회 모두 사용) + if (recordReviewCount >= 5) { openSnackbar({ message: '사용자의 독후감 작성 수가 5회를 초과했습니다.', variant: 'top', @@ -88,7 +88,7 @@ const MemoryAddButton = () => { return; } - // 모달 표시 + // 모달 표시 (잔여 횟수 = 5 - 사용한 횟수) openConfirm({ title: 'AI 독서감상문 생성 (Beta)', disc: `기록장에서 작성한 기록을 기반으로
독서감상문을 생성하시겠어요?
(서비스 내 잔여 이용횟수 : ${recordReviewCount}/5)`, From f82faca1063e2f58a38d0b3304329c717ce5e9a5 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Wed, 29 Oct 2025 23:06:41 +0900 Subject: [PATCH 26/34] =?UTF-8?q?style:=20AIWrite=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EB=84=88=EB=B9=84=EB=A5=BC=20=EB=8B=A4=EB=A5=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EB=93=A4=EA=B3=BC=20=EB=8F=99?= =?UTF-8?q?=EC=9D=BC=ED=95=98=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/aiwrite/AIWrite.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pages/aiwrite/AIWrite.tsx b/src/pages/aiwrite/AIWrite.tsx index 6beb0fb7..28d88651 100644 --- a/src/pages/aiwrite/AIWrite.tsx +++ b/src/pages/aiwrite/AIWrite.tsx @@ -132,8 +132,10 @@ const AIWrite = () => { const Container = styled.div` display: flex; flex-direction: column; - width: 100%; + min-width: 320px; + max-width: 767px; min-height: 100vh; + margin: 0 auto; background-color: ${colors.black.main}; padding-top: 56px; `; @@ -210,7 +212,10 @@ const ContentText = styled.div` const CopyButton = styled.button` position: fixed; bottom: 0; + left: 50%; + transform: translateX(-50%); width: 100%; + max-width: 767px; height: 50px; background-color: ${colors.purple.main}; color: ${colors.white}; From d24985d31265471bcf30ff545e29dc62ebb2aef5 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Wed, 29 Oct 2025 23:16:04 +0900 Subject: [PATCH 27/34] =?UTF-8?q?chore:=20=EC=A3=BC=EC=84=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/memory/MemoryAddButton/MemoryAddButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/memory/MemoryAddButton/MemoryAddButton.tsx b/src/components/memory/MemoryAddButton/MemoryAddButton.tsx index 9354d3c2..9b94a1a3 100644 --- a/src/components/memory/MemoryAddButton/MemoryAddButton.tsx +++ b/src/components/memory/MemoryAddButton/MemoryAddButton.tsx @@ -88,7 +88,7 @@ const MemoryAddButton = () => { return; } - // 모달 표시 (잔여 횟수 = 5 - 사용한 횟수) + // 모달 표시 openConfirm({ title: 'AI 독서감상문 생성 (Beta)', disc: `기록장에서 작성한 기록을 기반으로
독서감상문을 생성하시겠어요?
(서비스 내 잔여 이용횟수 : ${recordReviewCount}/5)`, From 162e032eaa3220b87b3922b613a1688328d86da5 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Fri, 31 Oct 2025 16:14:20 +0900 Subject: [PATCH 28/34] =?UTF-8?q?feat:=20=EB=AA=A8=EC=9E=84=20=EB=A7=8C?= =?UTF-8?q?=EB=93=A4=EA=B8=B0=20=EC=B1=85=20=EC=84=A0=ED=83=9D=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=AA=A8=EC=9E=84=20=EC=B1=85=20=ED=83=AD=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BookSearchBottomSheet.tsx | 3 +- .../BookSearchBottomSheet/BookSearchTabs.tsx | 5 +- .../BookSearchBottomSheet/useBookSearch.ts | 71 +++---------------- 3 files changed, 10 insertions(+), 69 deletions(-) diff --git a/src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx b/src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx index 2ecf80d4..0ea3a0ce 100644 --- a/src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx +++ b/src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx @@ -36,7 +36,6 @@ const BookSearchBottomSheet = ({ isOpen, onClose, onSelectBook }: BookSearchBott performSearch, loadMoreSearchResults, loadMoreSavedBooks, - loadMoreGroupBooks, } = useBookSearch(); // 컴포넌트가 열릴 때 초기 데이터 로드 @@ -89,7 +88,7 @@ const BookSearchBottomSheet = ({ isOpen, onClose, onSelectBook }: BookSearchBott if (isSearchMode) { return loadMoreSearchResults; } - return activeTab === 'saved' ? loadMoreSavedBooks : loadMoreGroupBooks; + return loadMoreSavedBooks; }; // 현재 상태에 맞는 무한 스크롤 정보 diff --git a/src/components/common/BookSearchBottomSheet/BookSearchTabs.tsx b/src/components/common/BookSearchBottomSheet/BookSearchTabs.tsx index ef5e8660..6ae170bd 100644 --- a/src/components/common/BookSearchBottomSheet/BookSearchTabs.tsx +++ b/src/components/common/BookSearchBottomSheet/BookSearchTabs.tsx @@ -1,6 +1,6 @@ import { TabContainer, Tab } from './BookSearchBottomSheet.styled'; -export type TabType = 'saved' | 'group'; +export type TabType = 'saved'; interface BookSearchTabsProps { activeTab: TabType; @@ -13,9 +13,6 @@ const BookSearchTabs = ({ activeTab, onTabChange }: BookSearchTabsProps) => { onTabChange('saved')}> 저장한 책 - onTabChange('group')}> - 모임 책 - ); }; diff --git a/src/components/common/BookSearchBottomSheet/useBookSearch.ts b/src/components/common/BookSearchBottomSheet/useBookSearch.ts index d7c0a9e8..4f048dca 100644 --- a/src/components/common/BookSearchBottomSheet/useBookSearch.ts +++ b/src/components/common/BookSearchBottomSheet/useBookSearch.ts @@ -9,7 +9,6 @@ export const useBookSearch = () => { const [filteredBooks, setFilteredBooks] = useState([]); const [activeTab, setActiveTab] = useState('saved'); const [savedBooks, setSavedBooks] = useState([]); - const [groupBooks, setGroupBooks] = useState([]); const [searchResults, setSearchResults] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -17,14 +16,11 @@ export const useBookSearch = () => { const [currentPage, setCurrentPage] = useState(1); const [hasNextPage, setHasNextPage] = useState(false); const [isLoadingMore, setIsLoadingMore] = useState(false); - - // 저장한 책/모임 책 무한 스크롤 관련 상태 + + // 저장한 책 무한 스크롤 관련 상태 const [savedBooksCursor, setSavedBooksCursor] = useState(null); - const [groupBooksCursor, setGroupBooksCursor] = useState(null); const [hasSavedBooksNext, setHasSavedBooksNext] = useState(false); - const [hasGroupBooksNext, setHasGroupBooksNext] = useState(false); const [isLoadingMoreSavedBooks, setIsLoadingMoreSavedBooks] = useState(false); - const [isLoadingMoreGroupBooks, setIsLoadingMoreGroupBooks] = useState(false); // API에서 받은 데이터를 Book 타입으로 변환하는 함수 const convertSavedBookToBook = (savedBook: SavedBook): Book => ({ @@ -81,42 +77,6 @@ export const useBookSearch = () => { } }; - // 모임 책 데이터 가져오기 (무한 스크롤) - const fetchGroupBooks = async (isLoadMore: boolean = false) => { - try { - if (isLoadMore) { - setIsLoadingMoreGroupBooks(true); - } else { - setIsLoading(true); - setGroupBooksCursor(null); - } - setError(null); - - const cursor = isLoadMore ? groupBooksCursor : null; - const response = await getSavedBooks('joining', cursor); - - if (response.isSuccess && response.data) { - if (isLoadMore) { - setGroupBooks(prev => [...prev, ...response.data.bookList]); - } else { - setGroupBooks(response.data.bookList); - } - setGroupBooksCursor(response.data.nextCursor); - setHasGroupBooksNext(!response.data.isLast); - } else { - setError(response.message || '모임 책을 불러오는데 실패했습니다.'); - } - } catch (err) { - console.error('모임 책 조회 오류:', err); - setError('모임 책을 불러오는데 실패했습니다.'); - } finally { - if (isLoadMore) { - setIsLoadingMoreGroupBooks(false); - } else { - setIsLoading(false); - } - } - }; // 실제 검색 API 호출 함수 const performSearch = async (query: string, page: number = 1, isNewSearch: boolean = true) => { @@ -189,14 +149,6 @@ export const useBookSearch = () => { await fetchSavedBooks(true); }; - // 더 많은 모임 책 로드 - const loadMoreGroupBooks = async () => { - if (isLoadingMoreGroupBooks || !hasGroupBooksNext) { - return; - } - - await fetchGroupBooks(true); - }; // 검색어 변경 핸들러 (디바운싱 적용) const handleSearchQueryChange = (query: string) => { @@ -229,10 +181,9 @@ export const useBookSearch = () => { } // 검색어가 없으면 저장된 책들을 표시 - const currentTabBooks = activeTab === 'saved' ? savedBooks : groupBooks; - const convertedBooks = currentTabBooks.map(convertSavedBookToBook); + const convertedBooks = savedBooks.map(convertSavedBookToBook); setFilteredBooks(convertedBooks); - }, [searchQuery, activeTab, savedBooks, groupBooks, searchResults]); + }, [searchQuery, savedBooks, searchResults]); // 탭 변경 핸들러 const handleTabChange = async (tab: TabType) => { @@ -242,8 +193,6 @@ export const useBookSearch = () => { // 탭 변경 시 해당 탭의 데이터가 없으면 API 호출 if (tab === 'saved' && savedBooks.length === 0) { await fetchSavedBooks(); - } else if (tab === 'group' && groupBooks.length === 0) { - await fetchGroupBooks(); } }; @@ -251,21 +200,18 @@ export const useBookSearch = () => { const loadInitialData = () => { if (activeTab === 'saved' && savedBooks.length === 0) { fetchSavedBooks(); - } else if (activeTab === 'group' && groupBooks.length === 0) { - fetchGroupBooks(); } }; // 현재 상태 계산 const isSearchMode = searchQuery.trim() !== ''; - const currentTabBooks = activeTab === 'saved' ? savedBooks : groupBooks; - const hasBooks = isSearchMode ? searchResults.length > 0 : currentTabBooks.length > 0; + const hasBooks = isSearchMode ? searchResults.length > 0 : savedBooks.length > 0; const showEmptyState = !isLoading && !error && !hasBooks; const showTabs = !isSearchMode; // 검색 모드가 아닐 때는 항상 탭 표시 - + // 현재 탭의 무한 스크롤 상태 - const currentTabHasNext = activeTab === 'saved' ? hasSavedBooksNext : hasGroupBooksNext; - const currentTabIsLoadingMore = activeTab === 'saved' ? isLoadingMoreSavedBooks : isLoadingMoreGroupBooks; + const currentTabHasNext = hasSavedBooksNext; + const currentTabIsLoadingMore = isLoadingMoreSavedBooks; // 컴포넌트 언마운트 시 타이머 정리 useEffect(() => { @@ -298,6 +244,5 @@ export const useBookSearch = () => { performSearch, loadMoreSearchResults, loadMoreSavedBooks, - loadMoreGroupBooks, }; }; From c29aceec15356872a3bfa6ce158988a76eeb4fb3 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Fri, 31 Oct 2025 16:36:06 +0900 Subject: [PATCH 29/34] =?UTF-8?q?feat:=20=EB=AA=A8=EC=9E=84=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=8B=9C=20=EC=B1=85=20=EC=84=A0=ED=83=9D=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=AA=A8=EC=9E=84=20=EC=B1=85=20=ED=83=AD=20?= =?UTF-8?q?=EC=88=A8=EA=B9=80=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BookSearchBottomSheet.tsx | 10 +-- .../BookSearchBottomSheet/BookSearchTabs.tsx | 10 ++- .../BookSearchBottomSheet/useBookSearch.ts | 71 ++++++++++++++++--- src/pages/group/CreateGroup.tsx | 1 + 4 files changed, 78 insertions(+), 14 deletions(-) diff --git a/src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx b/src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx index 0ea3a0ce..e0b1cef7 100644 --- a/src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx +++ b/src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx @@ -15,9 +15,10 @@ interface BookSearchBottomSheetProps { isOpen: boolean; onClose: () => void; onSelectBook: (book: Book) => void; + showGroupTab?: boolean; } -const BookSearchBottomSheet = ({ isOpen, onClose, onSelectBook }: BookSearchBottomSheetProps) => { +const BookSearchBottomSheet = ({ isOpen, onClose, onSelectBook, showGroupTab = true }: BookSearchBottomSheetProps) => { const { searchQuery, filteredBooks, @@ -36,7 +37,8 @@ const BookSearchBottomSheet = ({ isOpen, onClose, onSelectBook }: BookSearchBott performSearch, loadMoreSearchResults, loadMoreSavedBooks, - } = useBookSearch(); + loadMoreGroupBooks, + } = useBookSearch(showGroupTab); // 컴포넌트가 열릴 때 초기 데이터 로드 useEffect(() => { @@ -88,7 +90,7 @@ const BookSearchBottomSheet = ({ isOpen, onClose, onSelectBook }: BookSearchBott if (isSearchMode) { return loadMoreSearchResults; } - return loadMoreSavedBooks; + return activeTab === 'saved' ? loadMoreSavedBooks : loadMoreGroupBooks; }; // 현재 상태에 맞는 무한 스크롤 정보 @@ -108,7 +110,7 @@ const BookSearchBottomSheet = ({ isOpen, onClose, onSelectBook }: BookSearchBott /> {/* 탭 영역 */} - {showTabs && } + {showTabs && } {/* 책 목록 영역 */} diff --git a/src/components/common/BookSearchBottomSheet/BookSearchTabs.tsx b/src/components/common/BookSearchBottomSheet/BookSearchTabs.tsx index 6ae170bd..0d154c8b 100644 --- a/src/components/common/BookSearchBottomSheet/BookSearchTabs.tsx +++ b/src/components/common/BookSearchBottomSheet/BookSearchTabs.tsx @@ -1,18 +1,24 @@ import { TabContainer, Tab } from './BookSearchBottomSheet.styled'; -export type TabType = 'saved'; +export type TabType = 'saved' | 'group'; interface BookSearchTabsProps { activeTab: TabType; onTabChange: (tab: TabType) => void; + showGroupTab?: boolean; } -const BookSearchTabs = ({ activeTab, onTabChange }: BookSearchTabsProps) => { +const BookSearchTabs = ({ activeTab, onTabChange, showGroupTab = true }: BookSearchTabsProps) => { return ( onTabChange('saved')}> 저장한 책 + {showGroupTab && ( + onTabChange('group')}> + 모임 책 + + )} ); }; diff --git a/src/components/common/BookSearchBottomSheet/useBookSearch.ts b/src/components/common/BookSearchBottomSheet/useBookSearch.ts index 4f048dca..453bbd14 100644 --- a/src/components/common/BookSearchBottomSheet/useBookSearch.ts +++ b/src/components/common/BookSearchBottomSheet/useBookSearch.ts @@ -4,11 +4,12 @@ import { getSearchBooks, convertToSearchedBooks, type SearchedBook } from '@/api import type { Book } from './BookList'; import type { TabType } from './BookSearchTabs'; -export const useBookSearch = () => { +export const useBookSearch = (showGroupTab: boolean = true) => { const [searchQuery, setSearchQuery] = useState(''); const [filteredBooks, setFilteredBooks] = useState([]); const [activeTab, setActiveTab] = useState('saved'); const [savedBooks, setSavedBooks] = useState([]); + const [groupBooks, setGroupBooks] = useState([]); const [searchResults, setSearchResults] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -17,10 +18,13 @@ export const useBookSearch = () => { const [hasNextPage, setHasNextPage] = useState(false); const [isLoadingMore, setIsLoadingMore] = useState(false); - // 저장한 책 무한 스크롤 관련 상태 + // 저장한 책/모임 책 무한 스크롤 관련 상태 const [savedBooksCursor, setSavedBooksCursor] = useState(null); + const [groupBooksCursor, setGroupBooksCursor] = useState(null); const [hasSavedBooksNext, setHasSavedBooksNext] = useState(false); + const [hasGroupBooksNext, setHasGroupBooksNext] = useState(false); const [isLoadingMoreSavedBooks, setIsLoadingMoreSavedBooks] = useState(false); + const [isLoadingMoreGroupBooks, setIsLoadingMoreGroupBooks] = useState(false); // API에서 받은 데이터를 Book 타입으로 변환하는 함수 const convertSavedBookToBook = (savedBook: SavedBook): Book => ({ @@ -77,6 +81,42 @@ export const useBookSearch = () => { } }; + // 모임 책 데이터 가져오기 (무한 스크롤) + const fetchGroupBooks = async (isLoadMore: boolean = false) => { + try { + if (isLoadMore) { + setIsLoadingMoreGroupBooks(true); + } else { + setIsLoading(true); + setGroupBooksCursor(null); + } + setError(null); + + const cursor = isLoadMore ? groupBooksCursor : null; + const response = await getSavedBooks('joining', cursor); + + if (response.isSuccess && response.data) { + if (isLoadMore) { + setGroupBooks(prev => [...prev, ...response.data.bookList]); + } else { + setGroupBooks(response.data.bookList); + } + setGroupBooksCursor(response.data.nextCursor); + setHasGroupBooksNext(!response.data.isLast); + } else { + setError(response.message || '모임 책을 불러오는데 실패했습니다.'); + } + } catch (err) { + console.error('모임 책 조회 오류:', err); + setError('모임 책을 불러오는데 실패했습니다.'); + } finally { + if (isLoadMore) { + setIsLoadingMoreGroupBooks(false); + } else { + setIsLoading(false); + } + } + }; // 실제 검색 API 호출 함수 const performSearch = async (query: string, page: number = 1, isNewSearch: boolean = true) => { @@ -145,10 +185,18 @@ export const useBookSearch = () => { if (isLoadingMoreSavedBooks || !hasSavedBooksNext) { return; } - + await fetchSavedBooks(true); }; + // 더 많은 모임 책 로드 + const loadMoreGroupBooks = async () => { + if (isLoadingMoreGroupBooks || !hasGroupBooksNext) { + return; + } + + await fetchGroupBooks(true); + }; // 검색어 변경 핸들러 (디바운싱 적용) const handleSearchQueryChange = (query: string) => { @@ -181,9 +229,10 @@ export const useBookSearch = () => { } // 검색어가 없으면 저장된 책들을 표시 - const convertedBooks = savedBooks.map(convertSavedBookToBook); + const currentTabBooks = activeTab === 'saved' ? savedBooks : groupBooks; + const convertedBooks = currentTabBooks.map(convertSavedBookToBook); setFilteredBooks(convertedBooks); - }, [searchQuery, savedBooks, searchResults]); + }, [searchQuery, activeTab, savedBooks, groupBooks, searchResults]); // 탭 변경 핸들러 const handleTabChange = async (tab: TabType) => { @@ -193,6 +242,8 @@ export const useBookSearch = () => { // 탭 변경 시 해당 탭의 데이터가 없으면 API 호출 if (tab === 'saved' && savedBooks.length === 0) { await fetchSavedBooks(); + } else if (tab === 'group' && groupBooks.length === 0) { + await fetchGroupBooks(); } }; @@ -200,18 +251,21 @@ export const useBookSearch = () => { const loadInitialData = () => { if (activeTab === 'saved' && savedBooks.length === 0) { fetchSavedBooks(); + } else if (activeTab === 'group' && groupBooks.length === 0 && showGroupTab) { + fetchGroupBooks(); } }; // 현재 상태 계산 const isSearchMode = searchQuery.trim() !== ''; - const hasBooks = isSearchMode ? searchResults.length > 0 : savedBooks.length > 0; + const currentTabBooks = activeTab === 'saved' ? savedBooks : groupBooks; + const hasBooks = isSearchMode ? searchResults.length > 0 : currentTabBooks.length > 0; const showEmptyState = !isLoading && !error && !hasBooks; const showTabs = !isSearchMode; // 검색 모드가 아닐 때는 항상 탭 표시 // 현재 탭의 무한 스크롤 상태 - const currentTabHasNext = hasSavedBooksNext; - const currentTabIsLoadingMore = isLoadingMoreSavedBooks; + const currentTabHasNext = activeTab === 'saved' ? hasSavedBooksNext : hasGroupBooksNext; + const currentTabIsLoadingMore = activeTab === 'saved' ? isLoadingMoreSavedBooks : isLoadingMoreGroupBooks; // 컴포넌트 언마운트 시 타이머 정리 useEffect(() => { @@ -244,5 +298,6 @@ export const useBookSearch = () => { performSearch, loadMoreSearchResults, loadMoreSavedBooks, + loadMoreGroupBooks, }; }; diff --git a/src/pages/group/CreateGroup.tsx b/src/pages/group/CreateGroup.tsx index 9a4722ee..c51b3ba6 100644 --- a/src/pages/group/CreateGroup.tsx +++ b/src/pages/group/CreateGroup.tsx @@ -246,6 +246,7 @@ const CreateGroup = () => { isOpen={isBookSearchOpen} onClose={handleBookSearchClose} onSelectBook={handleBookSelect} + showGroupTab={false} /> From 1e3084e9dd74056ad0a84bfbcea0860cad4f26d5 Mon Sep 17 00:00:00 2001 From: Ji Ho June <129824629+ho0010@users.noreply.github.com> Date: Sun, 2 Nov 2025 15:25:50 +0900 Subject: [PATCH 30/34] =?UTF-8?q?fix:=20Fragment=EC=97=90=20key=EA=B0=80?= =?UTF-8?q?=20=EC=97=86=EC=96=B4=EC=84=9C=20=EC=83=9D=EA=B8=B0=EB=8A=94=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/feed/TotalFeed.tsx | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/components/feed/TotalFeed.tsx b/src/components/feed/TotalFeed.tsx index b183a129..4d2dbf86 100644 --- a/src/components/feed/TotalFeed.tsx +++ b/src/components/feed/TotalFeed.tsx @@ -1,4 +1,5 @@ import styled from '@emotion/styled'; +import { Fragment } from 'react'; import FollowList from './FollowList'; import FeedPost from './FeedPost'; import RecommendedFeedSection from './RecommendedFeedSection'; @@ -14,19 +15,13 @@ const TotalFeed = ({ showHeader, posts = [], isTotalFeed }: FeedListProps) => { {hasPosts ? ( <> {posts.map((post, index) => ( - <> - + + {/* 10개마다 추천 섹션 반복 표시 */} {(index + 1) % 10 === 0 && ( )} - + ))} ) : ( From 03853b1e6896460677f6e1ce7f7f14d210cd9871 Mon Sep 17 00:00:00 2001 From: Ji Ho June <129824629+ho0010@users.noreply.github.com> Date: Sun, 2 Nov 2025 15:30:49 +0900 Subject: [PATCH 31/34] =?UTF-8?q?fix:=20=EC=99=84=EB=A3=8C=EB=90=9C=20?= =?UTF-8?q?=EB=AA=A8=EC=9E=84=EB=B0=A9=EC=9D=84=20=EB=82=B4=20=EB=AA=A8?= =?UTF-8?q?=EC=9E=84=EB=B0=A9=20=EC=98=B5=EC=85=98=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EB=AA=A8=EC=9E=84=20=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=82=AC=EC=9A=A9=EC=84=B1=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=202=EC=B0=A8=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/MainHeader.tsx | 10 +- src/components/group/CompletedGroupModal.tsx | 195 ------------------- src/components/group/GroupCard.tsx | 16 +- src/components/group/MyGroupModal.tsx | 64 ++---- src/pages/group/Group.tsx | 8 +- 5 files changed, 38 insertions(+), 255 deletions(-) delete mode 100644 src/components/group/CompletedGroupModal.tsx diff --git a/src/components/common/MainHeader.tsx b/src/components/common/MainHeader.tsx index a2aaf386..29da8d20 100644 --- a/src/components/common/MainHeader.tsx +++ b/src/components/common/MainHeader.tsx @@ -1,6 +1,5 @@ import { useEffect, useState } from 'react'; import headerLogo from '../../assets/header/header-logo.svg'; -import groupDoneLogo from '../../assets/header/group-done.svg'; import findUserLogo from '../../assets/header/findUser.svg'; import bellLogo from '../../assets/header/bell.svg'; import bellExistLogo from '../../assets/header/exist-bell.svg'; @@ -40,11 +39,10 @@ const MainHeader = ({ type, leftButtonClick, rightButtonClick }: MainHeaderProps - + {type === 'home' && ( + + )} + void; -} - -const CompletedGroupModal = ({ onClose }: CompletedGroupModalProps) => { - const navigate = useNavigate(); - const [rooms, setRooms] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [nickname, setNickname] = useState(''); - - useEffect(() => { - document.body.style.overflow = 'hidden'; - return () => { - document.body.style.overflow = ''; - }; - }, []); - - const convertRoomToGroup = (room: Room): Group => { - return { - id: room.roomId.toString(), - title: room.roomName, - userName: '', - participants: room.memberCount, - maximumParticipants: room.recruitCount, - coverUrl: room.bookImageUrl, - deadLine: '', - isOnGoing: false, - type: room.type, - }; - }; - - const handleGroupCardClick = (group: Group) => { - navigate(`/group/detail/joined/${group.id}`); - }; - - useEffect(() => { - const fetchCompletedRooms = async () => { - try { - setIsLoading(true); - setError(null); - const response = await getMyRooms('expired', null); - if (response.isSuccess) { - setRooms(response.data.roomList); - } else { - setError(response.message); - } - } catch (error) { - console.error('완료된 방 목록 조회 실패:', error); - setError('완료된 방 목록을 불러오는데 실패했습니다.'); - } finally { - setIsLoading(false); - } - }; - - const fetchNickname = async () => { - try { - const profile = await getMyProfile(); - setNickname(profile.nickname); - } catch { - setNickname(''); - } - }; - - fetchCompletedRooms(); - fetchNickname(); - }, []); - - const convertedGroups = rooms.map(convertRoomToGroup); - return ( - - - } - onLeftClick={onClose} - /> - - {nickname - ? `${nickname}님이 참여했던 모임방들을 확인해보세요.` - : '참여했던 모임방들을 확인해보세요.'} - - - {isLoading ? ( - 로딩 중... - ) : error ? ( - {error} - ) : convertedGroups.length > 0 ? ( - convertedGroups.map(group => ( - handleGroupCardClick(group)} - /> - )) - ) : ( - - 완료된 모임방이 없어요 - 아직 완료된 모임방이 없습니다. - - )} - - - - ); -}; - -export default CompletedGroupModal; - -const Text = styled.p` - font-size: ${typography.fontSize.sm}; - font-weight: ${typography.fontWeight.regular}; - color: ${colors.white}; - margin: 20px; -`; - -const Content = styled.div<{ isEmpty?: boolean }>` - display: grid; - gap: 20px; - overflow-y: ${({ isEmpty }) => (isEmpty ? 'visible' : 'auto')}; - padding: 0 20px; - grid-template-columns: 1fr; - margin-bottom: 70px; - - @media (min-width: 584px) { - grid-template-columns: 1fr 1fr; - } - - & > *:only-child { - grid-column: 1 / -1; - } - &::-webkit-scrollbar { - display: none; - } - -ms-overflow-style: none; - scrollbar-width: none; -`; - -const LoadingMessage = styled.div` - display: flex; - justify-content: center; - align-items: center; - padding: 40px 20px; - color: ${colors.white}; - font-size: ${typography.fontSize.base}; -`; - -const ErrorMessage = styled.div` - display: flex; - justify-content: center; - align-items: center; - padding: 40px 20px; - color: #ff6b6b; - font-size: ${typography.fontSize.base}; -`; - -const EmptyState = styled.div` - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - padding: 40px 20px; - color: ${colors.grey[100]}; - text-align: center; - height: 70%; - position: absolute; - left: 50%; - transform: translateX(-50%); -`; - -const EmptyTitle = styled.p` - font-size: ${typography.fontSize.lg}; - font-weight: ${typography.fontWeight.semibold}; - margin-bottom: 8px; - color: ${colors.white}; -`; - -const EmptySubText = styled.p` - font-size: ${typography.fontSize.sm}; - font-weight: ${typography.fontWeight.regular}; - color: ${colors.grey[100]}; -`; diff --git a/src/components/group/GroupCard.tsx b/src/components/group/GroupCard.tsx index 79700a0a..797750b4 100644 --- a/src/components/group/GroupCard.tsx +++ b/src/components/group/GroupCard.tsx @@ -13,10 +13,14 @@ interface Props { onClick?: () => void; isFirstCard?: boolean; isPublic?: boolean; + isCompleted?: boolean; } export const GroupCard = forwardRef( - ({ group, isOngoing, type = 'main', isRecommend = false, onClick, isFirstCard }, ref) => { + ( + { group, isOngoing, type = 'main', isRecommend = false, onClick, isFirstCard, isCompleted }, + ref, + ) => { return ( @@ -33,9 +37,13 @@ export const GroupCard = forwardRef( people

{group.participants}

- / {group.maximumParticipants}명 + {!isCompleted && ( + / {group.maximumParticipants}명 + )} + {isCompleted && }
- {(type !== 'modal' || group.type !== 'expired') && + {!isCompleted && + (type !== 'modal' || group.type !== 'expired') && (isOngoing === true ? ( {group.deadLine} 종료 @@ -138,7 +146,7 @@ const Bottom = styled.div` const Participant = styled.div<{ isRecommend: boolean }>` display: flex; align-items: center; - gap: 6px; + gap: 4px; color: ${colors.white}; font-size: ${typography.fontSize.xs}; font-weight: ${typography.fontWeight.medium}; diff --git a/src/components/group/MyGroupModal.tsx b/src/components/group/MyGroupModal.tsx index 6605ece2..813b623a 100644 --- a/src/components/group/MyGroupModal.tsx +++ b/src/components/group/MyGroupModal.tsx @@ -21,7 +21,7 @@ export const MyGroupModal = ({ onClose }: MyGroupModalProps) => { }; }, []); const navigate = useNavigate(); - const [selected, setSelected] = useState<'진행중' | '모집중' | ''>(''); + const [selected, setSelected] = useState<'진행중' | '모집중' | '완료' | ''>(''); const [rooms, setRooms] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -52,12 +52,14 @@ export const MyGroupModal = ({ onClose }: MyGroupModalProps) => { setNextCursor(null); setIsLast(false); - const roomType: RoomType = + const roomType: RoomType | 'expired' = selected === '진행중' ? 'playing' : selected === '모집중' ? 'recruiting' - : 'playingAndRecruiting'; + : selected === '완료' + ? 'expired' + : 'playingAndRecruiting'; const response = await getMyRooms(roomType, null); @@ -87,12 +89,14 @@ export const MyGroupModal = ({ onClose }: MyGroupModalProps) => { isFetchingRef.current = true; setIsLoading(true); try { - const roomType: RoomType = + const roomType: RoomType | 'expired' = selected === '진행중' ? 'playing' : selected === '모집중' ? 'recruiting' - : 'playingAndRecruiting'; + : selected === '완료' + ? 'expired' + : 'playingAndRecruiting'; const res = await getMyRooms(roomType, nextCursor); if (res.isSuccess) { @@ -119,40 +123,6 @@ export const MyGroupModal = ({ onClose }: MyGroupModalProps) => { } }; - useEffect(() => { - const fetchRooms = async () => { - try { - setIsLoading(true); - setError(null); - setNextCursor(null); - setIsLast(false); - - const roomType: RoomType = - selected === '진행중' - ? 'playing' - : selected === '모집중' - ? 'recruiting' - : 'playingAndRecruiting'; - - const res = await getMyRooms(roomType, null); - if (res.isSuccess) { - setRooms(res.data.roomList); - setNextCursor(res.data.nextCursor); - setIsLast(res.data.isLast); - } else { - setError(res.message); - } - } catch (e) { - console.log(e); - setError('방 목록을 불러오는데 실패했습니다.'); - } finally { - setIsLoading(false); - } - }; - - fetchRooms(); - }, [selected]); - useEffect(() => { const tryFill = async () => { if (!contentRef.current || isLast) return; @@ -169,6 +139,7 @@ export const MyGroupModal = ({ onClose }: MyGroupModalProps) => { } }; tryFill(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [rooms, nextCursor, isLast]); useEffect(() => { @@ -180,7 +151,9 @@ export const MyGroupModal = ({ onClose }: MyGroupModalProps) => { const convertedGroups = rooms.map(convertRoomToGroup); const handleGroupCardClick = (group: Group) => { - if (selected === '모집중') { + if (selected === '완료') { + navigate(`/group/detail/joined/${group.id}`); + } else if (selected === '모집중') { navigate(`/group/detail/${group.id}`); } else if (selected === '진행중') { navigate(`/group/detail/joined/${group.id}`); @@ -202,7 +175,7 @@ export const MyGroupModal = ({ onClose }: MyGroupModalProps) => { /> - {(['진행중', '모집중'] as const).map(tab => ( + {(['진행중', '모집중', '완료'] as const).map(tab => ( { group={group} isOngoing={group.isOnGoing} type="modal" + isCompleted={selected === '완료'} onClick={() => handleGroupCardClick(group)} /> ))} @@ -235,14 +209,18 @@ export const MyGroupModal = ({ onClose }: MyGroupModalProps) => { ? '진행중인 모임방이 없어요' : selected === '모집중' ? '모집중인 모임방이 없어요' - : '참여중인 모임방이 없어요'} + : selected === '완료' + ? '완료된 모임방이 없어요' + : '참여중인 모임방이 없어요'} {selected === '진행중' ? '진행중인 모임방에 참여해보세요!' : selected === '모집중' ? '모집중인 모임방에 참여해보세요!' - : '첫 번째 모임방에 참여해보세요!'} + : selected === '완료' + ? '아직 완료된 모임방이 없습니다.' + : '첫 번째 모임방에 참여해보세요!'} )} diff --git a/src/pages/group/Group.tsx b/src/pages/group/Group.tsx index 9c43a6f8..24bbc767 100644 --- a/src/pages/group/Group.tsx +++ b/src/pages/group/Group.tsx @@ -8,7 +8,6 @@ import styled from '@emotion/styled'; import { RecruitingGroupCarousel, type Section } from '@/components/group/RecruitingGroupCarousel'; import { useState, useEffect } from 'react'; import { MyGroupModal } from '@/components/group/MyGroupModal'; -import CompletedGroupModal from '@/components/group/CompletedGroupModal'; import { useNavigate } from 'react-router-dom'; import makegroupfab from '../../assets/common/makegroupfab.svg'; import searchChar from '../../assets/common/searchChar.svg'; @@ -32,7 +31,6 @@ const convertRoomItemToGroup = ( const Group = () => { const navigate = useNavigate(); const [isMyGroupModalOpen, setIsMyGroupModalOpen] = useState(false); - const [isCompletedGroupModalOpen, setIsCompletedGroupModalOpen] = useState(false); const [sections, setSections] = useState([ { title: '최근 생성된 독서 모임방', groups: [] }, { title: '마감 임박한 독서 모임방', groups: [] }, @@ -86,9 +84,6 @@ const Group = () => { const openMyGroupModal = () => setIsMyGroupModalOpen(true); const closeMyGroupModal = () => setIsMyGroupModalOpen(false); - const openCompletedGroupModal = () => setIsCompletedGroupModalOpen(true); - const closeCompletedGroupModal = () => setIsCompletedGroupModalOpen(false); - const handleSearchBarClick = () => { navigate('/group/search'); }; @@ -108,10 +103,9 @@ const Group = () => { return ( {isMyGroupModalOpen && } - {isCompletedGroupModalOpen && } From 967390cd3e3fdf86dc9bdb39b76ae7aa5711869e Mon Sep 17 00:00:00 2001 From: Ji Ho June <129824629+ho0010@users.noreply.github.com> Date: Sun, 2 Nov 2025 15:56:01 +0900 Subject: [PATCH 32/34] =?UTF-8?q?remove:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/group/MyGroupModal.tsx | 2 +- src/pages/group/Group.tsx | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/group/MyGroupModal.tsx b/src/components/group/MyGroupModal.tsx index 813b623a..c950e909 100644 --- a/src/components/group/MyGroupModal.tsx +++ b/src/components/group/MyGroupModal.tsx @@ -52,7 +52,7 @@ export const MyGroupModal = ({ onClose }: MyGroupModalProps) => { setNextCursor(null); setIsLast(false); - const roomType: RoomType | 'expired' = + const roomType: RoomType = selected === '진행중' ? 'playing' : selected === '모집중' diff --git a/src/pages/group/Group.tsx b/src/pages/group/Group.tsx index 24bbc767..43c3b4fb 100644 --- a/src/pages/group/Group.tsx +++ b/src/pages/group/Group.tsx @@ -103,11 +103,7 @@ const Group = () => { return ( {isMyGroupModalOpen && } - + From f4eb20b5ba21d1ffadd72b935a04cd44bbd74096 Mon Sep 17 00:00:00 2001 From: Ji Ho June <129824629+ho0010@users.noreply.github.com> Date: Sun, 2 Nov 2025 16:02:39 +0900 Subject: [PATCH 33/34] =?UTF-8?q?remove:=20=EC=A4=91=EB=B3=B5=EB=90=9C=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=84=A0=EC=96=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/group/MyGroupModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/group/MyGroupModal.tsx b/src/components/group/MyGroupModal.tsx index c950e909..75351bd9 100644 --- a/src/components/group/MyGroupModal.tsx +++ b/src/components/group/MyGroupModal.tsx @@ -89,7 +89,7 @@ export const MyGroupModal = ({ onClose }: MyGroupModalProps) => { isFetchingRef.current = true; setIsLoading(true); try { - const roomType: RoomType | 'expired' = + const roomType: RoomType = selected === '진행중' ? 'playing' : selected === '모집중' From 2ad171b585227ee26e16b0f4e35cd63e48afbe40 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Tue, 11 Nov 2025 17:42:49 +0900 Subject: [PATCH 34/34] Revert "Merge pull request #284 from THIP-TextHip/feat/influencer" This reverts commit 154caf75896eda61910b2f2585834ced774a092e, reversing changes made to 8d5414e1a66ff03f3acfcb50f76437ad6e4de853. --- package.json | 2 - pnpm-lock.yaml | 31 -- src/assets/feed/lookmore-influencer.svg | 11 - src/components/feed/FeedPost.tsx | 2 +- src/components/feed/RecommendedFeedCard.tsx | 65 ---- .../feed/RecommendedFeedSection.tsx | 106 ------ src/components/feed/TotalFeed.tsx | 24 +- src/mocks/recommendedFeeds.mock.ts | 328 ------------------ 8 files changed, 11 insertions(+), 558 deletions(-) delete mode 100644 src/assets/feed/lookmore-influencer.svg delete mode 100644 src/components/feed/RecommendedFeedCard.tsx delete mode 100644 src/components/feed/RecommendedFeedSection.tsx delete mode 100644 src/mocks/recommendedFeeds.mock.ts diff --git a/package.json b/package.json index ea7b87df..da28e12c 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,6 @@ "@emotion/styled": "^11.14.0", "@types/react-datepicker": "^7.0.0", "axios": "^1.11.0", - "embla-carousel": "^8.6.0", - "embla-carousel-react": "^8.6.0", "react": "^19.1.0", "react-cookie": "^8.0.1", "react-datepicker": "^8.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f86b85f..fae23ebd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,12 +23,6 @@ importers: axios: specifier: ^1.11.0 version: 1.11.0 - embla-carousel: - specifier: ^8.6.0 - version: 8.6.0 - embla-carousel-react: - specifier: ^8.6.0 - version: 8.6.0(react@19.1.0) react: specifier: ^19.1.0 version: 19.1.0 @@ -872,19 +866,6 @@ packages: electron-to-chromium@1.5.151: resolution: {integrity: sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA==} - embla-carousel-react@8.6.0: - resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==} - peerDependencies: - react: ^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - - embla-carousel-reactive-utils@8.6.0: - resolution: {integrity: sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==} - peerDependencies: - embla-carousel: 8.6.0 - - embla-carousel@8.6.0: - resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==} - encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -2506,18 +2487,6 @@ snapshots: electron-to-chromium@1.5.151: {} - embla-carousel-react@8.6.0(react@19.1.0): - dependencies: - embla-carousel: 8.6.0 - embla-carousel-reactive-utils: 8.6.0(embla-carousel@8.6.0) - react: 19.1.0 - - embla-carousel-reactive-utils@8.6.0(embla-carousel@8.6.0): - dependencies: - embla-carousel: 8.6.0 - - embla-carousel@8.6.0: {} - encodeurl@2.0.0: {} error-ex@1.3.2: diff --git a/src/assets/feed/lookmore-influencer.svg b/src/assets/feed/lookmore-influencer.svg deleted file mode 100644 index 754f0fd5..00000000 --- a/src/assets/feed/lookmore-influencer.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/src/components/feed/FeedPost.tsx b/src/components/feed/FeedPost.tsx index 574a90c2..28d3105a 100644 --- a/src/components/feed/FeedPost.tsx +++ b/src/components/feed/FeedPost.tsx @@ -17,7 +17,7 @@ const Container = styled.div` `; const BorderBottom = styled.div` - width: 100%; + width: 94.8%; /* min-width: 280px; max-width: 500px; */ margin: 0 auto; diff --git a/src/components/feed/RecommendedFeedCard.tsx b/src/components/feed/RecommendedFeedCard.tsx deleted file mode 100644 index 1d087389..00000000 --- a/src/components/feed/RecommendedFeedCard.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import styled from '@emotion/styled'; -import PostBody from '../common/Post/PostBody'; -import PostFooter from '../common/Post/PostFooter'; -import PostHeader from '../common/Post/PostHeader'; -import type { PostData } from '../../types/post'; -import { colors } from '@/styles/global/global'; -import lookmoreInfluencer from '@/assets/feed/lookmore-influencer.svg'; - -interface RecommendedFeedCardProps extends PostData { - aliasColor?: string; -} - -const RecommendedFeedCard = (postData: RecommendedFeedCardProps) => { - const handleCardClick = () => { - window.open(`/feed/${postData.feedId}`, '_blank'); - }; - - return ( - - - - - - - - ); -}; - -const CardContainer = styled.div` - display: flex; - flex-direction: column; - gap: 16px; - padding: 12px; - background-color: ${colors.darkgrey.dark}; - border-radius: 12px; - width: 100%; - height: 100%; - - > *:last-child { - margin-top: auto; - } - - .right { - display: none; - } -`; - -const PostBodyWrapper = styled.div` - .content { - -webkit-line-clamp: 3 !important; - min-height: 60px; - } - - /* prettier-ignore */ - && img.lookmore-icon, - && .lookmore-icon { - content: url("${lookmoreInfluencer}") !important; - } - - > div > div:first-of-type { - pointer-events: none; - } -`; - -export default RecommendedFeedCard; diff --git a/src/components/feed/RecommendedFeedSection.tsx b/src/components/feed/RecommendedFeedSection.tsx deleted file mode 100644 index 594cf40f..00000000 --- a/src/components/feed/RecommendedFeedSection.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import styled from '@emotion/styled'; -import RecommendedFeedCard from './RecommendedFeedCard'; -import { colors, typography } from '@/styles/global/global'; -import useEmblaCarousel from 'embla-carousel-react'; -import type { EmblaOptionsType } from 'embla-carousel'; -import { allMockRecommendedFeeds } from '@/mocks/recommendedFeeds.mock'; - - -const SectionContainer = styled.div` - display: flex; - flex-direction: column; - width: 100%; - background-color: ${colors.black.main}; -`; - -const SectionHeader = styled.div` - padding: 40px 20px 20px; - display: flex; - flex-direction: column; -`; - -const HeaderText = styled.h2` - color: ${colors.white}; - font-size: ${typography.fontSize.xl}; - font-weight: ${typography.fontWeight.bold}; - line-height: normal; - margin-bottom: 8px; -`; - -const SubText = styled.p` - color: ${colors.grey[100]}; - font-size: ${typography.fontSize.sm}; - font-weight: ${typography.fontWeight.medium}; - line-height: 20px; - margin: 0; -`; - -const EmblaViewport = styled.div` - overflow: hidden; - padding: 0 20px; -`; - -const EmblaContainer = styled.div` - display: flex; - touch-action: pan-y pinch-zoom; - gap: 12px; -`; - -const EmblaSlide = styled.div` - transform: translate3d(0, 0, 0); - flex: 0 0 calc(100% - 10px); - min-width: 0; - padding-bottom: 40px; -`; - -const BorderBottom = styled.div` - width: 100%; - margin: 0 auto; - padding: 0 20px; - height: 6px; - background: #1c1c1c; -`; - -// Component -interface RecommendedFeedSectionProps { - sectionIndex?: number; -} - -const RecommendedFeedSection = ({ sectionIndex = 0 }: RecommendedFeedSectionProps) => { - const options: EmblaOptionsType = { - align: 'center', - slidesToScroll: 1, - containScroll: 'trimSnaps', - }; - - const [emblaRef] = useEmblaCarousel(options); - - // 섹션 인덱스에 따라 다른 5개 게시글 선택 - const startIndex = (sectionIndex * 5) % allMockRecommendedFeeds.length; - const selectedFeeds = [ - ...allMockRecommendedFeeds.slice(startIndex, startIndex + 5), - ...allMockRecommendedFeeds.slice(0, Math.max(0, startIndex + 5 - allMockRecommendedFeeds.length)), - ].slice(0, 5); - - return ( - - - 지금 뜨는 추천 글 - 비슷한 취향의 인플루언서, 작가가 - 추천하는 도서를 만나보세요. - - - - {selectedFeeds.map(feed => ( - - - - ))} - - - - - ); -}; - -export default RecommendedFeedSection; diff --git a/src/components/feed/TotalFeed.tsx b/src/components/feed/TotalFeed.tsx index 4d2dbf86..ddb533c0 100644 --- a/src/components/feed/TotalFeed.tsx +++ b/src/components/feed/TotalFeed.tsx @@ -1,29 +1,25 @@ import styled from '@emotion/styled'; -import { Fragment } from 'react'; import FollowList from './FollowList'; import FeedPost from './FeedPost'; -import RecommendedFeedSection from './RecommendedFeedSection'; import type { FeedListProps } from '../../types/post'; import { colors, typography } from '@/styles/global/global'; -const TotalFeed = ({ showHeader, posts = [], isTotalFeed }: FeedListProps) => { +const TotalFeed = ({ showHeader, posts = [], isTotalFeed, isLast = false }: FeedListProps) => { const hasPosts = posts.length > 0; return ( {hasPosts ? ( - <> - {posts.map((post, index) => ( - - - {/* 10개마다 추천 섹션 반복 표시 */} - {(index + 1) % 10 === 0 && ( - - )} - - ))} - + posts.map((post, index) => ( + + )) ) : (
피드에 작성된 글이 없어요
diff --git a/src/mocks/recommendedFeeds.mock.ts b/src/mocks/recommendedFeeds.mock.ts deleted file mode 100644 index 42d50ef2..00000000 --- a/src/mocks/recommendedFeeds.mock.ts +++ /dev/null @@ -1,328 +0,0 @@ -import { colors } from '@/styles/global/global'; -import type { PostData } from '@/types/post'; - -// 목업 데이터 (aliasColor 추가 필요) -interface MockPostData extends PostData { - aliasColor?: string; -} - -export const allMockRecommendedFeeds: MockPostData[] = [ - // 첫 번째 세트 (1-5) - { - feedId: 101, - creatorId: 1, - creatorNickname: '책덕후민지', - creatorProfileImageUrl: 'https://via.placeholder.com/36', - alias: '공식 인플루언서', - aliasColor: colors.neongreen, - postDate: '2시간 전', - isbn: '9788936434267', - bookTitle: '채식주의자', - bookAuthor: '한강', - contentBody: - '한강 작가님의 문장은 정말 날카롭고도 아름다워요. 문장 하나하나가 단순히 글이 아니라, 인간의 본성과 욕망, 그리고 사회의 폭력성을 정교하게 해부하는 칼날처럼 느껴졌어요. 읽는 내내 숨이 막히는 긴장감과 함께, 인간이라는 존재에 대한 불편한 진실을 마주해야 했습니다.', - contentUrls: [], - likeCount: 342, - commentCount: 28, - isSaved: false, - isLiked: false, - isPublic: true, - isWriter: false, - }, - { - feedId: 102, - creatorId: 2, - creatorNickname: '북튜버지수', - creatorProfileImageUrl: 'https://via.placeholder.com/36', - alias: '공식 인플루언서', - aliasColor: colors.neongreen, - postDate: '5시간 전', - isbn: '9788954676540', - bookTitle: '달러구트 꿈 백화점', - bookAuthor: '이미예', - contentBody: - '힐링이 필요할 때 읽기 좋은 책! 따뜻한 이야기가 마음을 녹여줍니다. 꿈을 판다는 독특한 설정이 매력적이고, 각 에피소드마다 담긴 메시지가 깊어요.', - contentUrls: [], - likeCount: 287, - commentCount: 19, - isSaved: false, - isLiked: false, - isPublic: true, - isWriter: false, - }, - { - feedId: 103, - creatorId: 3, - creatorNickname: '문학소녀윤아', - creatorProfileImageUrl: 'https://via.placeholder.com/36', - alias: '공식 인플루언서', - aliasColor: colors.neongreen, - postDate: '8시간 전', - isbn: '9788937460449', - bookTitle: '1984', - bookAuthor: '조지 오웰', - contentBody: - '지금 읽어도 너무나 현대적인 고전. 빅브라더의 감시 사회가 현실이 되어가는 것 같아 무섭기도 하네요. 모든 사람이 꼭 읽어야 할 필독서입니다.', - contentUrls: [], - likeCount: 521, - commentCount: 45, - isSaved: false, - isLiked: false, - isPublic: true, - isWriter: false, - }, - { - feedId: 104, - creatorId: 4, - creatorNickname: '작가지망생', - creatorProfileImageUrl: 'https://via.placeholder.com/36', - alias: '공식 인플루언서', - aliasColor: colors.neongreen, - postDate: '1일 전', - isbn: '9788932917245', - bookTitle: '불편한 편의점', - bookAuthor: '김호연', - contentBody: - '올해 읽은 책 중 최고예요! 독고 씨와 염 여사의 이야기가 너무 따뜻하고 감동적이었어요. 읽으면서 계속 울었던 것 같아요. 모두에게 추천합니다!', - contentUrls: [], - likeCount: 456, - commentCount: 38, - isSaved: false, - isLiked: false, - isPublic: true, - isWriter: false, - }, - { - feedId: 105, - creatorId: 5, - creatorNickname: '책읽는개발자', - creatorProfileImageUrl: 'https://via.placeholder.com/36', - alias: '공식 인플루언서', - aliasColor: colors.neongreen, - postDate: '2일 전', - isbn: '9788936434298', - bookTitle: '작별하지 않는다', - bookAuthor: '한강', - contentBody: - '한강 작가의 섬세한 문장들이 가슴을 울립니다. 상실과 기억, 그리고 삶에 대한 깊은 성찰을 담은 작품이에요. 천천히 음미하며 읽었습니다.', - contentUrls: [], - likeCount: 398, - commentCount: 31, - isSaved: false, - isLiked: false, - isPublic: true, - isWriter: false, - }, - // 두 번째 세트 (6-10) - { - feedId: 106, - creatorId: 6, - creatorNickname: '심야독서러버', - creatorProfileImageUrl: 'https://via.placeholder.com/36', - alias: '공식 인플루언서', - aliasColor: colors.neongreen, - postDate: '3일 전', - isbn: '9788954676533', - bookTitle: '미드나잇 라이브러리', - bookAuthor: '매트 헤이그', - contentBody: - '삶의 선택에 대해 다시 생각하게 되는 책이에요. 주인공 노라가 다양한 삶을 경험하는 과정이 너무 인상깊었어요. 지금 내 삶도 소중하다는 걸 깨닫게 해주는 작품입니다.', - contentUrls: [], - likeCount: 612, - commentCount: 52, - isSaved: false, - isLiked: false, - isPublic: true, - isWriter: false, - }, - { - feedId: 107, - creatorId: 7, - creatorNickname: '추리소설매니아', - creatorProfileImageUrl: 'https://via.placeholder.com/36', - alias: '공식 인플루언서', - aliasColor: colors.neongreen, - postDate: '4일 전', - isbn: '9788932917238', - bookTitle: '살인자의 기억법', - bookAuthor: '김영하', - contentBody: - '반전에 반전을 거듭하는 스토리가 정말 압권이에요! 김영하 작가님의 필력이 빛나는 작품입니다. 마지막 장면에서 소름이 쫙 돋았어요.', - contentUrls: [], - likeCount: 445, - commentCount: 37, - isSaved: false, - isLiked: false, - isPublic: true, - isWriter: false, - }, - { - feedId: 108, - creatorId: 8, - creatorNickname: '에세이추천러', - creatorProfileImageUrl: 'https://via.placeholder.com/36', - alias: '공식 인플루언서', - aliasColor: colors.neongreen, - postDate: '5일 전', - isbn: '9788936434274', - bookTitle: '여행의 이유', - bookAuthor: '김영하', - contentBody: - '여행에 대한 새로운 시각을 갖게 해주는 에세이입니다. 김영하 작가님의 통찰력 있는 글이 여행의 의미를 되새기게 만들어요. 여행을 좋아하는 분들께 강추합니다!', - contentUrls: [], - likeCount: 378, - commentCount: 29, - isSaved: false, - isLiked: false, - isPublic: true, - isWriter: false, - }, - { - feedId: 109, - creatorId: 9, - creatorNickname: '판타지덕후', - creatorProfileImageUrl: 'https://via.placeholder.com/36', - alias: '공식 인플루언서', - aliasColor: colors.neongreen, - postDate: '6일 전', - isbn: '9788983920966', - bookTitle: '해리포터와 마법사의 돌', - bookAuthor: 'J.K. 롤링', - contentBody: - '어렸을 때 읽고 다시 읽어봤는데, 여전히 마법 같은 책이에요. 호그와트에 입학하고 싶다는 꿈을 다시 꾸게 만드는 작품. 세대를 넘어 사랑받는 이유를 알 것 같아요.', - contentUrls: [], - likeCount: 589, - commentCount: 67, - isSaved: false, - isLiked: false, - isPublic: true, - isWriter: false, - }, - { - feedId: 110, - creatorId: 10, - creatorNickname: '자기계발러버', - creatorProfileImageUrl: 'https://via.placeholder.com/36', - alias: '공식 인플루언서', - aliasColor: colors.neongreen, - postDate: '1주 전', - isbn: '9788934986867', - bookTitle: '아주 작은 습관의 힘', - bookAuthor: '제임스 클리어', - contentBody: - '습관을 바꾸고 싶은 분들께 정말 추천해요. 1%의 개선이 쌓여 큰 변화를 만든다는 메시지가 와닿았어요. 실천 가능한 구체적인 방법들이 가득한 책입니다.', - contentUrls: [], - likeCount: 723, - commentCount: 81, - isSaved: false, - isLiked: false, - isPublic: true, - isWriter: false, - }, - // 세 번째 세트 (11-15) - { - feedId: 111, - creatorId: 11, - creatorNickname: '로맨스소설러버', - creatorProfileImageUrl: 'https://via.placeholder.com/36', - alias: '공식 인플루언서', - aliasColor: colors.neongreen, - postDate: '1주 전', - isbn: '9788952779847', - bookTitle: '82년생 김지영', - bookAuthor: '조남주', - contentBody: - '우리 시대 여성들의 이야기가 고스란히 담긴 책이에요. 공감되는 부분이 너무 많아서 읽으면서 여러 감정이 교차했습니다. 모든 세대가 읽어봤으면 하는 작품이에요.', - contentUrls: [], - likeCount: 567, - commentCount: 94, - isSaved: false, - isLiked: false, - isPublic: true, - isWriter: false, - }, - { - feedId: 112, - creatorId: 12, - creatorNickname: '과학책마니아', - creatorProfileImageUrl: 'https://via.placeholder.com/36', - alias: '공식 인플루언서', - aliasColor: colors.neongreen, - postDate: '1주 전', - isbn: '9788934942467', - bookTitle: '코스모스', - bookAuthor: '칼 세이건', - contentBody: - '우주에 대한 경외감을 느끼게 해주는 명저입니다. 과학책이지만 시적인 문장들이 가득해서 읽는 즐거움이 있어요. 우주의 광대함 앞에서 겸손해지게 만드는 책입니다.', - contentUrls: [], - likeCount: 412, - commentCount: 33, - isSaved: false, - isLiked: false, - isPublic: true, - isWriter: false, - }, - { - feedId: 113, - creatorId: 13, - creatorNickname: '역사덕후', - creatorProfileImageUrl: 'https://via.placeholder.com/36', - alias: '공식 인플루언서', - aliasColor: colors.neongreen, - postDate: '1주 전', - isbn: '9788934942696', - bookTitle: '사피엔스', - bookAuthor: '유발 하라리', - contentBody: - '인류의 역사를 완전히 새로운 시각으로 바라보게 되는 책이에요. 방대한 내용을 흥미진진하게 풀어냈어요. 읽으면서 계속 생각할 거리를 던져주는 작품입니다.', - contentUrls: [], - likeCount: 891, - commentCount: 103, - isSaved: false, - isLiked: false, - isPublic: true, - isWriter: false, - }, - { - feedId: 114, - creatorId: 14, - creatorNickname: '시집러버', - creatorProfileImageUrl: 'https://via.placeholder.com/36', - alias: '공식 인플루언서', - aliasColor: colors.neongreen, - postDate: '2주 전', - isbn: '9788936434021', - bookTitle: '저녁의 게임', - bookAuthor: '박상영', - contentBody: - '사랑과 관계에 대한 솔직한 이야기들이 담긴 소설이에요. 감정선이 세밀하게 그려져 있어서 읽는 내내 빠져들었습니다. 현대인의 고독과 연결에 대한 깊은 성찰이 담겨있어요.', - contentUrls: [], - likeCount: 334, - commentCount: 27, - isSaved: false, - isLiked: false, - isPublic: true, - isWriter: false, - }, - { - feedId: 115, - creatorId: 15, - creatorNickname: '고전문학러버', - creatorProfileImageUrl: 'https://via.placeholder.com/36', - alias: '공식 인플루언서', - aliasColor: colors.neongreen, - postDate: '2주 전', - isbn: '9788937462917', - bookTitle: '죄와 벌', - bookAuthor: '표도르 도스토옙스키', - contentBody: - '인간의 심리를 이토록 깊이있게 다룬 작품이 또 있을까요. 라스콜리니코프의 내면 묘사가 정말 압권입니다. 읽는 동안 철학적 질문들에 대해 끊임없이 고민하게 되는 걸작이에요.', - contentUrls: [], - likeCount: 478, - commentCount: 41, - isSaved: false, - isLiked: false, - isPublic: true, - isWriter: false, - }, -];