Skip to content

Conversation

@clyde-yoonjae
Copy link
Contributor

@clyde-yoonjae clyde-yoonjae commented Mar 1, 2025

📝 주요 작업 내용

모임 생성 및 수정 페이지에 사용되는 api, 컴포넌트, 페이지 구현입니다.

수정 모드는 서버컴포넌트에서 prefetch한 데이터 받아 meetingForm에 mode="edit" 및 initialData로 전달할 예정입니다. (현재 백엔드 수정API 존재하지않아 확장성만 열어두었습니다.)

커밋 순대로 리뷰해주시면 감사합니다.

아래는 폴더구조입니다.

app/
├── (home)
├── login
└── meeting/
   ├── components/
   │   └── form/
   │       └── form-filed/   // 폼 내부 필드컴포넌트 모음입니다.
   │           ├── CategoryField.tsx
   │           ├── DateField.tsx
   │           ├── DescriptionField.tsx
   │           ├── ImageField.tsx
   │           ├── index.ts
   │           ├── InfoMessage.tsx
   │           ├── LocationField.tsx
   │           ├── MemberLimitField.tsx
   │           ├── PrivacyField.tsx
   │           ├── RequireApprovalField.tsx
   │           ├── SubmitButton.tsx
   │           ├── TechStackField.tsx
   │           └── TitleField.tsx
   │       ├── MeetingForm.tsx // 수정 및 생성에 사용되는 클라이언트 컴포넌트입니다.(전체필드모음)
   │       └── validation.ts // 폼 유효성 검사 파일입니다.
   ├── constants/
   │   └── meeting-form/
   │       └── meetingConstants.tsx // 폼 내부에서 사용되는 모든 constants 모음입니다.
   ├── create-meeting/
   │   └── page.tsx // 생성페이지 서버컴포넌트 입니다.
   └── edit-meeting/
       └── [id]/
           └── page.tsx // 수정페이지 서버컴포넌트 입니다.

📺 스크린샷

image

🔗 참고 사항

  1. useMeetingFormMuation 에서 성공 및 실패시 에러처리를 prop으로 받지않고 사용되는 MeetingForm에서 try catch문으로 하드코딩 하였습니다. 콜백으로 넘기는 방식으로 리펙토링 하겠습니다.

  2. api 처리 과정에서, 토스트 띄우기 추가되어야 합니다.

  3. 현재 디자인 임의로 수정하면서 진행하여, 생성된 컴포넌트들 각각 하드코딩되어있어 추후 분리해야됩니다. ex) PrivacyFiled.tsx , RequireApprovalField.tsx

💬 리뷰 요구사항

@lee1nna 한나님과 페이지 겹쳐서, 구조 및 상수관리 같이하면 좋을 것 같습니다.
@Lee-Dong-Seok 동석님과 base64 유틸함수 중복 생성되었습니다. 추후 통일하면 될 것 같습니다.

📃 관련 이슈

DEVING-40

Summary by CodeRabbit

  • 새로운 기능
    • 회의 관리 페이지: 사용자가 모임을 쉽게 생성하고 수정할 수 있도록 회의 생성 및 수정 페이지가 추가되었습니다.
    • 향상된 폼 경험: 제목, 카테고리, 날짜, 위치, 이미지, 기술 스택 등 다양한 입력 필드와 실시간 검증 메시지가 제공됩니다.
    • 개선된 인터페이스: 직관적인 날짜 선택 기능 및 이미지 업로드 시 파일 크기와 형식 검증 기능이 강화되었습니다.
    • 홈페이지 업데이트: 헤로, 카테고리, 인기 그룹, CTA, 하단 배너 등 커뮤니티 참여를 유도하는 새로운 레이아웃이 도입되었습니다.

@coderabbitai
Copy link

coderabbitai bot commented Mar 1, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

이번 PR은 여러 컴포넌트와 파일에 새로운 기능과 개선사항을 추가합니다. ReactQueryDevtools를 레이아웃 파일에 추가하고, 회의 생성 및 수정에 필요한 다양한 폼 컴포넌트와 유효성 검사 로직, 상수, API 함수, 타입, 유틸리티 함수, 그리고 커스텀 훅을 도입하였습니다. 또한 UI 컴포넌트(예: DatePicker, TechSelector)와 Tech-stack 관련 컴포넌트에 버튼 타입을 명시적으로 지정하는 변경도 포함되어 있습니다.

Changes

파일(들) 변경 요약
src/app/layout.tsx @tanstack/react-query-devtoolsReactQueryDevtools 컴포넌트 import 추가
src/app/meeting/components/form/MeetingForm.tsx
src/app/meeting/components/form/form-filed/*.tsx
src/app/meeting/components/form/index.ts
회의 생성/수정을 위한 MeetingForm 컴포넌트 및 하위 폼 필드 컴포넌트(예: CategoryField, DateField, DescriptionField, ImageField, InfoMessage, LocationField, MemberLimitField, PrivacyField, RequireApprovalField, SubmitButton, TechStackField, TitleField) 추가
src/app/meeting/components/form/validation.ts 회의 폼 필드에 대한 유효성 검사 상수 및 메시지 추가
src/app/meeting/constants/meeting-form/meetingConstants.tsx MEETING_TYPES, JOIN_METHODS, PRIVACY_OPTIONS, IMAGE_CONFIG 등 회의 관련 상수 추가
src/app/meeting/create-meeting/page.tsx
src/app/meeting/edit-meeting/[id]/page.tsx
회의 생성 페이지와 수정 페이지 컴포넌트 추가
src/app/page.tsx 홈 페이지 레이아웃 개편 (영웅 섹션, 카테고리/그룹 섹션, CTA, 배너, 푸터 등 동적 콘텐츠 도입)
src/components/ui/form/DatePicker.tsx 달력 인터페이스를 갖춘 DatePicker 컴포넌트 도입
src/components/ui/tech-stack/TechSelector.tsx
src/components/ui/tech-stack/tech-stack-components/*.tsx
TechSelectorid 속성 추가 및 관련 tech-stack 컴포넌트들에 버튼 타입(type="button") 명시
src/hooks/mutations/useMeetingFormMutation.ts react-query를 활용한 회의 폼 mutation을 처리하는 커스텀 훅 추가
src/service/api/meetingForm.ts 회의 생성 및 수정 API 호출을 위한 함수(createMeeting, editMeeting) 추가
src/types/meetingForm.ts 회의 데이터를 위한 CreateMeetingPayload 인터페이스 추가
src/util/base64.ts 이미지 파일을 Base64로 변환하고, 이미지의 크기 및 형식 검증을 위한 유틸리티 함수들 추가

Sequence Diagram(s)

sequenceDiagram
    participant U as 사용자
    participant MF as MeetingForm 페이지
    participant MUT as useMeetingFormMutation
    participant API as 회의 API (createMeeting)
    participant NAV as 내비게이터

    U->>MF: 회의 폼 작성
    MF->>MUT: 폼 데이터 제출
    MUT->>API: API 요청 (POST 데이터)
    API-->>MUT: 응답 데이터 반환
    MUT-->>MF: 성공 상태 전달
    MF->>NAV: 회의 상세 페이지로 리디렉션
Loading

Suggested labels

📝 docs, ♻️ refactor

Suggested reviewers

  • clyde-yoonjae
  • Lee-Dong-Seok

Poem

당근도 춤추는 이 날에,
코드의 숲을 헤치며 달려가요,
새로운 회의의 꿈을 심어,
함수와 컴포넌트가 빛나죠,
버그 없이 즐겁게! 🥕✨
소중한 변경에 박수를 보내요!

  • Rabbit from CodeRabbit Inc.

📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3603efb and 755b9e3.

📒 Files selected for processing (1)
  • src/app/layout.tsx (1 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings

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 generate docstrings to generate docstrings for this 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.

@codecov
Copy link

codecov bot commented Mar 1, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

@github-actions
Copy link

github-actions bot commented Mar 1, 2025

CI Status Report

검사 결과

  • Lint: ✅ success
  • Format: ✅ success
  • Type Check: ✅ success
  • Tests: ✅ success
  • Build: ✅ success

✅ 모든 검사가 통과되었습니다.

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: 11

🔭 Outside diff range comments (1)
src/app/meeting/components/form/form-filed/DateField.tsx (1)

1-34: 💡 Verification agent

🧩 Analysis chain

파일 경로 이름 오타 확인 필요

현재 파일이 form-filed 디렉토리에 위치하고 있는데, 이는 form-field의 오타일 가능성이 있습니다. 디렉토리 이름을 확인해보세요.


🏁 Script executed:

#!/bin/bash
# Check the directory structure
find src/app/meeting/components/form -type d | grep -E "fil(e|ed)"

Length of output: 111


디렉토리 이름 오타 수정 요청

  • 검증 결과, 현재 디렉토리 이름이 src/app/meeting/components/form/form-filed로 존재합니다.
  • 만약 이 이름이 오타라면(form-field가 의도된 이름이라면), 해당 디렉토리 이름을 수정해주시기 바랍니다.
🧹 Nitpick comments (28)
src/types/meetingForm.ts (1)

1-13: 인터페이스 구조가 명확하게 정의되어 있습니다.

모임 생성에 필요한 모든 필드들이 잘 정의되어 있습니다. 다만 몇 가지 개선할 점이 있습니다:

  1. startDate는 문자열로 정의되어 있는데, 날짜 형식에 대한 주석이나 더 구체적인 타입(예: ISO 8601 형식)을 명시하면 좋을 것 같습니다.
  2. imageEncodedBase64는 큰 용량의 데이터를 API 요청에 포함시킬 수 있어 성능 이슈가 발생할 수 있습니다. 별도의 이미지 업로드 API를 고려해보는 것도 좋을 것 같습니다.
src/service/api/meetingForm.ts (1)

11-12: 미구현된 API 함수에 대한 명확한 설명 추가 필요

현재 editMeeting 함수는 구현되지 않았습니다. 한국어 주석이 있지만, 아직 구현되지 않은 이유와 향후 계획에 대한 더 자세한 설명을 영문 주석으로 추가하면 좋을 것 같습니다.

// 폼수정 API
-export const editMeeting = async () => {};
+/**
+ * Edit meeting API function
+ * @todo Implement when the backend API is available
+ * @param id Meeting ID to edit
+ * @param data Updated meeting data
+ */
+export const editMeeting = async (id: string, data: Partial<CreateMeetingPayload>) => {
+  // Will be implemented when the backend API is ready
+};
src/app/layout.tsx (1)

32-32: 개발 환경에서만 DevTools 표시 고려

ReactQueryDevtools는 개발 도구이므로 프로덕션 환경에서는 비활성화하는 것이 좋습니다. 환경 변수를 사용하여 개발 환경에서만 활성화되도록 조건부 렌더링을 고려해보세요.

-<ReactQueryDevtools initialIsOpen={false} />
+{process.env.NODE_ENV === 'development' && <ReactQueryDevtools initialIsOpen={false} />}
src/hooks/mutations/useMeetingFormMutation.ts (2)

6-13: 오류 처리 개선 필요

mutation 함수에서 오류 처리가 없습니다. React Query의 기본 오류 처리에만 의존하고 있어, 향후 더 세밀한 오류 처리가 필요할 수 있습니다.

오류 처리 로직을 추가하여 특정 API 오류에 더 구체적으로 대응할 수 있도록 개선하는 것이 좋습니다:

const createMeeting = useMutation({
  mutationFn: async (data: CreateMeetingPayload) => {
-   const response = await authAPI.post(meetingURL.create, data);
-   return response.data;
+   try {
+     const response = await authAPI.post(meetingURL.create, data);
+     return response.data;
+   } catch (error) {
+     // 오류 유형에 따른 처리
+     console.error('Meeting creation failed:', error);
+     throw error;
+   }
  },
});

1-24: 응답 타입 정의 추가 고려

현재 API 응답 데이터에 대한 타입 정의가 없습니다. 타입 안정성을 높이기 위해 응답 타입을 정의하는 것을 고려해 보세요.

응답 타입을 정의하고 제네릭으로 사용하면 타입 안정성이 향상됩니다:

+ import { MeetingResponse } from 'types/meetingForm'; // 응답 타입 정의 필요

const useMeetingFormMutation = () => {
-  const createMeeting = useMutation({
+  const createMeeting = useMutation<MeetingResponse, Error, CreateMeetingPayload>({
    mutationFn: async (data: CreateMeetingPayload) => {
      const response = await authAPI.post(meetingURL.create, data);
      return response.data;
    },
  });

  return {
    createMeeting,
    isLoading: createMeeting.isPending,
    isError: createMeeting.isError,
    isSuccess: createMeeting.isSuccess,
    error: createMeeting.error,
  };
};
src/app/meeting/components/form/form-filed/LocationField.tsx (1)

22-35: 필수 필드 시각적 표시 추가 고려

필드가 필수인 경우에 이를 시각적으로 표시하는 요소(예: 별표)가 없습니다. 사용자에게 필수 필드임을 명확하게 알려주는 것이 좋습니다.

필수 필드에 시각적 표시를 추가하는 것을 고려해보세요:

<label
  htmlFor="location"
  className="typo-body1 font-medium text-Cgray700"
>
- 모임 장소
+ 모임 장소{required && <span className="text-red-500 ml-1">*</span>}
</label>
src/app/meeting/components/form/form-filed/RequireApprovalField.tsx (1)

29-56: 중복 로직 리팩토링이 필요합니다.

조건 로직 (method.id === 'approval' && field.value) || (method.id === 'immediate' && !field.value)이 여러 곳에서 반복되고 있습니다. 가독성 및 유지보수성을 향상시키기 위해 이 로직을 변수로 추출하는 것이 좋겠습니다.

-                className={cn(
-                  'flex-1 cursor-pointer rounded-md border p-4 transition-all',
-                  (method.id === 'approval' && field.value) ||
-                    (method.id === 'immediate' && !field.value)
-                    ? 'border-main bg-main text-white'
-                    : 'border-Cgray300',
-                )}
-                onClick={() => field.onChange(method.id === 'approval')}
-                onKeyDown={(e) => {
-                  if (e.key === 'Enter' || e.key === ' ') {
-                    e.preventDefault();
-                    field.onChange(method.id === 'approval');
-                  }
-                }}
-                tabIndex={0}
-                role="radio"
-                aria-checked={
-                  (method.id === 'approval' && field.value) ||
-                  (method.id === 'immediate' && !field.value)
-                }
+                {() => {
+                  const isSelected = 
+                    (method.id === 'approval' && field.value) || 
+                    (method.id === 'immediate' && !field.value);
+                  
+                  return (
+                    <>
+                    className={cn(
+                      'flex-1 cursor-pointer rounded-md border p-4 transition-all',
+                      isSelected
+                        ? 'border-main bg-main text-white'
+                        : 'border-Cgray300',
+                    )}
+                    onClick={() => field.onChange(method.id === 'approval')}
+                    onKeyDown={(e) => {
+                      if (e.key === 'Enter' || e.key === ' ') {
+                        e.preventDefault();
+                        field.onChange(method.id === 'approval');
+                      }
+                    }}
+                    tabIndex={0}
+                    role="radio"
+                    aria-checked={isSelected}
+                    </>
+                  );
+                }}
src/app/meeting/components/form/form-filed/CategoryField.tsx (1)

42-71: 라디오 그룹 컴포넌트 추출을 고려해보세요.

여러 폼 필드 컴포넌트에서 라디오 버튼 그룹 관련 로직이 중복되고 있습니다. RequireApprovalField와 유사한 패턴이 사용되고 있으므로, 재사용 가능한 RadioGroup 컴포넌트로 추출하는 것을 고려해보세요. 이를 통해 중복 코드를 줄이고 유지보수성을 높일 수 있습니다.

src/app/meeting/components/form/form-filed/DescriptionField.tsx (1)

26-34: 접근성 및 사용자 경험 개선이 필요합니다.

textarea에 다음과 같은 개선사항을 추가하는 것이 좋겠습니다:

  1. 사용자에게 입력 제한에 대한 시각적 피드백 제공을 위한 maxLength 속성 추가
  2. 스크린 리더 사용자를 위한 오류 상태 표시를 위해 aria-invalid 속성 추가
  3. 사용자가 입력 길이를 확인할 수 있는 문자 수 카운터 추가
        id="content"
        placeholder="모임에 대한 설명을 입력해주세요"
        {...register('content', validation)}
+       maxLength={500}
+       aria-invalid={errors.content ? 'true' : 'false'}
        className={cn(
          'box-border h-32 w-full resize-none rounded-md bg-Cgray200 px-[16px] py-[14px] text-base text-Cgray700 caret-Cgray500 shadow-sm transition-colors placeholder:text-Cgray400 focus:outline-none',
          errors.content && 'border border-warning',
        )}
      />
+     <div className="flex justify-end">
+       <span className="typo-caption1 text-Cgray500">
+         {watch('content')?.length || 0}/500
+       </span>
+     </div>
src/app/meeting/components/form/form-filed/PrivacyField.tsx (1)

31-34: 조건부 스타일링 로직 단순화 제안

현재 조건부 스타일링 로직이 다소 복잡합니다. 가독성을 높이기 위해 다음과 같이 리팩토링할 수 있습니다:

-                  (option.id === 'public' && field.value) ||
-                    (option.id === 'private' && !field.value)
-                    ? 'border-main bg-main text-white'
-                    : 'border-Cgray300',
+                  (option.id === 'public') === field.value
+                    ? 'border-main bg-main text-white'
+                    : 'border-Cgray300',

동일한 리팩토링을 46-47라인에도 적용할 수 있습니다.

src/app/meeting/components/form/form-filed/TitleField.tsx (1)

21-35: 필수 표시 추가 고려

다른 필드들과의 일관성을 위해 필수 필드인 경우 라벨 옆에 * 표시를 추가하는 것을 고려해보세요.

      <label
        htmlFor="meetingTitle"
        className="typo-body1 font-medium text-Cgray700"
      >
        모임 이름
+       {required && <span className="ml-1 text-warning">*</span>}
      </label>
src/app/meeting/components/form/form-filed/TechStackField.tsx (1)

55-57: 사용자 안내 메시지 조건부 표시 고려

최대 선택 가능 개수에 대한 안내 메시지를 항상 표시하고 있습니다. 이 메시지를 조건부로 표시하거나 텍스트 색상을 좀 더 강조할 수 있는지 고려해보세요.

src/app/meeting/components/form/form-filed/MemberLimitField.tsx (2)

37-38: 불필요한 pattern 속성이 있습니다.

HTML input의 pattern 속성은 type="text"에만 적용되며, type="number"에는 적용되지 않습니다. 키보드 이벤트 핸들러로 이미 입력을 제한하고 있으므로 불필요한 속성입니다.

          type="number"
          placeholder="모임 정원을 입력해주세요"
          min={memberLimitValidation.min.value}
          max={memberLimitValidation.max.value}
-         pattern="[0-9]*"
          inputMode="numeric"

28-29: 필수 필드 표시 누락

다른 필드들과의 일관성을 위해 필수 필드인 경우 라벨 옆에 * 표시를 추가하는 것을 고려해보세요.

      <label
        htmlFor="maxMember"
        className="typo-body1 font-medium text-Cgray700"
      >
        모임 정원
+       {required && <span className="ml-1 text-warning">*</span>}
      </label>
src/util/base64.ts (1)

51-54: WebP 이미지 형식 지원 추가를 고려해 보세요.

현재 JPEG, PNG, JPG 형식만 지원하고 있습니다. 최근에는 WebP 형식이 많이 사용되며 파일 크기가 작아 웹 성능 향상에 도움이 됩니다.

export const validateImageType = (file: File): boolean => {
-  const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg'];
+  const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg', 'image/webp'];
  return allowedTypes.includes(file.type);
};
src/app/page.tsx (2)

5-57: 상수 데이터를 컴포넌트 외부로 분리하는 것이 좋겠습니다.

categoriesfeaturedGroups 같은 큰 데이터 구조는 컴포넌트 외부의 별도 파일로 분리하는 것이 좋습니다. 이렇게 하면 컴포넌트의 가독성이 향상되고 데이터를 재사용하기 쉬워집니다.

+ // src/app/constants/home.ts
+ export const categories = [
+   {
+     title: '취미',
+     description: '개발 관련 취미 활동을 함께할 동료를 찾아보세요.',
+     icon: '🎨',
+     color: 'bg-blue',
+   },
+   // ...나머지 카테고리
+ ];
+ 
+ export const featuredGroups = [
+   {
+     title: 'Next.js 스터디 그룹',
+     category: '스터디',
+     members: 8,
+     maxMembers: 10,
+     location: '온라인',
+     imageUrl: '/api/placeholder/300/160',
+   },
+   // ...나머지 그룹
+ ];

그리고 컴포넌트에서 import하여 사용:

import Image from 'next/image';
import Link from 'next/link';
+ import { categories, featuredGroups } from '../constants/home';

export default function Home() {
-  const categories = [
-    // ...
-  ];
-
-  const featuredGroups = [
-    // ...
-  ];

236-238: 저작권 연도 하드코딩 수정 필요

저작권 연도가 하드코딩되어 있습니다. 현재 연도를 동적으로 가져오는 것이 더 좋습니다.

- <p className="typo-caption2 text-Cgray500">
-   © 2025 DEVING. All rights reserved.
- </p>
+ <p className="typo-caption2 text-Cgray500">
+   © {new Date().getFullYear()} DEVING. All rights reserved.
+ </p>
src/app/meeting/components/form/form-filed/ImageField.tsx (2)

117-122: 이미지 업로드 접근성 개선 필요

파일 입력(input)이 hidden 클래스로 숨겨져 있어 키보드 탐색 시 접근이 불가능합니다. 시각적으로는 숨기되 스크린 리더 및 키보드 사용자를 위한 접근성을 유지하는 방법으로 개선해야 합니다.

<input
  type="file"
  id="image"
  accept={IMAGE_CONFIG.ACCEPTED_FORMATS.join(',')}
-  className="hidden"
+  className="sr-only" // 시각적으로만 숨김(스크린 리더는 읽을 수 있음)
  onChange={handleImageChange}
/>

142-155: 접근성 ARIA 속성 추가 필요

이미지 업로드 레이블에 ARIA 속성을 추가하여 보조 기술 사용자에게 더 많은 문맥 정보를 제공해야 합니다.

<label
  htmlFor="image"
  className="flex cursor-pointer flex-col items-center"
+  aria-label="이미지 업로드하기"
+  role="button"
+  tabIndex={0}
+  onKeyDown={(e) => {
+    if (e.key === 'Enter' || e.key === ' ') {
+      document.getElementById('image')?.click();
+    }
+  }}
>
src/components/ui/form/DatePicker.tsx (4)

148-155: isToday 함수에 불필요한 연산 최적화

isToday 함수가 렌더링마다 새 Date 객체를 생성합니다. 이 함수를 useMemo로 감싸거나 컴포넌트 외부로 분리하여 성능을 개선할 수 있습니다.

+ const today = new Date();
+ today.setHours(0, 0, 0, 0);

const isToday = (date: Date) => {
-  const today = new Date();
  return (
    date.getFullYear() === today.getFullYear() &&
    date.getMonth() === today.getMonth() &&
    date.getDate() === today.getDate()
  );
};

67-91: 날짜 계산 로직 개선 필요

현재 달력은 항상 42일(6주)을 표시하는데, 이는 불필요한 렌더링과 빈 공간을 만들 수 있습니다. 실제 달력에 필요한 최소한의 주 수만 계산하도록 개선하는 것이 좋습니다.

const getDaysInMonth = (date: Date) => {
  const year = date.getFullYear();
  const month = date.getMonth();
  const daysInMonth = new Date(year, month + 1, 0).getDate();
  const firstDayOfMonth = new Date(year, month, 1).getDay();

  const days: { date: Date; isCurrentMonth: boolean }[] = [];

  const prevMonthDays = new Date(year, month, 0).getDate();
  for (let i = firstDayOfMonth - 1; i >= 0; i--) {
    const prevDate = new Date(year, month - 1, prevMonthDays - i);
    days.push({ date: prevDate, isCurrentMonth: false });
  }

  for (let i = 1; i <= daysInMonth; i++) {
    days.push({ date: new Date(year, month, i), isCurrentMonth: true });
  }

-  const remainingDays = 42 - days.length;
+  // 필요한 만큼만 다음 달 날짜를 추가 (다음 주 완성에 필요한 날짜만)
+  const remainingDays = (7 - (days.length % 7)) % 7;
  for (let i = 1; i <= remainingDays; i++) {
    days.push({ date: new Date(year, month + 1, i), isCurrentMonth: false });
  }

  return days;
};

230-242: 요일 국제화 처리 개선

요일 이름(DAYS_OF_WEEK)이 한국어로 하드코딩되어 있습니다. 다국어 지원을 위해 국제화 라이브러리를 사용하거나 설정에서 불러오는 방식이 더 좋습니다.

- const DAYS_OF_WEEK = ['일', '월', '화', '수', '목', '금', '토'];
+ // 추후 국제화를 위한 설정 방식 (예시)
+ const DAYS_OF_WEEK = {
+   ko: ['일', '월', '화', '수', '목', '금', '토'],
+   en: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
+ };
+ // 현재는 한국어 사용
+ const currentLocaleDays = DAYS_OF_WEEK['ko'];

// 사용 시
- {DAYS_OF_WEEK.map((day, index) => (
+ {currentLocaleDays.map((day, index) => (

51-65: 이벤트 리스너 최적화 필요

useEffect 내부에서 직접 이벤트 리스너를 추가하는 대신 커스텀 훅이나 외부 라이브러리(예: useOnClickOutside)를 사용하면 코드를 간결하게 만들고 재사용성을 높일 수 있습니다.

+ // src/hooks/useOnClickOutside.ts
+ import { useEffect, RefObject } from 'react';
+
+ export function useOnClickOutside<T extends HTMLElement>(
+   ref: RefObject<T>,
+   handler: (event: MouseEvent | TouchEvent) => void
+ ) {
+   useEffect(() => {
+     const listener = (event: MouseEvent | TouchEvent) => {
+       if (!ref.current || ref.current.contains(event.target as Node)) {
+         return;
+       }
+       handler(event);
+     };
+     
+     document.addEventListener('mousedown', listener);
+     document.addEventListener('touchstart', listener);
+     
+     return () => {
+       document.removeEventListener('mousedown', listener);
+       document.removeEventListener('touchstart', listener);
+     };
+   }, [ref, handler]);
+ }

// 컴포넌트에서 사용
+ import { useOnClickOutside } from '@/hooks/useOnClickOutside';

// ...

- React.useEffect(() => {
-   const handleClickOutside = (event: MouseEvent) => {
-     if (
-       calendarRef.current &&
-       !calendarRef.current.contains(event.target as Node)
-     ) {
-       setIsOpen(false);
-     }
-   };
-   
-   document.addEventListener('mousedown', handleClickOutside);
-   return () => {
-     document.removeEventListener('mousedown', handleClickOutside);
-   };
- }, []);
+ useOnClickOutside(calendarRef, () => setIsOpen(false));
src/app/meeting/components/form/MeetingForm.tsx (3)

73-79: 이미지 처리 로직 개선이 필요합니다.

현재 이미지 처리가 직접적인 DOM 조작을 통해 이루어지고 있습니다. 이는 React의 선언적 프로그래밍 패턴과 맞지 않으며, 테스트하기 어렵고 예기치 않은 버그를 발생시킬 수 있습니다.

React의 ref를 사용하여 DOM 요소에 접근하는 방식으로 개선하는 것이 좋습니다:

+ const fileInputRef = React.useRef<HTMLInputElement>(null);

  const onSubmit = async (data: CreateMeetingPayload) => {
    try {
      // 이미지 처리
-     const fileInput = document.getElementById('image') as HTMLInputElement;
-     if (fileInput?.files && fileInput.files.length > 0) {
-       const imageData = await convertImageToBase64(fileInput.files[0]);
+     if (fileInputRef.current?.files && fileInputRef.current.files.length > 0) {
+       const imageData = await convertImageToBase64(fileInputRef.current.files[0]);
        data.imageName = imageData.name;
        data.imageEncodedBase64 = imageData.base64;
      }

그리고 ImageField 컴포넌트에 ref를 전달해야 합니다:

- <ImageField required={true} />
+ <ImageField required={true} ref={fileInputRef} />

40-42: 날짜 처리를 위한 라이브러리 사용을 고려해 보세요.

현재 날짜 형식 변환 로직이 직접 구현되어 있습니다. 이는 오류가 발생하기 쉽고 국제화(i18n)에 대응하기 어렵습니다.

date-fns 또는 dayjs와 같은 라이브러리를 사용하여 날짜 처리를 개선하는 것이 좋습니다:

- // 날짜 YYYY-MM-DD 형식으로 변환
- const today = new Date();
- const formattedToday = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
+ import { format } from 'date-fns';
+ 
+ // 날짜 YYYY-MM-DD 형식으로 변환
+ const today = new Date();
+ const formattedToday = format(today, 'yyyy-MM-dd');

45-48: 카테고리 ID 검색 로직 최적화가 필요합니다.

getCategoryId 함수는 매번 배열을 선형 검색합니다. 카테고리 목록이 많아질 경우 성능 저하가 발생할 수 있습니다.

컴포넌트 초기화 시 카테고리 라벨을 키로, ID를 값으로 하는 맵을 생성하여 조회 성능을 개선하는 것이 좋습니다:

+ // 카테고리 맵 생성 (라벨 -> ID)
+ const categoryMap = React.useMemo(() => {
+   const map = new Map();
+   MEETING_TYPES.forEach(type => map.set(type.label, type.id));
+   return map;
+ }, []);

  // 카테고리 라벨에서 ID 찾기 (URL 용)
  const getCategoryId = (label: string) => {
-   const category = MEETING_TYPES.find((type) => type.label === label);
-   return category ? category.id : '';
+   return categoryMap.get(label) || '';
  };
src/app/meeting/components/form/validation.ts (2)

68-71: MAX_SELECTIONS 상수를 다른 코드에서도 사용 가능하게 하는 것이 좋습니다.

현재 기술 스택 최대 선택 개수가 TECH_STACK_CONFIG 객체 내부에 중첩되어 있어, 다른 컴포넌트에서 이 값에 접근할 때 더 복잡한 참조가 필요합니다.

기술 스택 최대 선택 개수를 최상위 상수로 분리하는 것이 좋습니다:

// 기술 스택 관련 상수
+ export const MAX_TECH_STACK_SELECTIONS = 5;
+
export const TECH_STACK_CONFIG = {
-  MAX_SELECTIONS: 5,
+  MAX_SELECTIONS: MAX_TECH_STACK_SELECTIONS,
};

1-7: 타입 안전성 향상이 필요합니다.

현재 유효성 검사 규칙들이 타입 없이 일반 객체로 정의되어 있어, TypeScript의 타입 안전성 이점을 충분히 활용하지 못하고 있습니다.

각 유효성 검사 규칙에 대한 인터페이스를 정의하고, 이를 적용하면 타입 안전성을 높일 수 있습니다:

+ import { RegisterOptions } from 'react-hook-form';
+ 
+ // 텍스트 필드 유효성 검사 인터페이스
+ interface TextFieldValidation extends RegisterOptions {
+   required: string | boolean;
+   maxLength?: {
+     value: number;
+     message: string;
+   };
+   minLength?: {
+     value: number;
+     message: string;
+   };
+ }
+ 
+ // 숫자 필드 유효성 검사 인터페이스
+ interface NumberFieldValidation extends RegisterOptions {
+   required: string | boolean;
+   min?: {
+     value: number;
+     message: string;
+   };
+   max?: {
+     value: number;
+     message: string;
+   };
+   valueAsNumber?: boolean;
+ }

- export const meetingTitleValidation = {
+ export const meetingTitleValidation: TextFieldValidation = {
  required: '모임 이름은 필수입니다',
  maxLength: {
    value: 50,
    message: '모임 이름은 최대 50자까지 입력 가능합니다',
  },
};

// 모임 유형 유효성 검사
- export const meetingTypeValidation = {
+ export const meetingTypeValidation: RegisterOptions = {
  required: '모임 유형을 선택해주세요',
};

// 이하 다른 유효성 검사 규칙들도 같은 방식으로 타입 적용

Also applies to: 9-12, 14-21, 28-40, 50-61, 63-66

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c5db423 and eb6620a.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (31)
  • package.json (1 hunks)
  • src/app/layout.tsx (2 hunks)
  • src/app/meeting/components/form/MeetingForm.tsx (1 hunks)
  • src/app/meeting/components/form/form-filed/CategoryField.tsx (1 hunks)
  • src/app/meeting/components/form/form-filed/DateField.tsx (1 hunks)
  • src/app/meeting/components/form/form-filed/DescriptionField.tsx (1 hunks)
  • src/app/meeting/components/form/form-filed/ImageField.tsx (1 hunks)
  • src/app/meeting/components/form/form-filed/InfoMessage.tsx (1 hunks)
  • src/app/meeting/components/form/form-filed/LocationField.tsx (1 hunks)
  • src/app/meeting/components/form/form-filed/MemberLimitField.tsx (1 hunks)
  • src/app/meeting/components/form/form-filed/PrivacyField.tsx (1 hunks)
  • src/app/meeting/components/form/form-filed/RequireApprovalField.tsx (1 hunks)
  • src/app/meeting/components/form/form-filed/SubmitButton.tsx (1 hunks)
  • src/app/meeting/components/form/form-filed/TechStackField.tsx (1 hunks)
  • src/app/meeting/components/form/form-filed/TitleField.tsx (1 hunks)
  • src/app/meeting/components/form/form-filed/index.ts (1 hunks)
  • src/app/meeting/components/form/validation.ts (1 hunks)
  • src/app/meeting/constants/meeting-form/meetingConstants.tsx (1 hunks)
  • src/app/meeting/create-meeting/page.tsx (1 hunks)
  • src/app/meeting/edit-meeting/[id]/page.tsx (1 hunks)
  • src/app/page.tsx (1 hunks)
  • src/components/ui/form/DatePicker.tsx (1 hunks)
  • src/components/ui/tech-stack/TechSelector.tsx (2 hunks)
  • src/components/ui/tech-stack/tech-stack-components/CategoryTabs.tsx (1 hunks)
  • src/components/ui/tech-stack/tech-stack-components/SelectedTechButton.tsx (1 hunks)
  • src/components/ui/tech-stack/tech-stack-components/TabButton.tsx (1 hunks)
  • src/components/ui/tech-stack/tech-stack-components/TechButton.tsx (1 hunks)
  • src/hooks/mutations/useMeetingFormMutation.ts (1 hunks)
  • src/service/api/meetingForm.ts (1 hunks)
  • src/types/meetingForm.ts (1 hunks)
  • src/util/base64.ts (1 hunks)
✅ Files skipped from review due to trivial changes (5)
  • src/components/ui/tech-stack/tech-stack-components/CategoryTabs.tsx
  • src/components/ui/tech-stack/tech-stack-components/TechButton.tsx
  • src/app/meeting/edit-meeting/[id]/page.tsx
  • src/app/meeting/components/form/form-filed/index.ts
  • src/app/meeting/constants/meeting-form/meetingConstants.tsx
🔇 Additional comments (25)
src/app/meeting/create-meeting/page.tsx (1)

1-5: 간결하고 명확한 구현이 잘 되었습니다.

페이지 컴포넌트가 비동기 함수로 정의되어 Next.js 서버 컴포넌트 패턴을 잘 따르고 있습니다. MeetingForm 컴포넌트에 mode="create"를 전달하여 생성 모드로 폼을 초기화하는 방식이 깔끔합니다.

src/components/ui/tech-stack/tech-stack-components/SelectedTechButton.tsx (1)

32-37: 폼 제출 방지를 위한 button type 속성 추가 👍

버튼에 type="button" 속성을 추가한 것은 매우 좋은 개선사항입니다. 이렇게 하면 폼 내부에서 이 버튼이 사용될 때 의도치 않게 폼이 제출되는 것을 방지할 수 있습니다.

src/components/ui/tech-stack/tech-stack-components/TabButton.tsx (1)

17-19: 폼 제출 방지를 위한 button type 속성 추가 👍

버튼에 type="button" 속성을 추가한 것은 매우 좋은 개선사항입니다. 이렇게 하면 폼 내부에서 이 버튼이 사용될 때 의도치 않게 폼이 제출되는 것을 방지할 수 있습니다.

src/app/layout.tsx (1)

3-3: ReactQueryDevtools 임포트 추가 확인

ReactQueryDevtools가 적절하게 추가되었습니다.

src/app/meeting/components/form/form-filed/DateField.tsx (4)

1-4: 필요한 임포트가 적절하게 구성되어 있습니다.

DatePicker 컴포넌트와 react-hook-form의 useFormContext 훅, 그리고 CreateMeetingPayload 타입을 잘 가져오고 있습니다.


7-9: Props 인터페이스 정의가 깔끔합니다.

required 프롭에 기본값을 설정하여 사용성을 높인 점이 좋습니다.


11-19: 조건부 유효성 검사 로직이 잘 구현되어 있습니다.

required 프롭에 따라 유효성 검사 규칙을 조건부로 적용하는 방식이 유연하고 재사용성이 높습니다.


20-31: DatePicker 컴포넌트 사용이 적절합니다.

필요한 프롭들을 잘 전달하고 있으며, 특히 에러 메시지 처리와 유효성 검사 규칙 적용이 깔끔합니다.

src/app/meeting/components/form/form-filed/InfoMessage.tsx (1)

1-22: 깔끔하게 구현된 컴포넌트입니다.

정보 메시지를 표시하는 컴포넌트가 명확하게 구현되어 있습니다. Lucide React의 Info 아이콘을 활용하여 시각적인 요소를 추가하고, 사용자에게 모임 생성 후 수정 가능한 항목들을 명확하게 알려주는 UI입니다.

src/components/ui/tech-stack/TechSelector.tsx (3)

10-14: id 속성 추가로 접근성 및 테스트 가능성 향상됨

선택적 id 속성을 추가하여 컴포넌트의 접근성과 테스트 가능성을 향상시켰습니다. 이는 DOM 요소에 접근해야 하는 테스트 케이스나 특정 요소를 식별해야 하는 상황에서 유용합니다.


16-20: 적절하게 props 구조분해할당에 id 추가됨

컴포넌트의 props 구조분해할당에 id가 적절히 추가되었습니다.


41-41: id 속성 적용이 올바르게 구현됨

루트 div 요소에 id 속성이 올바르게 적용되었습니다.

src/hooks/mutations/useMeetingFormMutation.ts (2)

1-5: 필요한 의존성이 적절히 가져와짐

API 호출과 mutation 처리에 필요한 의존성들이 적절히 임포트되었습니다.


14-21: 반환 객체 구조 명확

mutation 상태를 다루는 속성들이 명확하게 반환되어 있어 컴포넌트에서 사용하기 편리합니다.

src/app/meeting/components/form/form-filed/LocationField.tsx (3)

7-9: 필수 여부를 props로 받는 유연한 설계

required 속성을 통해 필드의 필수 여부를 조정할 수 있는 유연한 설계가 적용되었습니다.


17-19: required 속성에 따른 validation 분기 처리 적절함

필수 여부에 따라 validation 규칙을 조건부로 적용하는 로직이 적절히 구현되었습니다.


1-40: Form Context 의존성에 대한 명시 필요

이 컴포넌트는 상위에 FormProvider가 존재한다고 가정합니다. 이러한 의존성을 문서화하거나 오류 처리를 추가하면 더 안정적인 컴포넌트가 될 수 있습니다.

컴포넌트가 FormProvider 없이 사용될 경우 오류를 표시하도록 처리를 추가할 수 있습니다:

const LocationField = ({ required = true }: LocationFieldProps) => {
  const {
    register,
    formState: { errors },
-  } = useFormContext<CreateMeetingPayload>();
+  } = useFormContext<CreateMeetingPayload>() || {};

+  if (!register) {
+    console.error('LocationField must be used within a FormProvider');
+    return null;
+  }

  // 나머지 코드...
};
src/app/meeting/components/form/form-filed/SubmitButton.tsx (1)

10-21: 깔끔한 구현입니다!

버튼 컴포넌트가 단일 책임을 가지며 잘 구현되어 있습니다. 기본값 설정과 로딩 상태 처리가 깔끔합니다.

src/app/meeting/components/form/form-filed/PrivacyField.tsx (2)

26-58: ARIA 속성이 잘 구현되어 있습니다.

컴포넌트에 접근성을 위한 ARIA 속성과 키보드 인터랙션이 잘 구현되어 있습니다. 라디오 버튼의 선택 상태에 따른 시각적 피드백도 적절히 제공되고 있습니다.


62-66: 오류 메시지 처리가 잘 구현되어 있습니다.

폼 유효성 검사 오류에 대한 시각적 피드백이 적절히 구현되어 있습니다.

src/app/meeting/components/form/form-filed/TitleField.tsx (1)

17-19: 유효성 검사 조건부 로직이 잘 구현되어 있습니다.

required 프롭에 따라 유효성 검사 규칙을 적절히 조정하는 로직이 깔끔하게 구현되어 있습니다.

src/app/meeting/components/form/form-filed/MemberLimitField.tsx (1)

39-54: 숫자 입력 제한 로직이 잘 구현되어 있습니다.

숫자만 입력할 수 있도록 키보드 이벤트를 처리하는 로직이 상세하게 잘 구현되어 있습니다.

src/util/base64.ts (2)

6-29: 이미지를 Base64로 변환하는 함수가 잘 구현되었습니다.

비동기 처리와 에러 핸들링이 적절하게 구현되어 있습니다. FileReader API를 사용하여 이미지 파일을 데이터 URL로 변환하고 접두사를 제거하는 로직이 명확합니다.


38-44: 이미지 크기 검증 함수 구현이 적절합니다.

최대 파일 크기에 대한 기본값 설정과 바이트 단위 변환 로직이 잘 작성되었습니다.

src/app/meeting/components/form/MeetingForm.tsx (1)

88-95: 편집 모드 구현이 미완성되었습니다.

현재 편집 모드의 API 호출 부분이 주석 처리되어 있습니다. 이는 향후 API가 구현될 때 업데이트가 필요함을 나타냅니다.

향후 API가 구현되면 이 부분을 완성하고, 적절한 테스트를 수행해야 합니다. 또한, 편집 모드에서도 이미지가 변경되지 않은 경우에 대한 처리 로직이 필요할 수 있습니다.

package.json Outdated
Comment on lines 22 to 30
"@types/react-datepicker": "^7.0.0",
"autoprefixer": "^10.4.20",
"axios": "^1.7.9",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.475.0",
"next": "14.1.0",
"react": "^18.2.0",
"react-datepicker": "^8.1.0",
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

react-datepicker 타입 버전과 라이브러리 버전 불일치 확인 필요

@types/react-datepicker는 버전 ^7.0.0인데 비해, react-datepicker 라이브러리는 버전 ^8.1.0입니다. 이러한 버전 불일치는 타입 정의가 최신 라이브러리 기능을 완전히 커버하지 못할 수 있어 타입 오류가 발생할 가능성이 있습니다.


🌐 Web query:

@types/react-datepicker 7.0.0과 react-datepicker 8.1.0의 호환성 확인

💡 Result:

@types/react-datepicker 7.0.0과 react-datepicker 8.1.0은 호환되지 않으며 별도 설치가 필요없습니다. react-datepicker 8.1.0부터는 자체 타입 정의를 포함하고 있기 때문입니다[3][10].

주요 호환성 정보:

  1. 타입 정의 중복 문제
    react-datepicker 2.0.0 버전 이후부터는 자체 타입 정의를 포함하고 있으며, @types/react-datepicker는 2023년 4월 27일부로 deprecated 처리되었습니다[7][10]. 두 패키지를 동시에 설치하면 타입 충돌이 발생할 수 있습니다.

  2. 권장 해결 방법

npm uninstall @types/react-datepicker
# 또는
yarn remove @types/react-datepicker

기존에 설치된 타입 정의 패키지를 제거해야 합니다[10].

  1. 에러 발생 시 추가 조치
    타입 관련 에러가 지속될 경우 다음 명령어로 캐시를 완전히 삭제해야 합니다:
rm -rf node_modules yarn.lock package-lock.json
npm install
# 또는
yarn install

이 방법은 검색 결과 [1]에서 제안된 의존성 문제 해결 방식과 일치합니다.

최신 버전 사용 시 주의사항:

  • showWeekPicker, selectsRange 등 일부 프로퍼티는 타입 정의 업데이트 지연으로 인해 임시 타입 캐스팅이 필요할 수 있음[2]
  • CSS 커스터마이징 시 전역 스타일시트에 클래스 재정의 필요[4][6][9]
  • React 18 환경에서 동작 확인 필요[1]

Citations:


리뷰 지적 사항: @types/react-datepicker 제거 필요

  • 문제점: package.json@types/react-datepicker가 버전 ^7.0.0으로 포함되어 있으나, react-datepicker 8.1.0부터는 자체 타입 정의를 포함하고 있어 충돌 및 타입 오류가 발생할 수 있습니다.
  • 해결방법: @types/react-datepicker 의존성을 제거해주세요. (예: npm uninstall @types/react-datepicker 또는 yarn remove @types/react-datepicker)

Comment on lines +6 to +9
export const createMeeting = async (data: CreateMeetingPayload) => {
const response = await authAPI.post(meetingURL.create, 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 호출 함수가 적절하게 구현되어 있습니다.

createMeeting 함수의 구현이 간결하고 명확합니다. 하지만 API 호출 중 발생할 수 있는 오류 처리가 없습니다.

다음과 같이 오류 처리를 추가하는 것을 권장합니다:

export const createMeeting = async (data: CreateMeetingPayload) => {
-  const response = await authAPI.post(meetingURL.create, data);
-  return response.data;
+  try {
+    const response = await authAPI.post(meetingURL.create, data);
+    return response.data;
+  } catch (error) {
+    // 오류 처리 로직 (로깅, 사용자 알림 등)
+    throw error; // 상위 컴포넌트에서 처리할 수 있도록 오류를 다시 throw
+  }
};
📝 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 const createMeeting = async (data: CreateMeetingPayload) => {
const response = await authAPI.post(meetingURL.create, data);
return response.data;
};
export const createMeeting = async (data: CreateMeetingPayload) => {
try {
const response = await authAPI.post(meetingURL.create, data);
return response.data;
} catch (error) {
// 오류 처리 로직 (로깅, 사용자 알림 등)
throw error; // 상위 컴포넌트에서 처리할 수 있도록 오류를 다시 throw
}
};

Comment on lines +17 to +19
const validation = required
? descriptionValidation
: { ...descriptionValidation, required: false };
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

watch 훅 누락 문제

위 수정사항을 적용하기 위해서는 watch 함수를 useFormContext에서 가져와야 합니다.

  const {
    register,
    formState: { errors },
+   watch,
  } = useFormContext<CreateMeetingPayload>();
📝 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 validation = required
? descriptionValidation
: { ...descriptionValidation, required: false };
const {
register,
formState: { errors },
watch,
} = useFormContext<CreateMeetingPayload>();

Comment on lines +42 to +46
maxSelections={maxSelections}
onSelectionChange={(selection) => {
field.onChange(selection);
handleTechStackChange(selection);
}}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

중복된 상태 업데이트 로직이 있습니다.

onSelectionChange 콜백 내에서 field.onChange(selection)handleTechStackChange(selection)가 모두 동일한 작업을 수행합니다. 불필요한 중복을 제거하는 것이 좋습니다.

            onSelectionChange={(selection) => {
              field.onChange(selection);
-             handleTechStackChange(selection);
            }}

또는 handleTechStackChange 함수를 제거하고 인라인으로 처리하는 방법도 있습니다:

-  const handleTechStackChange = (selection: string[]) => {
-    setValue('skillArray', selection);
-  };

...

            onSelectionChange={(selection) => {
              field.onChange(selection);
            }}
📝 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
maxSelections={maxSelections}
onSelectionChange={(selection) => {
field.onChange(selection);
handleTechStackChange(selection);
}}
maxSelections={maxSelections}
onSelectionChange={(selection) => {
field.onChange(selection);
}}

Comment on lines +170 to +172
<button className="typo-button1 rounded-full bg-main px-8 py-3 text-white transition-opacity hover:opacity-90">
모임 생성하기
</button>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

CTA 섹션의 모임 생성 버튼에도 링크 필요

CTA 섹션의 모임 생성 버튼도 모임 생성 페이지로 연결되어야 합니다.

- <button className="typo-button1 rounded-full bg-main px-8 py-3 text-white transition-opacity hover:opacity-90">
-   모임 생성하기
- </button>
+ <Link 
+   href="/meeting/create-meeting" 
+   className="typo-button1 rounded-full bg-main px-8 py-3 text-white transition-opacity hover:opacity-90">
+   모임 생성하기
+ </Link>
📝 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
<button className="typo-button1 rounded-full bg-main px-8 py-3 text-white transition-opacity hover:opacity-90">
모임 생성하기
</button>
<Link
href="/meeting/create-meeting"
className="typo-button1 rounded-full bg-main px-8 py-3 text-white transition-opacity hover:opacity-90">
모임 생성하기
</Link>

Comment on lines +93 to +105
const handleDeleteImage = () => {
setImagePreview(null);
setValue('imageName', '');
setValue('imageEncodedBase64', '');
setShowError(true);

if (required) {
setError('imageName', {
type: 'manual',
message: imageValidationMessages.required,
});
}
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

이미지 삭제 시 에러 처리 개선 필요

이미지를 삭제할 때 setShowError(true)를 즉시 호출하면 사용자가 아직 상호작용하지 않았는데도 오류 메시지가 표시됩니다. 필수가 아닌 경우에는 오류를 표시할 필요가 없습니다.

const handleDeleteImage = () => {
  setImagePreview(null);
  setValue('imageName', '');
  setValue('imageEncodedBase64', '');
-  setShowError(true);

  if (required) {
+    setShowError(true);
    setError('imageName', {
      type: 'manual',
      message: imageValidationMessages.required,
    });
  }
};
📝 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 handleDeleteImage = () => {
setImagePreview(null);
setValue('imageName', '');
setValue('imageEncodedBase64', '');
setShowError(true);
if (required) {
setError('imageName', {
type: 'manual',
message: imageValidationMessages.required,
});
}
};
const handleDeleteImage = () => {
setImagePreview(null);
setValue('imageName', '');
setValue('imageEncodedBase64', '');
if (required) {
setShowError(true);
setError('imageName', {
type: 'manual',
message: imageValidationMessages.required,
});
}
};

@@ -0,0 +1,167 @@
import { IMAGE_CONFIG } from '@/app/meeting/constants/meeting-form/meetingConstants';
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🛠️ Refactor suggestion

🧩 Analysis chain

폴더명 오타 수정 필요

현재 경로명이 "form-filed"로 되어 있는데, 이는 "form-field"의 오타로 보입니다. 폴더명을 수정하고 관련 import 경로도 모두 업데이트해야 합니다.


🏁 Script executed:

#!/bin/bash
# 현재 폴더 구조 확인
find src/app/meeting/components/form -type d -name "form-filed*" | grep -i "filed"

Length of output: 125


폴더명 오타 수정 필요: "form-filed" → "form-field"
현재 코드베이스를 확인한 결과, src/app/meeting/components/form 하위에 실제로 form-filed 폴더가 존재합니다. 오타로 보이며, 이를 form-field로 수정하고 관련된 모든 import 경로도 업데이트해 주세요.

  • 대상 폴더: src/app/meeting/components/form/form-filedsrc/app/meeting/components/form/form-field
  • 참고: 폴더명 변경에 따른 import 경로 업데이트가 다른 파일에도 영향을 미칠 수 있으니 전체 수정이 필요합니다.

Comment on lines 71 to 96
const onSubmit = async (data: CreateMeetingPayload) => {
try {
// 이미지 처리
const fileInput = document.getElementById('image') as HTMLInputElement;
if (fileInput?.files && fileInput.files.length > 0) {
const imageData = await convertImageToBase64(fileInput.files[0]);
data.imageName = imageData.name;
data.imageEncodedBase64 = imageData.base64;
}

if (mode === 'create') {
// 모임 생성
const result = await createMeeting.mutateAsync(data);

// 성공 시 상세 페이지로 이동
const categoryId = getCategoryId(data.categoryTitle);
router.push(`/meeting/${categoryId}/${result.id}`);
} else if (mode === 'edit' && meetingId) {
// 모임 수정 (TODO: API 구현 시 수정)
// const result = await updateMeeting.mutateAsync({ id: meetingId, data });

// 수정 성공 시 상세 페이지로 이동
const categoryId = getCategoryId(data.categoryTitle);
router.push(`/meeting/${categoryId}/${meetingId}`);
}
} catch (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

오류 처리 기능이 필요합니다.

현재 onSubmit 함수의 catch 블록이 비어 있어 오류가 발생할 경우 사용자에게 아무런 피드백이 제공되지 않습니다. 이는 사용자 경험에 부정적인 영향을 미칠 수 있습니다.

오류 처리를 추가하여 사용자에게 적절한 피드백을 제공하는 것이 좋습니다:

  try {
    // 이미지 처리
    const fileInput = document.getElementById('image') as HTMLInputElement;
    if (fileInput?.files && fileInput.files.length > 0) {
      const imageData = await convertImageToBase64(fileInput.files[0]);
      data.imageName = imageData.name;
      data.imageEncodedBase64 = imageData.base64;
    }

    if (mode === 'create') {
      // 모임 생성
      const result = await createMeeting.mutateAsync(data);

      // 성공 시 상세 페이지로 이동
      const categoryId = getCategoryId(data.categoryTitle);
      router.push(`/meeting/${categoryId}/${result.id}`);
    } else if (mode === 'edit' && meetingId) {
      // 모임 수정 (TODO: API 구현 시 수정)
      // const result = await updateMeeting.mutateAsync({ id: meetingId, data });

      // 수정 성공 시 상세 페이지로 이동
      const categoryId = getCategoryId(data.categoryTitle);
      router.push(`/meeting/${categoryId}/${meetingId}`);
    }
-  } catch (error) {}
+  } catch (error) {
+    console.error('모임 생성/수정 중 오류 발생:', error);
+    // 사용자에게 오류 알림 표시 (toast 또는 alert 사용)
+    // 예: toast.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 onSubmit = async (data: CreateMeetingPayload) => {
try {
// 이미지 처리
const fileInput = document.getElementById('image') as HTMLInputElement;
if (fileInput?.files && fileInput.files.length > 0) {
const imageData = await convertImageToBase64(fileInput.files[0]);
data.imageName = imageData.name;
data.imageEncodedBase64 = imageData.base64;
}
if (mode === 'create') {
// 모임 생성
const result = await createMeeting.mutateAsync(data);
// 성공 시 상세 페이지로 이동
const categoryId = getCategoryId(data.categoryTitle);
router.push(`/meeting/${categoryId}/${result.id}`);
} else if (mode === 'edit' && meetingId) {
// 모임 수정 (TODO: API 구현 시 수정)
// const result = await updateMeeting.mutateAsync({ id: meetingId, data });
// 수정 성공 시 상세 페이지로 이동
const categoryId = getCategoryId(data.categoryTitle);
router.push(`/meeting/${categoryId}/${meetingId}`);
}
} catch (error) {}
const onSubmit = async (data: CreateMeetingPayload) => {
try {
// 이미지 처리
const fileInput = document.getElementById('image') as HTMLInputElement;
if (fileInput?.files && fileInput.files.length > 0) {
const imageData = await convertImageToBase64(fileInput.files[0]);
data.imageName = imageData.name;
data.imageEncodedBase64 = imageData.base64;
}
if (mode === 'create') {
// 모임 생성
const result = await createMeeting.mutateAsync(data);
// 성공 시 상세 페이지로 이동
const categoryId = getCategoryId(data.categoryTitle);
router.push(`/meeting/${categoryId}/${result.id}`);
} else if (mode === 'edit' && meetingId) {
// 모임 수정 (TODO: API 구현 시 수정)
// const result = await updateMeeting.mutateAsync({ id: meetingId, data });
// 수정 성공 시 상세 페이지로 이동
const categoryId = getCategoryId(data.categoryTitle);
router.push(`/meeting/${categoryId}/${meetingId}`);
}
} catch (error) {
console.error('모임 생성/수정 중 오류 발생:', error);
// 사용자에게 오류 알림 표시 (toast 또는 alert 사용)
// 예: toast.error('모임 생성 중 오류가 발생했습니다. 다시 시도해주세요.');
}
}

Comment on lines +23 to +26
// 시작 날짜 유효성 검사
export const startDateValidation = {
required: '시작 날짜는 필수입니다',
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

날짜 유효성 검사 강화가 필요합니다.

현재 startDateValidation은 날짜 필수 여부만 확인하고 있습니다. 과거 날짜 선택 제한이나 형식 유효성 검사가 없습니다.

최소 날짜 검증을 추가하여 과거 날짜를 선택할 수 없도록 개선하는 것이 좋습니다:

// 시작 날짜 유효성 검사
export const startDateValidation = {
  required: '시작 날짜는 필수입니다',
+ validate: {
+   futureDate: (value: string) => {
+     const today = new Date();
+     today.setHours(0, 0, 0, 0);
+     const selectedDate = new Date(value);
+     return selectedDate >= today || '오늘 이후의 날짜를 선택해주세요';
+   },
+ },
};
📝 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 const startDateValidation = {
required: '시작 날짜는 필수입니다',
};
// 시작 날짜 유효성 검사
export const startDateValidation = {
required: '시작 날짜는 필수입니다',
validate: {
futureDate: (value: string) => {
const today = new Date();
today.setHours(0, 0, 0, 0);
const selectedDate = new Date(value);
return selectedDate >= today || '오늘 이후의 날짜를 선택해주세요';
},
},
};

Comment on lines +42 to +48
// 이미지 유효성 검사 메시지
export const imageValidationMessages = {
required: '대표 이미지는 필수입니다',
sizeError: (maxSize: number) =>
`이미지 크기는 최대 ${maxSize}MB까지 가능합니다`,
formatError: '지원되는 이미지 형식만 업로드 가능합니다 (JPG, PNG, JPEG)',
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

이미지 유효성 검사 함수가 없습니다.

이미지 유효성 검사 메시지는 정의되어 있지만, 실제로 크기와 형식을 검증하는 함수가 없습니다.

이미지 크기와 형식을 검증하는 함수를 추가하는 것이 좋습니다:

// 이미지 유효성 검사 메시지
export const imageValidationMessages = {
  required: '대표 이미지는 필수입니다',
  sizeError: (maxSize: number) =>
    `이미지 크기는 최대 ${maxSize}MB까지 가능합니다`,
  formatError: '지원되는 이미지 형식만 업로드 가능합니다 (JPG, PNG, JPEG)',
};

+ // 이미지 유효성 검사 함수
+ export const validateImage = (file: File | undefined, maxSizeMB = 5) => {
+   if (!file) return { valid: false, message: imageValidationMessages.required };
+   
+   // 형식 검사
+   const validFormats = ['image/jpeg', 'image/png', 'image/jpg'];
+   if (!validFormats.includes(file.type)) {
+     return { valid: false, message: imageValidationMessages.formatError };
+   }
+   
+   // 크기 검사 (MB 단위)
+   const maxSizeBytes = maxSizeMB * 1024 * 1024;
+   if (file.size > maxSizeBytes) {
+     return { valid: false, message: imageValidationMessages.sizeError(maxSizeMB) };
+   }
+   
+   return { valid: true, 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 const imageValidationMessages = {
required: '대표 이미지는 필수입니다',
sizeError: (maxSize: number) =>
`이미지 크기는 최대 ${maxSize}MB까지 가능합니다`,
formatError: '지원되는 이미지 형식만 업로드 가능합니다 (JPG, PNG, JPEG)',
};
// 이미지 유효성 검사 메시지
export const imageValidationMessages = {
required: '대표 이미지는 필수입니다',
sizeError: (maxSize: number) =>
`이미지 크기는 최대 ${maxSize}MB까지 가능합니다`,
formatError: '지원되는 이미지 형식만 업로드 가능합니다 (JPG, PNG, JPEG)',
};
// 이미지 유효성 검사 함수
export const validateImage = (file: File | undefined, maxSizeMB = 5) => {
if (!file) return { valid: false, message: imageValidationMessages.required };
// 형식 검사
const validFormats = ['image/jpeg', 'image/png', 'image/jpg'];
if (!validFormats.includes(file.type)) {
return { valid: false, message: imageValidationMessages.formatError };
}
// 크기 검사 (MB 단위)
const maxSizeBytes = maxSizeMB * 1024 * 1024;
if (file.size > maxSizeBytes) {
return { valid: false, message: imageValidationMessages.sizeError(maxSizeMB) };
}
return { valid: true, message: '' };
};

@dbswl701
Copy link
Contributor

dbswl701 commented Mar 2, 2025

image

모임 정원의 경우, type="number" 로 되어있어 만약 제출을 하려고 하면 해당 툴팁이 띄워집니다. 하지만 다른 유효성 검사들은 아래에 빨간 글씨로 에러 메시지가 나오고 있어 통일이 필요할 것 같습니다.
입력 시 숫자만 입력 가능하도록 하는건 어떨까요?

Copy link
Contributor

@dbswl701 dbswl701 left a comment

Choose a reason for hiding this comment

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

고생하셨습니다! react-hook-form을 엄청 깔끔하게 잘 쓰시는 것 같아요!
많이 배워갑니다~!

package.json Outdated
"lucide-react": "^0.475.0",
"next": "14.1.0",
"react": "^18.2.0",
"react-datepicker": "^8.1.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

react-datepicker 와 @types/react-datepicker 라이브러리는 현재 사용되지 않는 것 같습니다. 제거 부탁드립니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

에고 확인감사합니다!

<ReactQueryProviders>
<Header />
<div className="m-auto max-w-[1340px]">{children}</div>
<ReactQueryDevtools initialIsOpen={false} />
Copy link
Contributor

Choose a reason for hiding this comment

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

이 부분은 src/hooks/useReactQuery.tsx에 이미 존재하는데 이곳에서도 사용하신 이유가 있을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

지워두고 수정하겠습니다! 감사해요

Copy link
Contributor

Choose a reason for hiding this comment

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

요 페이지 임시로 만든거라 ai가 만들어줬다고 들었는데 뭐라 입력하셨길래 이렇게 잘 만들어주나요...?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

주제를 좀 명확히 입력했습니다!. 페이지 플로우랑 카테고리 및 관련 목데이터들 넣어주고 저희 테일윈드 config 넣은다음에 반응형고려해서 임시로 페이지 마크업 요청하니 잘 나오더군요

Comment on lines +40 to +42
// 날짜 YYYY-MM-DD 형식으로 변환
const today = new Date();
const formattedToday = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
Copy link
Contributor

Choose a reason for hiding this comment

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

요 부분은 util/date.ts에 같이 정리하면 좋을 것 같아요!

참고
https://discord.com/channels/1326085919555588139/1326085921501614120/1343815718965215252

Comment on lines 83 to 87
const result = await createMeeting.mutateAsync(data);

// 성공 시 상세 페이지로 이동
const categoryId = getCategoryId(data.categoryTitle);
router.push(`/meeting/${categoryId}/${result.id}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

오 저는 mutateAsync()를 이전에 사용해 본 적이 없는데, 받아온 데이터 값을 활용하기 위해 mutate()가 아니라 mutateAsync()를 사용하신걸까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

result를 받기 위한 비동기작업과, router push를 위한 네비게이션 둘다 비동기 작업이기 때문에, 선형적 흐름을 위해 사용하였습니다.

예시로 예전 타 프로젝트에서 모달 작업을 할때, 버튼을 클릭하면 모달이 닫히면서 함수를 호출하고 router.push 를 사용했을 때 비동기 작업이 순차적으로 이루어지지 않았던 경험이 있습니다.

따라 정리하자면 req가 오기전에 router.push가 수행됨을 막기위한 구조입니다.

Comment on lines +93 to +100
// API 형식 (YYYY-MM-DD)로 날짜 변환
const formatDateValue = (date: Date | null) => {
if (!date) return '';
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

이 기능은 위에서도 쓰이는 부분이 있는 것 같습니다. 따로 파일로 관리해서 재사용하면 좋을 것 같아요!

Comment on lines +102 to +116
// 화면에 표시되는 형식 (YYYY년 MM월 DD일)
const formatDateDisplay = (value: string) => {
if (!value) return '';

// YYYY-MM-DD 형식인지 확인
const datePattern = /^(\d{4})-(\d{2})-(\d{2})$/;
const match = datePattern.exec(value);

if (match) {
const [_, year, month, day] = match;
return `${year}${parseInt(month)}${parseInt(day)}일`;
}

return value; // 그 외의 경우 원본 값 반환
};
Copy link
Contributor

Choose a reason for hiding this comment

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

이 부분도요!

Comment on lines +1 to +12
export { default as CategoryField } from './CategoryField';
export { default as DateField } from './DateField';
export { default as DescriptionField } from './DescriptionField';
export { default as ImageField } from './ImageField';
export { default as InfoMessage } from './InfoMessage';
export { default as LocationField } from './LocationField';
export { default as MemberLimitField } from './MemberLimitField';
export { default as PrivacyField } from './PrivacyField';
export { default as RequireApprovalField } from './RequireApprovalField';
export { default as SubmitButton } from './SubmitButton';
export { default as TechStackField } from './TechStackField';
export { default as TitleField } from './TitleField';
Copy link
Contributor

Choose a reason for hiding this comment

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

오 배럴 파일 사용하셨군요! 이것도 다같이 통일하면 좋을 것 같아요!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

해당 필드가 너무많아 임시로 넣어두긴 하였습니다. 코드통일위해 제거도 가능합니다

Comment on lines +7 to +12
const createMeeting = useMutation({
mutationFn: async (data: CreateMeetingPayload) => {
const response = await authAPI.post(meetingURL.create, data);
return response.data;
},
});
Copy link
Contributor

Choose a reason for hiding this comment

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

이 부분은 이미 만들어두신 createMeeting() api 호출 함수 재사용 하면 될 것 같습니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

조금 더 자세히 말씀해주실 수 있으실까용?

@dbswl701
Copy link
Contributor

dbswl701 commented Mar 2, 2025

카테고리 선택이나, 가입 방식, 공개 여부 등의 선택이 필요한 부분에서 react-hook-form의 Controller를 사용하신 이유가 있으신가요? 궁금해서 여쭤봅니당!

@clyde-yoonjae
Copy link
Contributor Author

카테고리 선택이나, 가입 방식, 공개 여부 등의 선택이 필요한 부분에서 react-hook-form의 Controller를 사용하신 이유가 있으신가요? 궁금해서 여쭤봅니당!

보통은 input요소에 register를 통하여 입력 요소를 등록하는데, 현재 언급해주신 부분들은 input 타입의 radio가 아닌, div태그로 전부 디자인을 먼저 커스텀하였습니다. 그에 따라, Controller의 render 프롭을 사용하여 (field.value, field.onChange 등)에 접근할 수 있어, 커스텀 UI에서 해당 값들을 읽고 업데이트할 수 있게 합니다.

정리하자면, 커스텀된 UI 컴포넌트를 위한 선택이였습니다!

@clyde-yoonjae
Copy link
Contributor Author

image 모임 정원의 경우, type="number" 로 되어있어 만약 제출을 하려고 하면 해당 툴팁이 띄워집니다. 하지만 다른 유효성 검사들은 아래에 빨간 글씨로 에러 메시지가 나오고 있어 통일이 필요할 것 같습니다. 입력 시 숫자만 입력 가능하도록 하는건 어떨까요?

오... 영문은 안되는데 한글이 입력이되네요..

MemberLimitField.tsx

onKeyDown={(e) => {
  const allowedKeys = [
    'Backspace',
    'Tab',
    'Enter',
    'ArrowLeft',
    'ArrowRight',
    'ArrowUp',
    'ArrowDown',
  ];
  const isNumber = /^[0-9]$/.test(e.key);

  if (!isNumber && !allowedKeys.includes(e.key)) {
    e.preventDefault();
  }
}}

input type을 number 로 지정했고 허용한 키 지정해두어도, 한글이 입력되는 이유가, 조합형 문자이기 때문에 키보드 이벤트가 다르게 동작된다고 합니다. 다음은 관련 정보입니다.

> IME(Input Method Editor): 한글 입력 시 IME가 작동하면서 keydown 이벤트가 다르게 처리됩니다. 조합 중인 한글은 e.key 값이 다르게 인식되거나 이벤트가 다르게 발생할 수 있습니다.

> 이벤트 시점: keydown은 키가 눌린 시점에 발생하지만, 한글은 조합이 완료된 후에 입력되므로 이 시점에는 이미 keydown 이벤트가 지나간 상태입니다.

더 찾아보고 수정해보겠습니다.

@netlify
Copy link

netlify bot commented Mar 2, 2025

Deploy Preview for deving-together failed.

Name Link
🔨 Latest commit 755b9e3
🔍 Latest deploy log https://app.netlify.com/sites/deving-together/deploys/67c44666afc6be00083d4886

@MoimService MoimService deleted a comment from vercel bot Mar 2, 2025
@github-actions
Copy link

github-actions bot commented Mar 2, 2025

CI Status Report

검사 결과

  • Lint: ✅ success
  • Format: ✅ success
  • Type Check: ✅ success
  • Tests: ✅ success
  • Build: ✅ success

✅ 모든 검사가 통과되었습니다.

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: 0

🧹 Nitpick comments (3)
src/app/meeting/components/form/form-filed/MemberLimitField.tsx (2)

18-20: 유효성 검사 로직 분리 권장
required 여부에 따라 memberLimitValidation을 덮어쓰는 접근은 괜찮습니다. 다만, 이 로직을 더욱 재사용 가능하게 구조화하기 위해 별도 함수로 추출하거나 해당 폴더의 validation.js(또는 .ts) 내에서 처리하면 유지보수성에 도움이 될 것 같습니다.


60-60: 에러 메시지 표시 방식 통일성 확인
Input 컴포넌트의 errorMessage props를 통해 에러 메시지가 잘 표시되지만, 동일 폼의 다른 필드와 에러 표시 형식을 일관성 있게 유지하는지 점검해 보시길 권장드립니다.

src/app/meeting/components/form/MeetingForm.tsx (1)

88-95: 모임 수정 로직 구현 필요
edit 모드에서 updateMeeting API 호출이 TODO 상태입니다. 실제 수정 기능 구현 시 오류 처리와 성공 시 로직을 분리하여 관리하면 유지보수에 유리합니다. 필요하다면 구현 예시를 제안해 드릴 수 있습니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eb6620a and 3603efb.

📒 Files selected for processing (2)
  • src/app/meeting/components/form/MeetingForm.tsx (1 hunks)
  • src/app/meeting/components/form/form-filed/MemberLimitField.tsx (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: deploy-preview
🔇 Additional comments (2)
src/app/meeting/components/form/form-filed/MemberLimitField.tsx (1)

39-53: IME 입력 시 문제 가능성 검토
숫자 입력만 허용하려는 의도가 좋습니다. 하지만 브라우저 또는 사용 환경에 따라 IME를 거치는 입력(예: 한글)이 예상치 못하게 처리될 수 있습니다. 이로 인해 에러 메시지나 예외 처리가 필요할 수 있으니, 추가로 테스트를 진행하거나 문서화하면 좋겠습니다.

src/app/meeting/components/form/MeetingForm.tsx (1)

96-96: 에러 처리 미구현
이전 리뷰에서 언급된 것처럼, catch (error) {} 블록 내에 실제 에러 처리를 추가해야 사용자에게 오류 상황을 알릴 수 있습니다.

@github-actions
Copy link

github-actions bot commented Mar 2, 2025

CI Status Report

검사 결과

  • Lint: ✅ success
  • Format: ✅ success
  • Type Check: ✅ success
  • Tests: ✅ success
  • Build: ✅ success

✅ 모든 검사가 통과되었습니다.

dbswl701
dbswl701 previously approved these changes Mar 2, 2025
Lee-Dong-Seok
Lee-Dong-Seok previously approved these changes Mar 2, 2025
Copy link
Contributor

@Lee-Dong-Seok Lee-Dong-Seok left a comment

Choose a reason for hiding this comment

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

고생 많으셨습니다!! 코드 리뷰 천천히 진행 하겠습니다!👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍

@dbswl701 dbswl701 dismissed stale reviews from Lee-Dong-Seok and themself via 755b9e3 March 2, 2025 11:52
@github-actions
Copy link

github-actions bot commented Mar 2, 2025

CI Status Report

검사 결과

  • Lint: ✅ success
  • Format: ✅ success
  • Type Check: ❌ failure
  • Tests: ✅ success
  • Build: ❌ failure

❌ 일부 검사가 실패했습니다.

@dbswl701 dbswl701 merged commit 3aa6d9f into dev Mar 2, 2025
1 of 8 checks passed
@dbswl701 dbswl701 deleted the feat/markup/create-meeting/DEVING-40 branch March 2, 2025 11:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants