Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions public/assets/icons/ic-captain-check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions public/assets/icons/ic-email.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions public/assets/icons/ic-share.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions src/_apis/auth/user-apis.ts
Original file line number Diff line number Diff line change
@@ -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<User> {
return fetchApi<{ data: User }>('/auths/user', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
}).then((response) => response.data);
}
37 changes: 37 additions & 0 deletions src/_apis/crew/crew-detail-apis.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { fetchApi } from '@/src/utils/api';
import { CrewDetail } from '@/src/types/crew-card';

// 크루 λ””ν…ŒμΌ 보기
export async function getCrewDetail(id: number): Promise<CrewDetail> {
const url = `/api/crews/${id}`;

Expand All @@ -12,3 +13,39 @@ export async function getCrewDetail(id: number): Promise<CrewDetail> {
});
return response.data;
}

// 크루 μ°Έμ—¬
export async function joinCrew(crewId: number): Promise<void> {
const url = `/api/crews/${crewId}/join`;

await fetchApi<void>(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
}
Comment on lines +17 to +27
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

μ—λŸ¬ 처리 및 응닡 νƒ€μž… λͺ…μ‹œ ν•„μš”

API 호좜 μ‹€νŒ¨ μ‹œμ˜ μ—λŸ¬ μ²˜λ¦¬μ™€ 응닡 νƒ€μž…μ— λŒ€ν•œ λͺ…ν™•ν•œ μ •μ˜κ°€ ν•„μš”ν•©λ‹ˆλ‹€.

λ‹€μŒκ³Ό 같이 μˆ˜μ •ν•˜λŠ” 것을 μ œμ•ˆν•©λ‹ˆλ‹€:

+interface JoinCrewResponse {
+  success: boolean;
+  message?: string;
+}

-export async function joinCrew(crewId: number): Promise<void> {
+export async function joinCrew(crewId: number): Promise<JoinCrewResponse> {
   const url = `/api/crews/${crewId}/join`;

-  await fetchApi<void>(url, {
+  const response = await fetchApi<JoinCrewResponse>(url, {
     method: 'POST',
     headers: {
       'Content-Type': 'application/json',
     },
   });
+  return response;
 }
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 크루 μ°Έμ—¬
export async function joinCrew(crewId: number): Promise<void> {
const url = `/api/crews/${crewId}/join`;
await fetchApi<void>(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
}
interface JoinCrewResponse {
success: boolean;
message?: string;
}
export async function joinCrew(crewId: number): Promise<JoinCrewResponse> {
const url = `/api/crews/${crewId}/join`;
const response = await fetchApi<JoinCrewResponse>(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
return response;
}


// μ‚¬μš©μž 크루 νƒˆν‡΄
export async function leaveCrew(crewId: number): Promise<void> {
const url = `/api/crews/${crewId}/leave`;

await fetchApi<void>(url, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
}

// 주졜자 크루 μ·¨μ†Œ
export async function cancelCrew(crewId: number): Promise<void> {
const url = `/api/crews/${crewId}`;

await fetchApi<void>(url, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
}
Comment on lines +41 to +51
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

크루 μ·¨μ†Œ μž‘μ—…μ— λŒ€ν•œ μ•ˆμ „μž₯치 ν•„μš”

주졜자 κΆŒν•œ 확인 및 μ·¨μ†Œ μž‘μ—…μ— λŒ€ν•œ μ•ˆμ „μž₯μΉ˜κ°€ ν•„μš”ν•©λ‹ˆλ‹€.

λ‹€μŒ 사항듀을 κ³ λ €ν•΄μ•Ό ν•©λ‹ˆλ‹€:

  1. 주졜자 κΆŒν•œ 확인
  2. μ·¨μ†Œ μ „ 확인 절차
  3. 크루 멀버듀에 λŒ€ν•œ μ•Œλ¦Ό
-export async function cancelCrew(crewId: number): Promise<void> {
+export async function cancelCrew(
+  crewId: number,
+  organizerId: number,
+  confirmation: boolean
+): Promise<void> {
+  if (!confirmation) {
+    throw new Error('크루 μ·¨μ†Œ μž‘μ—…μ€ 확인이 ν•„μš”ν•©λ‹ˆλ‹€.');
+  }
+
+  // 주졜자 κΆŒν•œ 확인
+  const crewDetail = await getCrewDetail(crewId);
+  if (crewDetail.organizerId !== organizerId) {
+    throw new Error('크루 μ·¨μ†ŒλŠ” 주졜자만 κ°€λŠ₯ν•©λ‹ˆλ‹€.');
+  }

   const url = `/api/crews/${crewId}`;

Committable suggestion skipped: line range outside the PR's diff.

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { CrewReviewData } from '@/src/mock/review-data';
import CrewReviewList from './crew-review-list';

const meta: Meta<typeof CrewReviewList> = {
title: 'components/CrewReviewList',
title: 'Components/Detail/CrewReviewList',
component: CrewReviewList,
parameters: {
layout: 'fulled',
Expand Down
153 changes: 153 additions & 0 deletions src/app/(crew)/crew/detail/[id]/_components/detail-crew-container.tsx
Original file line number Diff line number Diff line change
@@ -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('🚫 크루 νƒˆν‡΄ 쀑 μ—λŸ¬κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.');
}
}
};
Comment on lines +67 to +79
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

크루 νƒˆν‡΄ ν›„ isMember μƒνƒœ μ—…λ°μ΄νŠΈ ν•„μš”

handleLeaveCrew ν•¨μˆ˜μ—μ„œ 크루λ₯Ό νƒˆν‡΄ν•œ 후에도 isMember μƒνƒœκ°€ λ³€κ²½λ˜μ§€ μ•Šμ•„ μ‚¬μš©μž μΈν„°νŽ˜μ΄μŠ€μ— μΌκ΄€λ˜μ§€ μ•Šμ€ 정보가 ν‘œμ‹œλ  수 μžˆμŠ΅λ‹ˆλ‹€. μ„±κ³΅μ μœΌλ‘œ νƒˆν‡΄ν•œ 경우 setIsMember(false)λ₯Ό ν˜ΈμΆœν•˜μ—¬ μƒνƒœλ₯Ό μ •ν™•ν•˜κ²Œ λ°˜μ˜ν•΄μ£Όμ„Έμš”.

μˆ˜μ • 사항:

await leaveCrew(id);
toast.success('크루λ₯Ό νƒˆν‡΄ν•˜μ˜€μŠ΅λ‹ˆλ‹€πŸ‘‹');
+ setIsMember(false);
await refetch();
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 handleLeaveCrew = async () => {
try {
await leaveCrew(id);
toast.success('크루λ₯Ό νƒˆν‡΄ν•˜μ˜€μŠ΅λ‹ˆλ‹€πŸ‘‹');
setIsMember(false);
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 <p>Loading...</p>;
}

if (fetchError) {
if (fetchError instanceof ApiError) {
try {
const errorData = JSON.parse(fetchError.message);

if (errorData.status === 'NOT_FOUND') {
return <p>크루 정보λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€</p>;
}
} catch (parseError) {
return <p>{`Error ${fetchError.status}: ${fetchError.message}`}</p>;
}
}
return <p>데이터 톡신에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.</p>;
}
Comment on lines +112 to +125
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

μ—λŸ¬ 처리 둜직 κ°œμ„ μœΌλ‘œ μ•ˆμ •μ„± ν–₯상

fetchError.messageλ₯Ό JSON으둜 νŒŒμ‹±ν•˜μ—¬ μ—λŸ¬λ₯Ό μ²˜λ¦¬ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€(115번 라인). κ·ΈλŸ¬λ‚˜ message 속성이 항상 JSON ν˜•μ‹μ„ 보μž₯ν•˜μ§€ μ•ŠμœΌλ―€λ‘œ, μ—λŸ¬ 처리 λ‘œμ§μ„ κ°œμ„ ν•  ν•„μš”κ°€ μžˆμŠ΅λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, fetchError 객체에 응닡 데이터가 ν¬ν•¨λ˜μ–΄ μžˆλ‹€λ©΄ ν•΄λ‹Ή 데이터λ₯Ό 직접 μ‚¬μš©ν•˜κ±°λ‚˜, μ„œλ²„ μΈ‘μ—μ„œ μΌκ΄€λœ μ—λŸ¬ 응닡 ꡬ쑰λ₯Ό μ œκ³΅ν•˜λ„λ‘ μš”μ²­ν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€.


if (!data) {
return <p>데이터λ₯Ό 뢈러올 수 μ—†μŠ΅λ‹ˆλ‹€.</p>;
}

return (
<>
<DetailCrewPresenter
data={data}
isCaptain={isCaptain}
isMember={isMember}
isJoining={isJoining}
handleJoinClick={handleJoinClick}
handleLeaveCrew={handleLeaveCrew}
handleDelete={handleDelete}
onShareClick={onShareClick}
/>

<ConfirmCancelModal
opened={confirmCancelOpened}
onClose={closeConfirmCancel}
onConfirm={handleConfirmCancel}
>
정말 μ‚­μ œν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?
</ConfirmCancelModal>
</>
);
}
Loading
Loading