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: 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/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/RecommendedFeedCard.tsx b/src/components/feed/RecommendedFeedCard.tsx new file mode 100644 index 00000000..1d087389 --- /dev/null +++ b/src/components/feed/RecommendedFeedCard.tsx @@ -0,0 +1,65 @@ +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 new file mode 100644 index 00000000..594cf40f --- /dev/null +++ b/src/components/feed/RecommendedFeedSection.tsx @@ -0,0 +1,106 @@ +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 ddb533c0..b183a129 100644 --- a/src/components/feed/TotalFeed.tsx +++ b/src/components/feed/TotalFeed.tsx @@ -1,25 +1,34 @@ 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 + 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, + }, +];