Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.53.1",
"react-hook-form-persist": "^3.0.0",
"react-intersection-observer": "^9.13.1",
"react-toastify": "^10.0.6",
"zustand": "^5.0.1"
Expand Down
39 changes: 28 additions & 11 deletions src/_apis/crew/crew.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { fetchApi } from '@/src/utils/api';
import { CreateCrewRequestTypes, CreateCrewResponseTypes } from '@/src/types/create-crew';
import {
CreateCrewRequestTypes,
CreateCrewResponseTypes,
EditCrewRequestTypes,
EditCrewResponseTypes,
} from '@/src/types/create-crew';

export async function createCrew(data: CreateCrewRequestTypes) {
try {
const response: { data: CreateCrewResponseTypes; status: number } = await fetchApi(
`/api/crews`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // Include authentication credentials
body: JSON.stringify(data),
const response: { data: CreateCrewResponseTypes } = await fetchApi(`/api/crews`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
);
credentials: 'include', // Include authentication credentials
body: JSON.stringify(data),
});
if (!response.data) {
throw new Error('Failed to create crew: No data received');
}
Expand All @@ -23,3 +25,18 @@ export async function createCrew(data: CreateCrewRequestTypes) {
throw error;
}
}

export async function editCrew(id: number, data: EditCrewRequestTypes) {
try {
await fetchApi(`/api/crews/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // Include authentication credentials
body: JSON.stringify(data),
});
} catch (error) {
throw error;
}
}
Comment on lines +29 to +42
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

editCrew 함수의 구현을 개선해야 합니다.

다음과 같은 중요한 개선사항들이 필요합니다:

  1. 응답 처리가 누락되어 있습니다
  2. 에러 처리가 미흡합니다
  3. 반환 타입이 명시되어 있지 않습니다

다음과 같이 수정하는 것을 제안드립니다:

-export async function editCrew(id: number, data: EditCrewRequestTypes) {
+export async function editCrew(id: number, data: EditCrewRequestTypes): Promise<EditCrewResponseTypes> {
   try {
-    await fetchApi(`/api/crews/${id}`, {
+    const response: { data: EditCrewResponseTypes } = await fetchApi(`/api/crews/${id}`, {
       method: 'PUT',
       headers: {
         'Content-Type': 'application/json',
       },
       credentials: 'include',
       body: JSON.stringify(data),
     });
+    if (!response.data) {
+      throw new Error('크루 수정 실패: 서버로부터 데이터를 받지 못했습니다');
+    }
+    return response.data;
   } catch (error) {
-    throw error;
+    if (error instanceof TypeError) {
+      throw new Error('크루 수정 실패: 네트워크 오류가 발생했습니다');
+    }
+    throw new Error(`크루 수정 실패: ${error.message}`);
   }
 }

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

🧰 Tools
🪛 Biome

[error] 40-40: The catch clause that only rethrows the original error is useless.

An unnecessary catch clause can be confusing.
Unsafe fix: Remove the try/catch clause.

(lint/complexity/noUselessCatch)

46 changes: 45 additions & 1 deletion src/_queries/crew/crew-detail-queries.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,53 @@
import { useQuery } from '@tanstack/react-query';
import { toast } from 'react-toastify';
import { useRouter } from 'next/navigation';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { createCrew, editCrew } from '@/src/_apis/crew/crew';
import { getCrewDetail } from '@/src/_apis/crew/crew-detail-apis';
import { CreateCrewRequestTypes, EditCrewRequestTypes } from '@/src/types/create-crew';

export function useGetCrewDetailQuery(id: number) {
return useQuery({
queryKey: ['crewDetail', id],
queryFn: () => getCrewDetail(id),
});
}

export function useCreateCrewQuery() {
const router = useRouter();
const queryClient = useQueryClient();

return useMutation({
mutationFn: (data: CreateCrewRequestTypes) => createCrew(data),
onSuccess: (data) => {
if (data === null || data === undefined) {
return;
}
queryClient.invalidateQueries({ queryKey: ['crewLists', 'crewDetail'] });
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

쿼리 무효화 시 올바른 사용법 적용

queryClient.invalidateQueries({ queryKey: ['crewLists', 'crewDetail'] });로 여러 쿼리를 무효화하려고 하고 있습니다. 그러나 이 방식은 의도한 대로 동작하지 않을 수 있습니다. 각각의 쿼리를 별도로 무효화하는 것이 좋습니다.

수정 제안:

-queryClient.invalidateQueries({ queryKey: ['crewLists', 'crewDetail'] });
+queryClient.invalidateQueries(['crewLists']);
+queryClient.invalidateQueries(['crewDetail']);
📝 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
queryClient.invalidateQueries({ queryKey: ['crewLists', 'crewDetail'] });
queryClient.invalidateQueries(['crewLists']);
queryClient.invalidateQueries(['crewDetail']);

toast.success('크루가 생성되었습니다.');
router.push(`/crew/detail/${data.crewId}`);
},
onError: (error) => {
toast.error(error.message);
},
});
}

export function useEditCrewQuery(id: number) {
const router = useRouter();
const queryClient = useQueryClient();

return useMutation({
mutationFn: (data: EditCrewRequestTypes) => editCrew(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['crewDetail'] });
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

쿼리 무효화 시 정확한 키 사용

현재 queryClient.invalidateQueries({ queryKey: ['crewDetail'] });로 쿼리를 무효화하고 있습니다. 그러나 crewDetail 쿼리는 id를 포함하므로 정확한 쿼리 키를 사용하여 무효화해야 합니다.

수정 제안:

-queryClient.invalidateQueries({ queryKey: ['crewDetail'] });
+queryClient.invalidateQueries(['crewDetail', id]);

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

toast.success('크루 정보가 수정되었습니다.');
if (router) {
router.push(`/crew/detail/${id}`);
}
},
onError: (error) => {
toast.error(error.message);
},
retry: false,
});
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState } from 'react';
import category from '@/src/data/category.json';
import InternalCategory from '@/src/app/_components/category/internal-category';
import MainCategory from '@/src/app/_components/category/main-category';
import InternalCategory from '@/src/app/(crew)/_components/category/internal-category';
import MainCategory from '@/src/app/(crew)/_components/category/main-category';

export interface CategoryContainerProps {
mainCategory: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default {
} as Meta<typeof CreateCrewForm>;

const Template: StoryFn<CreateCrewFormTypes> = function CreateCrewPageStory() {
return <CreateCrewForm data={initialValue} />;
return <CreateCrewForm data={initialValue} type="create" />;
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

타입 안전성 개선이 필요합니다

type prop이 "create" | "edit" 리터럴 타입으로 제한되어야 합니다. 현재는 문자열 리터럴로 하드코딩되어 있어 타입 안전성이 보장되지 않습니다.

다음과 같이 개선하는 것을 제안드립니다:

- return <CreateCrewForm data={initialValue} type="create" />;
+ const formType = 'create' as const;
+ return <CreateCrewForm data={initialValue} type={formType} />;
📝 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
return <CreateCrewForm data={initialValue} type="create" />;
const formType = 'create' as const;
return <CreateCrewForm data={initialValue} type={formType} />;

};

export const Default = Template.bind({});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,31 @@

import { useEffect, useState } from 'react';
import { Controller, useForm, useWatch } from 'react-hook-form';
import useFormPersist from 'react-hook-form-persist';
import { useRouter } from 'next/navigation';
import { NumberInput } from '@mantine/core';
import { getImageUrl } from '@/src/_apis/image/get-image-url';
import categoryData from '@/src/data/category.json';
import regionData from '@/src/data/region.json';
import Button from '@/src/components/common/input/button';
import DropDown from '@/src/components/common/input/drop-down';
import FileInputWrap from '@/src/components/common/input/file-input-wrap';
import TextInput from '@/src/components/common/input/text-input';
import Textarea from '@/src/components/common/input/textarea';
import { CreateCrewFormTypes } from '@/src/types/create-crew';
import { CreateCrewFormTypes, EditCrewResponseTypes } from '@/src/types/create-crew';
import ImgCrewSampleUrls from '@/public/assets/images/crew-sample';

export interface CreateCrewFormProps {
data: CreateCrewFormTypes;
type: 'create' | 'edit';
data: CreateCrewFormTypes | EditCrewResponseTypes;
Comment on lines +20 to +21
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

타입 정의가 개선되었습니다.

CreateCrewFormProps 인터페이스에 'create' | 'edit' 타입이 추가되어 폼의 용도가 명확해졌습니다. 하지만 isEdit prop이 여전히 존재하여 중복된 상태 체크가 발생할 수 있습니다.

다음과 같은 개선을 제안드립니다:

export interface CreateCrewFormProps {
  type: 'create' | 'edit';
  data: CreateCrewFormTypes | EditCrewResponseTypes;
- isEdit?: boolean; // 제거
  onEdit?: (data: CreateCrewFormTypes) => void;
  onSubmit?: (data: CreateCrewFormTypes) => void;
}

Also applies to: 27-27

isEdit?: boolean;
onEdit?: (data: CreateCrewFormTypes) => void;
onSubmit?: (data: CreateCrewFormTypes) => void;
}

export default function CreateCrewForm({
isEdit = false,
type,
isEdit,
onEdit = () => {},
onSubmit = () => {},
data,
Expand All @@ -34,18 +38,29 @@ export default function CreateCrewForm({
setValue,
trigger,
clearErrors,
watch,
formState: { errors, isValid, isSubmitting },
} = useForm<CreateCrewFormTypes>({
defaultValues: data,
mode: 'onBlur',
});

if (typeof window !== 'undefined') {
// eslint-disable-next-line react-hooks/rules-of-hooks
useFormPersist('createCrew', {
watch,
setValue,
storage: window.localStorage,
});
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

폼 데이터 지속성 구현 위치 개선 필요

useFormPersist 훅이 조건부로 실행되고 있어 React Hooks 규칙을 위반할 수 있습니다. 컴포넌트 최상위 레벨에서 실행되도록 수정이 필요합니다.

다음과 같이 수정을 제안드립니다:

- if (typeof window !== 'undefined') {
-   // eslint-disable-next-line react-hooks/rules-of-hooks
-   useFormPersist('createCrew', {
-     watch,
-     setValue,
-     storage: window.localStorage,
-   });
- }

+ useEffect(() => {
+   if (typeof window !== 'undefined') {
+     useFormPersist('createCrew', {
+       watch,
+       setValue,
+       storage: window.localStorage,
+     });
+   }
+ }, [watch, setValue]);

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


const [categoryIndex, setCategoryIndex] = useState(0);
const [regionIndex, setRegionIndex] = useState(0);

const title = useWatch({ control, name: 'title' });
const mainCategory = useWatch({ control, name: 'mainCategory' });
const mainLocation = useWatch({ control, name: 'mainLocation' });
const subLocation = useWatch({ control, name: 'subLocation' });
const introduce = useWatch({ control, name: 'introduce' });

const handleMainCategoryChange = (newValue: string | null) => {
Expand All @@ -59,13 +74,40 @@ export default function CreateCrewForm({
setValue('subLocation', null);
clearErrors('subLocation');
};

const handleFileChange = async (
file: File | string | null,
onChange: (value: string | File) => void,
) => {
if (file instanceof File) {
const imgResponse = await getImageUrl(file, 'CREW');
onChange(imgResponse?.imageUrl || '');
}
};
Comment on lines +87 to +95
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

파일 처리 로직에 오류 처리 추가 필요

getImageUrl API 호출 시 발생할 수 있는 오류에 대한 처리가 누락되어 있습니다.

다음과 같이 수정을 제안드립니다:

  const handleFileChange = async (
    file: File | string | null,
    onChange: (value: string | File) => void,
  ) => {
    if (file instanceof File) {
+     try {
        const imgResponse = await getImageUrl(file, 'CREW');
        onChange(imgResponse?.imageUrl || '');
+     } catch (error) {
+       console.error('이미지 업로드 실패:', error);
+       // TODO: 사용자에게 오류 메시지 표시
+     }
    }
  };
📝 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 handleFileChange = async (
file: File | string | null,
onChange: (value: string | File) => void,
) => {
if (file instanceof File) {
const imgResponse = await getImageUrl(file, 'CREW');
onChange(imgResponse?.imageUrl || '');
}
};
const handleFileChange = async (
file: File | string | null,
onChange: (value: string | File) => void,
) => {
if (file instanceof File) {
try {
const imgResponse = await getImageUrl(file, 'CREW');
onChange(imgResponse?.imageUrl || '');
} catch (error) {
console.error('이미지 업로드 실패:', error);
// TODO: 사용자에게 오류 메시지 표시
}
}
};


const handleClear = () => {
setValue('title', '');
setValue('mainCategory', '');
setValue('subCategory', '');
setValue('imageUrl', '');
setValue('mainLocation', '');
setValue('subLocation', '');
setValue('totalCount', 4);
setValue('introduce', '');
localStorage.removeItem('createCrew');
router.back();
};

useEffect(() => {
setCategoryIndex(categoryData.findIndex((category) => category.title.label === mainCategory));
setRegionIndex(regionData.findIndex((region) => region.main.label === mainLocation));
if (isEdit && subLocation === '') {
setValue('subLocation', '전체');
}
}, [mainCategory, mainLocation]);

return (
<form onSubmit={isEdit ? handleSubmit(onEdit) : handleSubmit(onSubmit)}>
<form onSubmit={type === 'edit' ? handleSubmit(onEdit) : handleSubmit(onSubmit)}>
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-3">
<div className="flex justify-between">
Expand Down Expand Up @@ -105,7 +147,6 @@ export default function CreateCrewForm({
)}
/>
</div>

<div className="flex flex-col gap-3">
<label
htmlFor="crew-category"
Expand All @@ -123,7 +164,7 @@ export default function CreateCrewForm({
{...field}
variant="default"
inWhere="form"
placeholder="메인 카테고리"
placeholder={isEdit ? field.value : '메인 카테고리'}
data={categoryData.map((category) => category.title)}
className="flex-1"
onChange={(value) => {
Expand All @@ -142,7 +183,7 @@ export default function CreateCrewForm({
{...field}
variant="default"
inWhere="form"
placeholder="세부 카테고리"
placeholder={isEdit && field.value ? field.value : '세부 카테고리'}
data={categoryData[categoryIndex]?.items || []}
className="flex-1"
error={errors.subCategory?.message}
Expand Down Expand Up @@ -179,6 +220,7 @@ export default function CreateCrewForm({
isEdit={isEdit}
sample={ImgCrewSampleUrls}
onChange={(newValue) => {
handleFileChange(newValue, field.onChange);
field.onChange(newValue);
trigger('imageUrl');
}}
Expand All @@ -205,7 +247,7 @@ export default function CreateCrewForm({
{...field}
variant="default"
inWhere="form"
placeholder="특별시/도"
placeholder={isEdit ? field.value : '특별시/도'}
data={regionData.map((region) => region.main)}
className="flex-1"
onChange={(value) => {
Expand All @@ -225,7 +267,7 @@ export default function CreateCrewForm({
{...field}
variant="default"
inWhere="form"
placeholder="시/군/구"
placeholder={isEdit && field.value ? field.value : '시/군/구'}
data={regionData[regionIndex]?.areas || []}
className="flex-1"
error={errors.subLocation?.message}
Expand Down Expand Up @@ -271,7 +313,7 @@ export default function CreateCrewForm({
크루 소개
</label>
<span>
<span className="text-blue-500">{introduce.length}</span>/100
<span className="text-blue-500">{introduce?.length}</span>/100
</span>
</div>
<Controller
Expand All @@ -293,11 +335,11 @@ export default function CreateCrewForm({
disabled={!isValid || isSubmitting}
className="btn-filled h-11 flex-1 text-base font-medium disabled:bg-gray-200"
>
{isEdit ? '수정' : '확인'}
{type === 'create' ? '만들기' : '수정'}
</Button>
<Button
type="button"
onClick={() => router.back()}
onClick={handleClear}
className="btn-outlined h-11 w-29.5 flex-1 text-base font-medium text-blue-500"
>
취소
Expand Down
Loading