diff --git a/public/assets/icons/ic-captain-check.svg b/public/assets/icons/ic-captain-check.svg new file mode 100644 index 00000000..991d4d34 --- /dev/null +++ b/public/assets/icons/ic-captain-check.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/assets/icons/ic-email.svg b/public/assets/icons/ic-email.svg new file mode 100644 index 00000000..52346c1d --- /dev/null +++ b/public/assets/icons/ic-email.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/assets/icons/ic-share.svg b/public/assets/icons/ic-share.svg new file mode 100644 index 00000000..2ef75cea --- /dev/null +++ b/public/assets/icons/ic-share.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/_apis/auth/user-apis.ts b/src/_apis/auth/user-apis.ts index bf4cf39a..d160523b 100644 --- a/src/_apis/auth/user-apis.ts +++ b/src/_apis/auth/user-apis.ts @@ -1,11 +1,11 @@ import { fetchApi } from '@/src/utils/api'; import { User } from '@/src/types/auth'; -export function getUser(): Promise<{ data: User }> { +export function getUser(): Promise { return fetchApi<{ data: User }>('/auths/user', { method: 'GET', headers: { 'Content-Type': 'application/json', }, - }); + }).then((response) => response.data); } diff --git a/src/_apis/crew/crew-detail-apis.ts b/src/_apis/crew/crew-detail-apis.ts index e2a7f13c..0f19cc06 100644 --- a/src/_apis/crew/crew-detail-apis.ts +++ b/src/_apis/crew/crew-detail-apis.ts @@ -1,6 +1,7 @@ import { fetchApi } from '@/src/utils/api'; import { CrewDetail } from '@/src/types/crew-card'; +// 크루 디테일 보기 export async function getCrewDetail(id: number): Promise { const url = `/api/crews/${id}`; @@ -12,3 +13,39 @@ export async function getCrewDetail(id: number): Promise { }); return response.data; } + +// 크루 참여 +export async function joinCrew(crewId: number): Promise { + const url = `/api/crews/${crewId}/join`; + + await fetchApi(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); +} + +// 사용자 크루 탈퇴 +export async function leaveCrew(crewId: number): Promise { + const url = `/api/crews/${crewId}/leave`; + + await fetchApi(url, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + }); +} + +// 주최자 크루 취소 +export async function cancelCrew(crewId: number): Promise { + const url = `/api/crews/${crewId}`; + + await fetchApi(url, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + }); +} 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 index adad796e..80ae242e 100644 --- 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 @@ -6,7 +6,7 @@ import { CrewReviewData } from '@/src/mock/review-data'; import CrewReviewList from './crew-review-list'; const meta: Meta = { - title: 'components/CrewReviewList', + title: 'Components/Detail/CrewReviewList', component: CrewReviewList, parameters: { layout: 'fulled', diff --git a/src/app/(crew)/crew/detail/[id]/_components/detail-crew-container.tsx b/src/app/(crew)/crew/detail/[id]/_components/detail-crew-container.tsx new file mode 100644 index 00000000..a73f9249 --- /dev/null +++ b/src/app/(crew)/crew/detail/[id]/_components/detail-crew-container.tsx @@ -0,0 +1,153 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import { useRouter } from 'next/navigation'; +import { useDisclosure } from '@mantine/hooks'; +import { cancelCrew, joinCrew, leaveCrew } from '@/src/_apis/crew/crew-detail-apis'; +import { useGetCrewDetailQuery } from '@/src/_queries/crew/crew-detail-queries'; +import { useAuthStore } from '@/src/store/use-auth-store'; +import { ApiError } from '@/src/utils/api'; +import ConfirmCancelModal from '@/src/components/common/modal/confirm-cancel-modal'; +import { User } from '@/src/types/auth'; +import DetailCrewPresenter from './detail-crew-presenter'; + +interface DetailCrewContainerProps { + id: number; +} + +export default function DetailCrew({ id }: DetailCrewContainerProps) { + const [isCaptain, setIsCaptain] = useState(false); + const [isMember, setIsMember] = useState(false); + const [isJoining, setIsJoining] = useState(false); + const [confirmCancelOpened, { open: openConfirmCancel, close: closeConfirmCancel }] = + useDisclosure(); + const router = useRouter(); + + const { user } = useAuthStore(); + + const isDataWrappedUser = (value: unknown): value is { data: User } => { + return typeof value === 'object' && value !== null && 'data' in value; + }; + + const currentUserId = isDataWrappedUser(user) ? user.data.id : user?.id; + + const { data, isLoading, error: fetchError, refetch } = useGetCrewDetailQuery(id); + + useEffect(() => { + if (currentUserId && data) { + const captain = data.crewMembers.find((member) => member.captain); + const memberExists = data.crewMembers.some((member) => member.id === currentUserId); + + setIsCaptain(captain?.id === currentUserId); + setIsMember(memberExists); + } + }, [currentUserId, data]); + + const handleJoinClick = async () => { + if (isJoining) return; + + setIsJoining(true); + try { + await joinCrew(id); + toast.success('크루에 참여하였습니다 🙌'); + setIsMember(true); + await refetch(); + } catch (joinError) { + if (joinError instanceof ApiError) { + toast.error(joinError.message); + } else { + toast.error('🚫 크루 참여 중 에러가 발생했습니다.'); + } + } finally { + setIsJoining(false); + } + }; + + const handleLeaveCrew = async () => { + try { + await leaveCrew(id); + toast.success('크루를 탈퇴하였습니다👋'); + await refetch(); + } catch (leaveError) { + if (leaveError instanceof ApiError) { + toast.error(leaveError.message); + } else { + toast.error('🚫 크루 탈퇴 중 에러가 발생했습니다.'); + } + } + }; + + const handleDelete = () => { + openConfirmCancel(); + }; + + const handleConfirmCancel = async () => { + try { + await cancelCrew(id); + toast.success('크루가 성공적으로 삭제되었습니다.'); + router.push('/'); + } catch (deleteError) { + toast.error('크루 삭제 중 에러가 발생했습니다.'); + } + }; + + const onShareClick = () => { + const url = window.location.href; + navigator.clipboard + .writeText(url) + .then(() => { + toast.success('URL이 복사되었습니다!'); + }) + .catch(() => { + toast.error('🚫 URL 복사에 실패했습니다. 다시 시도해주세요.'); + }); + }; + + // TODO: 로딩, 에러처리 추후 개선 + if (isLoading) { + return

Loading...

; + } + + if (fetchError) { + if (fetchError instanceof ApiError) { + try { + const errorData = JSON.parse(fetchError.message); + + if (errorData.status === 'NOT_FOUND') { + return

크루 정보를 찾을 수 없습니다

; + } + } catch (parseError) { + return

{`Error ${fetchError.status}: ${fetchError.message}`}

; + } + } + return

데이터 통신에 실패했습니다.

; + } + + if (!data) { + return

데이터를 불러올 수 없습니다.

; + } + + return ( + <> + + + + 정말 삭제하시겠습니까? + + + ); +} diff --git a/src/app/(crew)/crew/detail/[id]/_components/detail-crew-presenter.tsx b/src/app/(crew)/crew/detail/[id]/_components/detail-crew-presenter.tsx new file mode 100644 index 00000000..8c9d0ebf --- /dev/null +++ b/src/app/(crew)/crew/detail/[id]/_components/detail-crew-presenter.tsx @@ -0,0 +1,191 @@ +import Image from 'next/image'; +import { Menu } from '@mantine/core'; +import { cn } from '@/src/utils/cn'; +import Button from '@/src/components/common/input/button'; +import { Profile } from '@/src/components/common/profile'; +import ProgressBar from '@/src/components/common/progress-bar'; +import { CrewDetail, CrewDetailMember } from '@/src/types/crew-card'; +import Check from '@/public/assets/icons/ic-check.svg'; +import EmailIco from '@/public/assets/icons/ic-email.svg'; +import ShareIco from '@/public/assets/icons/ic-share.svg'; +import IcoUser from '@/public/assets/icons/ic-user.svg'; +import KebabIcon from '@/public/assets/icons/kebab-btn.svg'; + +interface DetailCrewPresenterProps { + data: CrewDetail; + isCaptain: boolean; + isMember: boolean; + isJoining: boolean; + handleJoinClick: () => void; + handleLeaveCrew: () => void; + handleDelete: () => void; + onShareClick: () => void; +} + +export default function DetailCrewPresenter({ + data, + isCaptain, + isMember, + handleJoinClick, + handleLeaveCrew, + handleDelete, + onShareClick, + isJoining, +}: DetailCrewPresenterProps) { + const { + id, + title, + introduce, + mainLocation, + subLocation, + participantCount, + totalCount, + imageUrl, + crewMembers, + confirmed, + } = data; + + // captain과 members 분리 + const captain = crewMembers.find((member) => member.captain) as CrewDetailMember; + const members = crewMembers.filter((member) => !member.captain); + + const getJoinButtonText = () => { + if (isMember) return '참여 완료'; + if (isJoining) return '참여 중...'; + return '크루 참여하기'; + }; + + return ( +
+ {/* 상단 이미지와 정보 영역 */} +
+ {title} +
+ + {/* 오버레이 컨테이너 */} +
+

{title}

+

+ {mainLocation} {subLocation} +

+ +
+ + {/* 오른쪽 하단 공유 및 케밥 버튼 */} +
+ + {isCaptain || isMember ? ( + + +
+ 더보기 +
+
+ + {isCaptain ? ( + <> + + 크루 수정하기 + + + 크루 삭제하기 + + + ) : ( + + 크루 탈퇴하기 + + )} + +
+ ) : null} +
+
+ + {/* 소개 및 참여 인원 영역 */} +
+ {/* 크루장, 소개 영역 */} +
+
+ +
+

+ {captain.nickname} 크루장 +

+
+ 이메일 아이콘 +

{captain!.email}

+
+
+
+
+

크루 소개

+
+ {introduce || '소개 정보가 없습니다.'} +
+
+
+ + {/* 참여 인원 영역 */} +
+
+
+ 유저 아이콘 + 참여인원 + {participantCount} + /{totalCount} +
+ {confirmed && ( +
+ 개설 확정 + 개설확정 +
+ )} +
+
+ +
+
+ {members.length > 0 ? ( +
+ {members.map((member) => ( +
+
+ +
+ {member.nickname} +
+ ))} +
+ ) : ( +
+

크루장 외에 아직 크루원이 없습니다.

+

크루에 참여해보세요!🙌

+
+ )} +
+
+
+
+ ); +} diff --git a/src/app/(crew)/crew/detail/[id]/_components/detail-crew-section.tsx b/src/app/(crew)/crew/detail/[id]/_components/detail-crew-section.tsx deleted file mode 100644 index f432dd53..00000000 --- a/src/app/(crew)/crew/detail/[id]/_components/detail-crew-section.tsx +++ /dev/null @@ -1,34 +0,0 @@ -'use client'; - -import { useGetCrewDetailQuery } from '@/src/_queries/crew/crew-detail-queries'; -import { ApiError } from '@/src/utils/api'; -import DetailCrewCard from '@/src/components/common/crew-list/detail-crew-card'; - -interface DetailCrewSectionProps { - id: number; -} - -export default function DetailCrewSection({ id }: DetailCrewSectionProps) { - const { data, isLoading, error } = useGetCrewDetailQuery(id); - - // TODO: 추후 에러 처리 수정 - if (isLoading) return

Loading...

; - - if (error) { - if (error instanceof ApiError) { - try { - const errorData = JSON.parse(error.message); - - if (errorData.status === 'NOT_FOUND') { - return

크루 정보를 찾을 수 없습니다

; - } - } catch { - return

{`Error ${error.status}: ${error.message}`}

; - } - } - return

데이터 통신에 실패했습니다.

; - } - - // data가 있을 때만 DetailCrewCard를 렌더링 - return data ? : null; -} diff --git a/src/app/(crew)/crew/detail/[id]/_components/detail-crew.stories.tsx b/src/app/(crew)/crew/detail/[id]/_components/detail-crew.stories.tsx new file mode 100644 index 00000000..b1c84bfe --- /dev/null +++ b/src/app/(crew)/crew/detail/[id]/_components/detail-crew.stories.tsx @@ -0,0 +1,178 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import DetailCrewPresenter from './detail-crew-presenter'; + +const meta: Meta = { + title: 'Components/Detail/DetailCrewPresenter', + component: DetailCrewPresenter, + parameters: { + layout: 'fullscreen', + }, + tags: ['autodocs'], + argTypes: { + handleJoinClick: { action: '참여 버튼 클릭' }, + handleLeaveCrew: { action: '탈퇴 버튼 클릭' }, + handleDelete: { action: '삭제 버튼 클릭' }, + onShareClick: { action: '공유 버튼 클릭' }, + }, +}; + +export default meta; +type Story = StoryObj; + +// 디폴트 스토리: isCaptain false, isMember false +export const Default: Story = { + args: { + data: { + id: 1, + title: '같이 물장구칠사람', + mainCategory: '기타', + subCategory: '스키', + mainLocation: '대전광역시', + introduce: + '크루 소개 크루 소개 크루 소개 크루 소개 크루 소개 크루 소개 크루 소개 크루 소개 크루 소개 크루 소개 크루 소개 크루 소개 크루 소개 크루 소개 크루 소개 크루 소개 크루 소개 크루 소개 크루 소개 크루 소개 크루 소개 크루 소개 크루 소개 크루 소개 크루 소개 크루 소개 크루 소개 크루 소개 크루 소개', + subLocation: '유성구', + participantCount: 8, + totalCount: 10, + confirmed: true, + imageUrl: + 'https://crewcrew.s3.ap-northeast-2.amazonaws.com/crew/4d0c5851-e6e2-4919-897a-b8d4e88a4f72', + totalGatheringCount: 5, + crewMembers: [ + { + id: 1, + nickname: 'John', + email: 'abc@asbc.com', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + captain: true, + }, + { + id: 2, + nickname: 'Jane', + email: 'abc@asbc.com', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + captain: false, + }, + { + id: 3, + nickname: 'Mike', + email: 'abc@asbc.com', + profileImageUrl: '', + captain: false, + }, + { + id: 4, + nickname: 'Lucy', + email: 'abc@asbc.com', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + captain: false, + }, + { + id: 5, + nickname: 'Tom', + email: 'abc@asbc.com', + profileImageUrl: '', + captain: false, + }, + { + id: 6, + nickname: 'Alice', + email: 'abc@asbc.com', + profileImageUrl: '', + captain: false, + }, + { + id: 7, + nickname: 'Bob', + email: 'abc@asbc.com', + profileImageUrl: '', + captain: false, + }, + { + id: 8, + nickname: 'Sophia', + email: 'abc@asbc.com', + profileImageUrl: '', + captain: false, + }, + ], + }, + isCaptain: false, + isMember: false, + }, +}; + +// isCaptain true, isMember true +export const IsCaptain: Story = { + args: { + data: { + id: 1, + title: '같이 물장구칠사람', + mainCategory: '기타', + subCategory: '스키', + mainLocation: '대전광역시', + introduce: '크루 소개', + subLocation: '유성구', + participantCount: 1, + totalCount: 10, + confirmed: true, + imageUrl: + 'https://crewcrew.s3.ap-northeast-2.amazonaws.com/crew/4d0c5851-e6e2-4919-897a-b8d4e88a4f72', + totalGatheringCount: 1, + crewMembers: [ + { + id: 1, + nickname: 'John', + email: 'abc@asbc.com', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + captain: true, + }, + ], + }, + isCaptain: true, + isMember: true, + }, +}; + +export const IsMember: Story = { + args: { + data: { + id: 1, + title: '같이 물장구칠사람', + mainCategory: '기타', + subCategory: '스키', + mainLocation: '대전광역시', + introduce: '', + subLocation: '유성구', + participantCount: 2, + totalCount: 10, + confirmed: true, + imageUrl: + 'https://crewcrew.s3.ap-northeast-2.amazonaws.com/crew/4d0c5851-e6e2-4919-897a-b8d4e88a4f72', + totalGatheringCount: 1, + crewMembers: [ + { + id: 1, + nickname: 'John', + email: 'abc@asbc.com', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + captain: true, + }, + { + id: 2, + nickname: 'Jane', + email: 'abc@asbc.com', + profileImageUrl: + 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', + captain: false, + }, + ], + }, + isCaptain: false, + isMember: true, + }, +}; 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 index 2bdce41f..aa2875c7 100644 --- a/src/app/(crew)/crew/detail/[id]/_components/rating-display.stories.tsx +++ b/src/app/(crew)/crew/detail/[id]/_components/rating-display.stories.tsx @@ -3,7 +3,7 @@ import { Meta, StoryFn } from '@storybook/react'; import RatingDisplay, { ReviewRateInfo } from './rating-display'; export default { - title: 'Components/RatingDisplay', + title: 'Components/Detail/RatingDisplay', component: RatingDisplay, tags: ['autodocs'], argTypes: { diff --git a/src/app/(crew)/crew/detail/[id]/page.tsx b/src/app/(crew)/crew/detail/[id]/page.tsx index 5437d08b..4e53a588 100644 --- a/src/app/(crew)/crew/detail/[id]/page.tsx +++ b/src/app/(crew)/crew/detail/[id]/page.tsx @@ -1,6 +1,6 @@ import { getGatheringList } from '@/src/_apis/crew/crew-gathering-list-apis'; import CreateGathering from './_components/create-gathering'; -import DetailCrewSection from './_components/detail-crew-section'; +import DetailCrew from './_components/detail-crew-container'; import GatheringListSection from './_components/gathering-list-section'; import CrewReviewSection from './_components/review-section'; @@ -16,7 +16,7 @@ export default async function CrewDetailPage({ params }: CrewDetailPageProps) {
- +
diff --git a/src/components/common/crew-list/detail-crew-card.stories.tsx b/src/components/common/crew-list/detail-crew-card.stories.tsx deleted file mode 100644 index 708ceea5..00000000 --- a/src/components/common/crew-list/detail-crew-card.stories.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import DetailCrewCard from './detail-crew-card'; - -const meta: Meta = { - title: 'Components/CrewCardList/DetailCrewCard', - component: DetailCrewCard, - parameters: { - layout: 'centered', - nextjs: { - appDirectory: true, - }, - }, - tags: ['autodocs'], - args: { - data: { - id: 1, - title: '같이 물장구칠사람', - mainLocation: '대전광역시', - subLocation: '유성구', - participantCount: 10, - totalCount: 20, - confirmed: true, - imageUrl: 'https://i.pinimg.com/564x/f8/8d/c5/f88dc5b857caf6c303ae5ef9dd12e7fb.jpg', - totalGatheringCount: 5, - crewMembers: [ - { - id: 1, - nickname: 'John', - profileImageUrl: - 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', - }, - { - id: 2, - nickname: 'Jane', - profileImageUrl: - 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', - }, - ], - }, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - data: { - id: 1, - title: '같이 물장구칠사람', - mainLocation: '대전광역시', - subLocation: '유성구', - participantCount: 10, - totalCount: 20, - confirmed: true, - imageUrl: 'https://i.pinimg.com/564x/f8/8d/c5/f88dc5b857caf6c303ae5ef9dd12e7fb.jpg', - totalGatheringCount: 5, - crewMembers: [], // 빈 배열이라도 기본값으로 설정 - }, - }, -}; - -export const NotConfirmed: Story = { - args: { - data: { - id: 2, - title: '물장구 동호회', - mainLocation: '서울특별시', - subLocation: '강남구', - participantCount: 5, - totalCount: 15, - confirmed: false, - imageUrl: 'https://i.pinimg.com/564x/e2/25/bb/e225bb492dc7a20a549f3c0abec28eb8.jpg', - totalGatheringCount: 3, - crewMembers: [], - }, - }, -}; diff --git a/src/components/common/crew-list/detail-crew-card.tsx b/src/components/common/crew-list/detail-crew-card.tsx deleted file mode 100644 index 817ffbc1..00000000 --- a/src/components/common/crew-list/detail-crew-card.tsx +++ /dev/null @@ -1,172 +0,0 @@ -'use client'; - -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 { CrewMember } 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 { - data: { - id: number; - title: string; - mainLocation: string; - subLocation: string; - participantCount: number; - totalCount: number; - confirmed: boolean; - imageUrl: string; - totalGatheringCount: number; - crewMembers: CrewMember[]; - }; -} - -export default function DetailCrewCard({ data }: DetailCrewCardProps) { - const { - id, - title, - mainLocation, - subLocation, - participantCount, - totalCount, - confirmed, - imageUrl, - totalGatheringCount, - crewMembers, - } = data; - 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 */} - {/* //TODO: captin, crew인 경우 로직 수정 */} - {/* {isCaptain ? ( - - -
- 더보기 -
-
- - - 크루 수정하기 - - - 크루 삭제하기 - - -
- ) : isCrew ? ( - - -
- 더보기 -
-
- - - 크루 탈퇴하기 - - -
- ) : null} */} - - {/* 썸네일 */} -
- {title} -
- -
- {/* 메인 내용 */} -
-
- - {title} - - | - - {mainLocation} {subLocation} - -
- - - {`현재 ${totalGatheringCount}개의 약속이 개설되어 있습니다.`} - -
- {/* 세부내용 */} -
-
-
-
- 모집 정원 - {participantCount}명 -
- -
-
- - {confirmed && ( - - 확인 - 개설 확정 - - )} -
- -
- 최소인원 2명 - 최대인원 {totalCount}명 -
-
-
-
- - {/* 삭제 확인 모달 */} - -

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

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

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

-
-
- ); -} diff --git a/src/components/common/profile/index.tsx b/src/components/common/profile/index.tsx index df1a6e69..db2055c7 100644 --- a/src/components/common/profile/index.tsx +++ b/src/components/common/profile/index.tsx @@ -1,7 +1,9 @@ 'use client'; import Image from 'next/image'; +import { cn } from '@/src/utils/cn'; import defaultImage from '@/public/assets/icons/default-profile.svg'; +import captainCheck from '@/public/assets/icons/ic-captain-check.svg'; import editImage from '@/public/assets/icons/profile-edit.svg'; /** @@ -11,6 +13,7 @@ import editImage from '@/public/assets/icons/profile-edit.svg'; * @param {'small' | 'medium' | 'large'} [props.size='large'] - 프로필의 크기를 결정 기본값은 'large' * @param {string} [props.imageUrl] - 이미지 URL을 설정 없으면 기본 이미지 * @param {boolean} [props.editable=false] - 프로필 편집시 사용 + * @param {boolean} [props.isCaptain=false] - 캡틴 여부를 판단 * @param {() => void} [props.onClick] - 프로필을 클릭 * @param {() => void} [props.onEdit] - 편집 버튼을 클릭시 */ @@ -19,6 +22,7 @@ interface ProfileProps { size?: 'small' | 'header' | 'large' | 'full'; imageUrl?: string; editable?: boolean; + isCaptain?: boolean; onClick?: () => void; onEdit?: () => void; } @@ -27,24 +31,35 @@ export function Profile({ size = 'full', imageUrl, editable = false, + isCaptain = false, onClick, onEdit, }: ProfileProps) { - const finalSize = editable ? 'large' : size; + const finalSize = editable || isCaptain ? 'large' : size; // 추후 디자인에 따라 수정 const sizeClasses = { small: 'w-6 h-6', - header: 'sm:w-7 sm:h-7 md:w-[40px] md:h-[40px] lg:w-[40px] lg:w-[40px]', + header: 'sm:w-7 sm:h-7 md:w-[40px] md:h-[40px] lg:w-[40px] lg:h-[40px]', large: 'w-14 h-14', full: 'w-full h-full', + captain: 'w-20 h-20', }; return ( -
+
- {editable && ( + {editable && !isCaptain && ( )} + {isCaptain && ( +
+ 캡틴 배지 +
+ )}
); } diff --git a/src/types/crew-card.d.ts b/src/types/crew-card.d.ts index 3a60707f..4f93e28a 100644 --- a/src/types/crew-card.d.ts +++ b/src/types/crew-card.d.ts @@ -36,12 +36,20 @@ export interface CrewMember { id: number; nickname: string; profileImageUrl?: string; - captain?: boolean; +} + +export interface CrewDetailMember { + id: number; + nickname: string; + email: string; + profileImageUrl?: string; + captain: boolean; } export interface CrewDetail { id: number; title: string; + introduce: string; mainCategory?: string; subCategory?: string; mainLocation: string; @@ -50,7 +58,7 @@ export interface CrewDetail { totalCount: number; imageUrl: string; totalGatheringCount: number; - crewMembers: CrewMember[]; + crewMembers: CrewDetailMember[]; confirmed: boolean; introduce?: string; }