Skip to content

Conversation

@yulrang
Copy link
Contributor

@yulrang yulrang commented Nov 14, 2024

🔖 Issue Ticket

#110

✍️ Description

크루 생성하기 페이지 API 연결했습니다~!

  • _api/crew/crew.ts 에 api 넣어놓았습니다
  • introduce 추가
  • 생성하기 실패시 Toast 띄우고 리다이렉트 X
  • 생성하기 성공시 해당 크루 상세페이지로 이동

✅ Checklist

PR

  • Branch Convention 확인

    feat/ 피쳐, fix/ 버그 수정, refactor/ 개선

  • Base Branch 확인
  • 적절한 Label 지정
  • Assignee 및 Reviewer 지정

Test

  • 로컬 작동 확인

Screenshot 2024-11-14 at 17 22 31

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 원격 URL에서 이미지를 불러오는 새로운 이미지 상수 ImgCrewSampleUrlsImgGatheringSampleUrls 추가.
    • 크루 및 모임 생성 양식에서 새로운 데이터 구조 CreateCrewFormTypesCreateGatheringFormTypes 도입.
    • 이미지 업로드 및 URL 검색을 위한 비동기 함수 getImageUrlcreateCrew 추가.
    • 로딩 상태를 표시하는 Loader 컴포넌트 도입.
    • 데이터 페칭 중 사용자 경험 향상을 위한 로딩 인디케이터 개선.
    • 파일 유효성 검사를 위한 새로운 함수 validateFile 추가.
  • 버그 수정

    • 오류 처리 및 데이터 유효성 검사 로직 개선.
  • 문서화

    • 타입 정의 및 인터페이스 명칭 변경에 대한 업데이트.

@coderabbitai
Copy link

coderabbitai bot commented Nov 14, 2024

Walkthrough

이 변경 사항은 여러 파일에서 로컬 이미지 가져오기를 원격 URL로 대체하는 새로운 상수(ImgCrewSampleUrls, ImgGatheringSampleUrls)를 도입합니다. 또한 새로운 비동기 함수(createCrew, getImageUrl)가 추가되어 API 호출을 통해 데이터를 처리합니다. 여러 컴포넌트에서 데이터 구조가 변경되었으며, 타입스크립트 인터페이스가 리팩토링되었습니다. 전반적으로 코드의 유지 관리성과 명확성이 향상되었습니다.

Changes

파일 변경 요약
public/assets/images/crew-sample/index.tsx 새로운 상수 ImgCrewSampleUrls 추가 및 기본 내보내기 수정
public/assets/images/gathering-sample/index.tsx 새로운 상수 ImgGatheringSampleUrls 추가 및 기본 내보내기 수정
src/_apis/crew/crew.ts 비동기 함수 createCrew 추가
src/_apis/image/get-image-url.ts 비동기 함수 getImageUrl 추가
src/_queries/crew/crew-list-queries.tsx useGetCrewListQuery 함수의 시그니처 업데이트 및 오류 처리 개선
src/app/(crew)/crew/_components/create-crew-form/create-crew-form.stories.tsx 가져오기 문장 통합 및 초기값 객체 수정
src/app/(crew)/crew/_components/create-crew-form/index.tsx CreateCrewRequestTypesCreateCrewFormTypes로 변경
src/app/(crew)/crew/_components/create-gathering-form/create-gathering-form.stories.tsx 초기값 변수 타입 업데이트
src/app/(crew)/crew/_components/create-gathering-form/index.tsx CreateGatheringRequestTypeCreateGatheringFormTypes로 변경
src/app/(crew)/crew/_components/create-gathering-modal/container.tsx CreateGatheringModalContainerPropsdata 타입 변경
src/app/(crew)/crew/_components/create-gathering-modal/presenter.tsx GatheringDetailModalPropsdata 타입 변경
src/app/(crew)/crew/create/page.tsx initialValue 타입 변경 및 비동기 작업 추가
src/app/(crew)/crew/detail/[id]/_components/create-gathering.tsx initialValue 타입 변경
src/app/(crew)/crew/detail/[id]/edit/page.tsx crew ID를 기반으로 crew 세부정보를 가져오는 로직 추가
src/app/(crew)/page.tsx 상태 초기화 및 사용자 인터페이스 개선
src/components/common/input/drop-down/index.tsx 내부 상태 관리 및 이벤트 처리 수정
src/components/common/input/file-input-wrap/file-input/index.tsx FileInputProps 인터페이스 수정 및 내부 로직 조정
src/components/common/input/file-input-wrap/file-sample/file-sample.stories.tsx 상태 및 함수 매개변수 타입 업데이트
src/components/common/input/file-input-wrap/file-sample/index.tsx FileSampleProps 인터페이스 수정
src/components/common/input/file-input-wrap/index.tsx FileInputProps 인터페이스 수정 및 FileInputWrap 로직 조정
src/types/create-crew.d.ts CreateCrewRequestTypes 인터페이스 이름 변경 및 새로운 인터페이스 추가
src/types/gathering-data.d.ts 여러 인터페이스 수정 및 새로운 인터페이스 추가
src/utils/api.ts ApiError 클래스 및 fetchApi 함수의 오류 처리 개선
tailwind.config.ts 최소 너비 및 높이 유틸리티 추가
src/app/(crew)/my-crew/hosted/page.tsx 로딩 표시기 조건 수정
src/app/(crew)/my-crew/joined/page.tsx CrewCardList의 props 간소화 및 로딩 표시기 조건 수정
src/components/common/crew-list/crew-card-list.stories.tsx useInfiniteScroll 훅의 반환 값 수정 및 렌더링 로직 개선
src/components/common/crew-list/crew-card-list.tsx isFetchingNextPage prop 제거 및 렌더링 로직 수정

Possibly related PRs

Suggested reviewers

  • HaeJungg
  • minkyung5x5
  • Rangbyeolang

🐇 새로운 이미지 URL로
크루 샘플을 바꾸었네,
비동기 함수로
데이터를 가져오고,
타입도 정리했지,
코드가 더 깔끔해졌어!
🐇

Warning

There were issues while running some tools. Please review the errors and either fix the tool’s configuration or disable the tool if it’s a critical failure.

🔧 eslint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/_apis/crew/crew.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

ESLint couldn't determine the plugin "react-hooks" uniquely.

  • /node_modules/eslint-plugin-react-hooks/index.js (loaded in ".eslintrc.js")
  • /node_modules/eslint-config-next/node_modules/eslint-plugin-react-hooks/index.js (loaded in ".eslintrc.js » eslint-config-next/core-web-vitals » /node_modules/eslint-config-next/index.js » plugin:react-hooks/recommended")

Please remove the "plugins" setting from either config or remove either plugin installation.

If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@yulrang yulrang linked an issue Nov 14, 2024 that may be closed by this pull request
3 tasks
@github-actions
Copy link

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 18

🧹 Outside diff range and nitpick comments (23)
public/assets/images/crew-sample/index.tsx (2)

Line range hint 1-5: 사용하지 않는 import문과 배열 제거 필요

ImgCrewSampleUrls로 전환했으므로 더 이상 사용하지 않는 로컬 이미지 import문과 ImgCrewSamples 배열을 제거하는 것이 좋습니다.

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

-import ImgCrewSample01 from './crew-sample-1.jpg';
-import ImgCrewSample02 from './crew-sample-2.jpg';
-import ImgCrewSample03 from './crew-sample-3.jpg';

-const ImgCrewSamples = [ImgCrewSample01, ImgCrewSample02, ImgCrewSample03];

6-10: S3 URL을 환경 변수로 관리 필요

하드코딩된 S3 URL은 유지보수와 환경 설정 변경에 어려움이 있을 수 있습니다. 기본 URL을 환경 변수로 분리하는 것이 좋습니다.

다음과 같은 방식을 제안합니다:

+const S3_BUCKET_URL = process.env.NEXT_PUBLIC_S3_BUCKET_URL;
+
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',
+  `${S3_BUCKET_URL}/crew/0e05d971-15a8-4a32-bf03-80d12cae392e`,
+  `${S3_BUCKET_URL}/crew/eb35c35d-829a-402b-8019-29e42f91589f`,
+  `${S3_BUCKET_URL}/crew/471b3b3b-b23c-48e8-8e6b-9a7ec31e1917`,
];
public/assets/images/gathering-sample/index.tsx (2)

Line range hint 1-5: 사용하지 않는 import문과 변수 제거 필요

더 이상 사용하지 않는 로컬 이미지 import문과 ImgGatheringSamples 변수를 제거해 주세요.

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

-import ImgGatheringSample01 from './gathering-sample-1.jpg';
-import ImgGatheringSample02 from './gathering-sample-2.jpg';
-import ImgGatheringSample03 from './gathering-sample-3.jpg';

-const ImgGatheringSamples = [ImgGatheringSample01, ImgGatheringSample02, ImgGatheringSample03];

6-12: 이미지 로딩 실패 대응 전략 추가 고려

외부 리소스를 사용하는 만큼, 이미지 로딩 실패 시의 대체 방안(fallback)을 고려해 보시는 것을 추천드립니다.

이미지 로딩 실패 시 대체 이미지를 표시하는 컴포넌트를 구현하는데 도움이 필요하시다면 말씀해 주세요.

src/types/create-crew.d.ts (2)

24-26: 응답 타입들이 명확하게 정의되었습니다.

CreateCrewResponseTypesGetImageUrlResponseTypes 인터페이스가 각각의 API 응답에 맞게 잘 정의되어 있습니다.

하지만 추가적인 문서화가 있으면 좋을 것 같습니다:

+/** 크루 생성 API 응답 타입 */
 export interface CreateCrewResponseTypes {
   crewId: number;
 }

+/** 이미지 URL 조회 API 응답 타입 */
 export interface GetImageUrlResponseTypes {
   imageUrl: string;
 }

Also applies to: 28-30


3-3: 주석 업데이트가 필요합니다.

"NOTE : imageURL 임시로 File로 지정" 주석이 현재 구현 상태와 일치하지 않습니다. 이 주석은 제거하거나 현재 구현 상태를 정확히 반영하도록 업데이트해야 합니다.

src/app/(crew)/crew/_components/create-gathering-modal/container.tsx (1)

Line range hint 17-18: API 연결 구현 필요

PR의 목적이 API 연결인데 TODO 주석만 있고 실제 구현이 되어있지 않습니다. PR 설명에 따르면 crew API 연결이 이 PR의 주요 목적이므로, TODO 주석을 실제 구현으로 대체해야 합니다.

  • handleSubmit: 약속 만들기 API 연결
  • handleEdit: 약속 수정하기 API 연결

API 연결 구현에 도움이 필요하시다면 말씀해 주세요. _api/crew/crew.ts에 구현된 API를 활용하여 구현 방안을 제시해 드리겠습니다.

Also applies to: 21-22

src/_queries/crew/crew-list-queries.tsx (1)

Line range hint 14-18: 에러 처리가 강화되었으나 추가 개선이 필요합니다.

undefined 응답에 대한 에러 처리가 추가되었지만, 더 구체적인 에러 메시지가 필요해 보입니다.

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

- throw new Error('Response is null');
+ throw new Error('크루 목록을 가져오는데 실패했습니다. 서버 응답이 없습니다.');
src/app/(crew)/crew/_components/create-gathering-modal/presenter.tsx (1)

Line range hint 25-31: 스타일 관련 리팩토링 제안

모달 스타일링에 대한 개선 제안:

  1. 모달 사이즈를 상수로 분리
  2. 스타일 객체를 컴포넌트 외부로 분리하여 재사용성 향상

다음과 같이 리팩토링하는 것을 고려해보세요:

+const MODAL_SIZE = '520px';
+const modalStyles = {
+  root: { '--modal-size': MODAL_SIZE },
+  content: {
+    boxShadow: '0 25px 50px -12px rgba(0,0,0,0.1)',
+    borderRadius: '12px',
+    overflow: 'hidden',
+  },
+};

 export default function CreateGatheringModalPresenter({
   // ...
 }) {
   return (
     <Modal
       // ...
-      styles={{
-        root: { '--modal-size': '520px' },
-        content: {
-          boxShadow: '0 25px 50px -12px rgba(0,0,0,0.1)',
-          borderRadius: '12px',
-          overflow: 'hidden',
-        },
-      }}
+      styles={modalStyles}
src/types/gathering-data.d.ts (1)

29-36: 타입 제약 조건 강화 필요

현재 주석으로만 표시된 제약 조건들을 타입 시스템을 통해 강제하면 좋을 것 같습니다.

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

export interface CreateGatheringFormTypes {
-  title: string; // 20자 이내
+  title: LimitedString<20>; // 20자 이내
-  introduce: string; // 소개글 100자 이내
+  introduce: LimitedString<100>; // 소개글 100자 이내
   dateTime: string;
   location: string;
   totalCount: number;
   imageUrl: File | string | null;
}

+type LimitedString<N extends number> = string & {
+  readonly length: number extends N ? never : number;
+};
src/components/common/input/file-input-wrap/file-sample/index.tsx (1)

12-12: 매개변수의 기본값 설정을 고려해보세요.

선택적 프로퍼티 isEditvalue에 대한 기본값을 설정하면 컴포넌트의 안정성이 향상될 것 같습니다.

-export default function FileSample({ isEdit, value, image, isBlur, onChange }: FileSampleProps) {
+export default function FileSample({ isEdit = false, value = '', image, isBlur, onChange }: FileSampleProps) {
src/components/common/input/file-input-wrap/file-sample/file-sample.stories.tsx (1)

32-35: 파일 변경 핸들러 개선 제안

파일 URL 처리로 전환하는 것은 좋은 방향이지만, 에러 처리를 추가하면 좋을 것 같습니다.

다음과 같이 에러 처리를 추가하는 것을 고려해보세요:

-  const handleFileChange = (file: string | null) => {
-    setSelectedFile(file);
-    setIsBlur(false); // 파일 선택 시 블러 해제
-  };
+  const handleFileChange = (file: string | null) => {
+    try {
+      if (file && !file.startsWith('http')) {
+        throw new Error('올바른 URL 형식이 아닙니다');
+      }
+      setSelectedFile(file);
+      setIsBlur(false);
+    } catch (error) {
+      console.error('파일 URL 처리 중 오류:', error);
+      // TODO: 사용자에게 에러 표시
+    }
+  };
src/app/(crew)/crew/detail/[id]/edit/page.tsx (1)

Line range hint 28-30: PATCH API 구현이 필요합니다

handleEdit 함수에 다음 구현이 필요합니다:

  • PATCH API 연동
  • 로딩 상태 처리
  • 에러 처리
  • 성공/실패 시 사용자 피드백
  • 페이지 네비게이션 처리

PATCH API 구현에 도움이 필요하시다면 말씀해 주세요.

src/app/(crew)/crew/_components/create-gathering-form/create-gathering-form.stories.tsx (1)

Line range hint 33-65: 스토리 변형 추가를 제안드립니다.

현재는 기본 스토리만 구현되어 있습니다. 다음과 같은 추가 변형을 고려해보시면 좋을 것 같습니다:

  • 미리 채워진 폼 상태
  • 에러 상태
  • 로딩 상태

예시 구현:

export const Prefilled = Template.bind({});
Prefilled.args = {
  data: {
    title: '테스트 모임',
    introduce: '모임 소개입니다',
    dateTime: '2024-01-01T12:00',
    location: '서울시 강남구',
    totalCount: 4,
    imageUrl: 'https://example.com/image.jpg'
  }
};

export const WithError = Template.bind({});
WithError.parameters = {
  mockData: {
    error: new Error('API 에러 발생')
  }
};

export const Loading = Template.bind({});
Loading.parameters = {
  mockData: {
    loading: true
  }
};
src/components/common/input/file-input-wrap/index.tsx (2)

8-11: 타입 정의에 대한 문서화가 필요합니다

인터페이스의 각 프로퍼티에 대한 JSDoc 문서화를 추가하면 좋겠습니다. 특히 isEdit의 용도와 valuestring 타입일 때의 예상 형식(URL인지, base64인지 등)을 명확히 설명해주세요.

예시:

 export interface FileInputProps {
+  /** 수정 모드 여부 */
   isEdit?: boolean;
+  /** 파일 값 (File 객체) 또는 이미지 URL */
   value: File | string | null;
+  /** 샘플 이미지 URL 배열 */
   sample: string[];
+  /** 값이 변경될 때 호출되는 콜백 함수 */
   onChange: (newValue: File | string | null) => void;
 }

Line range hint 19-31: 이벤트 핸들러에 오류 처리 로직 추가가 필요합니다

파일 및 문자열 입력에 대한 유효성 검사와 오류 처리가 없습니다. 다음 사항들을 고려해주세요:

  1. 파일 크기 제한
  2. 허용된 이미지 형식 검사
  3. URL 형식 검증

예시 코드:

const handleFileInput = (inputValue: File | null) => {
  if (inputValue) {
    if (inputValue.size > 5 * 1024 * 1024) {
      alert('파일 크기는 5MB를 초과할 수 없습니다.');
      return;
    }
    if (!inputValue.type.startsWith('image/')) {
      alert('이미지 파일만 업로드 가능합니다.');
      return;
    }
  }
  setIsOtherSelected(true);
  setIsSampleSelected(false);
  setFileValue(inputValue);
  onChange(inputValue);
};
src/components/common/input/drop-down/index.tsx (1)

79-79: 타입 안정성 개선 필요

Select 컴포넌트의 props 업데이트는 좋으나, 타입 안정성을 더욱 강화할 수 있습니다.

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

type Option = {
  value: string;
  label: string;
};

// onChange 핸들러의 타입을 명시적으로 정의
const handleChange = (value: string | null, option: Option | null) => {
  // ...
};

Also applies to: 83-83

src/components/common/crew-list/crew-card.tsx (1)

Line range hint 38-43: 성능 최적화를 위한 제안사항이 있습니다.

다음과 같은 개선사항을 고려해보시기 바랍니다:

  1. handleCardMouseUp 함수를 useCallback으로 메모이제이션하여 불필요한 재생성 방지
  2. 첫 화면에 표시되는 이미지의 경우 priority 속성 추가 검토
  3. CSS 클래스의 매직 넘버(예: h-[430px], h-[203px])를 변수나 상수로 관리

예시 코드:

+ import { useCallback } from 'react';

- const handleCardMouseUp = () => {
+ const handleCardMouseUp = useCallback(() => {
    if (!prefetched.has(CREWPAGE)) {
      router.prefetch(CREWPAGE);
      setPrefetched(new Set(prefetched).add(CREWPAGE));
    }
-  };
+  }, [CREWPAGE, prefetched, router]);

// Image 컴포넌트에 priority 추가
- <Image fill objectFit="cover" alt={title} src={imageUrl} />
+ <Image 
+   fill 
+   priority 
+   objectFit="cover" 
+   alt={title} 
+   src={imageUrl} 
+ />
src/components/common/input/file-input-wrap/file-input/index.tsx (1)

Line range hint 1-120: 코드 전반적인 개선 제안

파일 업로드 관련 유효성 검사와 에러 처리가 보완되면 좋을 것 같습니다:

  1. 파일 크기 제한
  2. 이미지 형식 검증
  3. 업로드 실패 시 에러 처리

다음과 같은 상수를 추가하고 검증 로직을 구현하는 것을 제안드립니다:

const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const ALLOWED_TYPES = ['image/jpeg', 'image/png'];

const validateFile = (file: File): string | null => {
  if (!ALLOWED_TYPES.includes(file.type)) {
    return '지원하지 않는 파일 형식입니다.';
  }
  if (file.size > MAX_FILE_SIZE) {
    return '파일 크기는 5MB를 초과할 수 없습니다.';
  }
  return null;
};

또한 에러 상태를 관리하기 위한 프롭을 추가하는 것을 고려해보세요:

interface FileInputProps {
  // ... existing props
  onError?: (error: string) => void;
}
src/app/(crew)/page.tsx (1)

19-19: 상수로 정의하여 재사용성 향상 필요

정렬 옵션의 초기값이 문자열 리터럴로 하드코딩되어 있습니다. 이는 유지보수성과 재사용성을 저하시킬 수 있습니다.

다음과 같이 상수로 정의하는 것을 추천드립니다:

+const SORT_OPTIONS = {
+  LATEST: '최신순',
+  POPULAR: '인기순',
+} as const;

-const [sort, setSort] = useState<string | null>('최신순');
+const [sort, setSort] = useState<string | null>(SORT_OPTIONS.LATEST);
src/app/(crew)/crew/_components/create-gathering-form/index.tsx (1)

91-98: 파일 유효성 검사 메시지를 더 친화적으로 개선해보세요.

현재 구현된 파일 검증 로직은 잘 작동하지만, 에러 메시지를 더 명확하게 개선할 수 있습니다.

다음과 같이 메시지를 수정해보세요:

-                      ? file.size <= 5242880 || '파일 크기는 5MB 이하여야 합니다.'
+                      ? file.size <= 5242880 || '이미지 크기가 너무 큽니다. 5MB 이하의 이미지를 선택해주세요.'
-                        'JPG, PNG 파일만 업로드 가능합니다.'
+                        '이미지는 JPG 또는 PNG 형식만 가능합니다. 다른 이미지를 선택해주세요.'
src/app/(crew)/crew/_components/create-crew-form/index.tsx (2)

13-20: 타입 정의 개선이 필요합니다.

CreateCrewFormTypesCreateCrewRequestTypes의 분리는 좋은 접근이지만, 두 타입 간의 관계가 명확하지 않습니다. API 요청 시 데이터 변환 로직이 필요할 수 있습니다.

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

// types/create-crew.ts
export interface BaseCrewData {
  // 공통 필드
}

export interface CreateCrewFormTypes extends BaseCrewData {
  // 폼 전용 필드
}

export interface CreateCrewRequestTypes extends BaseCrewData {
  // API 요청 전용 필드
}

180-184: 파일 업로드 상태 처리 개선이 필요합니다.

파일 업로드 시 사용자 피드백이 부족합니다. 업로드 진행 상태나 오류 메시지를 더 명확하게 표시해야 합니다.

로딩 상태와 에러 처리를 추가하는 것을 추천드립니다:

const [isUploading, setIsUploading] = useState(false);
const [uploadError, setUploadError] = useState<string | null>(null);

const handleFileChange = async (file: File) => {
  setIsUploading(true);
  setUploadError(null);
  
  try {
    field.onChange(file);
    await trigger('imageUrl');
  } catch (error) {
    setUploadError('파일 업로드 중 오류가 발생했습니다.');
  } finally {
    setIsUploading(false);
  }
};
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 243dc0d and 824ba7f.

📒 Files selected for processing (24)
  • public/assets/images/crew-sample/index.tsx (1 hunks)
  • public/assets/images/gathering-sample/index.tsx (1 hunks)
  • src/_apis/crew/crew.ts (1 hunks)
  • src/_apis/image/get-image-url.ts (1 hunks)
  • src/_queries/crew/crew-list-queries.tsx (1 hunks)
  • src/app/(crew)/crew/_components/create-crew-form/create-crew-form.stories.tsx (1 hunks)
  • src/app/(crew)/crew/_components/create-crew-form/index.tsx (4 hunks)
  • src/app/(crew)/crew/_components/create-gathering-form/create-gathering-form.stories.tsx (2 hunks)
  • src/app/(crew)/crew/_components/create-gathering-form/index.tsx (3 hunks)
  • src/app/(crew)/crew/_components/create-gathering-modal/container.tsx (1 hunks)
  • src/app/(crew)/crew/_components/create-gathering-modal/presenter.tsx (1 hunks)
  • src/app/(crew)/crew/create/page.tsx (2 hunks)
  • src/app/(crew)/crew/detail/[id]/_components/create-gathering.tsx (2 hunks)
  • src/app/(crew)/crew/detail/[id]/edit/page.tsx (1 hunks)
  • src/app/(crew)/page.tsx (2 hunks)
  • src/components/common/crew-list/crew-card.tsx (1 hunks)
  • src/components/common/input/drop-down/index.tsx (3 hunks)
  • src/components/common/input/file-input-wrap/file-input/index.tsx (2 hunks)
  • src/components/common/input/file-input-wrap/file-sample/file-sample.stories.tsx (1 hunks)
  • src/components/common/input/file-input-wrap/file-sample/index.tsx (2 hunks)
  • src/components/common/input/file-input-wrap/index.tsx (2 hunks)
  • src/types/create-crew.d.ts (1 hunks)
  • src/types/gathering-data.d.ts (1 hunks)
  • src/utils/api.ts (0 hunks)
💤 Files with no reviewable changes (1)
  • src/utils/api.ts
🔇 Additional comments (30)
src/_apis/crew/crew.ts (1)

1-2: 코드가 깔끔하고 잘 구성되어 있습니다!

타입 정의와 유틸리티 함수를 적절히 분리하여 가져오고 있습니다.

src/_apis/image/get-image-url.ts (1)

1-2: 타입 정의 파일의 위치를 확인해주세요.

create-crew.d.ts에 있는 타입 정의가 이미지 관련 타입에 적절한 위치인지 검토가 필요합니다.

이미지 관련 타입들을 src/types/image.d.ts와 같은 별도의 파일로 분리하는 것을 고려해보세요.

public/assets/images/crew-sample/index.tsx (1)

6-10: S3 버킷 설정 확인 필요

S3 버킷의 CORS 설정과 공개 접근 권한이 올바르게 구성되어 있는지 확인이 필요합니다.

✅ Verification successful

S3 버킷 접근성 확인 완료

S3 버킷의 접근성이 정상적으로 설정되어 있습니다. HTTP 200 응답과 함께 파일에 대한 공개 접근이 가능하며, 서버 측 암호화(AES256)도 적절히 구성되어 있습니다.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# S3 URL 접근성 확인
curl -I 'https://crewcrew.s3.ap-northeast-2.amazonaws.com/crew/0e05d971-15a8-4a32-bf03-80d12cae392e'

Length of output: 859

public/assets/images/gathering-sample/index.tsx (1)

6-10: S3 URL 접근성 확인 필요

S3 버킷의 URL이 올바르게 설정되어 있고 접근 가능한지 확인이 필요합니다.

다음 스크립트로 URL 접근성을 확인해보겠습니다:

✅ Verification successful

S3 URL 접근성 확인 완료

모든 S3 이미지 URL이 정상적으로 접근 가능함을 확인했습니다. 모든 URL에서 HTTP 200 OK 응답을 받았습니다.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: S3 URL 접근성 확인

for url in $(rg -o 'https://crewcrew\.s3.*?(?=["'\''])' -N); do
  echo "Checking URL: $url"
  curl -I "$url" | head -n 1
done

Length of output: 306


Script:

#!/bin/bash
# Description: S3 URL 접근성 확인 (수정된 버전)
rg --pcre2 'https://crewcrew\.s3[^"'\'']+' -o | while read -r url; do
  echo "Checking URL: $url"
  curl -I "$url" | head -n 1
done

Length of output: 5396


Script:

#!/bin/bash
# Description: S3 URL 접근성 확인 (재수정된 버전)
rg --pcre2 'https://crewcrew\.s3[^"'\'']+' -o | cut -d':' -f2- | while read -r url; do
  echo "Checking URL: $url"
  curl -I "$url" | head -n 1
done

Length of output: 5152

src/types/create-crew.d.ts (1)

14-22: API 요청 타입이 적절하게 정의되었습니다.

CreateCrewRequestTypes 인터페이스가 서버 API 스펙에 맞게 잘 정의되어 있습니다. 모든 필드가 필수값으로 지정되어 있어 타입 안정성이 보장됩니다.

src/app/(crew)/crew/_components/create-gathering-modal/container.tsx (1)

3-3: 타입 변경사항 확인 필요

타입이 CreateGatheringRequestType에서 CreateGatheringFormTypes로 변경되었습니다. 이 변경으로 인한 영향을 확인해야 합니다.

Also applies to: 9-9

✅ Verification successful

Let me check the type definitions to understand the changes better.


타입 변경이 적절하게 이루어졌습니다

CreateGatheringFormTypesCreateGatheringRequestTypes의 차이점은 imageUrl 필드의 타입입니다:

  • Form: File | string | null (프론트엔드 폼 데이터용)
  • Request: string (API 요청용)

이는 폼 데이터와 API 요청 데이터를 명확하게 구분하는 적절한 타입 설계입니다. 모든 사용처가 일관되게 변경되었으며, 영향도 분석 결과 문제가 없습니다.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: CreateGatheringRequestType 사용 여부 확인

# 이전 타입이 다른 곳에서 사용되고 있는지 확인
rg "CreateGatheringRequestType"

# 새로운 타입의 사용처 확인
rg "CreateGatheringFormTypes"

Length of output: 1994


Script:

#!/bin/bash
# Check the type definitions
cat src/types/gathering-data.d.ts

Length of output: 1273

src/_queries/crew/crew-list-queries.tsx (3)

1-1: 상대 경로에서 절대 경로로의 변경이 확인되었습니다.

절대 경로를 사용하면 파일 구조 변경에 더 유연하게 대응할 수 있어 적절한 변경으로 보입니다.


7-7: 캐시 키 구조가 개선되었습니다.

'crewLists'를 queryKey 배열의 첫 번째 요소로 추가한 것은 React Query의 캐시 관리 측면에서 적절한 변경입니다. 이는 crew 관련 쿼리들을 논리적으로 그룹화하는데 도움이 됩니다.


Line range hint 1-24: 페이지네이션 로직 검증이 필요합니다.

페이지네이션 구현이 올바르게 작동하는지 확인이 필요합니다.

✅ Verification successful

페이지네이션 구현이 정상적으로 확인되었습니다.

페이지네이션 관련 코드를 검토한 결과:

  • MainCrewListResponse 타입에서 hasNext 필드를 통해 다음 페이지 존재 여부를 올바르게 확인
  • useInfiniteScroll 훅을 사용하여 무한 스크롤 구현이 적절하게 되어있음
  • getNextPageParam 로직이 allPages.length를 다음 페이지 번호로 사용하여 순차적인 페이지 로드를 보장
  • 페이지 크기가 6으로 고정되어 일관된 데이터 로딩을 제공
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# 페이지네이션 관련 코드 검색
rg -A 5 "getNextPageParam|useInfiniteQuery" --type typescript

Length of output: 98


Script:

#!/bin/bash
# Search for pagination related code with ripgrep, without file type restriction
rg -A 5 "getNextPageParam|useInfiniteQuery"

Length of output: 4339


Script:

#!/bin/bash
# Find and examine all TypeScript/TSX files that might contain pagination logic
fd "\.(ts|tsx)$" | xargs rg -l "getNextPageParam|useInfiniteQuery|pageParam|hasNext"

Length of output: 507


Script:

#!/bin/bash
# Check the crew-list API implementation
fd "crew-list\.(ts|tsx)$" | xargs cat

Length of output: 844


Script:

#!/bin/bash
# Check the crew-card type definitions
fd "crew-card\.d\.ts$" | xargs cat

Length of output: 1384


Script:

#!/bin/bash
# Check the infinite scroll hook implementation
cat src/hooks/use-infinite-scroll.ts

Length of output: 797

src/app/(crew)/crew/_components/create-crew-form/create-crew-form.stories.tsx (1)

2-2: 타입 정의 가져오기 검토 필요

CreateCrewFormTypesCreateCrewRequestTypes를 동시에 가져오고 있는데, 실제로 두 타입이 모두 필요한지 확인이 필요합니다. 스토리북 컴포넌트에서는 폼 타입만 사용하는 것이 더 명확할 수 있습니다.

다음 스크립트를 실행하여 두 타입의 사용 현황을 확인해보세요:

✅ Verification successful

Let me analyze the results and provide a final response in Korean.

스토리북 컴포넌트에서 타입 사용이 적절합니다

CreateCrewFormTypesCreateCrewRequestTypes는 모두 필요한 타입입니다. 코드베이스 분석 결과:

  • CreateCrewForm 컴포넌트는 CreateCrewFormTypes를 폼 데이터 타입으로 사용
  • 스토리북 파일에서는 initialValueCreateCrewRequestTypes로 정의하고, 이를 CreateCrewForm 컴포넌트의 data prop으로 전달
  • 두 타입은 서로 다른 목적으로 사용되며, 폼 상태와 API 요청 데이터를 명확하게 구분
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: 두 타입의 사용 현황 확인

# CreateCrewFormTypes와 CreateCrewRequestTypes의 사용 패턴 검색
rg -A 2 "CreateCrewFormTypes|CreateCrewRequestTypes" 

Length of output: 4641

src/app/(crew)/crew/detail/[id]/_components/create-gathering.tsx (3)

8-8: 타입 임포트 변경이 적절해 보입니다.

CreateGatheringFormTypes로의 타입 변경이 타입 시스템의 일관성을 개선했습니다.


Line range hint 31-38: 모달 컴포넌트로의 데이터 전달이 적절해 보입니다.

CreateGatheringModalContainer에 전달되는 data prop의 타입이 새로운 CreateGatheringFormTypes와 일치하는지 확인했습니다.


Line range hint 23-29: 초기값 구조가 새로운 타입과 일치하는지 확인이 필요합니다.

initialValue 객체의 구조가 CreateGatheringFormTypes 타입과 일치하는지 확인해주세요. 특히 imageUrl 필드의 타입이 File | string | null로 변경된 것에 주의해주세요.

✅ Verification successful

Based on the results, I can now generate the final response since I have confirmed the type definition and can verify the initial values against it.

초기값이 타입 정의와 정확히 일치합니다.

initialValue 객체의 모든 필드가 CreateGatheringFormTypes 인터페이스와 정확히 일치합니다:

  • title: string ('')
  • introduce: string ('')
  • dateTime: string ('')
  • location: string ('')
  • totalCount: number (2)
  • imageUrl: File | string | null (null)
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: CreateGatheringFormTypes 타입 정의 확인

# 타입 정의 파일에서 CreateGatheringFormTypes 검색
rg -A 10 "CreateGatheringFormTypes" "src/types"

Length of output: 655

src/app/(crew)/crew/_components/create-gathering-modal/presenter.tsx (2)

3-3: 타입 임포트가 올바르게 추가되었습니다.

타입 정의의 임포트가 적절한 경로를 사용하여 추가되었습니다.


10-10: 타입 변경의 영향 범위를 확인해주세요.

CreateGatheringRequestType에서 CreateGatheringFormTypes로의 타입 변경이 다른 컴포넌트들과의 상호작용에 영향을 미칠 수 있습니다.

✅ Verification successful

Let me check the type definitions to understand the changes between these types.


타입 변경이 적절하게 이루어졌습니다.

CreateGatheringRequestTypes에서 CreateGatheringFormTypes로의 변경은 적절합니다. 주요 차이점은 imageUrl 필드의 타입이 string에서 File | string | null로 변경된 것으로, 이는 폼 입력 처리에 더 적합한 타입입니다. 모든 관련 컴포넌트들이 이미 새로운 타입을 일관되게 사용하고 있습니다:

  • create-gathering-form 컴포넌트
  • create-gathering-modal 컴포넌트
  • create-gathering.tsx 파일
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: 타입 사용 현황 확인

# CreateGatheringRequestType 사용 검색
echo "Checking old type usage:"
rg "CreateGatheringRequestType"

# CreateGatheringFormTypes 사용 검색
echo -e "\nChecking new type usage:"
rg "CreateGatheringFormTypes"

Length of output: 2118


Script:

#!/bin/bash
# Check the type definitions
echo "Type definitions comparison:"
cat src/types/gathering-data.d.ts

Length of output: 1340

src/types/gathering-data.d.ts (1)

38-44: 폼 타입과 요청 타입의 분리가 잘 되었습니다!

CreateGatheringFormTypesCreateGatheringRequestTypes를 분리한 것은 좋은 설계입니다. 특히 imageUrl의 타입을 요청 시에는 string으로 제한한 것이 적절합니다.

다만, 이전 코멘트와 마찬가지로 문자열 길이 제약 조건도 타입으로 강제하면 더 좋을 것 같습니다.

src/components/common/input/file-input-wrap/file-sample/index.tsx (1)

5-9: 인터페이스 변경사항이 적절합니다!

API 통합을 위한 타입 변경이 잘 이루어졌으며, 선택적 프로퍼티 처리가 적절합니다.

src/app/(crew)/crew/detail/[id]/edit/page.tsx (2)

4-7: 타입 일관성 검증이 필요합니다

CreateCrewFormTypes로의 타입 변경이 다른 컴포넌트들과의 일관성을 해치지 않는지 확인이 필요합니다.

✅ Verification successful

Let me analyze the results and generate a final response in Korean.

타입 변경은 적절하게 이루어졌습니다

CreateCrewFormTypes는 폼 컴포넌트와 관련된 모든 파일에서 일관되게 사용되고 있습니다:

  • src/types/create-crew.d.ts에 정의되어 있음
  • create-crew-form 컴포넌트에서 props 타입으로 올바르게 사용됨
  • create와 edit 페이지 모두에서 동일한 방식으로 활용됨
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the usage of CreateCrewFormTypes across the codebase

# Check for CreateCrewFormTypes usage
rg "CreateCrewFormTypes" -A 3

Length of output: 4161


16-26: 🛠️ Refactor suggestion

초기값 설정 로직 개선이 필요합니다

다음과 같은 문제점들이 있습니다:

  1. S3 URL이 하드코딩되어 있습니다
  2. TODO 주석이 모호합니다
  3. data에서 받아온 값으로 초기값을 설정해야 합니다

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

- // TODO : 테스트중
- const initialValue: CreateCrewFormTypes = {
-   title: '',
-   mainCategory: '',
-   subCategory: '',
-   imageUrl:
-     'https://crewcrew.s3.ap-northeast-2.amazonaws.com/crew/0e05d971-15a8-4a32-bf03-80d12cae392e',
-   mainLocation: '',
-   subLocation: '',
-   totalCount: 4,
- };
+ const initialValue: CreateCrewFormTypes = {
+   title: data.title,
+   mainCategory: data.mainCategory,
+   subCategory: data.subCategory,
+   imageUrl: data.imageUrl,
+   mainLocation: data.mainLocation,
+   subLocation: data.subLocation,
+   totalCount: data.totalCount,
+ };

Likely invalid or redundant comment.

src/app/(crew)/crew/_components/create-gathering-form/create-gathering-form.stories.tsx (2)

4-4: 타입 임포트가 적절히 업데이트되었습니다!

새로운 타입 시스템에 맞게 잘 반영되었습니다.


Line range hint 7-14: API 요구사항과 초기값 검증이 필요합니다.

초기값 구조가 API 요구사항과 일치하는지 확인이 필요합니다.

✅ Verification successful

초기값이 타입 정의와 일치하며 문제가 없습니다.

src/types/gathering-data.d.ts에 정의된 CreateGatheringFormTypes 인터페이스와 초기값의 구조를 비교한 결과:

  • title (string)
  • introduce (string)
  • dateTime (string)
  • location (string)
  • totalCount (number)
  • imageUrl (File | string | null)

모든 필드가 타입 정의와 일치하며, 각각 적절한 초기값이 설정되어 있습니다. 특히 주석에 명시된 제약사항(title 20자 이내, introduce 100자 이내)도 고려되어 있습니다.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# API 타입 정의와 초기값 구조 비교
ast-grep --pattern 'interface CreateGatheringFormTypes {
  $$$
}'

# API 엔드포인트에서 필수 필드 확인
rg -A 10 "crew/gathering" src/**/_api/**/*.ts

Length of output: 644


Script:

#!/bin/bash
# API 엔드포인트 확인을 위한 경로 검색
fd "api" --type directory

# API 관련 파일에서 gathering 관련 엔드포인트 검색
rg -g "*.ts" -g "*.tsx" "crew/gathering" 

# 추가로 Form 컴포넌트에서 validation 로직 확인
rg -g "*.ts" -g "*.tsx" "CreateGatheringForm.*validation"

Length of output: 243


Script:

#!/bin/bash
# API 디렉토리 내 파일 검색
fd . src/_apis/ --type file

# gathering 관련 API 요청/응답 타입 확인
rg -g "*.ts" "type.*Gathering" src/

# Form 컴포넌트의 validation 스키마 확인
rg -g "*.ts" -g "*.tsx" "schema.*gathering" -A 10

Length of output: 685


Script:

#!/bin/bash
# gathering API 파일 내용 확인
cat src/_apis/gathering/gathering-apis.tsx

# CreateGatheringForm 컴포넌트 검색
fd "create-gathering-form" --type file

# Form 컴포넌트의 유효성 검사 로직 확인
rg -g "*.ts" -g "*.tsx" "CreateGatheringForm.*schema|CreateGatheringForm.*validate" -A 10

Length of output: 637

src/components/common/input/file-input-wrap/index.tsx (1)

14-15: 초기 상태 처리 검토가 필요합니다

isEdit이 옵셔널 프로퍼티인데 명시적인 기본값 처리가 없습니다. 또한 valuenull일 때의 처리도 고려해야 합니다.

다음 사항들을 확인해주세요:

  1. isEdit의 기본값 설정
  2. valuenull일 때의 UI 처리
-export default function FileInputWrap({ isEdit, value, sample, onChange }: FileInputProps) {
+export default function FileInputWrap({ isEdit = false, value, sample, onChange }: FileInputProps) {
src/components/common/input/drop-down/index.tsx (1)

30-30: 내부 상태 관리 방식 검토 필요

currentValue 상태를 추가한 것은 좋은 시도이나, 부모 컴포넌트의 value와 동기화 문제가 발생할 수 있습니다.

src/components/common/crew-list/crew-card.tsx (1)

30-30: 동적 URL 라우팅으로의 변경이 적절히 이루어졌습니다.

하드코딩된 URL을 동적 URL로 변경한 것은 좋은 개선입니다. 각 크루의 고유 ID를 사용하여 올바른 상세 페이지로 라우팅됩니다.

라우팅 동작을 확인하기 위해 다음 스크립트를 실행하세요:

✅ Verification successful

동적 라우팅 구현이 올바르게 되어있음을 확인했습니다.

크루 상세 페이지의 동적 라우팅이 Next.js의 권장 방식대로 구현되어 있습니다:

  • /app/(crew)/crew/detail/[id] 경로에 상세 페이지 컴포넌트가 존재합니다
  • CrewCard 컴포넌트의 라우팅 로직이 적절히 구현되어 있으며, prefetch 최적화도 포함되어 있습니다
  • API 엔드포인트와 쿼리 로직이 ID 기반으로 올바르게 구성되어 있습니다
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# 설명: 크루 상세 페이지 라우팅 관련 코드 검증

# 크루 ID를 사용하는 다른 라우팅 로직 확인
rg -t typescript -t javascript "/crew/detail/[0-9]+" 

# 동적 라우팅 패턴 사용 확인
rg -t typescript -t javascript "/crew/detail/\$|/crew/detail/\[" 

Length of output: 190


Script:

#!/bin/bash
# Check routing patterns in TypeScript/JavaScript files
rg --type-add 'ts:*.{ts,tsx}' --type-add 'js:*.{js,jsx}' -t ts -t js "/crew/detail/[0-9]+|/crew/detail/\$|/crew/detail/\["

# Find Next.js page components for crew detail
fd -e tsx -e ts -e js -e jsx "crew.*detail"

# Check if there's a dynamic route handler for crew detail
fd -e tsx -e ts -e js -e jsx "\[.*\]" "pages"

Length of output: 387


Script:

#!/bin/bash
# Check the content of crew detail API and queries
cat src/_apis/detail/get-crew-detail.ts
cat src/_queries/detail/crew-detail-queries.ts

# Find Next.js pages directory structure
fd "pages"

# Look for dynamic route patterns in the entire codebase
rg --type-add 'ts:*.{ts,tsx}' --type-add 'js:*.{js,jsx}' -t ts -t js "\[id\]|\[crewId\]"

Length of output: 844


Script:

#!/bin/bash
# Find the app directory structure (Next.js 13+ convention)
fd "app"

# Look for crew-related route definitions
fd -e tsx -e ts -e js -e jsx "crew"

# Check the implementation of crew card component
cat src/components/common/crew-list/crew-card.tsx

Length of output: 4210

src/components/common/input/file-input-wrap/file-input/index.tsx (2)

7-8: 타입 정의가 개선되었습니다.

value 프롭에 문자열 타입이 추가되어 외부 URL을 처리할 수 있게 되었고, onChange 콜백의 타입이 간소화되어 더 명확해졌습니다.


13-13: 초기 상태 설정이 개선되었습니다.

value가 문자열인 경우 바로 미리보기를 표시하도록 수정되어, 기존 이미지가 있는 경우의 사용자 경험이 향상되었습니다.

src/app/(crew)/page.tsx (2)

Line range hint 1-132: 전반적인 구현이 잘 되어있습니다

컴포넌트의 구조와 구현이 전반적으로 잘 되어있습니다:

  • 무한 스크롤 구현이 적절합니다
  • 상태 관리가 깔끔합니다
  • 사용자 경험을 고려한 필터링 옵션들이 잘 구현되어 있습니다

다만, 상수 값들의 관리와 타입 안정성을 개선하면 더 좋을 것 같습니다.


25-28: 지역 선택 로직 검증 필요

지역 선택 로직이 label 기반으로 변경되었습니다. 이는 regionData의 구조와 일관성이 있는지 확인이 필요합니다.

✅ Verification successful

지역 선택 로직이 데이터 구조와 일치함을 확인했습니다

region.json 파일의 구조를 검토한 결과, 모든 지역 데이터가 main.label을 통해 일관되게 접근되고 있으며, 현재 구현된 로직이 데이터 구조와 정확히 일치합니다. 특히:

  • 모든 지역 데이터가 { main: { label: string, value: string }, areas: [] } 형식을 따름
  • "지역 전체" 처리를 포함한 예외 케이스도 올바르게 구현됨
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# regionData 파일의 구조 확인
cat src/data/region.json | jq '.[].main | {label, value}'

Length of output: 230


Script:

#!/bin/bash
# regionData 파일 찾기
fd region.ts -e ts -e json

# regionData 파일 내용 확인
rg -l "regionData" --type ts
rg "regionData.*=" --type ts -A 5

Length of output: 2199


Script:

#!/bin/bash
# regionData 정의 및 초기화 부분 찾기
rg "regionData.*=.*\[" --type ts -A 10

# regionData 타입 정의 찾기
rg "type.*Region" --type ts -A 5

Length of output: 75


Script:

#!/bin/bash
# 상수 데이터 파일 찾기
fd "region" --type f --full-path src/constants/ src/data/

# src/data 디렉토리 내의 모든 파일 검색
fd . --type f --full-path src/data/

# regionData import 문 찾기
rg "import.*region" --type ts

Length of output: 391


Script:

#!/bin/bash
# region.json 파일의 내용 확인
cat src/data/region.json

# region.json을 사용하는 다른 컴포넌트에서의 사용 패턴 확인
rg "regionData.*main\." --type ts -A 2 -B 2

Length of output: 12400

src/app/(crew)/crew/_components/create-gathering-form/index.tsx (1)

10-17: 타입 정의가 일관성 있게 적용되었습니다!

CreateGatheringFormTypes를 사용한 타입 정의가 전체적으로 잘 구현되었습니다. Props의 옵셔널 속성도 적절하게 정의되어 있습니다.

Also applies to: 27-27, 33-33

src/app/(crew)/crew/_components/create-crew-form/index.tsx (1)

37-37: 이미지 URL 상태 관리 검증이 필요합니다.

imageUrl이 File 객체와 문자열 URL 두 가지 타입을 모두 허용하는 것으로 보입니다. 이는 타입 안정성 측면에서 잠재적인 문제가 될 수 있습니다.

다음 스크립트로 이미지 URL 처리 로직을 검증해보세요:

Also applies to: 50-51

Comment on lines 4 to 14
export async function createCrew(data: CreateCrewRequestTypes) {
const response: { data: CreateCrewResponseTypes } = await fetchApi(`/api/crews`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // 인증 정보를 요청에 포함
body: JSON.stringify(data),
});
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

코드 개선이 필요한 부분이 있습니다.

다음 사항들을 개선하면 좋을 것 같습니다:

  1. 에러 처리가 누락되어 있습니다. API 호출 실패 시의 처리가 필요합니다.
  2. 응답 타입이 더 구체적일 필요가 있습니다.
  3. 코드 내 한글 주석은 영문으로 통일하는 것이 좋습니다.
  4. response에 대한 옵셔널 체이닝(?.)은 불필요해 보입니다.

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

 export async function createCrew(data: CreateCrewRequestTypes) {
-  const response: { data: CreateCrewResponseTypes } = await fetchApi(`/api/crews`, {
-    method: 'POST',
-    headers: {
-      'Content-Type': 'application/json',
-    },
-    credentials: 'include', // 인증 정보를 요청에 포함
-    body: JSON.stringify(data),
-  });
-  return response?.data;
+  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) {
+    throw new Error(`Failed to create crew: ${error instanceof Error ? error.message : 'Unknown error'}`);
+  }
 }
📝 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) {
const response: { data: CreateCrewResponseTypes } = await fetchApi(`/api/crews`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // 인증 정보를 요청에 포함
body: JSON.stringify(data),
});
return response?.data;
}
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) {
throw new Error(`Failed to create crew: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}

Comment on lines +4 to +7
export async function getImageUrl(
file: File | string | null,
type: 'MEMBER' | 'CREW' | 'GATHERING',
) {
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}`);
}

Comment on lines 13 to 18
const response: { data: GetImageUrlResponseTypes } = await fetchApi(`/api/images?type=${type}`, {
method: 'POST',
body: formData,
});
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.

⚠️ Potential issue

에러 처리와 응답 검증이 필요합니다.

API 호출 시 발생할 수 있는 에러 처리와 응답 데이터 검증이 미흡합니다.

다음과 같은 에러 처리 및 응답 검증 로직 추가를 제안드립니다:

-  const response: { data: GetImageUrlResponseTypes } = await fetchApi(`/api/images?type=${type}`, {
-    method: 'POST',
-    body: formData,
-  });
-  return response?.data;
+  try {
+    const response: { data: GetImageUrlResponseTypes } = await fetchApi(`/api/images?type=${type}`, {
+      method: 'POST',
+      body: formData,
+    });
+    
+    if (!response?.data?.imageUrl) {
+      throw new Error('이미지 URL을 받아오는데 실패했습니다.');
+    }
+    
+    return response.data;
+  } catch (error) {
+    console.error('이미지 업로드 중 오류 발생:', error);
+    throw new Error('이미지 업로드에 실패했습니다. 다시 시도해 주세요.');
+  }
📝 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,
});
return response?.data;
}
try {
const response: { data: GetImageUrlResponseTypes } = await fetchApi(`/api/images?type=${type}`, {
method: 'POST',
body: formData,
});
if (!response?.data?.imageUrl) {
throw new Error('이미지 URL을 받아오는데 실패했습니다.');
}
return response.data;
} catch (error) {
console.error('이미지 업로드 중 오류 발생:', error);
throw new Error('이미지 업로드에 실패했습니다. 다시 시도해 주세요.');
}

Comment on lines 8 to 11
const formData = new FormData();
if (file instanceof File) {
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.

⚠️ Potential issue

파일 유효성 검증 로직이 필요합니다.

현재 파일 크기와 타입에 대한 검증이 없어 보안 및 성능 이슈가 발생할 수 있습니다.

다음과 같은 검증 로직 추가를 제안드립니다:

 const formData = new FormData();
 if (file instanceof File) {
+  const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
+  const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif'];
+  
+  if (file.size > MAX_FILE_SIZE) {
+    throw new Error('파일 크기는 5MB를 초과할 수 없습니다.');
+  }
+  
+  if (!ALLOWED_TYPES.includes(file.type)) {
+    throw new Error('지원하지 않는 파일 형식입니다.');
+  }
+
   formData.append('file', file);
 }
📝 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 formData = new FormData();
if (file instanceof File) {
formData.append('file', file);
}
const formData = new FormData();
if (file instanceof File) {
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif'];
if (file.size > MAX_FILE_SIZE) {
throw new Error('파일 크기는 5MB를 초과할 수 없습니다.');
}
if (!ALLOWED_TYPES.includes(file.type)) {
throw new Error('지원하지 않는 파일 형식입니다.');
}
formData.append('file', file);
}

Comment on lines 4 to 12
export interface CreateCrewFormTypes {
title: string;
mainCategory: string;
subCategory: string | null;
mainLocation: string;
subLocation: string | null;
totalCount: number;
imageUrl: File | StaticImageData | null;
imageUrl: File | string | 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

타입 정의 개선이 필요합니다.

CreateCrewFormTypes 인터페이스에서 다음과 같은 문제점이 발견되었습니다:

  1. subCategorysubLocationnull을 허용하는데, API 요청 타입에서는 필수값으로 되어있습니다.
  2. imageUrlFile | string | null 타입을 허용하는데, 이는 너무 광범위한 타입 정의입니다.

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

 export interface CreateCrewFormTypes {
   title: string;
   mainCategory: string;
-  subCategory: string | null;
+  subCategory: string;
   mainLocation: string;
-  subLocation: string | null;
+  subLocation: string;
   totalCount: number;
-  imageUrl: File | string | null;
+  imageUrl: File | 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 interface CreateCrewFormTypes {
title: string;
mainCategory: string;
subCategory: string | null;
mainLocation: string;
subLocation: string | null;
totalCount: number;
imageUrl: File | StaticImageData | null;
imageUrl: File | string | null;
}
export interface CreateCrewFormTypes {
title: string;
mainCategory: string;
subCategory: string;
mainLocation: string;
subLocation: string;
totalCount: number;
imageUrl: File | null;
}

Comment on lines 68 to 71
if (value === null) {
onChange(null);
onChange('');
setCurrentValue(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

폼 검증 로직 확인 필요

null 값을 빈 문자열로 변경하는 것은 폼 검증 로직에 영향을 줄 수 있습니다. 특히 필수 필드 검증에서 문제가 발생할 수 있습니다.

다음과 같이 수정하는 것을 추천드립니다:

  useEffect(() => {
    if (value === null) {
-      onChange('');
+      onChange(null);
      setCurrentValue(null);
    }
  }, [value]);

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

Comment on lines +55 to +62
const handleChange = (newValue: string | null, option: { value: string; label: string }) => {
if (newValue === null) {
setCurrentValue(null);
onChange(null);
return;
}
setCurrentValue(newValue);
onChange(option.label);
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. null 처리 로직이 불필요하게 복잡합니다
  2. option.label만 전달하는 것은 데이터 손실을 초래할 수 있습니다

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

-  const handleChange = (newValue: string | null, option: { value: string; label: string }) => {
-    if (newValue === null) {
-      setCurrentValue(null);
-      onChange(null);
-      return;
-    }
-    setCurrentValue(newValue);
-    onChange(option.label);
+  const handleChange = (newValue: string | null, option: { value: string; label: string } | null) => {
+    setCurrentValue(newValue);
+    onChange(option?.label ?? 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
const handleChange = (newValue: string | null, option: { value: string; label: string }) => {
if (newValue === null) {
setCurrentValue(null);
onChange(null);
return;
}
setCurrentValue(newValue);
onChange(option.label);
const handleChange = (newValue: string | null, option: { value: string; label: string } | null) => {
setCurrentValue(newValue);
onChange(option?.label ?? null);

setPreview(null); // 블러 상태에서 미리보기 제거
}
}, [isBlur]);
}, [isBlur, value]);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

의존성 배열에 value를 추가하는 것이 좋습니다.

useEffect의 의존성 배열에 value를 추가하여 값이 변경될 때 미리보기가 정상적으로 초기화되도록 보장합니다.

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

- }, [isBlur, value]);
+ }, [isBlur, value, setPreview]);

setPreview를 의존성 배열에 추가하여 React의 exhaustive-deps 규칙을 준수하고, 상태 업데이트의 안정성을 보장할 수 있습니다.

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

mainCategory,
subCategory,
sortType: sort === 'latest' ? 'LATEST' : 'POPULAR',
sortType: sort === '최신순' ? 'LATEST' : 'POPULAR',
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 SORT_TYPE_MAPPING = {
+  [SORT_OPTIONS.LATEST]: 'LATEST',
+  [SORT_OPTIONS.POPULAR]: 'POPULAR',
+} as const;

-sortType: sort === '최신순' ? 'LATEST' : 'POPULAR',
+sortType: SORT_TYPE_MAPPING[sort as keyof typeof SORT_TYPE_MAPPING] ?? 'LATEST',

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

Comment on lines +26 to +27
const { isPending, mutate } = useMutation({
mutationFn: (data: CreateCrewRequestTypes) => createCrew(data),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

'isPending' 대신 'isLoading'을 사용해주세요.

useMutation 훅은 isPending 속성을 제공하지 않습니다. 로딩 상태를 확인하려면 isLoading 속성을 사용해야 합니다.

다음과 같이 수정해주세요:

- const { isPending, mutate } = useMutation({
+ const { isLoading, mutate } = useMutation({

그리고 아래 부분도 수정이 필요합니다:

- if (isPending)
+ if (isLoading)

Also applies to: 58-63

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

🧹 Outside diff range and nitpick comments (28)
src/_apis/crew/crew.ts (1)

10-10: 인증 관련 문서화 제안

인증 정보 포함에 대한 주석이 있어 좋습니다. 추가로 인증 실패 시의 처리 방법도 문서화하면 좋을 것 같습니다.

public/assets/images/crew-sample/index.tsx (2)

6-10: URL을 환경 설정으로 이동하는 것을 고려해 주세요.

하드코딩된 S3 URL을 환경 설정 파일로 이동하면 다음과 같은 이점이 있습니다:

  • 환경별로 다른 URL 사용 가능 (개발/스테이징/프로덕션)
  • URL 변경 시 중앙에서 관리 용이
  • 코드와 설정의 분리로 유지보수성 향상

예시 리팩토링:

+const S3_BUCKET_URL = process.env.NEXT_PUBLIC_S3_BUCKET_URL || 'https://crewcrew.s3.ap-northeast-2.amazonaws.com';
+
 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',
+  `${S3_BUCKET_URL}/crew/0e05d971-15a8-4a32-bf03-80d12cae392e`,
+  `${S3_BUCKET_URL}/crew/eb35c35d-829a-402b-8019-29e42f91589f`,
+  `${S3_BUCKET_URL}/crew/471b3b3b-b23c-48e8-8e6b-9a7ec31e1917`,
 ];

Line range hint 1-5: 사용하지 않는 로컬 이미지 임포트를 제거해 주세요.

ImgCrewSample01, ImgCrewSample02, ImgCrewSample03ImgCrewSamples 상수가 더 이상 사용되지 않습니다. 코드 정리를 위해 제거하는 것이 좋겠습니다.

-import ImgCrewSample01 from './crew-sample-1.jpg';
-import ImgCrewSample02 from './crew-sample-2.jpg';
-import ImgCrewSample03 from './crew-sample-3.jpg';
-
-const ImgCrewSamples = [ImgCrewSample01, ImgCrewSample02, ImgCrewSample03];

Also applies to: 12-12

public/assets/images/gathering-sample/index.tsx (2)

Line range hint 1-5: 사용하지 않는 import문과 상수 제거 필요

로컬 이미지 import문과 ImgGatheringSamples 상수가 더 이상 사용되지 않으므로 제거해야 합니다.

다음과 같이 수정해주세요:

-import ImgGatheringSample01 from './gathering-sample-1.jpg';
-import ImgGatheringSample02 from './gathering-sample-2.jpg';
-import ImgGatheringSample03 from './gathering-sample-3.jpg';

-const ImgGatheringSamples = [ImgGatheringSample01, ImgGatheringSample02, ImgGatheringSample03];

6-12: 이미지 로딩 에러 처리 추가 권장

원격 URL을 사용할 때는 이미지 로딩 실패에 대한 대비가 필요합니다. fallback 이미지나 에러 처리 로직 추가를 고려해주세요.

다음과 같은 방식으로 구현하는 것을 추천드립니다:

interface ImageUrlConfig {
  url: string;
  fallback: string;
}

const ImgGatheringSampleUrls: ImageUrlConfig[] = [
  {
    url: 'https://crewcrew.s3.ap-northeast-2.amazonaws.com/crew/1ff61110-e238-4fd6-8736-e6f03483f4df',
    fallback: '/assets/images/gathering-sample/gathering-sample-1.jpg'
  },
  // ... 나머지 이미지들도 동일한 방식으로 구성
];
src/types/create-crew.d.ts (1)

24-26: 응답 타입에 JSDoc 주석 추가 권장

응답 타입의 의미와 사용법을 명확히 하기 위해 JSDoc 주석 추가를 권장드립니다.

다음과 같이 주석을 추가해보세요:

+/**
+ * 크루 생성 API 응답 타입
+ * @property crewId - 생성된 크루의 고유 식별자
+ */
export interface CreateCrewResponseTypes {
  crewId: number;
}
src/app/(crew)/crew/_components/create-gathering-modal/container.tsx (2)

Line range hint 17-23: API 연결 구현이 필요합니다.

TODO 주석에 명시된 API 연결 작업이 아직 구현되지 않았습니다. PR의 목적이 API 연결인 만큼, 다음 사항들을 구현해 주시기 바랍니다:

  1. 약속 만들기 API 연결 (handleSubmit)
  2. 약속 수정하기 API 연결 (handleEdit)

API 연결 구현을 도와드릴까요? 다음과 같은 도움을 제공할 수 있습니다:

  • API 호출 함수 구현
  • 에러 핸들링 로직 추가
  • 로딩 상태 관리

Line range hint 1-35: 컴포넌트 에러 처리 개선이 필요합니다.

현재 모달 컴포넌트에 에러 처리 로직이 없습니다. API 연결 시 발생할 수 있는 다양한 에러 상황(네트워크 오류, 서버 오류 등)에 대한 처리가 필요합니다.

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

'use client';

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

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

export default function CreateGatheringModalContainer({
  opened,
  close,
  data,
}: CreateGatheringModalContainerProps) {
+  const [error, setError] = useState<string | null>(null);
+  const [isLoading, setIsLoading] = useState(false);

-  const handleSubmit = () => {
+  const handleSubmit = async () => {
+    try {
+      setIsLoading(true);
+      setError(null);
      // TODO : 약속 만들기 API 연결
+    } catch (err) {
+      setError('약속 생성 중 오류가 발생했습니다.');
+      console.error(err);
+      return;
+    } finally {
+      setIsLoading(false);
+    }
    close();
  };

-  const handleEdit = () => {
+  const handleEdit = async () => {
+    try {
+      setIsLoading(true);
+      setError(null);
      // TODO : 약속 수정하기 API 연결
+    } catch (err) {
+      setError('약속 수정 중 오류가 발생했습니다.');
+      console.error(err);
+      return;
+    } finally {
+      setIsLoading(false);
+    }
    close();
  };

  return (
    <CreateGatheringModalPresenter
      data={data}
      opened={opened}
      onClose={close}
      onEdit={handleEdit}
      onSubmit={handleSubmit}
+     error={error}
+     isLoading={isLoading}
    />
  );
}
src/app/(crew)/crew/detail/[id]/_components/create-gathering.tsx (1)

Line range hint 16-22: 사용자 경험 개선을 위한 제안사항이 있습니다.

현재 구현에서 다음 사항들을 고려해보시면 좋을 것 같습니다:

  1. 로딩 상태 처리
  2. 에러 발생 시 사용자 피드백
  3. 네트워크 상태에 따른 예외 처리

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

 const handleButtonClick = () => {
+  try {
     if (isAuth) {
+      // 로딩 상태 시작
+      setIsLoading(true);
       open(); // 로그인 상태일 경우 모달 열기
     } else {
+      // 비로그인 상태 메시지 표시
+      toast.info('로그인이 필요한 서비스입니다.');
       router.push('/login');
     }
+  } catch (error) {
+    toast.error('오류가 발생했습니다. 다시 시도해주세요.');
+  } finally {
+    setIsLoading(false);
+  }
 };
src/types/gathering-data.d.ts (1)

38-44: API 요청 타입과 폼 타입의 분리가 잘 되었습니다.

CreateGatheringRequestTypes가 API 요청에 특화된 타입으로 분리된 것이 좋은 설계입니다. 특히 imageUrlstring으로 고정되어 있어 서버 API 스펙과 일치합니다.

하지만 한 가지 제안사항이 있습니다:

 export interface CreateGatheringRequestTypes {
-  title: string; // 20자 이내
-  introduce: string; // 소개글 100자 이내
+  title: string;
+  introduce: string;
   dateTime: string;
   location: string;
   totalCount: number;
   imageUrl: string;
 }

API 요청 타입에서는 클라이언트 측 유효성 검사 관련 주석을 제거하는 것이 좋습니다. 이러한 제약사항은 폼 타입이나 유효성 검사 스키마에서 관리하는 것이 더 적절합니다.

src/app/(crew)/crew/detail/[id]/edit/page.tsx (1)

Line range hint 27-29: 크루 수정 API 구현이 필요합니다.

handleEdit 함수에 PATCH API 연동이 구현되어 있지 않습니다. 다음 사항들을 고려해주세요:

  • 수정 API 호출 구현
  • 로딩 상태 처리
  • 에러 처리
  • 성공 시 피드백 및 페이지 이동

API 연동 구현에 도움이 필요하시다면 말씀해 주세요.

src/app/(crew)/crew/_components/create-gathering-form/create-gathering-form.stories.tsx (1)

33-33: 함수 이름을 컨텍스트에 맞게 수정해주세요.

함수 이름이 CreateCrewPageStory로 되어 있는데, 이는 현재 구현하고 있는 gathering 컨텍스트와 맞지 않습니다. CreateGatheringStory와 같이 더 적절한 이름으로 변경하는 것이 좋겠습니다.

-const Template: StoryFn<CreateGatheringFormTypes> = function CreateCrewPageStory() {
+const Template: StoryFn<CreateGatheringFormTypes> = function CreateGatheringStory() {
src/components/common/input/file-input-wrap/index.tsx (2)

15-17: 상태 관리 로직을 단순화할 수 있습니다

현재 isOtherSelectedisSampleSelected를 별도로 관리하고 있지만, 이는 하나의 상태로 통합할 수 있습니다.

다음과 같이 개선해보세요:

type SelectionType = 'sample' | 'file' | null;
const [selectionType, setSelectionType] = useState<SelectionType>(null);

Line range hint 19-31: 이벤트 핸들러의 에러 처리와 성능 개선이 필요합니다

현재 이벤트 핸들러에서 다음과 같은 개선이 필요합니다:

  1. 입력값 유효성 검사가 없습니다
  2. 여러 상태 업데이트를 개별적으로 수행하고 있습니다

다음과 같이 개선해보세요:

const handleChange = (inputValue: string) => {
  if (!inputValue.trim()) {
    console.error('유효하지 않은 입력값입니다');
    return;
  }
  
  // 상태 업데이트를 일괄 처리
  setFileValue(inputValue);
  setSelectionType('sample');
  onChange(inputValue);
};
src/components/common/input/drop-down/index.tsx (2)

30-30: 상태 관리 로직이 개선되었습니다.

currentValue 상태를 추가하여 내부 선택값을 독립적으로 관리하는 것은 좋은 접근입니다. 하지만 초기값을 value prop과 동기화하는 것이 더 나을 것 같습니다.

다음과 같이 초기값을 설정하는 것을 고려해보세요:

-const [currentValue, setCurrentValue] = useState<string | null>(null);
+const [currentValue, setCurrentValue] = useState<string | null>(value);

79-79: Select 컴포넌트의 onChange 핸들러가 복잡합니다.

인라인 화살표 함수를 사용하는 대신 handleChange를 직접 전달하는 것이 더 깔끔할 것 같습니다. 또한 valuecurrentValue의 동기화 로직을 개선할 필요가 있습니다.

다음과 같은 수정을 고려해보세요:

      value={currentValue}
      onFocus={() => setIsFocused(true)}
      onBlur={() => setIsFocused(false)}
-     onChange={(_value, option) => handleChange(_value, option)}
+     onChange={handleChange}

Also applies to: 83-83

src/components/common/crew-list/crew-card.tsx (1)

Line range hint 34-42: 성능 최적화를 위한 제안사항이 있습니다.

현재 구현에서 개선할 수 있는 부분들이 있습니다:

  1. 마우스 이벤트 핸들러 최적화:
- const handleCardClick = () => {
-   router.push(CREWPAGE);
- };
-
- const handleCardMouseUp = () => {
+ const handleCardMouseUp = React.useCallback(() => {
    if (!prefetched.has(CREWPAGE)) {
      router.prefetch(CREWPAGE);
      setPrefetched(new Set(prefetched).add(CREWPAGE));
    }
- };
+ }, [CREWPAGE, prefetched, router]);
  1. 이미지 최적화:
- <Image fill objectFit="cover" alt={title} src={imageUrl} />
+ <Image 
+   fill 
+   objectFit="cover" 
+   alt={title} 
+   src={imageUrl}
+   loading="lazy"
+   sizes="(max-width: 768px) 100vw, 230px"
+   quality={85}
+ />
src/components/common/input/file-input-wrap/file-input/index.tsx (1)

Line range hint 1-120: 성능 최적화를 위한 제안사항

컴포넌트의 전반적인 구현은 잘 되어있으나, 다음과 같은 성능 최적화를 고려해볼 수 있습니다:

  1. 파일 미리보기 이미지 최적화
  2. 메모이제이션을 통한 불필요한 리렌더링 방지

다음과 같은 최적화를 제안드립니다:

+import { ChangeEvent, useEffect, useRef, useState, useCallback, useMemo } from 'react';

 export default function FileInput({ value, isBlur, onChange }: FileInputProps) {
-  const handleFileLoad = (file: File) => {
+  const handleFileLoad = useCallback((file: File) => {
     if (fileReader) {
       fileReader.abort();
     }
     // ... 나머지 코드
-  };
+  }, [fileReader]);

-  const debouncedHandleFileLoad = debounce(handleFileLoad, 300);
+  const debouncedHandleFileLoad = useMemo(
+    () => debounce(handleFileLoad, 300),
+    [handleFileLoad]
+  );
src/app/(crew)/page.tsx (2)

25-28: handleRegionChange 함수를 더 간단하게 리팩토링하면 좋을 것 같습니다.

현재 로직을 더 간결하게 작성할 수 있습니다.

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

-    const selectedRegion = regionData.find((dataItem) => dataItem.main.label === newValue);
-    if (selectedRegion?.main.label === '지역 전체') return '';
-    return selectedRegion ? selectedRegion.main.label : '';
+    return newValue === '지역 전체' ? '' : newValue;

44-44: 정렬 타입 매핑을 상수로 관리하면 좋을 것 같습니다.

현재 하드코딩된 문자열을 상수로 분리하여 관리하면 유지보수성이 향상될 것 같습니다.

다음과 같은 방식을 제안드립니다:

// src/constants/sort.ts
export const SORT_TYPES = {
  LATEST: '최신순',
  POPULAR: '인기순',
} as const;

export const SORT_TYPE_MAP = {
  [SORT_TYPES.LATEST]: 'LATEST',
  [SORT_TYPES.POPULAR]: 'POPULAR',
} as const;

그리고 컴포넌트에서는 다음과 같이 사용:

-      sortType: sort === '최신순' ? 'LATEST' : 'POPULAR',
+      sortType: SORT_TYPE_MAP[sort as keyof typeof SORT_TYPE_MAP],
src/app/(crew)/crew/_components/create-gathering-form/index.tsx (3)

91-98: 파일 유효성 검사 로직이 잘 구현되었습니다

파일 크기와 타입에 대한 유효성 검사가 적절하게 구현되었습니다. 다만, 다음과 같은 개선사항을 고려해보시면 좋을 것 같습니다:

  1. 이미지 업로드 전 클라이언트 측에서 압축을 수행하여 서버 부하를 줄이는 것을 고려해보세요.
  2. WebP 포맷 지원을 추가하면 더 효율적인 이미지 처리가 가능할 것 같습니다.
validate: {
  fileSize: (file) =>
    file && file instanceof File
      ? file.size <= 5242880 || '파일 크기는 5MB 이하여야 합니다.'
      : true,
  fileType: (file) =>
    file && file instanceof File
      ? ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'].includes(file.type) ||
        'JPG, PNG, WebP 파일만 업로드 가능합니다.'
      : true,
}

33-35: 폼 에러 메시지 개선 제안

현재 에러 메시지들이 다소 기본적입니다. 사용자 경험 향상을 위해 더 구체적인 메시지를 제공하면 좋을 것 같습니다.

rules: {
  required: '약속 이름을 입력해 주세요.',
  validate: {
    maxLength: (value) =>
      value.length <= 20 || '약속 이름은 20자를 초과할 수 없습니다.',
    minLength: (value) =>
      value.length >= 2 || '약속 이름은 최소 2자 이상이어야 합니다.'
  }
}

Line range hint 41-196: 접근성 개선 제안

폼의 기본적인 접근성은 잘 구현되어 있으나, 다음과 같은 개선사항을 제안드립니다:

  1. 에러 메시지에 aria-errormessage 속성 추가
  2. 필수 입력 필드에 aria-required="true" 추가
  3. 현재 입력 진행 상태를 스크린 리더에 전달하기 위한 aria-live 영역 추가

예시 구현:

<TextInput
  {...field}
  id="gathering-title"
  aria-required="true"
  aria-errormessage={errors.title?.message}
  aria-invalid={!!errors.title}
/>

<div aria-live="polite" className="sr-only">
  {isValid ? '모든 필드가 올바르게 입력되었습니다.' : ''}
</div>
src/app/(crew)/crew/_components/create-crew-form/index.tsx (4)

13-20: 타입 정의가 개선되었습니다.

타입 정의가 더 명확해졌으며, 폼 데이터의 일관성이 향상되었습니다. 다만, CreateCrewRequestTypes가 import 되었지만 사용되지 않는 것으로 보입니다.

사용하지 않는 CreateCrewRequestTypes import를 제거하는 것이 좋겠습니다:

-import { CreateCrewFormTypes, CreateCrewRequestTypes } from '@/src/types/create-crew';
+import { CreateCrewFormTypes } from '@/src/types/create-crew';

50-51: 이미지 URL 감시 로직에 설명이 필요합니다.

imageUrl 필드의 변경을 감시하는 로직이 추가되었습니다. 다른 watch 로직들처럼 주석을 추가하면 코드의 가독성이 향상될 것 같습니다.

+// 이미지 URL 값의 변화를 감지하여 유효성 검사 트리거
const imageUrl = useWatch({ control, name: 'imageUrl' });

167-174: 파일 유효성 검사 로직을 상수로 분리하면 좋겠습니다.

파일 크기와 타입 제한에 대한 매직 넘버와 메시지들을 상수로 분리하면 유지보수성이 향상될 것 같습니다.

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

+const FILE_VALIDATION = {
+  MAX_SIZE: 5242880,
+  ALLOWED_TYPES: ['image/jpeg', 'image/jpg', 'image/png'],
+  MESSAGES: {
+    SIZE: '파일 크기는 5MB 이하여야 합니다.',
+    TYPE: 'JPG, PNG 파일만 업로드 가능합니다.'
+  }
+} as const;

validate: {
  fileSize: (file) =>
    file && file instanceof File
-     ? file.size <= 5242880 || '파일 크기는 5MB 이하여야 합니다.'
+     ? file.size <= FILE_VALIDATION.MAX_SIZE || FILE_VALIDATION.MESSAGES.SIZE
      : true,
  fileType: (file) =>
    file && file instanceof File
-     ? ['image/jpeg', 'image/jpg', 'image/png'].includes(file.type) ||
-       'JPG, PNG 파일만 업로드 가능합니다.'
+     ? FILE_VALIDATION.ALLOWED_TYPES.includes(file.type) ||
+       FILE_VALIDATION.MESSAGES.TYPE
      : true,
}

180-184: 파일 입력 컴포넌트의 변경 처리가 개선되었습니다.

FileInputWrap 컴포넌트의 props가 적절하게 업데이트되었습니다. 다만, 파일 변경 시 즉시 유효성 검사를 트리거하는 것이 사용자 경험을 저하시킬 수 있습니다.

파일 선택 후 일정 시간이 지난 후에 유효성 검사를 수행하도록 개선하는 것을 제안드립니다:

onChange={(newValue) => {
  field.onChange(newValue);
-  trigger('imageUrl');
+  setTimeout(() => {
+    trigger('imageUrl');
+  }, 500);
}}
src/app/(crew)/crew/create/page.tsx (1)

33-34: 'console.error' 대신 로깅 모듈 사용을 고려하세요

프로덕션 환경에서 console.error를 사용하는 것은 권장되지 않습니다. 대신 에러 로깅을 위한 전용 모듈이나 서비스를 활용하여 에러를 관리하는 것이 좋습니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 243dc0d and 824ba7f.

📒 Files selected for processing (24)
  • public/assets/images/crew-sample/index.tsx (1 hunks)
  • public/assets/images/gathering-sample/index.tsx (1 hunks)
  • src/_apis/crew/crew.ts (1 hunks)
  • src/_apis/image/get-image-url.ts (1 hunks)
  • src/_queries/crew/crew-list-queries.tsx (1 hunks)
  • src/app/(crew)/crew/_components/create-crew-form/create-crew-form.stories.tsx (1 hunks)
  • src/app/(crew)/crew/_components/create-crew-form/index.tsx (4 hunks)
  • src/app/(crew)/crew/_components/create-gathering-form/create-gathering-form.stories.tsx (2 hunks)
  • src/app/(crew)/crew/_components/create-gathering-form/index.tsx (3 hunks)
  • src/app/(crew)/crew/_components/create-gathering-modal/container.tsx (1 hunks)
  • src/app/(crew)/crew/_components/create-gathering-modal/presenter.tsx (1 hunks)
  • src/app/(crew)/crew/create/page.tsx (2 hunks)
  • src/app/(crew)/crew/detail/[id]/_components/create-gathering.tsx (2 hunks)
  • src/app/(crew)/crew/detail/[id]/edit/page.tsx (1 hunks)
  • src/app/(crew)/page.tsx (2 hunks)
  • src/components/common/crew-list/crew-card.tsx (1 hunks)
  • src/components/common/input/drop-down/index.tsx (3 hunks)
  • src/components/common/input/file-input-wrap/file-input/index.tsx (2 hunks)
  • src/components/common/input/file-input-wrap/file-sample/file-sample.stories.tsx (1 hunks)
  • src/components/common/input/file-input-wrap/file-sample/index.tsx (2 hunks)
  • src/components/common/input/file-input-wrap/index.tsx (2 hunks)
  • src/types/create-crew.d.ts (1 hunks)
  • src/types/gathering-data.d.ts (1 hunks)
  • src/utils/api.ts (0 hunks)
💤 Files with no reviewable changes (1)
  • src/utils/api.ts
🔇 Additional comments (31)
src/_apis/crew/crew.ts (1)

5-5: 응답 타입 검증 로직 추가 권장

response 객체의 data 속성이 실제로 CreateCrewResponseTypes 타입과 일치하는지 런타임에 검증하는 것이 안전합니다.

타입 정의를 확인하기 위해 다음 스크립트를 실행합니다:

src/_apis/image/get-image-url.ts (3)

4-7: 타입 제한이 적절합니다.

파일 타입과 이미지 타입에 대한 제한이 명확하게 정의되어 있습니다.


1-18: API 엔드포인트 검증이 필요합니다.

/api/images 엔드포인트가 올바르게 구현되어 있는지 확인이 필요합니다.

#!/bin/bash
# Description: Verify the API endpoint implementation
rg -A 10 "'/api/images'" src/

1-2: 타입 정의 검증이 필요합니다.

GetImageUrlResponseTypes 타입이 올바르게 정의되어 있는지 확인이 필요합니다.

✅ Verification successful

타입이 올바르게 정의되어 있음을 확인했습니다.

GetImageUrlResponseTypes 인터페이스가 src/types/create-crew.d.ts 파일에 명확하게 정의되어 있으며, 예상대로 imageUrl: string 속성을 포함하고 있습니다. 타입 정의와 임포트 구문이 모두 정상적입니다.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the type definition exists and is properly defined
rg -A 5 "interface GetImageUrlResponseTypes" src/

Length of output: 131


Script:

#!/bin/bash
# Search for type definition in different formats
rg -A 5 "type GetImageUrlResponseTypes" src/
rg -A 5 "export.*GetImageUrlResponseTypes" src/

# Search for the file that might contain this type
fd "create-crew.ts" src/

# If found, examine the content of create-crew.ts
fd "create-crew.ts" src/ -x cat {}

Length of output: 305

public/assets/images/gathering-sample/index.tsx (1)

6-10: S3 URL의 유효성 검증 필요

프로덕션 S3 버킷의 URL을 사용하고 있습니다. 해당 이미지들이 실제로 존재하는지 확인이 필요합니다.

✅ Verification successful

S3 이미지 URL이 모두 정상적으로 접근 가능함을 확인했습니다

모든 S3 이미지 URL에 대해 HTTP 200 OK 응답을 받았으며, 이는 해당 이미지들이 실제로 존재하고 접근 가능하다는 것을 의미합니다.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify if the S3 images exist and are accessible

for url in \
  "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"
do
  curl -I "$url" 2>/dev/null | head -n 1
done

Length of output: 1264

src/types/create-crew.d.ts (3)

14-22: API 요청 타입 정의가 적절합니다

폼 데이터와 달리 API 요청 타입에서는:

  • 모든 필드가 필수값으로 지정되어 있습니다
  • imageUrlstring 타입으로 제한되어 있습니다
  • null 값을 허용하지 않아 API 계약이 명확합니다

28-29: 이미지 URL 형식에 대한 명세 필요

이미지 URL의 예상 형식과 제약사항을 문서화하면 좋을 것 같습니다:

  • URL의 기본 형식 (예: CDN URL 구조)
  • 지원하는 이미지 형식
  • URL의 유효 기간 (만료 시간이 있는 경우)

이미지 URL 처리 로직을 확인해보겠습니다:

#!/bin/bash
# Description: 이미지 URL 처리 로직 확인

# 이미지 URL 관련 로직 검색
rg -t typescript "GetImageUrlResponseTypes" -A 5

4-12: 폼 데이터 유효성 검증 로직 추가 필요

폼 데이터에서 API 요청 데이터로 변환하는 과정에서 다음 사항들에 대한 유효성 검증이 필요합니다:

  • imageUrlnull인 경우 처리
  • subCategorysubLocationnull인 경우 처리

다음 스크립트로 폼 데이터 처리 로직을 확인해보겠습니다:

✅ Verification successful

폼 데이터 유효성 검증이 적절히 구현되어 있음을 확인했습니다

검토 결과 handleSubmit 함수에서 다음과 같이 적절한 데이터 변환 및 유효성 검증이 이미 구현되어 있습니다:

  • imageUrlFile 타입인 경우 이미지 업로드 처리
  • subCategorysubLocationnull 값은 빈 문자열('')로 변환
  • API 요청 전 모든 필수 필드가 적절한 형식으로 변환됨
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: 폼 데이터 변환 및 유효성 검증 로직 확인

# 폼 데이터 처리 로직 검색
rg -t typescript "CreateCrewFormTypes.*CreateCrewRequestTypes" -A 10

Length of output: 177


Script:

#!/bin/bash
# Search for form data handling and validation logic
rg "CreateCrewFormTypes" -A 10

# Search for form submission or API call logic
rg "createCrew" -A 10

Length of output: 10937

src/app/(crew)/crew/_components/create-gathering-modal/container.tsx (1)

3-3: 타입 변경이 올바르게 적용되었습니다.

CreateGatheringRequestType에서 CreateGatheringFormTypes로의 타입 변경이 일관성 있게 적용되었습니다.

Also applies to: 9-9

src/_queries/crew/crew-list-queries.tsx (2)

7-7: 쿼리 키 구조가 적절히 개선되었습니다!

'crewLists'를 쿼리 키의 첫 번째 요소로 추가한 것은 React Query의 캐시 관리 측면에서 좋은 접근입니다. 이는 crew 관련 쿼리들을 논리적으로 그룹화하여 캐시 무효화나 관리가 더욱 용이해질 것입니다.


Line range hint 15-19: 에러 처리가 개선되었습니다!

undefined 응답에 대한 명시적인 에러 처리를 추가한 것은 디버깅과 오류 추적을 더욱 용이하게 만들어줄 것입니다. 이는 API 연동 시 발생할 수 있는 edge case를 적절히 처리하는 좋은 예시입니다.

src/app/(crew)/crew/_components/create-crew-form/create-crew-form.stories.tsx (3)

2-2: 타입 정의 구조 개선이 확인되었습니다.

CreateCrewFormTypesCreateCrewRequestTypes를 함께 가져오도록 수정된 것이 확인되었습니다. 이는 크루 생성 프로세스에서 사용되는 타입들을 더 명확하게 구분하고 있습니다.


9-9: imageUrl 초기값 타입 안전성이 개선되었습니다.

imageUrl의 초기값을 null에서 빈 문자열('')로 변경한 것이 확인되었습니다. 이는 타입 안전성을 향상시키는 좋은 변경사항입니다.


Line range hint 33-33: Template 타입 사용이 일관성 있게 적용되었는지 확인이 필요합니다.

CreateCrewFormTypes를 Template의 타입으로 사용하고 있습니다. 이 변경이 다른 컴포넌트들과 일관되게 적용되었는지 확인이 필요합니다.

✅ Verification successful

Let me analyze the usage of CreateCrewFormTypes across the codebase to verify its consistency.

From the search results, I can see that CreateCrewFormTypes is used consistently across all relevant components:

  1. Defined in src/types/create-crew.d.ts
  2. Used in create page (src/app/(crew)/crew/create/page.tsx)
  3. Used in edit page (src/app/(crew)/crew/detail/[id]/edit/page.tsx)
  4. Used in the form component (src/app/(crew)/crew/_components/create-crew-form/index.tsx)
  5. Used in the stories file (src/app/(crew)/crew/_components/create-crew-form/create-crew-form.stories.tsx)

Let me generate the final response:

Template 타입이 일관성 있게 적용되어 있습니다.

CreateCrewFormTypes는 crew 생성 및 수정과 관련된 모든 컴포넌트에서 일관되게 사용되고 있습니다. 스토리북의 Template 타입 사용도 다른 컴포넌트들과 일치합니다.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify consistent usage of CreateCrewFormTypes across components

# Search for CreateCrewFormTypes usage
rg "CreateCrewFormTypes" -A 3

# Search for other crew form related components
fd -e tsx -e ts | rg "crew.*form"

Length of output: 4486

src/app/(crew)/crew/detail/[id]/_components/create-gathering.tsx (1)

8-8: 타입 시스템 업데이트가 올바르게 적용되었습니다.

타입 정의가 RequestType에서 FormTypes로 일관성 있게 변경되었으며, initialValue 객체의 기본값이 적절하게 설정되어 있습니다.

Also applies to: 23-29

src/app/(crew)/crew/_components/create-gathering-modal/presenter.tsx (2)

3-3: 타입 임포트가 올바르게 추가되었습니다.

새로운 타입 정의를 올바른 경로에서 임포트하고 있습니다.


10-10: 타입 변경에 따른 호환성 검증이 필요합니다.

CreateGatheringFormTypes로의 타입 변경이 다른 컴포넌트들과의 호환성에 영향을 미칠 수 있습니다.

다음 스크립트를 실행하여 타입 호환성을 확인하세요:

✅ Verification successful

Let me gather more information about the type definition and its usage.


Let me check one more thing to see how this type is being used in other components and if there are any potential conflicts.


CreateGatheringFormTypes 타입은 일관되게 사용되고 있습니다.

검증 결과, CreateGatheringFormTypes 타입은 모든 관련 컴포넌트에서 동일한 방식으로 사용되고 있으며, 타입 호환성 문제가 없음을 확인했습니다:

  • Form 컴포넌트의 props 타입 정의
  • Modal의 presenter와 container 컴포넌트
  • 상세 페이지의 생성 컴포넌트
  • Storybook 구현

모든 컴포넌트가 동일한 타입을 일관되게 import하여 사용하고 있어 호환성 이슈가 없습니다.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: CreateGatheringFormTypes 사용 현황 및 호환성 확인

# CreateGatheringFormTypes를 사용하는 모든 파일 검색
echo "CreateGatheringFormTypes 사용 현황:"
rg -l "CreateGatheringFormTypes"

# CreateGatheringForm 컴포넌트 props 타입 확인
echo "\nCreateGatheringForm 컴포넌트 타입 정의:"
ast-grep --pattern 'interface $_ {
  data: $_
  onSubmit: $_
  onEdit: $_
  onClose: $_
}'

Length of output: 660


Script:

#!/bin/bash
# Check the type definition and its usage in detail

# First, let's see the actual type definition
echo "CreateGatheringFormTypes 타입 정의:"
rg -A 10 "type CreateGatheringFormTypes" src/types/gathering-data.d.ts

# Check how it's being used in the form component
echo "\nCreateGatheringForm 컴포넌트에서의 사용:"
rg -A 5 "CreateGatheringFormTypes" src/app/\(crew\)/crew/_components/create-gathering-form/index.tsx

# Check the modal implementation
echo "\n모달 컴포넌트에서의 사용:"
rg -A 5 "CreateGatheringFormTypes" src/app/\(crew\)/crew/_components/create-gathering-modal/presenter.tsx

Length of output: 1428


Script:

#!/bin/bash
# Check usage in stories and detail component
echo "스토리북에서의 사용:"
rg -A 5 "CreateGatheringFormTypes" src/app/\(crew\)/crew/_components/create-gathering-form/create-gathering-form.stories.tsx

echo "\n상세 페이지에서의 사용:"
rg -A 5 "CreateGatheringFormTypes" src/app/\(crew\)/crew/detail/\[id\]/_components/create-gathering.tsx

echo "\n컨테이너 컴포넌트에서의 사용:"
rg -A 5 "CreateGatheringFormTypes" src/app/\(crew\)/crew/_components/create-gathering-modal/container.tsx

Length of output: 1624

src/types/gathering-data.d.ts (1)

29-36: 타입 정의가 적절하게 구성되었습니다.

폼 데이터의 타입이 명확하게 정의되어 있으며, 특히 imageUrlFile | string | null 타입을 허용하는 것이 적절합니다. 이는 파일 업로드 과정에서 필요한 유연성을 제공합니다.

각 필드에 대한 제약사항이 주석으로 명시되어 있어 좋습니다.

src/components/common/input/file-input-wrap/file-sample/index.tsx (2)

5-9: 인터페이스 타입 변경이 적절합니다!

API 연동을 위한 타입 변경이 잘 이루어졌습니다. isEditvalue 프롭이 옵셔널로 추가되어 기존 코드와의 호환성도 유지되었습니다.


12-12: 프롭스 구조분해가 올바르게 구현되었습니다!

새로 추가된 isEditvalue 프롭이 인터페이스 정의에 맞게 잘 구현되었습니다.

src/components/common/input/file-input-wrap/file-sample/file-sample.stories.tsx (1)

29-29: 상태 타입 변경이 적절해 보입니다.

파일 객체 대신 URL 문자열을 사용하도록 변경한 것은 API 통합 관점에서 적절한 선택입니다. 이는 서버와의 통신을 단순화하고 상태 관리를 더 효율적으로 만들 것입니다.

src/app/(crew)/crew/detail/[id]/edit/page.tsx (1)

4-7: 필요한 의존성이 올바르게 추가되었습니다!

라우팅 파라미터 처리와 데이터 페칭을 위한 필수적인 의존성들이 잘 구성되어 있습니다.

src/app/(crew)/crew/_components/create-gathering-form/create-gathering-form.stories.tsx (1)

4-4: 타입 변경이 올바르게 적용되었습니다.

CreateGatheringRequestType에서 CreateGatheringFormTypes로의 타입 변경이 일관되게 적용되었으며, 초기값 구조도 적절합니다.

Also applies to: 7-14, 33-33

src/components/common/crew-list/crew-card.tsx (1)

30-30: 동적 URL 구현이 적절히 이루어졌습니다.

하드코딩된 URL을 동적 라우팅으로 변경한 것이 적절합니다. 각 크루의 고유 ID를 사용하여 올바른 상세 페이지로 이동하도록 구현되었습니다.

라우팅 처리가 올바르게 구현되었는지 확인하기 위해 다음 스크립트를 실행하세요:

✅ Verification successful

Let me gather more information about the route implementation.


동적 라우팅이 올바르게 구현되어 있음을 확인했습니다.

크루 상세 페이지의 라우팅이 Next.js의 동적 라우트 규칙에 맞게 구현되어 있습니다:

  • /crew/detail/[id] 경로에 페이지 컴포넌트가 존재합니다
  • params.id를 통해 URL 파라미터를 올바르게 처리하고 있습니다
  • 상세 페이지와 수정 페이지 모두 동일한 라우팅 구조를 따르고 있습니다

따라서 crew-card.tsxCREWPAGE 상수에서 사용하는 동적 URL 구조가 실제 라우팅 구현과 일치합니다.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# 설명: 크루 상세 페이지 라우팅 구현 확인

# app 디렉토리에서 crew/detail/[id] 라우트 구현 확인
fd -t d "detail" "src/app"

# 크루 상세 페이지 컴포넌트에서 id 파라미터 사용 확인
ast-grep --pattern 'params: { id }' 

Length of output: 146


Script:

#!/bin/bash
# 크루 상세 페이지 라우트 구현 확인

# detail 디렉토리의 page 파일 확인
fd "page" "src/app/(crew)/crew/detail"

# detail 디렉토리의 모든 파일 내용 확인
rg -l "" "src/app/(crew)/crew/detail"

# 동적 라우팅 파라미터 사용 확인
ast-grep --pattern 'params.id'

Length of output: 898

src/components/common/input/file-input-wrap/file-input/index.tsx (3)

7-8: 타입 정의가 개선되었습니다.

FileInputProps 인터페이스의 변경사항이 원격 URL을 지원하도록 적절히 수정되었습니다. value 타입에 string이 추가되어 URL 문자열을 처리할 수 있게 되었고, onChangeFile | null로 제한되어 타입 안정성이 향상되었습니다.


13-13: 초기 상태 설정 로직이 개선되었습니다.

preview 상태의 초기값 설정이 value의 타입에 따라 적절히 처리되도록 구현되었습니다. 문자열 URL인 경우 해당 값을 사용하고, 그렇지 않은 경우 null을 사용하는 방식은 적절합니다.


82-82: 의존성 배열에 value 추가가 필요할 수 있습니다.

useEffect의 의존성 배열에 value가 추가된 것은 적절하나, 이 변경으로 인한 부작용을 고려해야 합니다.

다음 시나리오들을 검증해주세요:

✅ Verification successful

Let me analyze the code further to understand the relationship between value and isBlur in the component.


value 의존성 추가는 적절합니다

useEffect 내부에서 value를 직접 참조하고 있고, isBlurvalue 값의 변경에 따라 미리보기를 제거하는 로직이 정확히 동작해야 하므로 의존성 배열에 value가 포함되는 것이 올바른 구현입니다. 부작용 검증 결과:

  • value 변경은 상위 컴포넌트에서 제어되며 FileInput 컴포넌트의 미리보기 상태만 영향을 받습니다
  • isBlur 상태 변경과 함께 작동하여 선택된 파일이 있을 때만 미리보기를 제거합니다
  • 컴포넌트 언마운트 시 정리(cleanup) 로직이 올바르게 구현되어 있습니다
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: value prop 변경에 따른 부작용 검증

# value prop이 변경되는 모든 상황 검색
rg -A 5 "FileInput.*value=" 

# isBlur 상태 변경 로직 검색
rg -A 5 "isBlur.*="

Length of output: 5191


Script:

#!/bin/bash
# Let's check the implementation of FileInput component
ast-grep --pattern 'const FileInput = ({ $$ }) => {
  $$$
}'

# Also check for any useEffect implementations in the component
rg -A 10 "useEffect" src/components/common/input/file-input-wrap/file-input/

Length of output: 3410

src/app/(crew)/page.tsx (1)

19-19: 상태 초기값이 UI와 일치하도록 수정되었습니다!

정렬 상태의 초기값을 영문에서 한글로 변경한 것이 드롭다운 UI와 일관성있게 잘 맞춰졌습니다.

src/app/(crew)/crew/_components/create-gathering-form/index.tsx (2)

107-107: 유효성 검사 트리거 로직이 개선되었습니다

불필요한 타입 체크를 제거하고 간결하게 구현되었습니다.


10-17: 타입 변경에 따른 사용처 확인 필요

CreateGatheringRequestType에서 CreateGatheringFormTypes로의 타입 변경이 이루어졌습니다. 이 변경이 다른 컴포넌트들에도 올바르게 적용되었는지 확인이 필요합니다.

✅ Verification successful

Let me check the type definitions to ensure they are properly aligned.


타입 변경이 적절하게 이루어졌습니다

CreateGatheringFormTypesCreateGatheringRequestTypes는 각각 폼 데이터와 API 요청을 위한 별도의 타입으로 명확하게 구분되어 있습니다. 주요 차이점은 imageUrl 필드의 타입(File | string | null vs string)이며, 이는 폼 처리 과정에서 필요한 적절한 구분입니다. 모든 관련 컴포넌트에서 CreateGatheringFormTypes를 일관되게 사용하고 있어 문제가 없습니다.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: 타입 사용처 확인

# CreateGatheringRequestType의 사용처 확인
rg "CreateGatheringRequestType"

# CreateGatheringFormTypes의 사용처 확인
rg "CreateGatheringFormTypes"

Length of output: 1994


Script:

#!/bin/bash
# Check the type definitions
cat src/types/gathering-data.d.ts

Length of output: 1273

src/app/(crew)/crew/_components/create-crew-form/index.tsx (1)

37-40: 폼 훅의 타입이 적절하게 업데이트되었습니다.

useForm 훅의 제네릭 타입이 CreateCrewFormTypes로 변경되어 폼 데이터의 타입 안정성이 향상되었습니다.

Comment on lines 4 to 14
export async function createCrew(data: CreateCrewRequestTypes) {
const response: { data: CreateCrewResponseTypes } = await fetchApi(`/api/crews`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // 인증 정보를 요청에 포함
body: JSON.stringify(data),
});
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 호출 시 발생할 수 있는 다양한 에러 상황(네트워크 오류, 서버 오류 등)에 대한 처리가 필요합니다. try-catch 구문을 사용하여 에러를 적절히 처리하는 것이 좋습니다.

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

 export async function createCrew(data: CreateCrewRequestTypes) {
+  try {
     const response: { data: CreateCrewResponseTypes } = await fetchApi(`/api/crews`, {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json',
       },
       credentials: 'include',
       body: JSON.stringify(data),
     });
     return response?.data;
+  } catch (error) {
+    throw new Error('크루 생성 중 오류가 발생했습니다: ' + error.message);
+  }
 }
📝 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) {
const response: { data: CreateCrewResponseTypes } = await fetchApi(`/api/crews`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // 인증 정보를 요청에 포함
body: JSON.stringify(data),
});
return response?.data;
}
export async function createCrew(data: CreateCrewRequestTypes) {
try {
const response: { data: CreateCrewResponseTypes } = await fetchApi(`/api/crews`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // 인증 정보를 요청에 포함
body: JSON.stringify(data),
});
return response?.data;
} catch (error) {
throw new Error('크루 생성 중 오류가 발생했습니다: ' + error.message);
}
}

Comment on lines 8 to 11
const formData = new FormData();
if (file instanceof File) {
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

파일 유효성 검사가 필요합니다.

파일 크기 제한과 파일 형식 검증이 누락되어 있습니다. 보안과 성능을 위해 이러한 검증을 추가하는 것이 좋습니다.

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

 if (file instanceof File) {
+  const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
+  const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif'];
+  
+  if (file.size > MAX_FILE_SIZE) {
+    throw new Error('파일 크기는 5MB를 초과할 수 없습니다.');
+  }
+  
+  if (!ALLOWED_TYPES.includes(file.type)) {
+    throw new Error('지원하지 않는 파일 형식입니다.');
+  }
+
   formData.append('file', file);
 }
📝 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 formData = new FormData();
if (file instanceof File) {
formData.append('file', file);
}
const formData = new FormData();
if (file instanceof File) {
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif'];
if (file.size > MAX_FILE_SIZE) {
throw new Error('파일 크기는 5MB를 초과할 수 없습니다.');
}
if (!ALLOWED_TYPES.includes(file.type)) {
throw new Error('지원하지 않는 파일 형식입니다.');
}
formData.append('file', file);
}

Comment on lines 13 to 17
const response: { data: GetImageUrlResponseTypes } = await fetchApi(`/api/images?type=${type}`, {
method: 'POST',
body: formData,
});
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.

⚠️ Potential issue

에러 처리 개선이 필요합니다.

API 호출 실패 시의 에러 처리가 누락되어 있습니다. 또한 response가 null일 경우의 처리도 보완이 필요합니다.

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

-  const response: { data: GetImageUrlResponseTypes } = await fetchApi(`/api/images?type=${type}`, {
-    method: 'POST',
-    body: formData,
-  });
-  return response?.data;
+  try {
+    const response: { data: GetImageUrlResponseTypes } = await fetchApi(`/api/images?type=${type}`, {
+      method: 'POST',
+      body: formData,
+    });
+    
+    if (!response || !response.data) {
+      throw new Error('이미지 업로드에 실패했습니다.');
+    }
+    
+    return response.data;
+  } catch (error) {
+    console.error('이미지 업로드 중 오류 발생:', error);
+    throw new Error('이미지 업로드 중 오류가 발생했습니다.');
+  }
📝 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,
});
return response?.data;
try {
const response: { data: GetImageUrlResponseTypes } = await fetchApi(`/api/images?type=${type}`, {
method: 'POST',
body: formData,
});
if (!response || !response.data) {
throw new Error('이미지 업로드에 실패했습니다.');
}
return response.data;
} catch (error) {
console.error('이미지 업로드 중 오류 발생:', error);
throw new Error('이미지 업로드 중 오류가 발생했습니다.');
}

Comment on lines 23 to 26
if (isEdit && value === image && radioInput.current) {
radioInput.current.checked = true;
}
}, [isBlur, value]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

useEffect 의존성 배열과 조건부 로직 개선이 필요합니다.

다음과 같은 문제점들이 있습니다:

  1. useEffect의 의존성 배열에 isEdit가 누락되었습니다.
  2. radioInput.current 접근 시 잠재적인 타이밍 이슈가 있을 수 있습니다.

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

  useEffect(() => {
    if (isBlur && radioInput.current) {
      radioInput.current.checked = false;
    }
-    if (isEdit && value === image && radioInput.current) {
+    if (!isBlur && isEdit && value === image && radioInput.current) {
      radioInput.current.checked = true;
    }
-  }, [isBlur, value]);
+  }, [isBlur, value, isEdit, image]);
📝 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
if (isEdit && value === image && radioInput.current) {
radioInput.current.checked = true;
}
}, [isBlur, value]);
if (isBlur && radioInput.current) {
radioInput.current.checked = false;
}
if (!isBlur && isEdit && value === image && radioInput.current) {
radioInput.current.checked = true;
}
}, [isBlur, value, isEdit, image]);

Comment on lines +32 to 35
const handleFileChange = (file: string | null) => {
setSelectedFile(file);
setIsBlur(false); // 파일 선택 시 블러 해제
};
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

핸들러 타입이 컴포넌트 인터페이스와 일치하지 않습니다.

FileSample 컴포넌트의 인터페이스 정의를 보면 onChange 핸들러는 (inputValue: string) => void 타입을 가져야 하지만, stories 파일에서는 (file: string | null) => void 타입으로 구현되어 있습니다. 이는 타입 불일치를 발생시킬 수 있습니다.

  • src/components/common/input/file-input-wrap/file-sample/file-sample.stories.tsx: handleFileChange 함수의 매개변수 타입을 string으로 수정해야 합니다.
  • src/components/common/input/file-input-wrap/file-sample/index.tsx: 만약 null 값이 필요하다면, 인터페이스의 onChange 타입을 (inputValue: string | null) => void로 수정해야 합니다.
🔗 Analysis chain

핸들러 타입 변경의 영향도를 확인해주세요.

상태 타입 변경에 맞춰 핸들러의 매개변수 타입도 적절히 변경되었습니다. 다만, 이 변경이 다른 컴포넌트에 미치는 영향을 확인할 필요가 있습니다.

다른 컴포넌트에서의 사용 패턴을 확인하기 위해 다음 스크립트를 실행해주세요:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# FileSample 컴포넌트의 사용 패턴 검색
rg -t tsx -t ts "FileSample.*onChange" --ignore-file src/components/common/input/file-input-wrap/file-sample/file-sample.stories.tsx

# 파일 관련 핸들러의 타입 정의 검색
ast-grep --pattern 'interface $_ {
  $$$
  onChange?: $_;
  $$$
}'

Length of output: 2366


Script:

#!/bin/bash
# FileSample 컴포넌트의 사용 패턴 검색 (수정된 명령어)
rg -t typescript -t tsx "FileSample.*onChange" -A 2 -B 2

# 파일 관련 핸들러의 타입 정의 검색
rg -t typescript -t tsx "interface.*FileSample" -A 10

# FileSample 컴포넌트 정의 검색
ast-grep --pattern 'interface FileSampleProps {
  $$$
}'

Length of output: 873

Comment on lines 68 to +70
if (value === null) {
onChange(null);
onChange('');
setCurrentValue(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

useEffect에서의 onChange 호출이 불필요합니다.

valuenull일 때 빈 문자열을 전달하는 것은 일관성 없는 동작을 유발할 수 있습니다. null을 그대로 유지하는 것이 더 명확할 것 같습니다.

다음과 같이 수정하는 것을 추천드립니다:

  useEffect(() => {
    if (value === null) {
-     onChange('');
+     onChange(null);
      setCurrentValue(null);
    }
  }, [value]);
📝 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
if (value === null) {
onChange(null);
onChange('');
setCurrentValue(null);
if (value === null) {
onChange(null);
setCurrentValue(null);

Comment on lines +55 to +62
const handleChange = (newValue: string | null, option: { value: string; label: string }) => {
if (newValue === null) {
setCurrentValue(null);
onChange(null);
return;
}
setCurrentValue(newValue);
onChange(option.label);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

handleChange 함수의 타입 안정성을 개선해야 합니다.

option 매개변수의 타입이 인라인으로 정의되어 있습니다. 재사용성과 유지보수성을 위해 별도의 타입으로 분리하는 것이 좋습니다.

다음과 같이 타입을 분리하는 것을 추천드립니다:

+interface DropDownOption {
+  value: string;
+  label: string;
+}

-const handleChange = (newValue: string | null, option: { value: string; label: string }) => {
+const handleChange = (newValue: string | null, option: DropDownOption) => {
📝 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 handleChange = (newValue: string | null, option: { value: string; label: string }) => {
if (newValue === null) {
setCurrentValue(null);
onChange(null);
return;
}
setCurrentValue(newValue);
onChange(option.label);
interface DropDownOption {
value: string;
label: string;
}
const handleChange = (newValue: string | null, option: DropDownOption) => {
if (newValue === null) {
setCurrentValue(null);
onChange(null);
return;
}
setCurrentValue(newValue);
onChange(option.label);

Comment on lines +53 to +54
setValue('mainCategory', newValue || '');
setValue('subCategory', null);
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

Comment on lines 26 to 37
const { isPending, mutate } = useMutation({
mutationFn: (data: CreateCrewRequestTypes) => createCrew(data),
onSuccess: (response) => {
queryClient.invalidateQueries({ queryKey: ['crewLists', 'crewDetail'] });
router.push(`/crew/detail/${response?.crewId}`);
},
onError: (error) => {
// eslint-disable-next-line no-console
console.error(error);
Toast({ message: '크루 생성하기에 실패했습니다.', type: 'error' });
},
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

'isPending' 대신 'isLoading'을 사용해야 합니다

React Query의 useMutation 훅에서는 isPending이 아니라 isLoading 상태 값을 제공합니다. 현재 코드에서 isPending을 사용하고 있어 정상적으로 동작하지 않을 수 있습니다.

수정 사항:

- const { isPending, mutate } = useMutation({
+ const { isLoading, mutate } = useMutation({

그리고 아래와 같이 변경하십시오:

- if (isPending)
+ if (isLoading)

Also applies to: 58-63

Comment on lines +41 to +44
if (data.imageUrl instanceof File) {
const imgResponse = await getImageUrl(data.imageUrl, 'CREW');
newImageUrl = imgResponse?.imageUrl as string;
}
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' 호출 시 에러 처리가 필요합니다

getImageUrl 함수 호출 중 에러가 발생할 수 있으므로, 이를 처리하기 위한 에러 핸들링이 필요합니다. try-catch 블록을 사용하여 예외 상황에 대비하고, 사용자에게 적절한 메시지를 전달하는 것이 좋습니다.

수정된 코드:

if (data.imageUrl instanceof File) {
+  try {
    const imgResponse = await getImageUrl(data.imageUrl, 'CREW');
    newImageUrl = imgResponse?.imageUrl as string;
+  } catch (error) {
+    console.error(error);
+    Toast({ message: '이미지 업로드에 실패했습니다.', type: 'error' });
+    return;
+  }
}
📝 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
if (data.imageUrl instanceof File) {
const imgResponse = await getImageUrl(data.imageUrl, 'CREW');
newImageUrl = imgResponse?.imageUrl as string;
}
if (data.imageUrl instanceof File) {
try {
const imgResponse = await getImageUrl(data.imageUrl, 'CREW');
newImageUrl = imgResponse?.imageUrl as string;
} catch (error) {
console.error(error);
Toast({ message: '이미지 업로드에 실패했습니다.', type: 'error' });
return;
}
}

Copy link
Contributor

@minkyung5x5 minkyung5x5 left a comment

Choose a reason for hiding this comment

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

크루생성이되다니!! 수고하셨습니다!

@CodeitFESI4-Team1 CodeitFESI4-Team1 deleted a comment from coderabbitai bot Nov 14, 2024
@CodeitFESI4-Team1 CodeitFESI4-Team1 deleted a comment from coderabbitai bot Nov 14, 2024
@CodeitFESI4-Team1 CodeitFESI4-Team1 deleted a comment from coderabbitai bot Nov 14, 2024
@CodeitFESI4-Team1 CodeitFESI4-Team1 deleted a comment from coderabbitai bot Nov 14, 2024
@CodeitFESI4-Team1 CodeitFESI4-Team1 deleted a comment from coderabbitai bot Nov 14, 2024
@CodeitFESI4-Team1 CodeitFESI4-Team1 deleted a comment from coderabbitai bot Nov 14, 2024
@CodeitFESI4-Team1 CodeitFESI4-Team1 deleted a comment from coderabbitai bot Nov 14, 2024
@CodeitFESI4-Team1 CodeitFESI4-Team1 deleted a comment from coderabbitai bot Nov 14, 2024
@CodeitFESI4-Team1 CodeitFESI4-Team1 deleted a comment from coderabbitai bot Nov 14, 2024
@CodeitFESI4-Team1 CodeitFESI4-Team1 deleted a comment from coderabbitai bot Nov 14, 2024
@CodeitFESI4-Team1 CodeitFESI4-Team1 deleted a comment from coderabbitai bot Nov 14, 2024
@CodeitFESI4-Team1 CodeitFESI4-Team1 deleted a comment from coderabbitai bot Nov 14, 2024
@CodeitFESI4-Team1 CodeitFESI4-Team1 deleted a comment from coderabbitai bot Nov 14, 2024
@CodeitFESI4-Team1 CodeitFESI4-Team1 deleted a comment from coderabbitai bot Nov 14, 2024
@CodeitFESI4-Team1 CodeitFESI4-Team1 deleted a comment from coderabbitai bot Nov 14, 2024
@CodeitFESI4-Team1 CodeitFESI4-Team1 deleted a comment from coderabbitai bot Nov 14, 2024
@github-actions
Copy link

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (4)
src/app/(crew)/crew/detail/[id]/edit/page.tsx (1)

Line range hint 29-31: PATCH API 구현이 필요합니다.

현재 handleEdit 함수가 구현되지 않았습니다. 크루 수정 기능이 정상적으로 동작하기 위해서는 다음 작업이 필요합니다:

  • PATCH API 엔드포인트 연결
  • 수정된 데이터 유효성 검증
  • 성공/실패 시 적절한 피드백 처리

PATCH API 구현에 도움이 필요하신가요? GitHub 이슈를 생성해드릴 수 있습니다.

src/app/(crew)/crew/create/page.tsx (2)

16-25: 폼 필드의 유효성 검증이 필요합니다.

초기값이 잘 설정되어 있지만, 각 필드에 대한 유효성 검증이 추가되면 좋을 것 같습니다:

  • title: 최소/최대 길이
  • totalCount: 최소/최대 인원수
  • introduce: 최소/최대 길이

46-55: 데이터 변환 로직 개선이 필요합니다.

현재 코드는 각 필드를 개별적으로 매핑하고 있어 유지보수가 어려울 수 있습니다. 다음과 같이 개선하는 것을 추천드립니다:

- const newData: CreateCrewRequestTypes = {
-   title: data.title,
-   mainCategory: data.mainCategory,
-   subCategory: data.subCategory ?? '',
-   imageUrl: newImageUrl ?? '',
-   mainLocation: data.mainLocation,
-   subLocation: data.subLocation ?? '',
-   totalCount: data.totalCount,
-   introduce: data.introduce,
- };
+ const newData: CreateCrewRequestTypes = {
+   ...data,
+   imageUrl: newImageUrl ?? '',
+   subCategory: data.subCategory ?? '',
+   subLocation: data.subLocation ?? '',
+ };
src/app/(crew)/crew/_components/create-crew-form/index.tsx (1)

268-289: 소개글 필드에 대한 개선사항을 제안드립니다.

현재 구현에 다음 사항들이 보완되면 좋을 것 같습니다:

  1. 필수 입력 여부 검증
  2. 최소 글자수 제한
  3. 특수문자 필터링
  4. 스크린 리더 지원을 위한 aria-label 추가

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

 <Controller
   name="introduce"
   control={control}
+  rules={{
+    required: '소개글은 필수 입력사항입니다.',
+    minLength: {
+      value: 10,
+      message: '최소 10자 이상 입력해주세요.',
+    },
+    pattern: {
+      value: /^[가-힣a-zA-Z0-9\s.,!?()]*$/,
+      message: '허용되지 않는 특수문자가 포함되어 있습니다.',
+    }
+  }}
   render={({ field }) => (
     <Textarea
       {...field}
       placeholder="크루 소개글을 100자 이내로 입력해주세요."
       maxLength={100}
+      aria-label="크루 소개글 입력"
       inputClassNames="h-40 py-2.5 px-4 bg-gray-100 placeholder:text-gray-400 font-pretendard text-base font-medium rounded-xl"
+      error={errors.introduce?.message}
     />
   )}
 />
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 99719e9 and ef8fc4f.

📒 Files selected for processing (7)
  • src/app/(crew)/crew/_components/create-crew-form/create-crew-form.stories.tsx (1 hunks)
  • src/app/(crew)/crew/_components/create-crew-form/index.tsx (5 hunks)
  • src/app/(crew)/crew/create/page.tsx (1 hunks)
  • src/app/(crew)/crew/detail/[id]/edit/page.tsx (1 hunks)
  • src/app/(crew)/page.tsx (4 hunks)
  • src/types/create-crew.d.ts (1 hunks)
  • tailwind.config.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/app/(crew)/crew/_components/create-crew-form/create-crew-form.stories.tsx
  • src/app/(crew)/page.tsx
  • src/types/create-crew.d.ts
  • tailwind.config.ts
🔇 Additional comments (7)
src/app/(crew)/crew/detail/[id]/edit/page.tsx (3)

4-7: 필요한 의존성이 올바르게 추가되었습니다.

새로운 기능 구현을 위한 적절한 의존성들이 추가되었습니다.


11-14: 이전 리뷰에서 지적된 로딩/에러 처리가 여전히 누락되어 있습니다.


16-26: ⚠️ Potential issue

초기값 설정에 대한 이전 리뷰 의견이 여전히 유효합니다.

새로 추가된 'introduce' 필드의 초기화가 필요합니다.

introduce 필드가 추가되었지만, API 응답 데이터를 활용하지 않고 빈 문자열로 초기화되어 있습니다. API 응답의 해당 필드를 사용하도록 수정이 필요합니다.

  const initialValue: CreateCrewFormTypes = {
    ...
-   introduce: '',
+   introduce: data.introduce || '',
  };
src/app/(crew)/crew/create/page.tsx (2)

5-11: 새로운 의존성이 적절하게 추가되었습니다.

필요한 컴포넌트와 훅들이 잘 임포트되었으며, 타입 정의도 명확합니다.


27-38: 이전 리뷰 의견이 아직 반영되지 않았습니다.

src/app/(crew)/crew/_components/create-crew-form/index.tsx (2)

14-14: 타입 정의가 적절히 업데이트되었습니다!

폼 타입과 프롭스가 새로운 요구사항에 맞게 잘 수정되었습니다.

Also applies to: 17-21, 38-38


52-53: 이전 리뷰 코멘트가 여전히 유효합니다.

카테고리와 지역 선택 로직에 대한 이전 리뷰 의견이 아직 반영되지 않았습니다.

Also applies to: 58-59, 63-64

Comment on lines +166 to +173
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, // 문자열인 경우 파일 타입 검사를 건너뜁니다.
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 '올바르지 않은 파일입니다.';
};

@github-actions
Copy link

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (4)
src/app/(crew)/my-crew/hosted/page.tsx (1)

Line range hint 17-23: 로딩 상태 처리가 개선되었습니다.

초기 로딩과 추가 데이터 로딩 상태를 통합하여 일관된 사용자 경험을 제공합니다. 하지만 다음 사항들을 고려해보시기 바랍니다:

  1. 로딩 상태와 에러 상태에 대한 접근성 개선
  2. 스켈레톤 UI 도입 검토

다음과 같이 개선해보시는 것은 어떨까요?

-      {status === 'pending' || isFetchingNextPage ? (
-        <div className="flex justify-center py-10">
-          <Loader size="sm" />
-        </div>
+      {status === 'pending' || isFetchingNextPage ? (
+        <div className="flex justify-center py-10" role="status" aria-label="로딩 중">
+          <Loader size="sm" />
+          <span className="sr-only">로딩 중...</span>
+        </div>
       ) : (
         <div ref={ref} className="h-[1px]" />
       )}
-      {status === 'error' && <p className="py-10 text-center">에러가 발생했습니다.</p>}
+      {status === 'error' && (
+        <p className="py-10 text-center" role="alert">
+          에러가 발생했습니다.
+        </p>
+      )}
src/app/(crew)/my-crew/joined/page.tsx (1)

Line range hint 17-24: 로딩 상태 처리가 개선되었습니다.

로딩 상태 처리가 다음과 같이 개선되었습니다:

  • 초기 로딩과 추가 데이터 로딩 상태가 통합되어 일관된 사용자 경험 제공
  • 무한 스크롤 감지를 위한 ref 엘리먼트가 적절히 배치됨

하지만 다음 사항을 고려해보시기 바랍니다:

로딩 상태 관련 로직을 별도의 컴포넌트로 분리하여 재사용성을 높이는 것을 제안드립니다:

+const LoadingIndicator = () => (
+  <div className="flex justify-center py-10">
+    <Loader size="sm" />
+  </div>
+);

 export default function MyCrewJoinedPage() {
   // ...
   return (
     <div>
       <CrewCardList inWhere="my-crew" data={data ?? { pages: [], pageParams: [] }} />
-      {status === 'pending' || isFetchingNextPage ? (
-        <div className="flex justify-center py-10">
-          <Loader size="sm" />
-        </div>
-      ) : (
+      {status === 'pending' || isFetchingNextPage ? <LoadingIndicator /> : (
         <div ref={ref} className="h-[1px]" />
       )}
       {status === 'error' && <p className="py-10 text-center">에러가 발생했습니다.</p>}
     </div>
   );
 }
src/components/common/crew-list/crew-card-list.stories.tsx (1)

45-57: 에러 메시지와 로딩 상태 개선이 필요합니다

현재 구현은 잘 되어있지만, 다음과 같은 개선사항을 제안드립니다:

  1. 에러 메시지를 좀 더 구체적으로 표시하면 좋을 것 같습니다.
  2. 로딩 상태에 대한 접근성 속성 추가가 필요합니다.

다음과 같이 수정해보세요:

- {status === 'pending' || isFetchingNextPage ? (
-   <div className="flex justify-center py-10">
-     <Loader size="sm" />
-   </div>
- ) : (
-   <div ref={ref} className="h-[1px]" />
- )}
- {status === 'error' && <p className="py-10 text-center">에러가 발생했습니다.</p>}
+ {status === 'pending' || isFetchingNextPage ? (
+   <div className="flex justify-center py-10" role="status" aria-label="로딩 중">
+     <Loader size="sm" />
+   </div>
+ ) : (
+   <div ref={ref} className="h-[1px]" />
+ )}
+ {status === 'error' && (
+   <p className="py-10 text-center" role="alert">
+     데이터를 불러오는 중 문제가 발생했습니다. 잠시 후 다시 시도해 주세요.
+   </p>
+ )}
src/components/common/crew-list/crew-card-list.tsx (1)

Line range hint 24-35: 에러 처리 개선 필요

현재 구현에서는 기본적인 데이터 존재 여부만 확인하고 있습니다. 더 견고한 에러 처리가 필요합니다.

다음 사항들을 고려해보세요:

  1. React Error Boundary 추가
  2. 로딩 상태와 에러 상태를 명확히 구분
  3. 사용자 친화적인 에러 메시지 표시

예시 구현:

import { ErrorBoundary } from 'react-error-boundary';

function ErrorFallback({ error }: { error: Error }) {
  return (
    <div className="flex justify-center py-10">
      <p>데이터를 불러오는  문제가 발생했습니다: {error.message}</p>
    </div>
  );
}

// 사용시
<ErrorBoundary FallbackComponent={ErrorFallback}>
  <CrewCardList data={data} inWhere={inWhere} />
</ErrorBoundary>
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between ef8fc4f and 6d9c094.

📒 Files selected for processing (5)
  • src/app/(crew)/my-crew/hosted/page.tsx (1 hunks)
  • src/app/(crew)/my-crew/joined/page.tsx (1 hunks)
  • src/app/(crew)/page.tsx (5 hunks)
  • src/components/common/crew-list/crew-card-list.stories.tsx (2 hunks)
  • src/components/common/crew-list/crew-card-list.tsx (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/(crew)/page.tsx
🔇 Additional comments (4)
src/app/(crew)/my-crew/hosted/page.tsx (1)

16-16: 데이터 폴백 패턴이 적절하게 구현되었습니다.

nullish 병합 연산자를 사용하여 데이터가 없는 경우에 대한 처리가 잘 되어있습니다. 빈 배열을 기본값으로 제공하여 컴포넌트의 안정성을 보장합니다.

src/app/(crew)/my-crew/joined/page.tsx (1)

16-16: 데이터 기본값 처리가 개선되었습니다.

빈 데이터에 대한 기본값 처리가 명시적으로 추가되어 컴포넌트의 안정성이 향상되었습니다.

src/components/common/crew-list/crew-card-list.stories.tsx (1)

31-40: 정렬 방식 변경에 대한 검증이 필요합니다

'LATEST'에서 'POPULAR'로 변경된 정렬 방식이 요구사항과 일치하는지 확인이 필요합니다.

✅ Verification successful

정렬 방식 'POPULAR' 설정이 올바르게 적용되었습니다

스토리북의 'POPULAR' 정렬 방식은 실제 애플리케이션의 구현과 일치합니다:

  • page.tsx에서 기본값이 '인기순'으로 설정되어 있으며, 이는 'POPULAR' 타입으로 매핑됩니다
  • 타입 정의(crew-card.d.ts)에서도 'POPULAR'가 유효한 정렬 옵션으로 명시되어 있습니다
  • README.md에도 인기순 정렬 기능이 명시되어 있습니다
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the sort type usage across the codebase

# Check for other instances of sortType
rg "sortType.*POPULAR|LATEST" -A 3

Length of output: 792


Script:

#!/bin/bash
# Check for sort type related UI components or configurations
rg "최신순|인기순" -A 3

Length of output: 1765

src/components/common/crew-list/crew-card-list.tsx (1)

Line range hint 19-21: 이전 리뷰 코멘트 참조

타입 캐스팅 관련 이슈는 이전 리뷰에서 이미 지적되었습니다.

Comment on lines +51 to +60
participantCount={
inWhere === 'my-crew'
? (inform as MyCrewList)?.currentCount
: (inform as MainCrewList)?.participantCount
}
totalGatheringCount={
inWhere === 'my-crew'
? (inform as MyCrewList)?.totalGathering
: (inform as MainCrewList)?.totalGatheringCount
}
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 CrewData = MyCrewList | MainCrewList;

function isMyCrewList(data: CrewData): data is MyCrewList {
  return 'crewMembers' in data;
}

// 컴포넌트 내부에서
const getParticipantCount = (data: CrewData) => 
  isMyCrewList(data) ? data.currentCount : data.participantCount;

const getTotalGatheringCount = (data: CrewData) =>
  isMyCrewList(data) ? data.totalGathering : data.totalGatheringCount;

// JSX에서 사용
participantCount={getParticipantCount(inform)}
totalGatheringCount={getTotalGatheringCount(inform)}

Comment on lines +46 to +48
? 'https://media.istockphoto.com/id/1396814518/vector/image-coming-soon-no-photo-no-thumbnail-image-available-vector-illustration.jpg?s=612x612&w=0&k=20&c=hnh2OZgQGhf0b46-J2z7aHbIWwq8HNlSDaNp2wn_iko='
: inform?.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

이미지 URL 처리 로직 개선 필요

현재 구현에는 두 가지 문제가 있습니다:

  1. 'string' 리터럴과의 비교는 신뢰할 수 없는 방식입니다
  2. 하드코딩된 URL은 상수로 분리되어야 합니다

다음과 같이 개선해보세요:

+ const DEFAULT_IMAGE_URL = 'https://media.istockphoto.com/id/1396814518/vector/image-coming-soon-no-photo-no-thumbnail-image-available-vector-illustration.jpg?s=612x612&w=0&k=20&c=hnh2OZgQGhf0b46-J2z7aHbIWwq8HNlSDaNp2wn_iko=';

- inform?.imageUrl === 'string'
-   ? 'https://media.istockphoto.com/id/1396814518/vector/image-coming-soon-no-photo-no-thumbnail-image-available-vector-illustration.jpg?s=612x612&w=0&k=20&c=hnh2OZgQGhf0b46-J2z7aHbIWwq8HNlSDaNp2wn_iko='
-   : inform?.imageUrl
+ !inform?.imageUrl || inform.imageUrl === 'string'
+   ? DEFAULT_IMAGE_URL
+   : inform.imageUrl

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

@github-actions
Copy link

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Outside diff range and nitpick comments (6)
src/utils/validate-file.ts (3)

15-18: 파일 크기 상수화 필요

매직 넘버를 상수로 분리하여 관리하면 유지보수성이 향상됩니다.

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

+const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
+
 // 파일 크기 검증
-    if (file.size > 5 * 1024 * 1024) {
+    if (file.size > MAX_FILE_SIZE) {
       return '파일 크기는 5MB 이하여야 합니다.';
     }

20-23: MIME 타입 배열 상수화 및 중복 제거 필요

MIME 타입 배열이 확장자 검증에서도 사용될 수 있으므로, 상수로 분리하여 중복을 제거해야 합니다.

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

+const VALID_MIME_TYPES = ['image/jpeg', 'image/jpg', 'image/png'] as const;
+
 // MIME 타입 검증
-    if (!['image/jpeg', 'image/jpg', 'image/png'].includes(file.type)) {
+    if (!VALID_MIME_TYPES.includes(file.type)) {
       return '올바른 이미지 형식이 아닙니다.';
     }

25-30: 반환 타입 명시 필요

함수의 반환 타입을 명시적으로 지정하여 타입 안정성을 향상시켜야 합니다.

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

-const validateFile = (file: File | string) => {
+const validateFile = (file: File | string): boolean | string => {
src/_apis/image/get-image-url.ts (2)

6-9: 타입 정의 개선이 필요합니다.

타입 안정성을 높이기 위해 문자열 리터럴 타입을 상수로 정의하는 것이 좋습니다.

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

+const IMAGE_TYPES = ['MEMBER', 'CREW', 'GATHERING'] as const;
+type ImageType = typeof IMAGE_TYPES[number];
+
 export async function getImageUrl(
   file: File | string | null,
-  type: 'MEMBER' | 'CREW' | 'GATHERING',
+  type: ImageType,
 ) {

35-35: 코드 문서화 개선이 필요합니다.

파일이 File 인스턴스가 아닌 경우 null을 반환하는 이유와 의도를 설명하는 주석이 있으면 좋겠습니다.

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

+  // string이나 null이 전달된 경우 추가 처리 없이 null 반환
   return null;
src/types/gathering-data.d.ts (1)

39-45: 타입 중복을 줄일 수 있는 기회가 있습니다.

CreateGatheringRequestTypesCreateGatheringFormTypesimageUrl을 제외하고 동일한 필드를 가지고 있습니다. 다음과 같이 타입을 재사용하는 것을 고려해보세요:

interface BaseGatheringTypes {
  title: string; // 20자 이내
  introduce: string; // 소개글 100자 이내
  dateTime: string;
  location: string;
  totalCount: number;
}

export interface CreateGatheringFormTypes extends BaseGatheringTypes {
  imageUrl: File | string | null;
}

export interface CreateGatheringRequestTypes extends BaseGatheringTypes {
  imageUrl: string;
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 6d9c094 and 2cc00f1.

📒 Files selected for processing (5)
  • src/_apis/crew/crew.ts (1 hunks)
  • src/_apis/image/get-image-url.ts (1 hunks)
  • src/app/(crew)/crew/create/page.tsx (1 hunks)
  • src/types/gathering-data.d.ts (1 hunks)
  • src/utils/validate-file.ts (1 hunks)
🧰 Additional context used
🪛 Biome
src/_apis/crew/crew.ts

[error] 23-23: 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)

src/_apis/image/get-image-url.ts

[error] 32-32: 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)

🔇 Additional comments (10)
src/_apis/crew/crew.ts (1)

4-25: 🛠️ Refactor suggestion

에러 처리 및 입력값 검증 개선 필요

현재 구현에서 다음과 같은 개선이 필요합니다:

  1. 현재 try-catch 구문이 에러를 단순히 다시 던지기만 하고 있어 불필요합니다.
  2. 에러 메시지가 영문으로 되어있어 일관성이 떨어집니다.
  3. API 응답에 대한 구체적인 타입 검증이 부족합니다.

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

-export async function createCrew(data: CreateCrewRequestTypes) {
+export async function createCrew(data: CreateCrewFormTypes) {
+  if (!data.title || !data.content) {
+    throw new Error('제목과 내용은 필수 입력 항목입니다.');
+  }
+
   const response: { data: CreateCrewResponseTypes; status: number } = await fetchApi(
     `/api/crews`,
     {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json',
       },
       credentials: 'include',
       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) {
-    throw error;
-  }
 }

API 응답 타입이 올바르게 정의되어 있는지 확인이 필요합니다:

🧰 Tools
🪛 Biome

[error] 23-23: 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)

src/types/gathering-data.d.ts (2)

30-37: 폼 인터페이스의 타입 정의가 명확합니다!

필드별 제한사항이 주석으로 잘 명시되어 있으며, imageUrl의 타입이 File | string | null로 확장된 것이 적절합니다. 이를 통해 파일 업로드와 기존 URL 모두를 처리할 수 있습니다.


Line range hint 1-45: 이미지 URL 처리 방식의 일관성을 확인해주세요.

여러 인터페이스에서 imageUrl 속성을 사용하고 있습니다. 다음 사항들을 확인해주세요:

src/app/(crew)/crew/create/page.tsx (7)

3-11: LGTM! 필요한 의존성들이 잘 추가되었습니다.

모든 필수 컴포넌트와 유틸리티가 적절하게 임포트되었습니다.


16-24: LGTM! initialValue가 새로운 타입 정의에 맞게 잘 구성되었습니다.

introduce 필드가 적절하게 추가되었고, 모든 필수 필드가 포함되어 있습니다.


27-39: useMutation 훅의 상태 속성을 수정해야 합니다.

이전 리뷰에서 지적된 대로, isPending 대신 isLoading을 사용해야 합니다.

- const { isPending, mutate } = useMutation({
+ const { isLoading, mutate } = useMutation({

43-46: 이미지 업로드 에러 처리가 필요합니다.

이전 리뷰에서 지적된 대로, getImageUrl 호출에 대한 에러 처리가 필요합니다.

if (data.imageUrl instanceof File) {
+  try {
    const imgResponse = await getImageUrl(data.imageUrl, 'CREW');
    newImageUrl = imgResponse?.imageUrl as string;
+  } catch (error) {
+    console.error(error);
+    toast.error('이미지 업로드에 실패했습니다.');
+    return;
+  }
}

47-58: LGTM! 데이터 변환 로직이 잘 구현되었습니다.

폼 데이터에서 API 요청 데이터로의 변환이 누락 없이 잘 처리되었습니다.


61-66: 로딩 상태 확인 속성을 수정해야 합니다.

이전 리뷰에서 지적된 대로, 여기서도 isPending 대신 isLoading을 사용해야 합니다.

- if (isPending)
+ if (isLoading)

63-66: LGTM! 로딩 UI가 잘 구현되었습니다.

로딩 상태를 표시하는 UI가 중앙에 적절하게 배치되어 있습니다.

Comment on lines +8 to +13
const extension = file.name.split('.').pop()?.toLowerCase();
const validExtensions = ['jpg', 'jpeg', 'png'];

if (!extension || !validExtensions.includes(extension)) {
return 'JPG, PNG 파일만 업로드 가능합니다.';
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

파일 확장자 검증 로직 개선 필요

현재 구현은 파일 이름을 조작하여 우회할 수 있는 위험이 있습니다. MIME 타입과 함께 복합적인 검증이 필요합니다.

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

-    const extension = file.name.split('.').pop()?.toLowerCase();
-    const validExtensions = ['jpg', 'jpeg', 'png'];
-
-    if (!extension || !validExtensions.includes(extension)) {
-      return 'JPG, PNG 파일만 업로드 가능합니다.';
-    }
+    const extension = file.name.split('.').pop()?.toLowerCase();
+    const validExtensions = ['jpg', 'jpeg', 'png'];
+    const validMimeTypes = ['image/jpeg', 'image/jpg', 'image/png'];
+
+    if (!extension || 
+        !validExtensions.includes(extension) || 
+        !validMimeTypes.includes(file.type)) {
+      return 'JPG, PNG 파일만 업로드 가능합니다.';
+    }
📝 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 extension = file.name.split('.').pop()?.toLowerCase();
const validExtensions = ['jpg', 'jpeg', 'png'];
if (!extension || !validExtensions.includes(extension)) {
return 'JPG, PNG 파일만 업로드 가능합니다.';
}
const extension = file.name.split('.').pop()?.toLowerCase();
const validExtensions = ['jpg', 'jpeg', 'png'];
const validMimeTypes = ['image/jpeg', 'image/jpg', 'image/png'];
if (!extension ||
!validExtensions.includes(extension) ||
!validMimeTypes.includes(file.type)) {
return 'JPG, PNG 파일만 업로드 가능합니다.';
}

Comment on lines +1 to +3
const validateFile = (file: File | string) => {
// 문자열(기존 URL)인 경우 검증 통과
if (typeof file === 'string') return true;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

타입 검사 로직 개선 필요

string 타입의 경우 무조건 true를 반환하는 것은 잠재적인 보안 위험이 있습니다. URL 형식 검증이나 허용된 도메인 검사가 필요할 수 있습니다.

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

 const validateFile = (file: File | string) => {
-  // 문자열(기존 URL)인 경우 검증 통과
-  if (typeof file === 'string') return true;
+  // 문자열(기존 URL)인 경우 URL 형식 검증
+  if (typeof file === 'string') {
+    try {
+      const url = new URL(file);
+      const allowedDomains = ['your-domain.com', 'cdn-domain.com'];
+      return allowedDomains.some(domain => url.hostname.endsWith(domain));
+    } catch {
+      return '올바른 URL 형식이 아닙니다.';
+    }
+  }

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

Comment on lines +18 to +29
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 응답 처리 강화가 필요합니다.

현재 응답의 data 필드만 확인하고 있습니다. imageUrl 필드의 존재 여부도 확인하여 더 안전한 처리가 필요합니다.

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

       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('이미지 URL을 받아오지 못했습니다');
       }
📝 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('이미지 URL을 받아오지 못했습니다');
}
return response.data;

Comment on lines +12 to +16
try {
if (!validateFile(file)) {
throw new Error('Invalid file');
}
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

파일 검증 에러 처리 개선이 필요합니다.

현재 파일 검증 실패 시 발생하는 에러 메시지가 너무 일반적입니다. 사용자가 어떤 검증에 실패했는지 알 수 있도록 구체적인 에러 메시지를 제공해야 합니다.

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

-      if (!validateFile(file)) {
-        throw new Error('Invalid file');
+      const validationResult = validateFile(file);
+      if (!validationResult.isValid) {
+        Toast({ message: validationResult.error, type: 'error' });
+        throw new Error(validationResult.error);
       }

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

Comment on lines +30 to +33
} catch (error) {
// eslint-disable-next-line no-console
throw error;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

불필요한 에러 처리 로직이 있습니다.

현재 catch 블록은 단순히 에러를 다시 throw하고 있어 불필요합니다. 또한 주석으로 처리된 eslint-disable도 제거할 수 있습니다.

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

-    try {
       if (!validateFile(file)) {
         throw new Error('Invalid file');
       }
       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;
-    } catch (error) {
-      // eslint-disable-next-line no-console
-      throw error;
-    }
📝 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
} catch (error) {
// eslint-disable-next-line no-console
throw error;
}
if (!validateFile(file)) {
throw new Error('Invalid file');
}
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;
🧰 Tools
🪛 Biome

[error] 32-32: 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)

Copy link
Contributor

@HaeJungg HaeJungg left a comment

Choose a reason for hiding this comment

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

수고하셨습니다👏👍

@yulrang yulrang merged commit ab16aad into develop Nov 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

크루 만들기 API 연결

4 participants