Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b32a804
✨ Feat: 크루 생성 초안 작성
yulrang Nov 13, 2024
d0be986
Merge branch 'develop' into Feat/110/CreateCrewAPI
yulrang Nov 14, 2024
91249ed
🐛 Fix: sample 이미지 동작 수정
yulrang Nov 14, 2024
e857a5d
🐛 Fix: 오타 수정
yulrang Nov 14, 2024
2d9bb31
♻️ Refactor: useMutation 사용
yulrang Nov 14, 2024
b206203
🐛 Fix: create 동작개선
yulrang Nov 14, 2024
613be1a
🐛 Fix: 오류 수정
yulrang Nov 14, 2024
7d062fb
🐛 Fix: file input 수정
yulrang Nov 14, 2024
8fef8ea
Merge branch 'develop' into Feat/110/CreateCrewAPI
yulrang Nov 14, 2024
e739a1a
🐛 Fix: 오류 수정
yulrang Nov 14, 2024
901ea86
🚨 Fix: 빌드 오류 수정
yulrang Nov 14, 2024
74b19b6
Fix: 파일인풋 동작 수정
yulrang Nov 14, 2024
d999f45
🐛 Fix: 에러 토스트 수정
yulrang Nov 14, 2024
031c7ca
🐛 Fix: 최신순,인기순 동작 수정
yulrang Nov 14, 2024
824ba7f
✨ Feat: 로딩처리 추가
yulrang Nov 14, 2024
6e86fa5
🐛 Fix : 오류 처리 추가
yulrang Nov 14, 2024
99719e9
Merge branch 'develop' into Feat/110/CreateCrewAPI
yulrang Nov 14, 2024
1666eb9
💄 Design: 검색버튼 디자인 적용
yulrang Nov 14, 2024
2425584
✨ Feat: introduce 항목 추가
yulrang Nov 15, 2024
7a03e8d
Merge branch 'develop' into Feat/110/CreateCrewAPI
yulrang Nov 15, 2024
a8db729
🚨 Fix: 빌드 오류 수정
yulrang Nov 15, 2024
ef8fc4f
Merge branch 'Feat/110/CreateCrewAPI' of https://github.com/CodeitFES…
yulrang Nov 15, 2024
6d9c094
♻️ Refactor: 코드 리팩토링
yulrang Nov 15, 2024
1bc4a75
Refactor: validateFile 분리
yulrang Nov 15, 2024
69e8afc
Merge branch 'develop' into Feat/110/CreateCrewAPI
yulrang Nov 15, 2024
2cc00f1
🐛 Fix : 오류처리 수정, 토스트 추가
yulrang Nov 15, 2024
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
7 changes: 6 additions & 1 deletion public/assets/images/crew-sample/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,10 @@ import ImgCrewSample02 from './crew-sample-2.jpg';
import ImgCrewSample03 from './crew-sample-3.jpg';

const ImgCrewSamples = [ImgCrewSample01, ImgCrewSample02, ImgCrewSample03];
const ImgCrewSampleUrls = [
'https://crewcrew.s3.ap-northeast-2.amazonaws.com/crew/0e05d971-15a8-4a32-bf03-80d12cae392e',
'https://crewcrew.s3.ap-northeast-2.amazonaws.com/crew/eb35c35d-829a-402b-8019-29e42f91589f',
'https://crewcrew.s3.ap-northeast-2.amazonaws.com/crew/471b3b3b-b23c-48e8-8e6b-9a7ec31e1917',
];

export default ImgCrewSamples;
export default ImgCrewSampleUrls;
7 changes: 6 additions & 1 deletion public/assets/images/gathering-sample/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,10 @@ import ImgGatheringSample02 from './gathering-sample-2.jpg';
import ImgGatheringSample03 from './gathering-sample-3.jpg';

const ImgGatheringSamples = [ImgGatheringSample01, ImgGatheringSample02, ImgGatheringSample03];
const ImgGatheringSampleUrls = [
'https://crewcrew.s3.ap-northeast-2.amazonaws.com/crew/1ff61110-e238-4fd6-8736-e6f03483f4df',
'https://crewcrew.s3.ap-northeast-2.amazonaws.com/crew/85ad6a48-a1cb-4c8f-b713-94b9e18b023a',
'https://crewcrew.s3.ap-northeast-2.amazonaws.com/crew/60bc093c-cb3a-4628-93b9-fbf19e47a1ac',
];

export default ImgGatheringSamples;
export default ImgGatheringSampleUrls;
28 changes: 28 additions & 0 deletions src/_apis/crew/crew.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { fetchApi } from '@/src/utils/api';
import { CreateCrewRequestTypes, CreateCrewResponseTypes } from '@/src/types/create-crew';

export async function createCrew(data: CreateCrewRequestTypes) {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

함수 시그니처 업데이트 필요

타입 이름 변경에 맞춰 함수 파라미터 타입을 업데이트해야 합니다.

-export async function createCrew(data: CreateCrewRequestTypes) {
+export async function createCrew(data: CreateCrewFormTypes) {
📝 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 createCrew(data: CreateCrewRequestTypes) {
export async function createCrew(data: CreateCrewFormTypes) {

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),
},
);

if (!response.data) {
throw new Error('Failed to create crew: No data received');
}

return response.data;
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
}
return null;
}
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. 에러 발생 시 단순히 로깅만 하고 null을 반환하는 것은 적절하지 않습니다.
  2. 입력값 검증이 누락되어 있습니다.
  3. 에러 메시지가 영어로 되어있는데, 한글로 통일하는 것이 좋습니다.

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

-export async function createCrew(data: CreateCrewRequestTypes) {
+export async function createCrew(data: CreateCrewFormTypes) {
+  // 입력값 검증
+  if (!data.title || !data.content) {
+    throw new Error('필수 입력값이 누락되었습니다.');
+  }
+
   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),
       },
     );

     if (!response.data) {
-      throw new Error('Failed to create crew: No data received');
+      throw new Error('크루 생성 응답 데이터가 없습니다.');
     }

     return response.data;
   } catch (error) {
-    // eslint-disable-next-line no-console
-    console.error(error);
+    throw new Error(`크루 생성 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`);
   }
-  return null;
 }
📝 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 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),
},
);
if (!response.data) {
throw new Error('Failed to create crew: No data received');
}
return response.data;
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
}
return null;
}
export async function createCrew(data: CreateCrewFormTypes) {
// 입력값 검증
if (!data.title || !data.content) {
throw new Error('필수 입력값이 누락되었습니다.');
}
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),
},
);
if (!response.data) {
throw new Error('크루 생성 응답 데이터가 없습니다.');
}
return response.data;
} catch (error) {
throw new Error(`크루 생성 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`);
}
}

42 changes: 42 additions & 0 deletions src/_apis/image/get-image-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { fetchApi } from '@/src/utils/api';
import Toast from '@/src/components/common/toast';
import { GetImageUrlResponseTypes } from '@/src/types/create-crew';

export async function getImageUrl(
file: File | string | null,
type: 'MEMBER' | 'CREW' | 'GATHERING',
) {
Comment on lines +6 to +9
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 파라미터의 유효성을 검증하는 로직이 없습니다. 잘못된 타입이 전달될 경우 런타임 에러가 발생할 수 있습니다.

다음과 같이 타입 검증을 추가하는 것을 제안드립니다:

+const VALID_TYPES = ['MEMBER', 'CREW', 'GATHERING'] as const;
+type ImageType = typeof VALID_TYPES[number];
+
 export async function getImageUrl(
   file: File | string | null,
-  type: 'MEMBER' | 'CREW' | 'GATHERING',
+  type: ImageType,
 ) {
+  if (!VALID_TYPES.includes(type)) {
+    throw new Error(`유효하지 않은 이미지 타입입니다: ${type}`);
+  }
📝 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 getImageUrl(
file: File | string | null,
type: 'MEMBER' | 'CREW' | 'GATHERING',
) {
const VALID_TYPES = ['MEMBER', 'CREW', 'GATHERING'] as const;
type ImageType = typeof VALID_TYPES[number];
export async function getImageUrl(
file: File | string | null,
type: ImageType,
) {
if (!VALID_TYPES.includes(type)) {
throw new Error(`유효하지 않은 이미지 타입입니다: ${type}`);
}

const formData = new FormData();
if (file instanceof File) {
try {
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const ALLOWED_TYPES = ['image/jpeg', 'image/png'];

if (file.size > MAX_FILE_SIZE) {
Toast({ message: '파일 크기는 5MB를 초과할 수 없습니다.', type: 'error' });
throw new Error('파일 크기 초과');
}
if (!ALLOWED_TYPES.includes(file.type)) {
Toast({ message: '지원하지 않는 파일 형식입니다.', type: 'error' });
throw new Error('지원하지 않는 파일 형식');
}
formData.append('file', file);

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

상수 정의 위치 및 오류 메시지 개선이 필요합니다.

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

+const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
+const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'] as const;
+const FILE_TYPE_ERROR = '지원하지 않는 파일 형식입니다. (지원 형식: JPG, PNG, GIF, WebP)';
+const FILE_SIZE_ERROR = '파일 크기는 5MB를 초과할 수 없습니다.';
+
 export async function getImageUrl(
   file: File | string | null,
   type: 'MEMBER' | 'CREW' | 'GATHERING',
 ) {
   const formData = new FormData();
   if (file instanceof File) {
     try {
-      const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
-      const ALLOWED_TYPES = ['image/jpeg', 'image/png'];
-
       if (file.size > MAX_FILE_SIZE) {
-        Toast({ message: '파일 크기는 5MB를 초과할 수 없습니다.', type: 'error' });
-        throw new Error('파일 크기 초과');
+        Toast({ message: FILE_SIZE_ERROR, type: 'error' });
+        throw new Error(`File size exceeds limit: ${file.size} bytes`);
       }
       if (!ALLOWED_TYPES.includes(file.type)) {
-        Toast({ message: '지원하지 않는 파일 형식입니다.', type: 'error' });
-        throw new Error('지원하지 않는 파일 형식');
+        Toast({ message: FILE_TYPE_ERROR, type: 'error' });
+        throw new Error(`Unsupported file type: ${file.type}`);
       }
📝 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
try {
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const ALLOWED_TYPES = ['image/jpeg', 'image/png'];
if (file.size > MAX_FILE_SIZE) {
Toast({ message: '파일 크기는 5MB를 초과할 수 없습니다.', type: 'error' });
throw new Error('파일 크기 초과');
}
if (!ALLOWED_TYPES.includes(file.type)) {
Toast({ message: '지원하지 않는 파일 형식입니다.', type: 'error' });
throw new Error('지원하지 않는 파일 형식');
}
formData.append('file', file);
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'] as const;
const FILE_TYPE_ERROR = '지원하지 않는 파일 형식입니다. (지원 형식: JPG, PNG, GIF, WebP)';
const FILE_SIZE_ERROR = '파일 크기는 5MB를 초과할 수 없습니다.';
try {
if (file.size > MAX_FILE_SIZE) {
Toast({ message: FILE_SIZE_ERROR, type: 'error' });
throw new Error(`File size exceeds limit: ${file.size} bytes`);
}
if (!ALLOWED_TYPES.includes(file.type)) {
Toast({ message: FILE_TYPE_ERROR, type: 'error' });
throw new Error(`Unsupported file type: ${file.type}`);
}
formData.append('file', file);

const response: { data: GetImageUrlResponseTypes } = await fetchApi(
`/api/images?type=${type}`,
{
method: 'POST',
body: formData,
},
);
if (!response.data) {
throw new Error('Failed to get image: No data received');
}
return response.data;
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 응답 처리 개선이 필요합니다.

응답 처리 시 더 구체적인 에러 타입과 메시지가 필요합니다.

       const response: { data: GetImageUrlResponseTypes } = await fetchApi(
         `/api/images?type=${type}`,
         {
           method: 'POST',
           body: formData,
         },
       );
-      if (!response.data) {
-        throw new Error('Failed to get image: No data received');
+      if (!response?.data?.imageUrl) {
+        throw new Error(
+          `이미지 업로드 실패: 서버 응답 누락 (type: ${type}, size: ${file.size})`
+        );
       }
       return response.data;
📝 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 response: { data: GetImageUrlResponseTypes } = await fetchApi(
`/api/images?type=${type}`,
{
method: 'POST',
body: formData,
},
);
if (!response.data) {
throw new Error('Failed to get image: No data received');
}
return response.data;
const response: { data: GetImageUrlResponseTypes } = await fetchApi(
`/api/images?type=${type}`,
{
method: 'POST',
body: formData,
},
);
if (!response?.data?.imageUrl) {
throw new Error(
`이미지 업로드 실패: 서버 응답 누락 (type: ${type}, size: ${file.size})`
);
}
return response.data;

} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
}
}
return null;
}
1 change: 1 addition & 0 deletions src/_queries/crew/crew-list-queries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export function useGetCrewListQuery({
const { size, sort = ['string'] } = pageable;
return {
queryKey: [
'crewLists',
condition.keyword,
condition.mainLocation,
condition.mainCategory,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { Meta, StoryFn } from '@storybook/react';
import { CreateCrewRequestTypes } from '@/src/types/create-crew';
import CreateCrewForm, { CreateCrewFormTypes } from '.';
import { CreateCrewFormTypes, CreateCrewRequestTypes } from '@/src/types/create-crew';
import CreateCrewForm from '.';

const initialValue: CreateCrewRequestTypes = {
title: '',
mainCategory: '',
subCategory: '',
imageUrl: null,
imageUrl: '',
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

초기값 타입 일관성 개선 필요

imageUrl의 초기값이 빈 문자열('')로 설정되어 있습니다. 이는 다음과 같은 고려사항이 있습니다:

  1. API 요청 시 빈 문자열과 null 값의 처리 방식이 다를 수 있습니다.
  2. 폼 검증 시 빈 문자열 처리에 대한 로직이 필요할 수 있습니다.

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

-  imageUrl: '',
+  imageUrl: null,

또는 타입 정의에서 명시적으로 빈 문자열을 허용하도록 수정이 필요합니다.

📝 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
imageUrl: '',
imageUrl: null,

mainLocation: '',
subLocation: '',
totalCount: 0,
introduce: '',
};

export default {
Expand Down
74 changes: 48 additions & 26 deletions src/app/(crew)/crew/_components/create-crew-form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,23 @@ 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 { CreateCrewRequestTypes } from '@/src/types/create-crew';
import ImgCrewSamples from '@/public/assets/images/crew-sample';
import Textarea from '@/src/components/common/input/textarea';
import { CreateCrewFormTypes } from '@/src/types/create-crew';
import ImgCrewSampleUrls from '@/public/assets/images/crew-sample';

export interface CreateCrewFormTypes {
data: CreateCrewRequestTypes;
export interface CreateCrewFormProps {
data: CreateCrewFormTypes;
isEdit?: boolean;
onEdit?: (data: CreateCrewRequestTypes) => void;
onSubmit?: (data: CreateCrewRequestTypes) => void;
onEdit?: (data: CreateCrewFormTypes) => void;
onSubmit?: (data: CreateCrewFormTypes) => void;
}

export default function CreateCrewForm({
isEdit = false,
onEdit = () => {},
onSubmit = () => {},
data,
}: CreateCrewFormTypes) {
}: CreateCrewFormProps) {
const router = useRouter();
const {
control,
Expand All @@ -34,7 +35,7 @@ export default function CreateCrewForm({
trigger,
clearErrors,
formState: { errors, isValid, isSubmitting },
} = useForm<CreateCrewRequestTypes>({
} = useForm<CreateCrewFormTypes>({
defaultValues: data,
mode: 'onBlur',
});
Expand All @@ -43,24 +44,24 @@ export default function CreateCrewForm({
const [regionIndex, setRegionIndex] = useState(0);

const title = useWatch({ control, name: 'title' });
// mainCategory와 mainLocation 값의 변화를 감지하여 인덱스를 설정
const mainCategory = useWatch({ control, name: 'mainCategory' });
const mainLocation = useWatch({ control, name: 'mainLocation' });
const introduce = useWatch({ control, name: 'introduce' });

const handleMainCategoryChange = (newValue: string | null) => {
setValue('mainCategory' as const, newValue as CreateCrewRequestTypes['mainCategory']);
setValue('subCategory' as const, null as CreateCrewRequestTypes['subCategory']);
setValue('mainCategory', newValue || '');
setValue('subCategory', null);
Comment on lines +52 to +53
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

카테고리와 지역 선택 로직의 개선이 필요합니다.

현재 구현에서 몇 가지 개선이 필요한 부분이 있습니다:

  1. findIndex가 -1을 반환할 경우에 대한 처리가 없습니다.
  2. 값이 없을 때 빈 문자열로 설정하는 것보다 null로 설정하는 것이 더 명확할 수 있습니다.

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

const handleMainCategoryChange = (newValue: string | null) => {
-  setValue('mainCategory', newValue || '');
+  setValue('mainCategory', newValue);
  setValue('subCategory', null);
  clearErrors('subCategory');
};

const handleMainLocationChange = (newValue: string | null) => {
-  setValue('mainLocation', newValue || '');
+  setValue('mainLocation', newValue);
  setValue('subLocation', null);
  clearErrors('subLocation');
};

useEffect(() => {
-  setCategoryIndex(categoryData.findIndex((category) => category.title.label === mainCategory));
-  setRegionIndex(regionData.findIndex((region) => region.main.label === mainLocation));
+  const newCategoryIndex = categoryData.findIndex((category) => category.title.label === mainCategory);
+  const newRegionIndex = regionData.findIndex((region) => region.main.label === mainLocation);
+  setCategoryIndex(newCategoryIndex !== -1 ? newCategoryIndex : 0);
+  setRegionIndex(newRegionIndex !== -1 ? newRegionIndex : 0);
}, [mainCategory, mainLocation]);

Also applies to: 59-60, 64-65

clearErrors('subCategory');
};

const handleMainLocationChange = (newValue: string | null) => {
setValue('mainLocation' as const, newValue as CreateCrewRequestTypes['mainLocation']);
setValue('subLocation' as const, null as CreateCrewRequestTypes['subLocation']);
setValue('mainLocation', newValue || '');
setValue('subLocation', null);
clearErrors('subLocation');
};
useEffect(() => {
setCategoryIndex(categoryData.findIndex((category) => category.title.value === mainCategory));
setRegionIndex(regionData.findIndex((region) => region.main.value === mainLocation));
setCategoryIndex(categoryData.findIndex((category) => category.title.label === mainCategory));
setRegionIndex(regionData.findIndex((region) => region.main.label === mainLocation));
}, [mainCategory, mainLocation]);

return (
Expand Down Expand Up @@ -162,24 +163,24 @@ export default function CreateCrewForm({
required: '이미지를 선택해주세요.',
validate: {
fileSize: (file) =>
file && file instanceof File && file.size <= 5242880
? true
: '파일 크기는 5MB 이하여야 합니다.',
file && file instanceof File
? file.size <= 5242880 || '파일 크기는 5MB 이하여야 합니다.'
: true, // 문자열인 경우 크기 검사를 건너뜁니다.
fileType: (file) =>
file &&
file instanceof File &&
['image/jpeg', 'image/jpg', 'image/png'].includes(file.type)
? true
: 'JPG, PNG 파일만 업로드 가능합니다.',
file && file instanceof File
? ['image/jpeg', 'image/jpg', 'image/png'].includes(file.type) ||
'JPG, PNG 파일만 업로드 가능합니다.'
: true, // 문자열인 경우 파일 타입 검사를 건너뜁니다.
Comment on lines +166 to +173
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. 악성 파일 업로드 방지를 위한 추가 보안 조치

다음과 같은 개선된 검증 로직을 제안드립니다:

const validateFile = (file: File | string) => {
  // 문자열(기존 URL)인 경우 검증 통과
  if (typeof file === 'string') return true;
  
  // 파일인 경우 상세 검증 수행
  if (file instanceof File) {
    // 파일 확장자 검증
    const extension = file.name.split('.').pop()?.toLowerCase();
    const validExtensions = ['jpg', 'jpeg', 'png'];
    
    if (!extension || !validExtensions.includes(extension)) {
      return 'JPG, PNG 파일만 업로드 가능합니다.';
    }
    
    // 파일 크기 검증
    if (file.size > 5 * 1024 * 1024) {
      return '파일 크기는 5MB 이하여야 합니다.';
    }
    
    // MIME 타입 검증
    if (!['image/jpeg', 'image/jpg', 'image/png'].includes(file.type)) {
      return '올바른 이미지 형식이 아닙니다.';
    }
    
    return true;
  }
  
  return '올바르지 않은 파일입니다.';
};

},
}}
render={({ field }) => (
<FileInputWrap
{...field}
sample={ImgCrewSamples}
isEdit={isEdit}
sample={ImgCrewSampleUrls}
onChange={(newValue) => {
field.onChange(newValue);
if (newValue instanceof File) trigger('imageUrl');
trigger('imageUrl');
}}
/>
)}
Expand Down Expand Up @@ -264,7 +265,28 @@ export default function CreateCrewForm({
)}
/>
</div>

<div className="flex flex-col gap-3">
<div className="flex justify-between">
<label htmlFor="gathering-introduce" className="text-base font-semibold text-gray-800">
크루 소개
</label>
<span>
<span className="text-blue-500">{introduce.length}</span>/100
</span>
</div>
<Controller
name="introduce"
control={control}
render={({ field }) => (
<Textarea
{...field}
placeholder="크루 소개글을 100자 이내로 입력해주세요."
maxLength={100}
inputClassNames="h-40 py-2.5 px-4 bg-gray-100 placeholder:text-gray-400 font-pretendard text-base font-medium rounded-xl"
/>
)}
/>
</div>
<div className="flex justify-between gap-4 pt-18">
<Button
type="submit"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Button, Modal, ScrollArea } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { Meta, StoryFn } from '@storybook/react';
import { CreateGatheringRequestType } from '@/src/types/gathering-data';
import { CreateGatheringFormTypes } from '@/src/types/gathering-data';
import CreateGatheringForm from '.';

const initialValue: CreateGatheringRequestType = {
const initialValue: CreateGatheringFormTypes = {
title: '',
introduce: '',
dateTime: '',
Expand All @@ -30,7 +30,7 @@ export default {
},
} as Meta<typeof CreateGatheringForm>;

const Template: StoryFn<CreateGatheringRequestType> = function CreateCrewPageStory() {
const Template: StoryFn<CreateGatheringFormTypes> = function CreateCrewPageStory() {
const [createModalOpened, { open: openCreateModal, close: closeCreateModal }] =
useDisclosure(false);

Expand Down
35 changes: 17 additions & 18 deletions src/app/(crew)/crew/_components/create-gathering-form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import DateTimePicker from '@/src/components/common/input/date-time-picker';
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 { CreateGatheringRequestType } from '@/src/types/gathering-data';
import ImgGatheringSamples from '@/public/assets/images/gathering-sample';
import { CreateGatheringFormTypes } from '@/src/types/gathering-data';
import ImgGatheringSampleUrls from '@/public/assets/images/gathering-sample';

export interface CreateGatheringFormTypes {
data: CreateGatheringRequestType;
export interface CreateGatheringFormProps {
data: CreateGatheringFormTypes;
isEdit?: boolean;
onEdit?: (data: CreateGatheringRequestType) => void;
onSubmit?: (data: CreateGatheringRequestType) => void;
onEdit?: (data: CreateGatheringFormTypes) => void;
onSubmit?: (data: CreateGatheringFormTypes) => void;
onClose: () => void;
}

Expand All @@ -24,13 +24,13 @@ export default function CreateGatheringForm({
onSubmit = () => {},
onClose,
data,
}: CreateGatheringFormTypes) {
}: CreateGatheringFormProps) {
const {
control,
handleSubmit,
trigger,
formState: { errors, isValid, isSubmitting },
} = useForm<CreateGatheringRequestType>({
} = useForm<CreateGatheringFormTypes>({
defaultValues: data,
mode: 'onBlur',
});
Expand Down Expand Up @@ -88,24 +88,23 @@ export default function CreateGatheringForm({
required: '이미지를 선택해주세요.',
validate: {
fileSize: (file) =>
file && file instanceof File && file.size <= 5242880
? true
: '파일 크기는 5MB 이하여야 합니다.',
file && file instanceof File
? file.size <= 5242880 || '파일 크기는 5MB 이하여야 합니다.'
: true, // 문자열인 경우 크기 검사를 건너뜁니다.
fileType: (file) =>
file &&
file instanceof File &&
['image/jpeg', 'image/jpg', 'image/png'].includes(file.type)
? true
: 'JPG, PNG 파일만 업로드 가능합니다.',
file && file instanceof File
? ['image/jpeg', 'image/jpg', 'image/png'].includes(file.type) ||
'JPG, PNG 파일만 업로드 가능합니다.'
: true, // 문자열인 경우 파일 타입 검사를 건너뜁니다.
},
}}
render={({ field }) => (
<FileInputWrap
{...field}
sample={ImgGatheringSamples}
sample={ImgGatheringSampleUrls}
onChange={(newValue) => {
field.onChange(newValue);
if (newValue instanceof File) trigger('imageUrl');
trigger('imageUrl');
}}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
'use client';

import { CreateGatheringRequestType } from '@/src/types/gathering-data';
import { CreateGatheringFormTypes } from '@/src/types/gathering-data';
import CreateGatheringModalPresenter from './presenter';

export interface CreateGatheringModalContainerProps {
opened: boolean;
close: () => void;
data: CreateGatheringRequestType;
data: CreateGatheringFormTypes;
}

export default function CreateGatheringModalContainer({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Modal, ScrollArea } from '@mantine/core';
import CreateGatheringForm from '@/src/app/(crew)/crew/_components/create-gathering-form';
import { CreateGatheringRequestType } from '@/src/types/gathering-data';
import { CreateGatheringFormTypes } from '@/src/types/gathering-data';

export interface GatheringDetailModalProps {
opened: boolean;
onClose: () => void;
onSubmit: () => void;
onEdit: () => void;
data: CreateGatheringRequestType;
data: CreateGatheringFormTypes;
}

export default function CreateGatheringModalPresenter({
Expand Down
Loading