diff --git a/src/_apis/detail/get-crew-detail.ts b/src/_apis/detail/get-crew-detail.ts new file mode 100644 index 00000000..1e31c7be --- /dev/null +++ b/src/_apis/detail/get-crew-detail.ts @@ -0,0 +1,33 @@ +import { fetchApi } from '@/src/utils/api'; + +type CrewMember = { + id: number; + nickname: string; + profileImageUrl?: string; +}; + +type CrewDetail = { + id: number; + title: string; + mainLocation: string; + subLocation: string; + participantCount: number; + totalCount: number; + isConfirmed: boolean; + imageUrl: string; + totalGatheringCount: number; + CrewMembers: CrewMember[]; + isCaptain: boolean; + isCrew: boolean; +}; + +export async function getCrewDetail(): Promise { + const response = await fetchApi('/api/mock-api/detail?type=crewDetail', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + return response; +} diff --git a/src/_apis/detail/get-gathering-list.ts b/src/_apis/detail/get-gathering-list.ts new file mode 100644 index 00000000..77080400 --- /dev/null +++ b/src/_apis/detail/get-gathering-list.ts @@ -0,0 +1,29 @@ +import { fetchApi } from '@/src/utils/api'; + +type GatheringList = { + id: number; + title: string; + dateTime: string; + location: string; + currentCount: number; + totalCount: number; + imageUrl: string; + isLiked: boolean; +}; + +export async function getGatheringList(): Promise { + const response = await fetchApi('/api/mock-api/detail?type=gatherings', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + return response.map((item) => ({ + ...item, + dateTime: item.dateTime, + currentCount: item.currentCount, + totalCount: item.totalCount, + isLiked: item.isLiked ?? false, + })); +} diff --git a/src/_apis/detail/get-review-list.ts b/src/_apis/detail/get-review-list.ts new file mode 100644 index 00000000..6be9b750 --- /dev/null +++ b/src/_apis/detail/get-review-list.ts @@ -0,0 +1,62 @@ +import { CrewReview } from '@/src/types/review'; + +export interface ReviewRateInfo { + totalRate: number; + averageRate: number; + ratingsData: Array<{ score: number; count: number }>; +} + +export interface ReviewListData { + info: ReviewRateInfo; + data: CrewReview[]; + totalItems: number; + totalPages: number; + currentPage: number; +} + +export async function getReviewList(page: number, limit: number): Promise { + const response = await fetch(`/api/mock-api/detail?type=reviews`); + const reviewData: CrewReview[] = await response.json(); // 리뷰 데이터를 배열로 바로 받음 + + // 데이터가 비어 있는 경우 기본값 반환 + if (!reviewData || reviewData.length === 0) { + return { + info: { + totalRate: 0, + averageRate: 0, + ratingsData: [5, 4, 3, 2, 1].map((score) => ({ score, count: 0 })), + }, + data: [], + totalItems: 0, + totalPages: 0, + currentPage: page, + }; + } + + // 페이지네이션 적용 + const startIndex = (page - 1) * limit; + const paginatedData = reviewData.slice(startIndex, startIndex + limit); + + // 통계 정보 생성 + const totalRate = reviewData.reduce((sum, review) => sum + review.rate, 0); + const averageRate = reviewData.length ? totalRate / reviewData.length : 0; + + const ratingsData = [5, 4, 3, 2, 1].map((score) => ({ + score, + count: reviewData.filter((review) => review.rate === score).length, + })); + + const info: ReviewRateInfo = { + totalRate, + averageRate, + ratingsData, + }; + + return { + info, + data: paginatedData, + totalItems: reviewData.length, + totalPages: Math.ceil(reviewData.length / limit), + currentPage: page, + }; +} diff --git a/src/app/(crew)/api-test/page.tsx b/src/app/(crew)/api-test/page.tsx index 26ab8119..eca9ed48 100644 --- a/src/app/(crew)/api-test/page.tsx +++ b/src/app/(crew)/api-test/page.tsx @@ -5,7 +5,7 @@ import { useQuery } from '@tanstack/react-query'; import { getUsersQuery } from '@/src/_queries/useGetUserQuery'; import { ApiError } from '@/src/utils/api'; -// react-query 예시 +// FIX: react-query로 임시로 작성된 코드입니다. 추후 삭제 export default function TestPage() { const { data: users, error, isLoading, isError } = useQuery(getUsersQuery()); diff --git a/src/app/(crew)/crew/detail/[id]/_components/crew-review-list.stories.tsx b/src/app/(crew)/crew/detail/[id]/_components/crew-review-list.stories.tsx new file mode 100644 index 00000000..adad796e --- /dev/null +++ b/src/app/(crew)/crew/detail/[id]/_components/crew-review-list.stories.tsx @@ -0,0 +1,55 @@ +import React, { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import ClientProvider from '@/src/components/client-provider'; +import { CrewReview } from '@/src/types/review'; +import { CrewReviewData } from '@/src/mock/review-data'; +import CrewReviewList from './crew-review-list'; + +const meta: Meta = { + title: 'components/CrewReviewList', + component: CrewReviewList, + parameters: { + layout: 'fulled', + nextjs: { + appDirectory: true, + }, + }, + tags: ['autodocs'], + decorators: [ + (Story) => ( + + + + ), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +function RenderReviewPagination() { + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 6; + + // 페이지에 맞는 리뷰 데이터 가져오기 + const totalItems = CrewReviewData.data.length; + const totalPages = Math.ceil(totalItems / itemsPerPage); + + const currentReviews = CrewReviewData.data.slice( + (currentPage - 1) * itemsPerPage, + currentPage * itemsPerPage, + ); + + return ( + + ); +} + +export const Default: Story = { + render: () => , +}; diff --git a/src/app/(crew)/crew/detail/[id]/_components/crew-review-list.tsx b/src/app/(crew)/crew/detail/[id]/_components/crew-review-list.tsx new file mode 100644 index 00000000..facece23 --- /dev/null +++ b/src/app/(crew)/crew/detail/[id]/_components/crew-review-list.tsx @@ -0,0 +1,64 @@ +'use client'; + +import React from 'react'; +import { Pagination } from '@mantine/core'; +import ReviewCard from '@/src/components/common/review-list/review-card'; +import { CrewReview } from '@/src/types/review'; + +interface CrewReviewListProps { + reviews: CrewReview[]; + totalPages: number; + currentPage: number; + onPageChange: (page: number) => void; +} + +export default function CrewReviewList({ + reviews, + totalPages, + currentPage, + onPageChange, +}: CrewReviewListProps) { + return ( +
+
+ {reviews.map((review) => ( + + ))} +
+
+ +
+
+ ); +} diff --git a/src/app/(crew)/crew/detail/[id]/_components/rating-display.stories.tsx b/src/app/(crew)/crew/detail/[id]/_components/rating-display.stories.tsx new file mode 100644 index 00000000..b7f8e547 --- /dev/null +++ b/src/app/(crew)/crew/detail/[id]/_components/rating-display.stories.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { Meta, StoryFn } from '@storybook/react'; +import RatingDisplay, { ReviewRateInfo } from './rating-display'; + +export default { + title: 'Components/RatingDisplay', + component: RatingDisplay, + tags: ['autodocs'], + argTypes: { + totalRate: { control: 'number', description: '총 평가 개수' }, + averageRate: { control: 'number', description: '평균 평점' }, + ratingsData: { control: 'object', description: '각 점수별 평가 개수' }, + }, +} as Meta; + +interface RatingDisplayStoryProps { + totalRate: number; + averageRate: number; + ratingsData: { score: number; count: number }[]; +} + +// Template을 함수 선언으로 변경하고 StoryFn 타입을 사용 +const Template: StoryFn = function Template(args) { + const { totalRate, averageRate, ratingsData } = args; + const reviewRateInfo: ReviewRateInfo = { totalRate, averageRate, ratingsData }; + return ; +}; + +// 스토리 정의 +export const Default = Template.bind({}); +Default.args = { + totalRate: 24, + averageRate: 3.5, + ratingsData: [ + { score: 5, count: 6 }, + { score: 4, count: 9 }, + { score: 3, count: 4 }, + { score: 2, count: 3 }, + { score: 1, count: 2 }, + ], +}; + +export const HighRating = Template.bind({}); +HighRating.args = { + totalRate: 15, + averageRate: 4.7, + ratingsData: [ + { score: 5, count: 10 }, + { score: 4, count: 3 }, + { score: 3, count: 1 }, + { score: 2, count: 1 }, + { score: 1, count: 0 }, + ], +}; + +export const LowRating = Template.bind({}); +LowRating.args = { + totalRate: 20, + averageRate: 1.8, + ratingsData: [ + { score: 5, count: 1 }, + { score: 4, count: 1 }, + { score: 3, count: 2 }, + { score: 2, count: 5 }, + { score: 1, count: 11 }, + ], +}; diff --git a/src/app/(crew)/crew/detail/[id]/_components/rating-display.tsx b/src/app/(crew)/crew/detail/[id]/_components/rating-display.tsx new file mode 100644 index 00000000..14417550 --- /dev/null +++ b/src/app/(crew)/crew/detail/[id]/_components/rating-display.tsx @@ -0,0 +1,83 @@ +'use client'; + +import ProgressBar from '@/src/components/common/progress-bar'; + +interface HeartProps { + fillPercentage: number; +} + +export interface ReviewRateInfo { + totalRate: number; + averageRate: number; + ratingsData: { score: number; count: number }[]; +} + +interface RatingDisplayProps { + reviewRateInfo: ReviewRateInfo; +} + +function Heart({ fillPercentage }: HeartProps) { + const clipPathId = `heartClip-${Math.random()}`; + + return ( + + + + + + + + + + ); +} + +export default function RatingDisplay({ reviewRateInfo }: RatingDisplayProps) { + const { totalRate, averageRate, ratingsData } = reviewRateInfo; + + const renderHearts = () => { + const hearts = []; + for (let i = 0; i < 5; i += 1) { + const fillPercentage = Math.min(1, Math.max(0, averageRate - i)); + hearts.push(); + } + return hearts; + }; + + return ( +
+ {/* 왼쪽: 평균 평점 및 하트 표시 */} +
+
(총 {totalRate}개의 평가)
+
+ 평점 {averageRate.toFixed(1)} + /5 +
+
{renderHearts()}
+
+ + {/* 오른쪽: 각 점수별 프로그레스 바 */} +
+ {ratingsData.map(({ score, count }) => ( +
+
{score}점
+
+ +
+
+ {count}/ + {totalRate} +
+
+ ))} +
+
+ ); +} diff --git a/src/app/(crew)/crew/detail/[id]/_components/review-section.tsx b/src/app/(crew)/crew/detail/[id]/_components/review-section.tsx new file mode 100644 index 00000000..f432705f --- /dev/null +++ b/src/app/(crew)/crew/detail/[id]/_components/review-section.tsx @@ -0,0 +1,38 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { ReviewListData, getReviewList } from '@/src/_apis/detail/get-review-list'; +import CrewReviewList from './crew-review-list'; +import RatingDisplay from './rating-display'; + +export default function CrewReviewSection() { + const [reviewData, setReviewData] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + const limit = 5; + + useEffect(() => { + async function fetchReviewData() { + const data = await getReviewList(currentPage, limit); + setReviewData(data); + } + fetchReviewData(); + }, [currentPage]); + + const handlePageChange = (page: number) => setCurrentPage(page); + + if (!reviewData) return
Loading...
; + + return ( +
+
+ +
+ +
+ ); +} diff --git a/src/app/(crew)/crew/detail/[id]/page.tsx b/src/app/(crew)/crew/detail/[id]/page.tsx index 091e2eea..bda0aeac 100644 --- a/src/app/(crew)/crew/detail/[id]/page.tsx +++ b/src/app/(crew)/crew/detail/[id]/page.tsx @@ -1,114 +1,42 @@ -'use client'; +import { getCrewDetail } from '@/src/_apis/detail/get-crew-detail'; +import { getGatheringList } from '@/src/_apis/detail/get-gathering-list'; +import Button from '@/src/components/common/button'; +import DetailCrewCard from '@/src/components/common/crew-list/detail-crew-card'; +import GatheringCardCarousel from '@/src/components/gathering-list/gathering-card-carousel'; +import CrewReviewSection from './_components/review-section'; -import { Button } from '@mantine/core'; -import { useDisclosure } from '@mantine/hooks'; -import GatheringDetailModalContainer from '@/src/components/gathering-detail-modal/container'; -import { CreateGatheringRequestType } from '@/src/types/gathering-data'; -import CreateGatheringModalContainer from '../../_components/create-gathering-modal/container'; - -const mockData = { - id: 1, - title: '아침 타임 에너지 요가', - introduce: '공지사항입니다. 다들 이번 약속 잊지 않으셨죠? 꼭 참여 부탁드립니다~', - dateTime: '2024-10-30T00:32:12.306Z', - location: '서울시 강남구 역삼동 오피스타워 3층', - currentCount: 5, - totalCount: 10, - imageUrl: - 'https://www.dabur.com/Blogs/Doshas/Importance%20and%20Benefits%20of%20Yoga%201020x450.jpg', - isLiked: false, - isGatherCaptain: false, - isParticipant: true, - participants: [ - { - id: 1, - profileImageUrl: - 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQUMrcQB5OJ-ETzPc6wHnjxjC-36__MGw3JcA&s', - nickname: '럽윈즈올', - email: 'user@email.com', - }, - { - id: 2, - profileImageUrl: - 'https://imgcdn.stablediffusionweb.com/2024/5/13/c0541236-e690-4dff-a27e-30a0355e5ea0.jpg', - nickname: '모닝러너', - email: 'user@email.com', - }, - { - id: 3, - profileImageUrl: - 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQUMrcQB5OJ-ETzPc6wHnjxjC-36__MGw3JcA&s', - nickname: '동글동글이', - email: 'user@email.com', - }, - { - id: 4, - profileImageUrl: - 'https://imgcdn.stablediffusionweb.com/2024/5/13/c0541236-e690-4dff-a27e-30a0355e5ea0.jpg', - nickname: '해보자고', - email: 'user@email.com', - }, - { - id: 5, - profileImageUrl: - 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQUMrcQB5OJ-ETzPc6wHnjxjC-36__MGw3JcA&s', - nickname: '두잇저스트', - email: 'user@email.com', - }, - { - id: 6, - profileImageUrl: - 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQUMrcQB5OJ-ETzPc6wHnjxjC-36__MGw3JcA&s', - nickname: '럽윈즈올', - email: 'user@email.com', - }, - { - id: 7, - profileImageUrl: - 'https://imgcdn.stablediffusionweb.com/2024/5/13/c0541236-e690-4dff-a27e-30a0355e5ea0.jpg', - nickname: '모닝러너', - email: 'user@email.com', - }, - { - id: 8, - profileImageUrl: - 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQUMrcQB5OJ-ETzPc6wHnjxjC-36__MGw3JcA&s', - nickname: '동글동글이', - email: 'user@email.com', - }, - ], -}; - -// TODO : 임시로 작성됨. GatheringCardContainer 안쪽으로 이동 예정 -export default function CrewDetailPage() { - const [createModalOpened, { open: openCreateModal, close: closeCreateModal }] = - useDisclosure(false); - const [detailModalOpened, { open: openDetailModal, close: closeDetailModal }] = - useDisclosure(false); - - const initialValue: CreateGatheringRequestType = { - title: '', - introduce: '', - dateTime: '', - location: '', - totalCount: 0, - imageUrl: null, - }; +export default async function CrewDetailPage() { + const crewDetail = await getCrewDetail(); + const gatheringList = await getGatheringList(); return ( -
- - - - +
+
+
+
+ +
+
+
+
+
+

크루 약속 잡기

+ +
+
+ +
+
+
+
+
+

크루 리뷰

+ +
+
+
); } diff --git a/src/app/(crew)/detail/[id]/page.tsx b/src/app/(crew)/detail/[id]/page.tsx deleted file mode 100644 index 11718b4a..00000000 --- a/src/app/(crew)/detail/[id]/page.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import CrewCard from '@/src/components/common/crew-list/crew-card'; -import DetailCrewCard from '@/src/components/common/crew-list/detail-crew-card'; - -export default function CrewDetailPage() { - const crewData = { - id: 1, - name: '엄청긴크루이름엄청긴크루이름엄청긴크', - location: '경기도', - participantCount: 20, - capacity: 24, - thumbnail: 'https://i.pinimg.com/564x/f8/8d/c5/f88dc5b857caf6c303ae5ef9dd12e7fb.jpg', - isConfirmed: true, - gatheringCount: 3, - crewList: [ - { - id: 1, - nickname: 'User1', - imageUrl: 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', - }, - { - id: 2, - nickname: 'User2', - }, - { - id: 3, - nickname: 'User3', - imageUrl: 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', - }, - { - id: 4, - nickname: 'User4', - imageUrl: 'https://i.pinimg.com/564x/e2/e6/47/e2e64732424094c4e9e2643aaaf4389e.jpg', - }, - { - id: 5, - nickname: 'User5', - imageUrl: 'https://i.pinimg.com/564x/17/06/45/170645a5f7b8a76f04c15b226b22cf90.jpg', - }, - ], - }; - - return ( -
- - -
- ); -} diff --git a/src/app/(crew)/detail/layout.tsx b/src/app/(crew)/detail/layout.tsx deleted file mode 100644 index 996a94d1..00000000 --- a/src/app/(crew)/detail/layout.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { DatesProvider } from '@mantine/dates'; - -export default function CrewDetailLayout({ - children, // will be a page or nested layout -}: { - children: React.ReactNode; -}) { - return ( - -
{children}
-
- ); -} diff --git a/src/app/(crew)/mypage/page.tsx b/src/app/(crew)/mypage/page.tsx index a2e00a91..3d922dae 100644 --- a/src/app/(crew)/mypage/page.tsx +++ b/src/app/(crew)/mypage/page.tsx @@ -7,7 +7,7 @@ import ReviewCardList from '@/src/components/common/review-list/review-card-list import Tabs from '@/src/components/common/tab'; import ProfileCardContainer from '@/src/components/my-page/profile-card/container'; import { ReviewInformResponse } from '@/src/types/review'; -import { fetchMyReviewData } from '../api/mock-api/review'; +import { fetchMyReviewData } from '../../api/mock-api/review'; const mockData = { id: 1, diff --git a/src/app/(crew)/page.tsx b/src/app/(crew)/page.tsx index 6731414a..78e078d0 100644 --- a/src/app/(crew)/page.tsx +++ b/src/app/(crew)/page.tsx @@ -5,7 +5,7 @@ import Image from 'next/image'; import { Divider } from '@mantine/core'; import regionData from '@/src/data/region.json'; import { useInfiniteScroll } from '@/src/hooks/useInfiniteScroll'; -import { fetchCrewData } from '@/src/app/(crew)/api/mock-api/crew'; +import { fetchCrewData } from '@/src/app/api/mock-api/crew'; import CategoryContainer from '@/src/components/common/category/category-container'; import CrewCardList from '@/src/components/common/crew-list/crew-card-list'; import HeroCrew from '@/src/components/common/hero/hero-crew'; diff --git a/src/app/(crew)/api/mock-api/crew.ts b/src/app/api/mock-api/crew.ts similarity index 100% rename from src/app/(crew)/api/mock-api/crew.ts rename to src/app/api/mock-api/crew.ts diff --git a/src/app/api/mock-api/detail/route.ts b/src/app/api/mock-api/detail/route.ts new file mode 100644 index 00000000..25bb5de8 --- /dev/null +++ b/src/app/api/mock-api/detail/route.ts @@ -0,0 +1,144 @@ +import { NextResponse } from 'next/server'; +import { CrewReviewData } from '@/src/mock/review-data'; + +// FIX: 데이터 패칭 확인을 위한 목 api 추후 삭제 예정 + +const data = { + crewDetail: { + id: 1, + title: '크루 제목 제목 제목', + mainLocation: '서울특별시', + subLocation: '강남구', + participantCount: 5, + totalCount: 20, + isConfirmed: true, + imageUrl: 'https://i.pinimg.com/564x/f8/8d/c5/f88dc5b857caf6c303ae5ef9dd12e7fb.jpg', + totalGatheringCount: 5, + isCaptain: true, + isCrew: true, + CrewMembers: [ + { id: 1, nickname: '이름1' }, + { + id: 2, + nickname: '이름2', + profileImageUrl: 'https://i.pinimg.com/564x/9d/b8/86/9db886bb5475cc35a7f450831f4125bc.jpg', + }, + { + id: 3, + nickname: 'User3', + profileImageUrl: 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + { + id: 4, + nickname: 'User4', + profileImageUrl: 'https://i.pinimg.com/564x/e2/e6/47/e2e64732424094c4e9e2643aaaf4389e.jpg', + }, + { + id: 5, + nickname: 'User5', + profileImageUrl: 'https://i.pinimg.com/564x/17/06/45/170645a5f7b8a76f04c15b226b22cf90.jpg', + }, + ], + }, + gatherings: [ + { + id: 101, + title: '가나다라마가나다라마가나다라마가', + dateTime: '2024-12-15T07:30', + location: '한강공원', + currentCount: 2, + totalCount: 4, + imageUrl: + 'https://images.unsplash.com/photo-1601758260892-a62c486ace97?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', + isLiked: true, + }, + { + id: 102, + title: '등산 모임', + dateTime: '2024-11-12T09:00', + location: '서울 강남구 개포동 대모산', + currentCount: 5, + totalCount: 10, + imageUrl: + 'https://images.unsplash.com/photo-1516978101789-720eacb59e79?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NjV8fGNhdHxlbnwwfHwwfHx8Mg%3D%3D', + isLiked: false, + }, + { + id: 103, + title: '등산 모임', + dateTime: '2024-11-15T09:00', + location: '경기 과천시 중앙동 관악산', + currentCount: 10, + totalCount: 10, + imageUrl: + 'https://images.unsplash.com/photo-1516978101789-720eacb59e79?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NjV8fGNhdHxlbnwwfHwwfHx8Mg%3D%3D', + isLiked: true, + }, + { + id: 104, + title: '등산 모임', + dateTime: '2024-11-12T09:00', + location: '아차산', + currentCount: 2, + totalCount: 4, + imageUrl: + 'https://images.unsplash.com/photo-1516978101789-720eacb59e79?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NjV8fGNhdHxlbnwwfHwwfHx8Mg%3D%3D', + isLiked: false, + }, + { + id: 105, + title: '러닝', + dateTime: '2024-11-12T09:00', + location: '서울 영등포구 여의동로 330 한강사업본부 여의도안내센터', + currentCount: 0, + totalCount: 20, + imageUrl: + 'https://images.unsplash.com/photo-1516978101789-720eacb59e79?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NjV8fGNhdHxlbnwwfHwwfHx8Mg%3D%3D', + isLiked: true, + }, + ], + gatheringDetail: [ + { + id: 101, + title: '가나다라마가나다라마가나다라마가', + dateTime: '2024-12-15T07:30', + location: '한강공원', + currentCount: 8, + totalCount: 12, + imageUrl: + 'https://images.unsplash.com/photo-1601758260892-a62c486ace97?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', + isLiked: true, + introduce: '소개글 입니다~~~~~~~~', + isCaptain: true, + isParticipant: true, + participants: [ + { id: 1, nickname: 'User1' }, + { + id: 2, + nickname: 'User2', + imageUrl: 'https://i.pinimg.com/564x/9d/b8/86/9db886bb5475cc35a7f450831f4125bc.jpg', + }, + ], + }, + ], +}; + +// API 엔드포인트 핸들러 +export async function GET(req: Request) { + const { searchParams } = new URL(req.url); + const type = searchParams.get('type'); + + // type에 따라 다른 데이터 반환 + switch (type) { + case 'crewDetail': + return NextResponse.json(data.crewDetail); + case 'gatherings': + return NextResponse.json(data.gatherings); + case 'gatheringDetail': + return NextResponse.json(data.gatheringDetail); + case 'reviews': + return NextResponse.json(CrewReviewData.data); + default: + return NextResponse.json({ error: 'Invalid type' }, { status: 400 }); + } +} diff --git a/src/app/(crew)/api/mock-api/review.ts b/src/app/api/mock-api/review.ts similarity index 79% rename from src/app/(crew)/api/mock-api/review.ts rename to src/app/api/mock-api/review.ts index bd7551f6..ec1fada3 100644 --- a/src/app/(crew)/api/mock-api/review.ts +++ b/src/app/api/mock-api/review.ts @@ -1,17 +1,17 @@ -import { ReviewInformResponse } from '@/src/types/review'; +import { CrewReviewInformResponse, ReviewInformResponse } from '@/src/types/review'; import { CrewReviewData, MyReviewData } from '@/src/mock/review-data'; // NOTE : 크루 리뷰 데이터 fetch -export const fetchCrewReviewData = (page: number, limit: number) => { +export const fetchCrewReviewData = (page: number, limit: number = 6) => { const startIndex = page * limit; const endIndex = startIndex + limit; const data = CrewReviewData.data.slice(startIndex, endIndex); - return new Promise((resolve) => { + return new Promise((resolve) => { setTimeout(() => { resolve({ data, - hasNextPage: endIndex < CrewReviewData.data.length, + totalItems: CrewReviewData.data.length, }); }); }); diff --git a/src/app/(crew)/api/test-api/route.ts b/src/app/api/test-api/route.ts similarity index 88% rename from src/app/(crew)/api/test-api/route.ts rename to src/app/api/test-api/route.ts index b4c0095b..d773e8fe 100644 --- a/src/app/(crew)/api/test-api/route.ts +++ b/src/app/api/test-api/route.ts @@ -1,5 +1,7 @@ import { NextResponse } from 'next/server'; +// FIX: react-query로 임시로 작성된 코드입니다. 추후 삭제 + export async function GET() { const users = [ { diff --git a/src/components/common/button/index.tsx b/src/components/common/button/index.tsx index 4eea4851..fccb2006 100644 --- a/src/components/common/button/index.tsx +++ b/src/components/common/button/index.tsx @@ -1,4 +1,4 @@ -import { ReactNode } from 'react'; +import { MouseEventHandler, ReactNode } from 'react'; /** * Button 컴포넌트 @@ -12,8 +12,8 @@ import { ReactNode } from 'react'; export interface ButtonProps { children: ReactNode; className?: string; + onClick?: MouseEventHandler; disabled?: boolean; - onClick?: () => void; onMouseEnter?: () => void; type?: 'button' | 'submit' | 'reset'; } diff --git a/src/components/common/crew-list/crew-card-list.stories.tsx b/src/components/common/crew-list/crew-card-list.stories.tsx index 1807e642..0707c3b6 100644 --- a/src/components/common/crew-list/crew-card-list.stories.tsx +++ b/src/components/common/crew-list/crew-card-list.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react'; import { useInfiniteScroll } from '@/src/hooks/useInfiniteScroll'; -import { fetchCrewData } from '@/src/app/(crew)/api/mock-api/crew'; +import { fetchCrewData } from '@/src/app/api/mock-api/crew'; import { CrewCardInformResponse } from '@/src/types/crew-card'; import ClientProvider from '../../client-provider'; import CrewCardList from './crew-card-list'; diff --git a/src/components/common/crew-list/crew-card-list.tsx b/src/components/common/crew-list/crew-card-list.tsx index e15d5e27..b8a7dd3b 100644 --- a/src/components/common/crew-list/crew-card-list.tsx +++ b/src/components/common/crew-list/crew-card-list.tsx @@ -1,4 +1,5 @@ import React, { forwardRef } from 'react'; +import { Loader } from '@mantine/core'; import { InfiniteData } from '@tanstack/react-query'; import { CrewCardInformResponse } from '@/src/types/crew-card'; import CrewCard from './crew-card'; @@ -6,36 +7,52 @@ import CrewCard from './crew-card'; export interface CrewCardListProps { data: InfiniteData | undefined; isFetchingNextPage: boolean; + inWhere?: 'my-crew'; } function CrewCardList( - { data, isFetchingNextPage }: CrewCardListProps, + { data, isFetchingNextPage, inWhere }: CrewCardListProps, ref: React.Ref, ) { const crewDataList = data?.pages.flatMap((page) => page.data) ?? []; + const gridColsStyle = inWhere === 'my-crew' ? '' : 'lg:grid-cols-2'; - if (!crewDataList) return

loading...

; + if (!crewDataList) + return ( +
+ +
+ ); return ( -
-
    + <> +
      {crewDataList.map((inform) => (
    • ))}
    - {isFetchingNextPage ?

    loading...

    :
    } -
    + {isFetchingNextPage ? ( +
    + +
    + ) : ( +
    + )} + ); } diff --git a/src/components/common/crew-list/crew-card.tsx b/src/components/common/crew-list/crew-card.tsx index f0bbbbe6..1d9f1b7f 100644 --- a/src/components/common/crew-list/crew-card.tsx +++ b/src/components/common/crew-list/crew-card.tsx @@ -4,6 +4,7 @@ import { useState } from 'react'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; import ProgressBar from '@/src/components/common/progress-bar/index'; +import { CrewMemberList } from '@/src/types/crew-card'; import Check from '@/public/assets/icons/ic-check.svg'; import UserIco from '@/public/assets/icons/ic-user.svg'; @@ -11,25 +12,31 @@ interface CrewCardProps { id: number; name: string; location: string; + detailedLocation: string; participantCount: number; capacity: number; isConfirmed: boolean; thumbnail: string; gatheringCount: number; + inWhere?: 'my-crew'; + crewMember?: CrewMemberList[]; } export default function CrewCard({ id, name, location, + detailedLocation, participantCount, capacity, isConfirmed, thumbnail, gatheringCount, + crewMember, + inWhere, }: CrewCardProps) { const [prefetched, setPrefetched] = useState(new Set()); - const CREWPAGE = `/detail/${id}`; + const CREWPAGE = `/crew/detail/${id}`; const router = useRouter(); const handleCardClick = () => { @@ -60,7 +67,9 @@ export default function CrewCard({ {name} - | {location} + + | {location} {detailedLocation} +
    {`현재 ${gatheringCount}개의 약속이 개설되어 있습니다.`} diff --git a/src/components/common/crew-list/detail-crew-card.stories.tsx b/src/components/common/crew-list/detail-crew-card.stories.tsx index 86c58075..6454507e 100644 --- a/src/components/common/crew-list/detail-crew-card.stories.tsx +++ b/src/components/common/crew-list/detail-crew-card.stories.tsx @@ -14,20 +14,21 @@ const meta: Meta = { tags: ['autodocs'], args: { id: 0, - name: '같이 물장구칠사람', - location: '충청', - thumbnail: 'https://i.pinimg.com/564x/f8/8d/c5/f88dc5b857caf6c303ae5ef9dd12e7fb.jpg', - gatheringCount: 5, // 기본 값 추가 - crewList: [ + title: '같이 물장구칠사람', + mainLocation: '대전광역시', + subLocation: '유성구', + imageUrl: 'https://i.pinimg.com/564x/f8/8d/c5/f88dc5b857caf6c303ae5ef9dd12e7fb.jpg', + totalGatheringCount: 5, // 기본 값 추가 + CrewMembers: [ { id: 1, nickname: 'John', - imageUrl: 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + profileImageUrl: 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', }, { id: 2, nickname: 'Jane', - imageUrl: 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + profileImageUrl: 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', }, ], // 기본 프로필 리스트 추가 }, @@ -38,7 +39,7 @@ type Story = StoryObj; export const Default: Story = { args: { - capacity: 20, + totalCount: 20, participantCount: 10, isConfirmed: true, }, @@ -46,7 +47,7 @@ export const Default: Story = { export const NotConfirmed: Story = { args: { - capacity: 10, + totalCount: 10, participantCount: 1, isConfirmed: false, }, @@ -54,7 +55,7 @@ export const NotConfirmed: Story = { export const Fulled: Story = { args: { - capacity: 5, + totalCount: 5, participantCount: 5, isConfirmed: true, }, diff --git a/src/components/common/crew-list/detail-crew-card.tsx b/src/components/common/crew-list/detail-crew-card.tsx index b7309282..7a8bf2cc 100644 --- a/src/components/common/crew-list/detail-crew-card.tsx +++ b/src/components/common/crew-list/detail-crew-card.tsx @@ -2,65 +2,107 @@ import Image from 'next/image'; import { Menu } from '@mantine/core'; +import { useDisclosure } from '@mantine/hooks'; import Profiles from '@/src/components/common/crew-list/profiles'; +import ConfirmCancelModal from '@/src/components/common/modal/confirm-cancel-modal'; import ProgressBar from '@/src/components/common/progress-bar/index'; +import { CrewMemberList } from '@/src/types/crew-card'; import Check from '@/public/assets/icons/ic-check.svg'; import KebabIcon from '@/public/assets/icons/kebab-btn.svg'; interface DetailCrewCardProps { id: number; - name: string; - location: string; + title: string; + mainLocation: string; + subLocation: string; participantCount: number; - capacity: number; + totalCount: number; isConfirmed: boolean; - thumbnail: string; - gatheringCount: number; - crewList: CrewInfoType[]; -} - -interface CrewInfoType { - id: number; - nickname: string; - imageUrl?: string | null; + imageUrl: string; + totalGatheringCount: number; + CrewMembers: CrewMemberList[]; + isCaptain: boolean; + isCrew: boolean; } export default function DetailCrewCard({ id, - name, - location, + title, + mainLocation, + subLocation, participantCount, - capacity, + totalCount, isConfirmed, - thumbnail, - gatheringCount, - crewList, + imageUrl, + totalGatheringCount, + CrewMembers, + isCaptain, + isCrew, }: DetailCrewCardProps) { + const [confirmCancelOpened, { open: openConfirmCancel, close: closeConfirmCancel }] = + useDisclosure(); + const [leaveCrewModalOpened, { open: openLeaveCrewModal, close: closeLeaveCrewModal }] = + useDisclosure(); + const handleDelete = () => { - // 삭제 로직 + openConfirmCancel(); + }; + + const handleLeaveCrew = () => { + openLeaveCrewModal(); + }; + + const handleConfirmDelete = () => { + // TODO : 삭제 로직 + closeConfirmCancel(); + }; + + const handleConfirmLeaveCrew = () => { + // TODO : 탈퇴 로직 + closeLeaveCrewModal(); }; return ( -
    - - -
    - 더보기 -
    -
    - - - 크루 수정하기 - - - 삭제하기 - - -
    +
    + {/* eslint-disable-next-line no-nested-ternary */} + {isCaptain ? ( + + +
    + 더보기 +
    +
    + + + 크루 수정하기 + + + 크루 삭제하기 + + +
    + ) : isCrew ? ( + + +
    + 더보기 +
    +
    + + + 크루 탈퇴하기 + + +
    + ) : null} {/* 썸네일 */}
    - {name} + {title}
    @@ -68,14 +110,16 @@ export default function DetailCrewCard({
    - {name} + {title} | - {location} + + {mainLocation} {subLocation} +
    - {`현재 ${gatheringCount}개의 약속이 개설되어 있습니다.`} + {`현재 ${totalGatheringCount}개의 약속이 개설되어 있습니다.`}
    {/* 세부내용 */} @@ -86,7 +130,7 @@ export default function DetailCrewCard({ 모집 정원 {participantCount}명
    - +
    @@ -97,14 +141,32 @@ export default function DetailCrewCard({ )}
    - +
    최소인원 2명 - 최대인원 {capacity}명 + 최대인원 {totalCount}명
+ + {/* 삭제 확인 모달 */} + +

정말로 크루를 삭제하시겠습니까?

+
+ + {/* 탈퇴 확인 모달 */} + +

정말로 크루에서 탈퇴하시겠습니까?

+
); } diff --git a/src/components/common/crew-list/profiles.tsx b/src/components/common/crew-list/profiles.tsx index 44ebd384..c697cbbe 100644 --- a/src/components/common/crew-list/profiles.tsx +++ b/src/components/common/crew-list/profiles.tsx @@ -1,30 +1,33 @@ import { Profile } from '@/src/components/common/profile'; - -interface CrewInfoType { - id: number; - nickname: string; - imageUrl?: string | null; -} +import { CrewMemberList } from '@/src/types/crew-card'; interface ProfilesProps { - profiles: CrewInfoType[]; + profiles: CrewMemberList[]; + size?: 'small' | 'medium'; } -export default function Profiles({ profiles }: ProfilesProps) { - const shows = 4; // 최대 표시할 프로필 수를 고정값으로 설정 +export default function Profiles({ size = 'small', profiles }: ProfilesProps) { + const shows = 4; const visibleProfiles = profiles.slice(0, shows); const extraCount = profiles.length - shows; + const sizeMap = { + small: 'w-6 h-6', + medium: 'w-7.5 h-7.5', + }; + return ( -
    +
      {visibleProfiles.map((profile) => ( -
    • - +
    • +
    • ))} {extraCount > 0 && ( -
    • - +
    • + +{extraCount}
    • diff --git a/src/components/common/gathering-card/container.tsx b/src/components/common/gathering-card/container.tsx index dabde7bf..5141d288 100644 --- a/src/components/common/gathering-card/container.tsx +++ b/src/components/common/gathering-card/container.tsx @@ -1,6 +1,9 @@ 'use client'; import { useState } from 'react'; +import { useDisclosure } from '@mantine/hooks'; +import { GatheringDetailType } from '@/src/types/gathering-data'; +import GatheringDetailModalContainer from '../../gathering-detail-modal/container'; import GatheringCardPresenter from './presenter'; interface GatheringCardContainerProps { @@ -16,6 +19,41 @@ interface GatheringCardContainerProps { className?: string; } +const dummyGatheringData: GatheringDetailType = { + id: 1, + title: '모임이름', + dateTime: '2024-12-31T10:00:00', + location: '경기 과천시 중앙동 관악산', + currentCount: 2, + totalCount: 10, + imageUrl: + 'https://images.unsplash.com/photo-1601758260892-a62c486ace97?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', + isLiked: false, + introduce: '이 모임은 예시 모임입니다. 함께 즐거운 시간을 보낼 수 있는 모임이에요!', + isGatherCaptain: true, + isParticipant: true, + participants: [ + { + id: 1, + profileImageUrl: 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + nickname: '가나다', + email: 'hong@example.com', + }, + { + id: 2, + profileImageUrl: 'https://i.pinimg.com/564x/9d/b8/86/9db886bb5475cc35a7f450831f4125bc.jpg', + nickname: '라마바', + email: 'kim@example.com', + }, + { + id: 3, + profileImageUrl: '', + nickname: '가나다', + email: 'lee@example.com', + }, + ], +}; + export default function GatheringCard({ id, title, @@ -27,6 +65,8 @@ export default function GatheringCard({ isLiked: initialIsLiked, className, }: GatheringCardContainerProps) { + const [opened, { open, close }] = useDisclosure(false); + // 날짜 비교 const gatheringDate = new Date(dateTime); const today = new Date(); @@ -47,23 +87,32 @@ export default function GatheringCard({ setIsLiked((prev) => !prev); }; + const openModal = () => { + // TODO: 모임 상세보기 API 연결 + open(); + }; + return ( - alert('카드클릭!')} - className={className} - /> + <> + + {opened && ( + + )} + ); } diff --git a/src/components/common/gathering-card/gathering-card.stories.tsx b/src/components/common/gathering-card/gathering-card.stories.tsx index b38d3a7a..82f23620 100644 --- a/src/components/common/gathering-card/gathering-card.stories.tsx +++ b/src/components/common/gathering-card/gathering-card.stories.tsx @@ -5,7 +5,11 @@ const meta: Meta = { title: 'Components/GatheringCard', component: GatheringCard, parameters: { - layout: 'centered', + layout: 'fullscreen', + backgrounds: { + default: 'light-gray', + values: [{ name: 'light-gray', value: '#F9FAFB' }], + }, }, tags: ['autodocs'], argTypes: { diff --git a/src/components/common/gathering-card/presenter.tsx b/src/components/common/gathering-card/presenter.tsx index 4ab5f6d0..629af398 100644 --- a/src/components/common/gathering-card/presenter.tsx +++ b/src/components/common/gathering-card/presenter.tsx @@ -1,10 +1,15 @@ +import { MouseEvent } from 'react'; import Image from 'next/image'; import { Badge } from '@mantine/core'; +import { cn } from '@/src/hooks/cn'; import { formatDate } from '@/src/utils/format-date'; +import Button from '@/src/components/common/button'; import LikeBtn from '@/src/components/common/button/like-btn'; import IcoPerson from '@/public/assets/icons/person.svg'; import IcoTimer from '@/public/assets/icons/timer.svg'; +// TODO: 스케레톤UI 적용(처음 로딩시 카드가 늘어나는 현상) + export interface GatheringCardPresenterProps { id: number; imageUrl: string; @@ -40,6 +45,11 @@ export default function GatheringCardPresenter({ }: GatheringCardPresenterProps) { const { date, time } = formatDate(dateTime); + const handleClick = (e: MouseEvent) => { + e.stopPropagation(); + onLikeToggle(); + }; + return (
      timer icon -

      {deadlineMessage}

      +

      {deadlineMessage}

      )} -
      -

      - {title} -

      -
      - | -

      {location}

      -
      -
      - - {date} - - - {time} - +
      +
      +

      + {title} +

      +
      + | +

      {location}

      +
      +
      + + {date} + + + {time} + +
      -

      +

      person icon 참여인원 {currentCount}/{totalCount}

      -
      +
      @@ -114,18 +130,9 @@ export default function GatheringCardPresenter({

      마감된 모임입니다

      다음 기회에 만나요 🙏

      - {/* '모임 보내주기' 버튼 */} - +
      )} diff --git a/src/components/common/header/header.stories.tsx b/src/components/common/header/header.stories.tsx index 67ff6a06..2dda18fd 100644 --- a/src/components/common/header/header.stories.tsx +++ b/src/components/common/header/header.stories.tsx @@ -1,4 +1,3 @@ -import { action } from '@storybook/addon-actions'; import type { Meta, StoryFn } from '@storybook/react'; import { useAuthStore } from '@/src/store/use-auth-store'; import Header from '@/src/components/common/header/container'; diff --git a/src/components/common/header/presenter.tsx b/src/components/common/header/presenter.tsx index a3cfd5e9..26ce2d80 100644 --- a/src/components/common/header/presenter.tsx +++ b/src/components/common/header/presenter.tsx @@ -21,8 +21,8 @@ export default function HeaderPresenter({ const pathname = usePathname(); return (
      -
      -
      +
      +
      crew logo crew logo diff --git a/src/components/common/profile/index.tsx b/src/components/common/profile/index.tsx index ba4240bc..7ff64c73 100644 --- a/src/components/common/profile/index.tsx +++ b/src/components/common/profile/index.tsx @@ -24,7 +24,7 @@ interface ProfileProps { } export function Profile({ - size = 'large', + size = 'full', imageUrl, editable = false, onClick, diff --git a/src/components/common/progress-bar/progress-bar.stories.tsx b/src/components/common/progress-bar/progress-bar.stories.tsx index 74df86d8..66a9a7df 100644 --- a/src/components/common/progress-bar/progress-bar.stories.tsx +++ b/src/components/common/progress-bar/progress-bar.stories.tsx @@ -1,4 +1,3 @@ -// ProgressBar.stories.tsx import React from 'react'; import type { Meta, StoryFn } from '@storybook/react'; import ProgressBar, { ProgressBarProps } from './index'; diff --git a/src/components/common/review-heart/hearts.tsx b/src/components/common/review-heart/hearts.tsx index a1db5514..f8ff8007 100644 --- a/src/components/common/review-heart/hearts.tsx +++ b/src/components/common/review-heart/hearts.tsx @@ -1,11 +1,12 @@ import Heart from '@/public/assets/icons/ic-heart'; export default function ReviewHearts({ score }: { score: number }) { - const filledHearts = Math.ceil((score / 100) * 5); + const filledHearts = Math.round(score); return (
      {Array.from({ length: 5 }).map((_, index) => ( - + // eslint-disable-next-line react/no-array-index-key + ))}
      ); diff --git a/src/components/common/review-list/review-card-list.stories.tsx b/src/components/common/review-list/review-card-list.stories.tsx index 7298927f..4053651f 100644 --- a/src/components/common/review-list/review-card-list.stories.tsx +++ b/src/components/common/review-list/review-card-list.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react'; import { useInfiniteScroll } from '@/src/hooks/useInfiniteScroll'; -import { fetchCrewReviewData, fetchMyReviewData } from '@/src/app/(crew)/api/mock-api/review'; +import { fetchMyReviewData } from '@/src/app/api/mock-api/review'; import { ReviewInformResponse } from '@/src/types/review'; import ClientProvider from '../../client-provider'; import ReviewCardList from './review-card-list'; @@ -27,23 +27,23 @@ const meta: Meta = { export default meta; type Story = StoryObj; -function RenderCrewReviewCardList({ isMine = false, clickable = false }) { - const { data, ref, isFetchingNextPage } = useInfiniteScroll({ - queryKey: ['review'], - queryFn: ({ pageParam = 0 }) => fetchCrewReviewData(pageParam, 3), - getNextPageParam: (lastPage, allPages) => - lastPage.hasNextPage ? allPages.length + 1 : undefined, - }); - return ( - - ); -} +// function RenderCrewReviewCardList({ isMine = false, clickable = false }) { +// const { data, ref, isFetchingNextPage } = useInfiniteScroll({ +// queryKey: ['review'], +// queryFn: ({ pageParam = 0 }) => fetchCrewReviewData(pageParam, 3), +// getNextPageParam: (lastPage, allPages) => +// lastPage.hasNextPage ? allPages.length + 1 : undefined, +// }); +// return ( +// +// ); +// } function RenderMyReviewCardList({ isMine = true, clickable = false }) { const { data, ref, isFetchingNextPage } = useInfiniteScroll({ @@ -64,10 +64,10 @@ function RenderMyReviewCardList({ isMine = true, clickable = false }) { ); } -export const CrewReviewCardList: Story = { - render: () => , - args: {}, -}; +// export const CrewReviewCardList: Story = { +// render: () => , +// args: {}, +// }; export const MyReviewCardList: Story = { render: () => , diff --git a/src/components/common/review-list/review-card.tsx b/src/components/common/review-list/review-card.tsx index 31f0d01d..cbbae0b8 100644 --- a/src/components/common/review-list/review-card.tsx +++ b/src/components/common/review-list/review-card.tsx @@ -1,9 +1,8 @@ 'use client'; import React, { useState } from 'react'; -import Image from 'next/image'; import { useRouter } from 'next/navigation'; -import { Button, Menu } from '@mantine/core'; +import { Button } from '@mantine/core'; import { formatDateWithYear } from '@/src/utils/format-date'; import { ReviewerType } from '@/src/types/review'; import { Profile } from '../profile'; @@ -22,7 +21,6 @@ interface ReviewCardProps { crewId: number; clickable?: boolean; isMine?: boolean; - crewName?: string; gatheringLocation?: string; gatheringName?: string; @@ -78,7 +76,7 @@ export default function ReviewCard({ role="presentation" onClick={handleClick} onMouseEnter={handlePrefetch} - className={`flex h-full items-end gap-[15px] ${!isMine ? 'border-b-[2px] border-b-[#e6e6e6] py-4' : 'rounded-[12px] p-6 shadow-bg'} bg-white lg:gap-[40px] ${clickable ? 'cursor-pointer' : 'cursor-default'}`} + className={`flex h-full items-end gap-[15px] ${!isMine ? 'border-b-[2px] border-b-[#F3F4F6] py-4' : 'rounded-[12px] p-6 shadow-bg'} bg-white lg:gap-[40px] ${clickable ? 'cursor-pointer' : 'cursor-default'}`} >
      {isMine && ( @@ -91,7 +89,7 @@ export default function ReviewCard({
      {!isMine && ( <> - {reviewer && } + {reviewer && } {reviewer?.nickname} diff --git a/src/components/gathering-detail-modal/presenter.tsx b/src/components/gathering-detail-modal/presenter.tsx index c74ad5b5..5f5cd7ea 100644 --- a/src/components/gathering-detail-modal/presenter.tsx +++ b/src/components/gathering-detail-modal/presenter.tsx @@ -3,6 +3,7 @@ import { Modal, ScrollArea } from '@mantine/core'; import { formatDate } from '@/src/utils/format-date'; import isToday from '@/src/utils/is-today'; import Button from '@/src/components/common/button'; +import { Profile } from '@/src/components/common/profile'; import { GatheringDetailType } from '@/src/types/gathering-data'; import IcoClock from '@/public/assets/icons/ic-clock.svg'; import IcoUser from '@/public/assets/icons/ic-user.svg'; @@ -100,12 +101,7 @@ export default function GatheringDetailModalPresenter({ {data?.participants.map((participant) => (
    • - 유저 이미지 +
      {participant?.nickname}
    • diff --git a/src/components/gathering-list/gathering-card-carousel.tsx b/src/components/gathering-list/gathering-card-carousel.tsx index b2b7cd96..133f7ce9 100644 --- a/src/components/gathering-list/gathering-card-carousel.tsx +++ b/src/components/gathering-list/gathering-card-carousel.tsx @@ -2,71 +2,130 @@ import { useEffect, useState } from 'react'; import Image from 'next/image'; -import { Carousel } from '@mantine/carousel'; -import { useMediaQuery } from '@mantine/hooks'; import GatheringCard from '@/src/components/common/gathering-card/container'; -import { gatheringData } from '@/src/mock/gathering-data'; import IcoLeft from '@/public/assets/icons/ic-left.svg'; import IcoRight from '@/public/assets/icons/ic-right.svg'; -/* eslint-disable no-nested-ternary */ -/* eslint-disable react/no-array-index-key */ +interface GatheringDataType { + id: number; + title: string; + dateTime: string; + location: string; + currentCount: number; + totalCount: number; + imageUrl: string; + isLiked: boolean; +} -export default function GatheringCardCarousel() { - // 반응형 구간 체크 - const isMobile = useMediaQuery('(max-width: 744px)'); - const isTablet = useMediaQuery('(min-width: 745px) and (max-width: 1200px)'); +interface GatheringCardCarouselProps { + gatheringData: GatheringDataType[]; +} - // 상태 변수 초기화 - const [slideSize, setSlideSize] = useState('100%'); - const [slidesToScroll, setSlidesToScroll] = useState(1); - const [maxWidth, setMaxWidth] = useState(340); +export default function CustomGatheringCardCarousel({ gatheringData }: GatheringCardCarouselProps) { + const [currentIndex, setCurrentIndex] = useState(0); + const [slidesToShow, setSlidesToShow] = useState(1); + const [slideSize, setSlideSize] = useState('w-full'); - // 화면 크기 변화에 따른 캐러셀 설정 업데이트 useEffect(() => { - if (isMobile) { - setSlideSize('100%'); - setSlidesToScroll(1); - setMaxWidth(340); - } else if (isTablet) { - setSlideSize('50%'); - setSlidesToScroll(2); - setMaxWidth(360 * 2 + 16); - } else { - setSlideSize('33.33%'); - setSlidesToScroll(3); - setMaxWidth(380 * 3 + 32); - } - }, [isMobile, isTablet]); + const handleResize = () => { + const screenWidth = window.innerWidth; + let newSlidesToShow = 1; + let newSlideSize = 'w-full'; + + if (screenWidth <= 744) { + newSlidesToShow = 1; + newSlideSize = 'w-full'; + } else if (screenWidth <= 1200) { + newSlidesToShow = 2; + newSlideSize = 'w-[calc(50%-8px)]'; + } else { + newSlidesToShow = 3; + newSlideSize = 'w-[calc(33.33%-12px)]'; + } + + setSlidesToShow(newSlidesToShow); + setSlideSize(newSlideSize); + setCurrentIndex(0); + }; + + handleResize(); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [gatheringData.length]); - const cardClassName = isMobile ? 'w-[340px]' : isTablet ? 'w-[360px]' : 'w-[380px]'; + const totalSlides = gatheringData.length; + + const handlePrev = () => { + setCurrentIndex((prevIndex) => (prevIndex >= slidesToShow ? prevIndex - slidesToShow : 0)); + }; + + const handleNext = () => { + setCurrentIndex((prevIndex) => + prevIndex + slidesToShow < totalSlides ? prevIndex + slidesToShow : prevIndex, + ); + }; return ( -
      - } - previousControlIcon={left icon} - withIndicators - height="auto" - slideGap="16px" - slideSize={slideSize} - slidesToScroll={slidesToScroll} - loop={false} - align="start" - dragFree - classNames={{ - indicator: 'gathering-carousel-indicator', - control: 'gathering-carousel-control', - }} - controlSize={32} - withControls - > - {gatheringData.data.map((card, index) => ( - - - +
      +
      +
      1 ? 'gap-4' : 'gap-0' + }`} + style={{ + transform: `translateX(calc(-${(100 / slidesToShow) * currentIndex}% - ${ + currentIndex * (slidesToShow > 1 ? 16 / slidesToShow : 0) + }px))`, + width: `${(100 / slidesToShow) * totalSlides}%`, + }} + > + {gatheringData.map((card) => ( +
      + +
      + ))} +
      +
      + + {/* Left and Right Control Buttons */} + {currentIndex > 0 && ( + + )} + {currentIndex + slidesToShow < totalSlides && ( + + )} + + {/* Custom Indicators */} +
      + {Array.from({ length: Math.ceil(totalSlides / slidesToShow) }).map((_, i) => ( + ))} - +
      ); } diff --git a/src/components/gathering-list/gathering-carousel.stories.tsx b/src/components/gathering-list/gathering-carousel.stories.tsx index 78e97b55..c3ae6c41 100644 --- a/src/components/gathering-list/gathering-carousel.stories.tsx +++ b/src/components/gathering-list/gathering-carousel.stories.tsx @@ -1,6 +1,43 @@ import type { Meta, StoryObj } from '@storybook/react'; import GatheringCardCarousel from './gathering-card-carousel'; +// 더미 데이터 정의 +const mockGatheringData = [ + { + id: 1, + title: '가나다라마바사 요가 모임', + dateTime: '2024-12-15T07:30', + location: '서울, 한강공원', + currentCount: 8, + totalCount: 12, + imageUrl: + 'https://images.unsplash.com/photo-1601758260892-a62c486ace97?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', + isLiked: true, + }, + { + id: 2, + title: '등산 모임', + dateTime: '2024-11-12T09:00', + location: '서울 강남구 개포동 대모산', + currentCount: 5, + totalCount: 10, + imageUrl: + 'https://images.unsplash.com/photo-1516978101789-720eacb59e79?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NjV8fGNhdHxlbnwwfHwwfHx8Mg%3D%3D', + isLiked: false, + }, + { + id: 3, + title: '러닝 모임', + dateTime: '2024-11-15T09:00', + location: '서울 영등포구 여의동로 330', + currentCount: 10, + totalCount: 20, + imageUrl: + 'https://images.unsplash.com/photo-1516978101789-720eacb59e79?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NjV8fGNhdHxlbnwwfHwwfHx8Mg%3D%3D', + isLiked: true, + }, +]; + const meta: Meta = { title: 'Components/GatheringCardCarousel', component: GatheringCardCarousel, @@ -13,4 +50,9 @@ const meta: Meta = { export default meta; type Story = StoryObj; -export const Default: Story = {}; +// 스토리북에서 gatheringData prop을 전달 +export const Default: Story = { + args: { + gatheringData: mockGatheringData, + }, +}; diff --git a/src/components/gathering-list/gathering-list.tsx b/src/components/gathering-list/gathering-list.tsx index 5c6eb7ca..b07df59d 100644 --- a/src/components/gathering-list/gathering-list.tsx +++ b/src/components/gathering-list/gathering-list.tsx @@ -70,8 +70,26 @@ export default function GatheringList({ gatheringData }: GatheringListProps) { total={Math.ceil(pagination.totalCount / limit)} value={page} onChange={setPage} - color="indigo" - radius="md" + styles={{ + control: { + border: 'none', + backgroundColor: 'transparent', + '&[data-active]': { + backgroundColor: 'transparent', + fontWeight: 'var(--pagination-active-font-weight)', + color: 'var(--pagination-active-color)', + boxShadow: 'none', + }, + '&:hover': { + backgroundColor: 'transparent', + }, + }, + root: { + '--pagination-active-color': '#3388FF', + '--pagination-active-font-weight': '700', + }, + }} + size="sm" />
      diff --git a/src/hooks/cn.ts b/src/hooks/cn.ts new file mode 100644 index 00000000..ad499034 --- /dev/null +++ b/src/hooks/cn.ts @@ -0,0 +1,5 @@ +type ClassValue = string | undefined | null | false; + +export function cn(...classes: ClassValue[]): string { + return classes.filter(Boolean).join(' '); +} diff --git a/src/mock/crew-data.ts b/src/mock/crew-data.ts index 46424627..321b6b3c 100644 --- a/src/mock/crew-data.ts +++ b/src/mock/crew-data.ts @@ -9,7 +9,7 @@ export const crewData: { data: CrewCardInform[] } = { name: '엄청긴크루이름엄청긴크루이름엄청긴크루이름엄청긴크루이름', location: '경기도', detailedLocation: '어디구 어디로', - participantCount: 20, + participantCount: 5, capacity: 24, images: [ { imagePath: 'https://i.pinimg.com/564x/f8/8d/c5/f88dc5b857caf6c303ae5ef9dd12e7fb.jpg' }, @@ -19,6 +19,38 @@ export const crewData: { data: CrewCardInform[] } = { updatedDate: new Date('2024-11-05'), isConfirmed: true, gatheringCount: 3, + crewMember: [ + { + id: 1, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + { + id: 2, + nickname: 'User2', + profileImageUrl: + 'https://i.pinimg.com/564x/9d/b8/86/9db886bb5475cc35a7f450831f4125bc.jpg', + }, + { + id: 3, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + { + id: 4, + nickname: 'User4', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/e6/47/e2e64732424094c4e9e2643aaaf4389e.jpg', + }, + { + id: 5, + nickname: 'User5', + profileImageUrl: + 'https://i.pinimg.com/564x/17/06/45/170645a5f7b8a76f04c15b226b22cf90.jpg', + }, + ], }, { crewId: 2, @@ -37,6 +69,68 @@ export const crewData: { data: CrewCardInform[] } = { updatedDate: new Date('2024-11-05'), isConfirmed: true, gatheringCount: 3, + crewMember: [ + { + id: 1, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + { + id: 2, + nickname: 'User2', + profileImageUrl: + 'https://i.pinimg.com/564x/9d/b8/86/9db886bb5475cc35a7f450831f4125bc.jpg', + }, + { + id: 3, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + { + id: 4, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + { + id: 5, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + { + id: 6, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + { + id: 7, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + { + id: 8, + nickname: 'User2', + profileImageUrl: + 'https://i.pinimg.com/564x/9d/b8/86/9db886bb5475cc35a7f450831f4125bc.jpg', + }, + { + id: 9, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + { + id: 10, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + ], }, { crewId: 3, @@ -55,6 +149,14 @@ export const crewData: { data: CrewCardInform[] } = { updatedDate: new Date('2024-11-05'), isConfirmed: false, gatheringCount: 3, + crewMember: [ + { + id: 1, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + ], }, { crewId: 4, @@ -73,6 +175,14 @@ export const crewData: { data: CrewCardInform[] } = { updatedDate: new Date('2024-11-05'), gatheringCount: 3, isConfirmed: false, + crewMember: [ + { + id: 1, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + ], }, { crewId: 5, @@ -91,6 +201,14 @@ export const crewData: { data: CrewCardInform[] } = { updatedDate: new Date('2024-11-05'), isConfirmed: false, gatheringCount: 5, + crewMember: [ + { + id: 1, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + ], }, { crewId: 6, @@ -109,6 +227,68 @@ export const crewData: { data: CrewCardInform[] } = { updatedDate: new Date('2024-11-05'), isConfirmed: true, gatheringCount: 5, + crewMember: [ + { + id: 1, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + { + id: 2, + nickname: 'User2', + profileImageUrl: + 'https://i.pinimg.com/564x/9d/b8/86/9db886bb5475cc35a7f450831f4125bc.jpg', + }, + { + id: 3, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + { + id: 4, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + { + id: 5, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + { + id: 6, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + { + id: 7, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + { + id: 8, + nickname: 'User2', + profileImageUrl: + 'https://i.pinimg.com/564x/9d/b8/86/9db886bb5475cc35a7f450831f4125bc.jpg', + }, + { + id: 9, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + { + id: 10, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + ], }, { crewId: 7, @@ -117,7 +297,7 @@ export const crewData: { data: CrewCardInform[] } = { name: '엄청긴크루이름엄청긴크루이름엄청긴크루이름엄청긴크루이름', location: '경기도', detailedLocation: '어디구 어디로', - participantCount: 20, + participantCount: 5, capacity: 24, images: [ { imagePath: 'https://i.pinimg.com/564x/f8/8d/c5/f88dc5b857caf6c303ae5ef9dd12e7fb.jpg' }, @@ -127,6 +307,38 @@ export const crewData: { data: CrewCardInform[] } = { updatedDate: new Date('2024-11-05'), isConfirmed: true, gatheringCount: 5, + crewMember: [ + { + id: 1, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + { + id: 2, + nickname: 'User2', + profileImageUrl: + 'https://i.pinimg.com/564x/9d/b8/86/9db886bb5475cc35a7f450831f4125bc.jpg', + }, + { + id: 3, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + { + id: 4, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + { + id: 5, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + ], }, { crewId: 8, @@ -145,6 +357,68 @@ export const crewData: { data: CrewCardInform[] } = { updatedDate: new Date('2024-11-05'), isConfirmed: true, gatheringCount: 5, + crewMember: [ + { + id: 1, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + { + id: 2, + nickname: 'User2', + profileImageUrl: + 'https://i.pinimg.com/564x/9d/b8/86/9db886bb5475cc35a7f450831f4125bc.jpg', + }, + { + id: 3, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + { + id: 4, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + { + id: 5, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + { + id: 6, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + { + id: 7, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + { + id: 8, + nickname: 'User2', + profileImageUrl: + 'https://i.pinimg.com/564x/9d/b8/86/9db886bb5475cc35a7f450831f4125bc.jpg', + }, + { + id: 9, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + { + id: 10, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + ], }, { crewId: 9, @@ -153,7 +427,7 @@ export const crewData: { data: CrewCardInform[] } = { name: '같이 달릴사람 구함', location: '부산', detailedLocation: '어디구 어디로', - participantCount: 1, + participantCount: 3, capacity: 5, images: [ { imagePath: 'https://i.pinimg.com/564x/f8/8d/c5/f88dc5b857caf6c303ae5ef9dd12e7fb.jpg' }, @@ -163,6 +437,26 @@ export const crewData: { data: CrewCardInform[] } = { updatedDate: new Date('2024-11-05'), isConfirmed: false, gatheringCount: 5, + crewMember: [ + { + id: 1, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + { + id: 2, + nickname: 'User2', + profileImageUrl: + 'https://i.pinimg.com/564x/9d/b8/86/9db886bb5475cc35a7f450831f4125bc.jpg', + }, + { + id: 3, + nickname: 'User2', + profileImageUrl: + 'https://i.pinimg.com/564x/9d/b8/86/9db886bb5475cc35a7f450831f4125bc.jpg', + }, + ], }, { crewId: 10, @@ -181,6 +475,14 @@ export const crewData: { data: CrewCardInform[] } = { updatedDate: new Date('2024-11-05'), isConfirmed: false, gatheringCount: 5, + crewMember: [ + { + id: 1, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + ], }, { crewId: 11, @@ -199,6 +501,14 @@ export const crewData: { data: CrewCardInform[] } = { updatedDate: new Date('2024-11-05'), isConfirmed: false, gatheringCount: 5, + crewMember: [ + { + id: 1, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + ], }, { crewId: 12, @@ -217,6 +527,68 @@ export const crewData: { data: CrewCardInform[] } = { updatedDate: new Date('2024-11-05'), isConfirmed: true, gatheringCount: 5, + crewMember: [ + { + id: 1, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + { + id: 2, + nickname: 'User2', + profileImageUrl: + 'https://i.pinimg.com/564x/9d/b8/86/9db886bb5475cc35a7f450831f4125bc.jpg', + }, + { + id: 3, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + { + id: 4, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + { + id: 5, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + { + id: 6, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + { + id: 7, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + { + id: 8, + nickname: 'User2', + profileImageUrl: + 'https://i.pinimg.com/564x/9d/b8/86/9db886bb5475cc35a7f450831f4125bc.jpg', + }, + { + id: 9, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + { + id: 10, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + ], }, { crewId: 13, @@ -225,7 +597,7 @@ export const crewData: { data: CrewCardInform[] } = { name: '엄청긴크루이름엄청긴크루이름엄청긴크루이름엄청긴크루이름', location: '경기도', detailedLocation: '어디구 어디로', - participantCount: 20, + participantCount: 2, capacity: 24, images: [ { imagePath: 'https://i.pinimg.com/564x/f8/8d/c5/f88dc5b857caf6c303ae5ef9dd12e7fb.jpg' }, @@ -235,6 +607,20 @@ export const crewData: { data: CrewCardInform[] } = { updatedDate: new Date('2024-11-05'), isConfirmed: true, gatheringCount: 6, + crewMember: [ + { + id: 1, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + { + id: 2, + nickname: 'User2', + profileImageUrl: + 'https://i.pinimg.com/564x/9d/b8/86/9db886bb5475cc35a7f450831f4125bc.jpg', + }, + ], }, { crewId: 14, @@ -253,6 +639,68 @@ export const crewData: { data: CrewCardInform[] } = { updatedDate: new Date('2024-11-05'), isConfirmed: true, gatheringCount: 0, + crewMember: [ + { + id: 1, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + { + id: 2, + nickname: 'User2', + profileImageUrl: + 'https://i.pinimg.com/564x/9d/b8/86/9db886bb5475cc35a7f450831f4125bc.jpg', + }, + { + id: 3, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + { + id: 4, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + { + id: 5, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + { + id: 6, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + { + id: 7, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + { + id: 8, + nickname: 'User2', + profileImageUrl: + 'https://i.pinimg.com/564x/9d/b8/86/9db886bb5475cc35a7f450831f4125bc.jpg', + }, + { + id: 9, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + { + id: 10, + nickname: 'User3', + profileImageUrl: + 'https://i.pinimg.com/564x/41/56/d8/4156d8253b6d76e5455d28b44bd1a1e0.jpg', + }, + ], }, { crewId: 15, @@ -271,6 +719,14 @@ export const crewData: { data: CrewCardInform[] } = { updatedDate: new Date('2024-11-05'), isConfirmed: false, gatheringCount: 5, + crewMember: [ + { + id: 1, + nickname: 'User1', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + }, + ], }, ], }; diff --git a/src/mock/review-data.ts b/src/mock/review-data.ts index db528600..40990d37 100644 --- a/src/mock/review-data.ts +++ b/src/mock/review-data.ts @@ -8,8 +8,9 @@ export const CrewReviewData: { data: CrewReview[] } = { { crewId: 1, id: 1, - rate: 30, - comment: '최고의 모임~', + rate: 5, + comment: + '글이 길어질 경우 글이 길어질 경우 확인 글이 길어질 경우 글이 길어질 경우 글이 길어질 경우 글이 길어질 경우 글이 길어질 경우 글이 길어질 경우 글이 길어질 경우 글이 길어질 경우 글이 길어질 경우 글이 길어질 경우 글이 길어질 경우 글이 길어질 경우 글이 길어질 경우 글이 길어질 경우 글이 길어질 경우 글이 길어질 경우 글이 길어질 경우 글이 길어질 경우 최고의 모임~', createdAt: '2024-10-30T00:30', reviewer: { id: 1, @@ -20,7 +21,7 @@ export const CrewReviewData: { data: CrewReview[] } = { { crewId: 1, id: 2, - rate: 40, + rate: 2, comment: '솔직히 좀 별로..', createdAt: '2024-10-30T00:34', reviewer: { @@ -30,9 +31,9 @@ export const CrewReviewData: { data: CrewReview[] } = { }, }, { - crewId: 2, + crewId: 1, id: 3, - rate: 90, + rate: 5, comment: '진짜 좋은 모임이었다.. 굿', createdAt: '2024-10-30T00:50', reviewer: { @@ -42,9 +43,9 @@ export const CrewReviewData: { data: CrewReview[] } = { }, }, { - crewId: 3, + crewId: 1, id: 4, - rate: 80, + rate: 5, comment: '모임계를 뒤집어 엎으셨다', createdAt: '2024-10-30T00:52', reviewer: { @@ -54,9 +55,9 @@ export const CrewReviewData: { data: CrewReview[] } = { }, }, { - crewId: 4, + crewId: 1, id: 5, - rate: 80, + rate: 4, comment: '좋아용', createdAt: '2024-10-31T00:52', reviewer: { @@ -66,9 +67,9 @@ export const CrewReviewData: { data: CrewReview[] } = { }, }, { - crewId: 4, + crewId: 1, id: 5, - rate: 20, + rate: 1, comment: '별로였음', createdAt: '2024-10-31T00:52', reviewer: { @@ -78,9 +79,9 @@ export const CrewReviewData: { data: CrewReview[] } = { }, }, { - crewId: 4, + crewId: 1, id: 5, - rate: 70, + rate: 4, comment: '나만 좋았냐', createdAt: '2024-10-31T00:54', reviewer: { @@ -92,7 +93,7 @@ export const CrewReviewData: { data: CrewReview[] } = { { crewId: 1, id: 6, - rate: 40, + rate: 0, comment: '솔직히 좀 별로..', createdAt: '2024-10-30T00:56', reviewer: { @@ -102,9 +103,9 @@ export const CrewReviewData: { data: CrewReview[] } = { }, }, { - crewId: 2, + crewId: 1, id: 7, - rate: 90, + rate: 5, comment: '진짜 좋은 모임이었다.. 굿', createdAt: '2024-10-30T00:56', reviewer: { @@ -114,9 +115,9 @@ export const CrewReviewData: { data: CrewReview[] } = { }, }, { - crewId: 3, + crewId: 1, id: 8, - rate: 80, + rate: 4, comment: '모임계를 뒤집어 엎으셨다', createdAt: '2024-10-30T00:56', reviewer: { @@ -126,9 +127,9 @@ export const CrewReviewData: { data: CrewReview[] } = { }, }, { - crewId: 4, + crewId: 1, id: 9, - rate: 80, + rate: 5, comment: '좋아용', createdAt: '2024-10-31T00:56', reviewer: { @@ -138,9 +139,9 @@ export const CrewReviewData: { data: CrewReview[] } = { }, }, { - crewId: 4, + crewId: 1, id: 10, - rate: 20, + rate: 2, comment: '별로였음', createdAt: '2024-10-31T00:56', reviewer: { @@ -150,9 +151,9 @@ export const CrewReviewData: { data: CrewReview[] } = { }, }, { - crewId: 4, + crewId: 1, id: 11, - rate: 70, + rate: 5, comment: '나만 좋았냐', createdAt: '2024-10-31T00:56', reviewer: { @@ -162,9 +163,9 @@ export const CrewReviewData: { data: CrewReview[] } = { }, }, { - crewId: 3, + crewId: 1, id: 12, - rate: 80, + rate: 4, comment: '모임계를 뒤집어 엎으셨다', createdAt: '2024-10-30T00:56', reviewer: { @@ -174,9 +175,9 @@ export const CrewReviewData: { data: CrewReview[] } = { }, }, { - crewId: 4, + crewId: 1, id: 13, - rate: 80, + rate: 5, comment: '좋아용', createdAt: '2024-10-31T00:56', reviewer: { @@ -186,9 +187,9 @@ export const CrewReviewData: { data: CrewReview[] } = { }, }, { - crewId: 4, + crewId: 1, id: 14, - rate: 20, + rate: 1, comment: '별로였음', createdAt: '2024-10-31T00:56', reviewer: { @@ -198,9 +199,9 @@ export const CrewReviewData: { data: CrewReview[] } = { }, }, { - crewId: 4, + crewId: 1, id: 15, - rate: 70, + rate: 4, comment: '나만 좋았냐', createdAt: '2024-10-31T00:56', reviewer: { @@ -220,7 +221,7 @@ export const MyReviewData: { data: MyReview[] } = { crewName: '크루크루크루크루크루크루', gatheringName: '모임모임모임약속약속약속', id: 3, - rate: 50, + rate: 3, comment: '리뷰리뷰리뷰리뷰리뷰리뷰리뷰리뷰리뷰리뷰', createdAt: '2024-10-31T00:56', gatheringLocation: '어느동', @@ -230,7 +231,7 @@ export const MyReviewData: { data: MyReview[] } = { crewName: '최초의크루이자최후의크루', gatheringName: '천지창조', id: 4, - rate: 20, + rate: 4, comment: '다시는 안 간다', createdAt: '2024-10-31T00:56', gatheringLocation: '어느동', @@ -240,7 +241,7 @@ export const MyReviewData: { data: MyReview[] } = { crewName: '마지막크루...최종의최종의최종', gatheringName: '친구들아모여라', id: 5, - rate: 80, + rate: 2, comment: '펭귄이랑 친구하고싶어요', createdAt: '2024-10-31T00:56', gatheringLocation: '어느동', @@ -250,7 +251,7 @@ export const MyReviewData: { data: MyReview[] } = { crewName: '할일없는사람만들어오세요', gatheringName: '숨쉬기모임', id: 6, - rate: 100, + rate: 5, comment: '복식호흡을 할 수 있게 됐어요 감사합니다', createdAt: '2024-10-31T00:56', gatheringLocation: '어느동', @@ -260,7 +261,7 @@ export const MyReviewData: { data: MyReview[] } = { crewName: '크루크루크루크루크루크루', gatheringName: '모임모임모임약속약속약속', id: 7, - rate: 50, + rate: 3, comment: '리뷰리뷰리뷰리뷰리뷰리뷰리뷰리뷰리뷰리뷰', createdAt: '2024-10-31T00:56', gatheringLocation: '어느동', @@ -270,7 +271,7 @@ export const MyReviewData: { data: MyReview[] } = { crewName: '최초의크루이자최후의크루', gatheringName: '천지창조', id: 8, - rate: 20, + rate: 4, comment: '다시는 안 간다', createdAt: '2024-10-31T00:56', gatheringLocation: '어느동', @@ -280,7 +281,7 @@ export const MyReviewData: { data: MyReview[] } = { crewName: '마지막크루...최종의최종의최종', gatheringName: '친구들아모여라', id: 9, - rate: 80, + rate: 3, comment: '펭귄이랑 친구하고싶어요', createdAt: '2024-10-31T00:56', gatheringLocation: '어느동', @@ -290,7 +291,7 @@ export const MyReviewData: { data: MyReview[] } = { crewName: '할일없는사람만들어오세요', gatheringName: '숨쉬기모임', id: 10, - rate: 100, + rate: 5, comment: '복식호흡을 할 수 있게 됐어요 감사합니다', createdAt: '2024-10-31T00:56', gatheringLocation: '어느동', @@ -300,7 +301,7 @@ export const MyReviewData: { data: MyReview[] } = { crewName: '크루크루크루크루크루크루', gatheringName: '모임모임모임약속약속약속', id: 11, - rate: 50, + rate: 4, comment: '리뷰리뷰리뷰리뷰리뷰리뷰리뷰리뷰리뷰리뷰', createdAt: '2024-10-31T00:56', gatheringLocation: '어느동', @@ -310,7 +311,7 @@ export const MyReviewData: { data: MyReview[] } = { crewName: '최초의크루이자최후의크루', gatheringName: '천지창조', id: 12, - rate: 20, + rate: 3, comment: '다시는 안 간다', createdAt: '2024-10-31T00:56', gatheringLocation: '어느동', @@ -320,7 +321,7 @@ export const MyReviewData: { data: MyReview[] } = { crewName: '마지막크루...최종의최종의최종', gatheringName: '친구들아모여라', id: 13, - rate: 80, + rate: 2, comment: '펭귄이랑 친구하고싶어요', createdAt: '2024-10-31T00:56', gatheringLocation: '어느동', @@ -330,7 +331,7 @@ export const MyReviewData: { data: MyReview[] } = { crewName: '할일없는사람만들어오세요', gatheringName: '숨쉬기모임', id: 14, - rate: 100, + rate: 5, comment: '복식호흡을 할 수 있게 됐어요 감사합니다', createdAt: '2024-10-31T00:56', gatheringLocation: '어느동', diff --git a/src/types/crew-card.d.ts b/src/types/crew-card.d.ts index 47c93ec8..d0a29ce1 100644 --- a/src/types/crew-card.d.ts +++ b/src/types/crew-card.d.ts @@ -22,4 +22,11 @@ export type CrewCardInform = { updatedDate: Date; isConfirmed: boolean; gatheringCount: number; + crewMember: CrewMemberList[]; }; + +export interface CrewMemberList { + id: number; + nickname: string; + profileImageUrl?: string | null; +} diff --git a/src/types/review.d.ts b/src/types/review.d.ts index ba2050c7..fa71291c 100644 --- a/src/types/review.d.ts +++ b/src/types/review.d.ts @@ -3,6 +3,11 @@ export interface ReviewInformResponse { hasNextPage: boolean; } +export interface CrewReviewInformResponse { + data: CrewReview[] | MyReview[]; + totalItems: number; +} + // NOTE: 크루 전체 리뷰 export interface CrewReview { crewId: number; diff --git a/src/utils/api.ts b/src/utils/api.ts index 46a0475b..adfc7154 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -1,7 +1,7 @@ import { useAuthStore } from '@/src/store/use-auth-store'; // TODO: 추후 API URL 수정 -const API_BASE_URL = 'http://localhost:3009'; +const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3009'; export class ApiError extends Error { constructor(