-
Notifications
You must be signed in to change notification settings - Fork 4
Feat/markup/create meeting/DEVING-40 모임생성 및 수정 페이지 구현 #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…t/markup/create-meeting/DEVING-40
…t/markup/create-meeting/DEVING-40
|
Caution Review failedThe pull request is closed. Walkthrough이번 PR은 여러 컴포넌트와 파일에 새로운 기능과 개선사항을 추가합니다. Changes
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: 회의 상세 페이지로 리디렉션
Suggested labels
Suggested reviewers
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (1)
✨ Finishing Touches
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? 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
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)
Other keywords and placeholders
CodeRabbit Configuration File (
|
Codecov ReportAll modified and coverable lines are covered by tests ✅ |
CI Status Report검사 결과
✅ 모든 검사가 통과되었습니다. |
There was a problem hiding this 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: 인터페이스 구조가 명확하게 정의되어 있습니다.모임 생성에 필요한 모든 필드들이 잘 정의되어 있습니다. 다만 몇 가지 개선할 점이 있습니다:
startDate는 문자열로 정의되어 있는데, 날짜 형식에 대한 주석이나 더 구체적인 타입(예: ISO 8601 형식)을 명시하면 좋을 것 같습니다.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에 다음과 같은 개선사항을 추가하는 것이 좋겠습니다:
- 사용자에게 입력 제한에 대한 시각적 피드백 제공을 위한
maxLength속성 추가- 스크린 리더 사용자를 위한 오류 상태 표시를 위해
aria-invalid속성 추가- 사용자가 입력 길이를 확인할 수 있는 문자 수 카운터 추가
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: 상수 데이터를 컴포넌트 외부로 분리하는 것이 좋겠습니다.
categories와featuredGroups같은 큰 데이터 구조는 컴포넌트 외부의 별도 파일로 분리하는 것이 좋습니다. 이렇게 하면 컴포넌트의 가독성이 향상되고 데이터를 재사용하기 쉬워집니다.+ // 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
⛔ Files ignored due to path filters (1)
package-lock.jsonis 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
| "@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", |
There was a problem hiding this comment.
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].
주요 호환성 정보:
-
타입 정의 중복 문제
react-datepicker 2.0.0 버전 이후부터는 자체 타입 정의를 포함하고 있으며, @types/react-datepicker는 2023년 4월 27일부로 deprecated 처리되었습니다[7][10]. 두 패키지를 동시에 설치하면 타입 충돌이 발생할 수 있습니다. -
권장 해결 방법
npm uninstall @types/react-datepicker
# 또는
yarn remove @types/react-datepicker기존에 설치된 타입 정의 패키지를 제거해야 합니다[10].
- 에러 발생 시 추가 조치
타입 관련 에러가 지속될 경우 다음 명령어로 캐시를 완전히 삭제해야 합니다:
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:
- 1: https://dohye1.tistory.com/24
- 2: [@types/react-datepicker]: selectsRange prop only accepts undefined and showWeekPicker is not documented DefinitelyTyped/DefinitelyTyped#67499
- 3: https://www.npmjs.com/package/react-datepicker
- 4: https://velog.io/@taemin4u/React-datepicker-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-%EC%BB%A4%EC%8A%A4%ED%85%80
- 5: https://npmpackage.info/package/@types/react-datepicker
- 6: https://doooodle932.tistory.com/150
- 7: https://npmjs.com/package/@types/react-datepicker/v/4.11.0
- 8: https://socket.dev/npm/package/@types/react-datepicker
- 9: https://rizzui.com/docs/integrations/react-datepicker
- 10: https://www.npmjs.com/package/@types/react-datepicker
리뷰 지적 사항: @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)
| export const createMeeting = async (data: CreateMeetingPayload) => { | ||
| const response = await authAPI.post(meetingURL.create, data); | ||
| return response.data; | ||
| }; |
There was a problem hiding this comment.
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.
| 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 | |
| } | |
| }; |
| const validation = required | ||
| ? descriptionValidation | ||
| : { ...descriptionValidation, required: false }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| const validation = required | |
| ? descriptionValidation | |
| : { ...descriptionValidation, required: false }; | |
| const { | |
| register, | |
| formState: { errors }, | |
| watch, | |
| } = useFormContext<CreateMeetingPayload>(); |
| maxSelections={maxSelections} | ||
| onSelectionChange={(selection) => { | ||
| field.onChange(selection); | ||
| handleTechStackChange(selection); | ||
| }} |
There was a problem hiding this comment.
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.
| maxSelections={maxSelections} | |
| onSelectionChange={(selection) => { | |
| field.onChange(selection); | |
| handleTechStackChange(selection); | |
| }} | |
| maxSelections={maxSelections} | |
| onSelectionChange={(selection) => { | |
| field.onChange(selection); | |
| }} |
| <button className="typo-button1 rounded-full bg-main px-8 py-3 text-white transition-opacity hover:opacity-90"> | ||
| 모임 생성하기 | ||
| </button> |
There was a problem hiding this comment.
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.
| <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> |
| const handleDeleteImage = () => { | ||
| setImagePreview(null); | ||
| setValue('imageName', ''); | ||
| setValue('imageEncodedBase64', ''); | ||
| setShowError(true); | ||
|
|
||
| if (required) { | ||
| setError('imageName', { | ||
| type: 'manual', | ||
| message: imageValidationMessages.required, | ||
| }); | ||
| } | ||
| }; |
There was a problem hiding this comment.
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.
| 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'; | |||
There was a problem hiding this comment.
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-filed→src/app/meeting/components/form/form-field - 참고: 폴더명 변경에 따른 import 경로 업데이트가 다른 파일에도 영향을 미칠 수 있으니 전체 수정이 필요합니다.
| 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) {} |
There was a problem hiding this comment.
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.
| 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('모임 생성 중 오류가 발생했습니다. 다시 시도해주세요.'); | |
| } | |
| } |
| // 시작 날짜 유효성 검사 | ||
| export const startDateValidation = { | ||
| required: '시작 날짜는 필수입니다', | ||
| }; |
There was a problem hiding this comment.
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.
| // 시작 날짜 유효성 검사 | |
| 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 || '오늘 이후의 날짜를 선택해주세요'; | |
| }, | |
| }, | |
| }; |
| // 이미지 유효성 검사 메시지 | ||
| export const imageValidationMessages = { | ||
| required: '대표 이미지는 필수입니다', | ||
| sizeError: (maxSize: number) => | ||
| `이미지 크기는 최대 ${maxSize}MB까지 가능합니다`, | ||
| formatError: '지원되는 이미지 형식만 업로드 가능합니다 (JPG, PNG, JPEG)', | ||
| }; |
There was a problem hiding this comment.
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.
| // 이미지 유효성 검사 메시지 | |
| 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
left a comment
There was a problem hiding this 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", |
There was a problem hiding this comment.
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 라이브러리는 현재 사용되지 않는 것 같습니다. 제거 부탁드립니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
에고 확인감사합니다!
src/app/layout.tsx
Outdated
| <ReactQueryProviders> | ||
| <Header /> | ||
| <div className="m-auto max-w-[1340px]">{children}</div> | ||
| <ReactQueryDevtools initialIsOpen={false} /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분은 src/hooks/useReactQuery.tsx에 이미 존재하는데 이곳에서도 사용하신 이유가 있을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
지워두고 수정하겠습니다! 감사해요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요 페이지 임시로 만든거라 ai가 만들어줬다고 들었는데 뭐라 입력하셨길래 이렇게 잘 만들어주나요...?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
주제를 좀 명확히 입력했습니다!. 페이지 플로우랑 카테고리 및 관련 목데이터들 넣어주고 저희 테일윈드 config 넣은다음에 반응형고려해서 임시로 페이지 마크업 요청하니 잘 나오더군요
| // 날짜 YYYY-MM-DD 형식으로 변환 | ||
| const today = new Date(); | ||
| const formattedToday = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; |
There was a problem hiding this comment.
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
| const result = await createMeeting.mutateAsync(data); | ||
|
|
||
| // 성공 시 상세 페이지로 이동 | ||
| const categoryId = getCategoryId(data.categoryTitle); | ||
| router.push(`/meeting/${categoryId}/${result.id}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오 저는 mutateAsync()를 이전에 사용해 본 적이 없는데, 받아온 데이터 값을 활용하기 위해 mutate()가 아니라 mutateAsync()를 사용하신걸까요?
There was a problem hiding this comment.
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가 수행됨을 막기위한 구조입니다.
| // 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}`; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 기능은 위에서도 쓰이는 부분이 있는 것 같습니다. 따로 파일로 관리해서 재사용하면 좋을 것 같아요!
| // 화면에 표시되는 형식 (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; // 그 외의 경우 원본 값 반환 | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분도요!
| 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'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오 배럴 파일 사용하셨군요! 이것도 다같이 통일하면 좋을 것 같아요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
해당 필드가 너무많아 임시로 넣어두긴 하였습니다. 코드통일위해 제거도 가능합니다
| const createMeeting = useMutation({ | ||
| mutationFn: async (data: CreateMeetingPayload) => { | ||
| const response = await authAPI.post(meetingURL.create, data); | ||
| return response.data; | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분은 이미 만들어두신 createMeeting() api 호출 함수 재사용 하면 될 것 같습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
조금 더 자세히 말씀해주실 수 있으실까용?
|
카테고리 선택이나, 가입 방식, 공개 여부 등의 선택이 필요한 부분에서 react-hook-form의 Controller를 사용하신 이유가 있으신가요? 궁금해서 여쭤봅니당! |
보통은 input요소에 register를 통하여 입력 요소를 등록하는데, 현재 언급해주신 부분들은 input 타입의 radio가 아닌, div태그로 전부 디자인을 먼저 커스텀하였습니다. 그에 따라, Controller의 render 프롭을 사용하여 (field.value, field.onChange 등)에 접근할 수 있어, 커스텀 UI에서 해당 값들을 읽고 업데이트할 수 있게 합니다. 정리하자면, 커스텀된 UI 컴포넌트를 위한 선택이였습니다! |
❌ Deploy Preview for deving-together failed.
|
CI Status Report검사 결과
✅ 모든 검사가 통과되었습니다. |
There was a problem hiding this 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컴포넌트의errorMessageprops를 통해 에러 메시지가 잘 표시되지만, 동일 폼의 다른 필드와 에러 표시 형식을 일관성 있게 유지하는지 점검해 보시길 권장드립니다.src/app/meeting/components/form/MeetingForm.tsx (1)
88-95: 모임 수정 로직 구현 필요
edit모드에서updateMeetingAPI 호출이 TODO 상태입니다. 실제 수정 기능 구현 시 오류 처리와 성공 시 로직을 분리하여 관리하면 유지보수에 유리합니다. 필요하다면 구현 예시를 제안해 드릴 수 있습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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) {}블록 내에 실제 에러 처리를 추가해야 사용자에게 오류 상황을 알릴 수 있습니다.
CI Status Report검사 결과
✅ 모든 검사가 통과되었습니다. |
Lee-Dong-Seok
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
고생 많으셨습니다!! 코드 리뷰 천천히 진행 하겠습니다!👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍
CI Status Report검사 결과
❌ 일부 검사가 실패했습니다. |


📝 주요 작업 내용
모임 생성 및 수정 페이지에 사용되는 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 // 수정페이지 서버컴포넌트 입니다.📺 스크린샷
🔗 참고 사항
useMeetingFormMuation 에서 성공 및 실패시 에러처리를 prop으로 받지않고 사용되는 MeetingForm에서 try catch문으로 하드코딩 하였습니다. 콜백으로 넘기는 방식으로 리펙토링 하겠습니다.
api 처리 과정에서, 토스트 띄우기 추가되어야 합니다.
현재 디자인 임의로 수정하면서 진행하여, 생성된 컴포넌트들 각각 하드코딩되어있어 추후 분리해야됩니다. ex) PrivacyFiled.tsx , RequireApprovalField.tsx
💬 리뷰 요구사항
@lee1nna 한나님과 페이지 겹쳐서, 구조 및 상수관리 같이하면 좋을 것 같습니다.
@Lee-Dong-Seok 동석님과 base64 유틸함수 중복 생성되었습니다. 추후 통일하면 될 것 같습니다.
📃 관련 이슈
DEVING-40
Summary by CodeRabbit