-
Notifications
You must be signed in to change notification settings - Fork 1
Fix/158 리팩토링 #159
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
Fix/158 리팩토링 #159
Conversation
Walkthrough이미지 그리드에 모달 확대 및 로드 오류 대체가 추가되고, 예약 흐름이 useBooking 훅으로 분리되었습니다. 다수 컴포넌트의 로컬 타입이 공용 타입 모듈로 이동되었고 예제 모달 및 일부 목 데이터가 삭제되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant U as 사용자
participant IG as ImageGrid
participant M as Modal
U->>IG: 썸네일/메인 이미지 클릭
IG->>IG: selectedImage 설정, isOpen = true
IG->>M: 모달 열기 (선택 이미지 전달)
U->>M: 닫기 클릭
M->>IG: isOpen = false
Note right of IG: 이미지 로드 오류 발생 시 해당 인덱스를<br>`DEFAULT_BG`로 교체 (setImage)
sequenceDiagram
participant U as 사용자
participant BI as BookingInterface (UI)
participant HB as useBooking 훅
participant API as /activities/{id}/reservation
U->>BI: 날짜/시간/인원 선택
BI->>HB: 현재 선택 상태 조회 (isBookable, buttonText)
U->>BI: 예약 버튼 클릭
BI->>HB: handleBooking()
HB->>API: POST(selectedTimeId, participants)
API-->>HB: 성공
HB-->>BI: onBooking=false, 상태 초기화
HB-->>U: 성공 토스트 표시
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
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: 30
🔭 Outside diff range comments (25)
src/components/DatePicker/CalendarBody.tsx (2)
69-76: 선택/오늘 날짜까지 흐리게 처리되는 부작용 가능성 — 조건 보완 필요현재
!isHighlighted면 무조건opacity-50이 적용되어, 하이라이트 목록에 없는 “선택된 날짜”나 “오늘”까지 반투명해질 수 있습니다. 두 상태는 항상 또렷해야 하므로 조건을 보완해 주세요.적용 예시:
- } ${isHighlighted && !isSelected ? 'bg-yellow-300' : ''} ${!isHighlighted ? 'opacity-50' : ''}`} + } ${isHighlighted && !isSelected ? 'bg-yellow-300' : ''} ${!isHighlighted && !isSelected && !isToday ? 'opacity-50' : ''}`}
46-48: 하이라이트 판정 성능/간결성 개선 (옵션)
highlightDates.some(...)는 날짜 수가 커질수록 비용이 증가합니다. 날짜 문자열 Set으로 미리 변환해 판정을 O(1)로 줄일 수 있습니다.적용 아이디어(파일 상단 인접 위치에 추가):
const highlightSet = useMemo( () => new Set(highlightDates.map((d) => d.format('YYYY-MM-DD'))), [highlightDates], );판정부 교체:
const isHighlighted = highlightSet.has(day.format('YYYY-MM-DD'));src/app/(with-header)/myactivity/utils/dateValidatoin.ts (1)
1-1: 파일명 오탈자: dateValidatoin → dateValidation로 정정 권장유틸 경로 오탈자는 검색/유지보수성을 떨어뜨립니다. 파일명/모든 import 경로를 일괄 정정해 주세요. 필요 시 일괄 치환 스크립트 제공 가능합니다.
src/components/FloatingBox/BookingButton.tsx (2)
13-23: button 기본 type 명시 및 접근성 개선 제안폼 내부에서 뜻하지 않은 submit을 막기 위해 type='button' 명시를 권장합니다. 로딩 중 상태를 보조기기에 전달하기 위해 aria-busy 도입도 추천합니다.
return ( <button + type='button' onClick={onClick} disabled={disabled || onBooking} + aria-busy={onBooking} className={cn( 'relative mt-4 mb-6 w-full max-w-sm rounded-lg py-10 font-medium transition-colors', disabled || onBooking ? 'cursor-not-allowed bg-gray-300 text-gray-500' : 'bg-green-800 text-white hover:bg-green-900', className, )} >
24-31: 미세 문구 교정 및 라이브 영역 추가 제안사용자 피드백 가시성을 높이기 위해 aria-live='polite'를 감싸는 컨테이너에 추가하고, 한국어 띄어쓰기를 교정하면 좋습니다.
- {onBooking ? ( - <div className='flex items-center justify-center gap-2'> + {onBooking ? ( + <div className='flex items-center justify-center gap-2' aria-live='polite'> <span className='h-10 w-10 animate-spin rounded-full border-2 border-white border-t-transparent' /> - <p>요청전송중...</p> + <p>요청 전송 중...</p> </div> ) : ( children )}src/app/(with-header)/myactivity/components/MainImageSelect.tsx (1)
19-28: “메인 이미지는 1장만” 정책을 UI에서도 명확히 반영하세요현재 업로드 버튼이 항상 노출되어 있어, 이미 메인 이미지가 있을 때도 추가 업로드를 유도하는 UX가 됩니다. 메인 이미지가 존재하면 업로드 버튼을 숨기거나 “변경” 액션으로 명확히 표현하는 것이 좋습니다.
메인 이미지가 있을 때 업로드 버튼을 숨기는 간단한 예시:
{/* 업로드 버튼을 메인 이미지가 없을 때만 노출 */} {!mainImage && <ImageUpload onImageSelect={onImageSelect} />} {mainImage && ( <ImagePreview image={mainImage} onRemove={onImageRemove} alt="메인 이미지" /> )}src/app/(with-header)/myactivity/[id]/components/EditActivityForm.tsx (1)
86-93: subImages → url 배열 변환은 useMemo로 메모이제이션 권장렌더마다 매번 map이 실행됩니다. 변환 비용은 작지만, 패턴상 useMemo로 의도를 드러내면 가독성과 미세 최적화에 도움이 됩니다.
예시:
// 파일 상단 import { useMemo } from 'react'; // 내부 const subImageUrls = useMemo(() => subImages.map((img) => img.url), [subImages]); <ImageSection mainImage={mainImage} subImage={subImageUrls} onMainImageSelect={handleMainImageSelect} onMainImageRemove={handleMainImageRemove} onSubImageAdd={handleSubImagesAdd} onSubImageRemove={handleSubImageRemove} />src/app/(with-header)/myactivity/components/ImageSection.tsx (1)
23-27: 서브 이미지 최대 개수(4)를 상위에서 제어 가능하도록 확장 여지 마련현재 최대 개수 로직은 하위 컴포넌트에 하드코딩되어 있을 가능성이 큽니다. 향후 페이지별 정책 차이를 허용하려면 maxCount 같은 prop을 SubImageSelect에 전달하는 구조로 확장성을 열어두는 것을 권장합니다.
src/app/(with-header)/activities/[id]/components/Title.tsx (1)
27-29: useParams 제네릭으로 id의 문자열 보장을 선언하세요as string 캐스팅 대신 useParams 제네릭으로 타입을 명확히 하면 후속 캐스팅이 불필요합니다.
- const { id } = useParams(); + const { id } = useParams<{ id: string }>();src/app/(with-header)/myactivity/components/SubImageSelect.tsx (2)
10-14: 최대 업로드 개수 상수화 및 핸들러 메모이제이션 제안
- 매직 넘버(4)를 상수로 추출해 의도를 드러내세요.
- handleImageUpload는 자식에게 전달되므로 useCallback으로 감싸면 불필요한 리렌더를 줄일 수 있습니다.
예시:
import { useCallback } from 'react'; // 필요 시: import { toast } from 'sonner'; const MAX_SUB_IMAGES = 4; const handleImageUpload = useCallback((file: File) => { if (subImage.length >= MAX_SUB_IMAGES) { // 선택: 사용자에게 피드백 제공 // toast.info(`이미지는 최대 ${MAX_SUB_IMAGES}장까지 업로드할 수 있어요.`); return; } onImagesAdd([file]); }, [subImage, onImagesAdd]);
27-33: 리스트 key로 index 대신 안정적인 식별자 사용 권장파일 배열이 변경/재정렬될 때 index key는 예기치 않은 재사용 문제를 유발할 수 있습니다. 문자열 URL 또는 File 메타를 사용해 안정적인 key를 구성하세요.
예시:
{subImage.map((img, idx) => { const key = typeof img === 'string' ? img : `${img.name}-${img.size}-${img.lastModified ?? idx}`; return ( <ImagePreview key={key} image={img} onRemove={() => onImageRemove(idx)} alt={`소개 이미지 ${idx + 1}`} /> ); })}src/components/FloatingBox/BookingInterface.tsx (2)
101-115: 클릭 가능한 div를 버튼으로 교체하여 접근성 개선모바일 섹션에서 모달 오픈 트리거가 div+onClick으로 구현되어 키보드 포커스/역할이 없습니다. 버튼 요소로 교체하거나 role='button'과 keydown 핸들링을 추가하세요. 아래처럼 교체를 권장합니다.
- <div - onClick={() => setIsOpen(true)} - className='mb-4 animate-pulse cursor-pointer text-sm text-gray-600' - > + <button + type='button' + onClick={() => setIsOpen(true)} + className='mb-4 animate-pulse text-left text-sm text-gray-600' + aria-label='날짜 선택 모달 열기' + > {selectedDate && selectedTime ? ( <h2> {selectedDate instanceof Date - ? selectedDate.toLocaleDateString() + ? selectedDate.toLocaleDateString() : selectedDate} /{selectedTime} </h2> ) : ( '날짜 선택하기' )} - </div> + </button>
69-74: 날짜 포맷 일관성 확보(로케일 명시 또는 포맷터 추출)toLocaleDateString 기본값은 런타임 환경에 따라 표시가 달라질 수 있습니다. ko-KR 명시 또는 공용 포맷터를 사용해 일관성을 확보하세요.
간단 적용 예:
- ? selectedDate.toLocaleDateString() + ? selectedDate.toLocaleDateString('ko-KR') : selectedDate}혹은 파일 상단에 포맷터 함수를 두고 재사용:
const formatDate = (d: Date) => d.toLocaleDateString('ko-KR');Also applies to: 106-112
src/app/(with-header)/activities/[id]/components/ReviewSection.tsx (3)
84-86: 에러 처리 순서 오류: 에러를 “데이터 없음”으로 치환될 수 있음현재 isError 분기가 “데이터 없음” 분기 뒤에 있어, 실제 에러가 발생해도 “작성된 후기가 없습니다”로 표기될 수 있습니다. 에러 분기를 상단으로 이동하세요.
if (isLoading) { return (/* ... */); } - if (!reviewData || reviewData.reviews.length === 0) { - return (/* ... */); - } - - if (isError) { - throw new Error('리뷰섹션에서 에러가 발생했습니다.'); - } + if (isError) { + throw new Error('리뷰섹션에서 에러가 발생했습니다.'); + } + + if (!reviewData || reviewData.reviews.length === 0) { + return (/* ... */); + }
41-51: useCallback 과다 사용: 단순 map 렌더는 인라인이 더 간결3개 아이템 제한(size=3)에서 메모이제이션 이득이 없고, 함수 호출 형태(ReviewComponent())도 혼동을 줍니다. 인라인 렌더로 단순화하세요.
- const ReviewComponent = useCallback(() => { - return reviewData?.reviews.map((review: ReviewProps) => ( - <ReviewCard - key={review.id} - userName={review.user.nickname} - avatarSrc={review.user.profileImageUrl} - date={review.createdAt.slice(0, 10).replace(/-/g, '.')} - reviewText={review.content} - /> - )); - }, [reviewData?.reviews]); + const reviews = reviewData?.reviews ?? [];그리고 사용처:
- {ReviewComponent()} + {reviews.map((review: ReviewProps) => ( + <ReviewCard + key={review.id} + userName={review.user.nickname} + avatarSrc={review.user.profileImageUrl} + date={review.createdAt.slice(0, 10).replace(/-/g, '.')} + reviewText={review.content} + /> + ))}
47-48: 날짜 포맷 슬라이싱 취약: 타임존/포맷 불일치 가능문자열 slice로 날짜를 자르면 ISO 변형이나 타임존 오프셋에 취약합니다. Date 파싱 후 toLocaleDateString 사용을 권장합니다.
- date={review.createdAt.slice(0, 10).replace(/-/g, '.')} + date={new Date(review.createdAt).toLocaleDateString('ko-KR')}src/app/(with-header)/myactivity/components/ImagePreview.tsx (2)
31-35: 이미지 성능 최적화(지연 로딩) 제안프리뷰 그리드일 가능성이 높아 lazy 로딩과 디코딩 힌트를 추가하면 그리기 지연을 줄일 수 있습니다.
<img src={src || '/placeholder.svg'} className='h-full w-full object-cover' alt={alt} + loading='lazy' + decoding='async' />
32-33: 필수 수정 요청: ‘/placeholder.svg’ 파일이 존재하지 않습니다.현재 코드(
src/app/(with-header)/myactivity/components/ImagePreview.tsx32–33행)에서 참조하는/placeholder.svg가 레포에 없으므로, 이미지가 빈 상태로 표시됩니다. 다음 중 하나를 선택해 수정해주세요:
public/assets/svg/에 있는profile-default.svg등 기존 SVG 파일을 사용하도록 경로 변경/public/assets/svg/placeholder.svg파일을 추가하여 참조 경로와 일치시키기src/app/(with-header)/myactivity/components/InfoSection.tsx (2)
55-60: 가격 입력 제약 강화(마이너 UX/밸리데이션 개선)음수/비수치 입력을 예방하고 모바일 키패드를 최적화하기 위해 다음 속성 추가를 권장합니다.
<Input type='number' placeholder='가격' className='w-full appearance-none' value={price} + min={0} + step={1} + inputMode='numeric' + pattern='[0-9]*' onChange={(e) => onPriceChange(e.target.value)} />
24-33: 제목 입력 접근성 보강 제안placeholder만으로는 레이블 역할이 충분치 않습니다. 시각적 레이블을 추가하거나 aria-label을 부여하는 것이 좋습니다.
예:
- <Input + <label htmlFor='title' className='sr-only'>제목</label> + <Input id='title' type='text' placeholder='제목'src/app/(with-header)/myactivity/components/CategoryInput.tsx (1)
15-21: select 제어값의 undefined 방지 및 접근성 보완
CategoryProps에서category가 선택적이면,value={category}는 undefined를 흘릴 수 있어 제어/비제어 경고가 날 수 있습니다. 또한name/aria-label추가로 접근성을 개선할 수 있습니다.- <select + <select className={`w-full appearance-none rounded-md border border-gray-800 bg-white px-20 py-17 ${ category ? 'text-black' : 'text-gray-400' }`} id='category' - value={category} + name='category' + aria-label='카테고리' + value={category ?? ''} onChange={(e) => onCategoryChange(e.target.value)} >src/app/(with-header)/myactivity/hooks/useCreateActivityForm.ts (1)
122-137: 스케줄 입력값(날짜/시간) 공란 검증 추가 권장
dates.length만 확인하면{ date: '', startTime: '', endTime: '' }같은 공란 상태가 통과됩니다. 서버 단에서 실패하기 전에 클라이언트에서 빠르게 피드백을 주는 편이 UX에 좋습니다.const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if ( !title || !category || !description || !address || !price || !mainImage || dates.length === 0 ) { toast.error('모든 값은 필수값입니다!'); return; } + const hasIncompleteSlots = dates.some( + (d) => !d.date || !d.startTime || !d.endTime, + ); + if (hasIncompleteSlots) { + toast.error('스케줄의 날짜/시간을 모두 입력해주세요.'); + return; + } mutation.mutate(); };src/app/(with-header)/activities/[id]/components/ReviewTitle.tsx (1)
5-32: 의존성 배열 누락으로 요약 텍스트가 갱신되지 않을 수 있음
summary는rating도 사용하지만, 의존성 배열에rating이 없습니다.reviewCount만 변할 때만 갱신되어 rating 변경이 반영되지 않습니다.- }, [reviewCount]); + }, [reviewCount, rating]);추가로,
useEffect + setState대신useMemo로 파생값을 계산하는 방식도 고려할 수 있습니다(선택).src/app/(with-header)/myactivity/components/ImageUpload.tsx (1)
12-17: multiple=true일 때 모든 선택 파일 처리
multiple이 true여도 현재는 첫 번째 파일만 전달합니다. 선택된 모든 파일을 순회 처리하는 편이 기대 동작에 가깝습니다(상위 콜백 시그니처가 단일 파일을 받는 형태라면, 파일별로 한 번씩 호출).const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { - const file = e.target.files?.[0]; - if (file) { - onImageSelect(file); - } + if (multiple && e.target.files?.length) { + Array.from(e.target.files).forEach((f) => onImageSelect(f)); + return; + } + const file = e.target.files?.[0]; + if (file) onImageSelect(file); };src/app/(with-header)/myactivity/components/ScheduleSelectForm.tsx (1)
45-51: 불필요한 외부 인덱스 접근 제거(가독성/안전성 개선)이미
map스코프 내에dateSlot이 있으므로dates[index]....대신dateSlot....을 바로 참조하면 가독성이 좋아지고, 비동기 이벤트 처리 중 인덱스-배열 드리프트 가능성도 줄일 수 있습니다.아래처럼 간소화할 수 있습니다.
- const end = dates[index].endTime; + const end = dateSlot.endTime; if (end && isInvalidTimeRange(value, end)) { toast.error('시작 시간은 종료 시간보다 빨라야 합니다.'); return; } onDateChange(index, 'startTime', value); }} onEndTimeChange={(index, value) => { - const start = dates[index].startTime; + const start = dateSlot.startTime; if (start && isInvalidTimeRange(start, value)) { toast.error('종료 시간은 시작 시간보다 늦어야 합니다.'); return; } onDateChange(index, 'endTime', value); }}Also applies to: 53-59
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
public/assets/img/default-bg.pngis excluded by!**/*.png
📒 Files selected for processing (35)
src/app/(with-header)/activities/[id]/components/ImageGrid.tsx(4 hunks)src/app/(with-header)/activities/[id]/components/ReviewSection.tsx(1 hunks)src/app/(with-header)/activities/[id]/components/ReviewTitle.tsx(1 hunks)src/app/(with-header)/activities/[id]/components/Title.tsx(2 hunks)src/app/(with-header)/activities/[id]/hooks/useDeleteActivity.ts(1 hunks)src/app/(with-header)/activities/[id]/mock/mock.ts(0 hunks)src/app/(with-header)/myactivity/[id]/components/EditActivityForm.tsx(1 hunks)src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts(2 hunks)src/app/(with-header)/myactivity/components/AddressInput.tsx(1 hunks)src/app/(with-header)/myactivity/components/CategoryInput.tsx(3 hunks)src/app/(with-header)/myactivity/components/FormSection.tsx(1 hunks)src/app/(with-header)/myactivity/components/ImagePreview.tsx(2 hunks)src/app/(with-header)/myactivity/components/ImageSection.tsx(1 hunks)src/app/(with-header)/myactivity/components/ImageUpload.tsx(1 hunks)src/app/(with-header)/myactivity/components/InfoSection.tsx(1 hunks)src/app/(with-header)/myactivity/components/MainImageSelect.tsx(1 hunks)src/app/(with-header)/myactivity/components/ScheduleSelect.tsx(1 hunks)src/app/(with-header)/myactivity/components/ScheduleSelectForm.tsx(1 hunks)src/app/(with-header)/myactivity/components/SubImageSelect.tsx(1 hunks)src/app/(with-header)/myactivity/hooks/useCreateActivityForm.ts(2 hunks)src/app/(with-header)/myactivity/utils/dateValidatoin.ts(1 hunks)src/components/DatePicker/CalendarBody.tsx(1 hunks)src/components/FloatingBox/BookingButton.tsx(1 hunks)src/components/FloatingBox/BookingInterface.tsx(2 hunks)src/components/FloatingBox/PriceDisplay.tsx(1 hunks)src/components/FloatingBox/TabletPopup.tsx(0 hunks)src/components/Modal/Example/TestModal.tsx(0 hunks)src/components/Modal/Example/TestModalButton.tsx(0 hunks)src/components/Modal/Example/UncontrolledModal.tsx(0 hunks)src/components/Modal/Example/readme(0 hunks)src/constants/AvatarConstants.ts(1 hunks)src/hooks/useBooking.ts(1 hunks)src/types/activityDetailType.ts(1 hunks)src/types/addEditExperienceType.ts(1 hunks)src/types/bookingInterfaceType.ts(1 hunks)
💤 Files with no reviewable changes (6)
- src/components/Modal/Example/UncontrolledModal.tsx
- src/components/Modal/Example/readme
- src/app/(with-header)/activities/[id]/mock/mock.ts
- src/components/Modal/Example/TestModal.tsx
- src/components/Modal/Example/TestModalButton.tsx
- src/components/FloatingBox/TabletPopup.tsx
🧰 Additional context used
🧬 Code Graph Analysis (10)
src/types/bookingInterfaceType.ts (2)
src/types/buttonTypes.ts (1)
ButtonProps(20-23)src/components/Button.tsx (1)
Button(18-49)
src/components/DatePicker/CalendarBody.tsx (3)
src/types/datePickerTypes.ts (1)
CalendarBodyProps(3-9)src/types/calendarType.ts (1)
CalendarBodyProps(10-16)src/components/DatePicker/DatePicker.tsx (1)
DatePicker(20-96)
src/constants/AvatarConstants.ts (1)
src/types/AvatarType.ts (1)
AvatarProps(5-32)
src/types/activityDetailType.ts (1)
src/types/dashboardTypes.ts (2)
MyActivity(4-16)ActivityReservation(54-70)
src/types/addEditExperienceType.ts (1)
src/types/activityDetailType.ts (1)
Schedule(69-74)
src/app/(with-header)/myactivity/hooks/useCreateActivityForm.ts (1)
src/app/(with-header)/myactivity/components/ReservationForm.tsx (1)
DateSlot(14-18)
src/app/(with-header)/activities/[id]/components/ImageGrid.tsx (2)
src/types/activityDetailType.ts (1)
ImageGridProps(1-4)src/constants/AvatarConstants.ts (1)
DEFAULT_BG(10-10)
src/components/FloatingBox/PriceDisplay.tsx (4)
src/components/FloatingBox/TotalPriceDisplay.tsx (1)
TotalPriceDisplay(3-12)src/app/(with-header)/mypage/reservations/components/ReservationCard.tsx (1)
ReservationCard(21-111)src/app/(with-header)/components/PopularCard.tsx (1)
PopularCard(11-40)src/app/(with-header)/components/ExperienceCard.tsx (1)
ExperienceCard(11-44)
src/app/(with-header)/myactivity/components/CategoryInput.tsx (1)
src/app/(with-header)/components/CategoryFilter.tsx (2)
CategoryFilterProps(7-11)CategoryFilter(13-34)
src/components/FloatingBox/BookingInterface.tsx (2)
src/hooks/useBooking.ts (1)
useBooking(11-74)src/types/bookingStoreTypes.ts (1)
BookingState(1-23)
🪛 Biome (2.1.2)
src/app/(with-header)/activities/[id]/components/ImageGrid.tsx
[error] 109-113: Static Elements should not be interactive.
To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value.
(lint/a11y/noStaticElementInteractions)
[error] 109-113: Enforce to have the onClick mouse event with the onKeyUp, the onKeyDown, or the onKeyPress keyboard event.
Actions triggered using mouse events should have corresponding keyboard events to account for keyboard-only navigation.
(lint/a11y/useKeyWithClickEvents)
[error] 123-127: Enforce to have the onClick mouse event with the onKeyUp, the onKeyDown, or the onKeyPress keyboard event.
Actions triggered using mouse events should have corresponding keyboard events to account for keyboard-only navigation.
(lint/a11y/useKeyWithClickEvents)
[error] 123-127: Static Elements should not be interactive.
To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value.
(lint/a11y/noStaticElementInteractions)
🔇 Additional comments (27)
src/app/(with-header)/activities/[id]/hooks/useDeleteActivity.ts (1)
25-27: router.push 호출 전에 toast.success 호출 권장
src/app/layout.tsx(43행)에 이미<Toaster richColors position='top-center' />가 전역 마운트되어 있어, 페이지 전환 시에도 Toaster가 언마운트되지 않습니다. 그럼에도 불구하고 토스트 메시지를 놓치지 않도록 호출 순서를 아래처럼 변경하는 것을 권장합니다.적용 예시:
- router.push(`/`); - toast.success('체험을 삭제했습니다!'); + toast.success('체험을 삭제했습니다!'); + router.push(`/`);
- 전역 마운트 위치: src/app/layout.tsx:43
<Toaster richColors position='top-center' />확인됨[optional_refactors_recommended]
src/constants/AvatarConstants.ts (2)
10-10: 기본 배경 이미지 상수 추가 좋습니다이미지 에러 시 일관된 대체 이미지를 위한 상수화 방향 적절합니다. 소비처(ImageGrid 등)에서 동일 경로를 사용하도록 유지해 주세요.
10-10: 확인 완료: public/assets/img/default-bg.png 파일이 존재합니다.
public 디렉터리에 해당 리소스가 있어 런타임 404 오류는 발생하지 않습니다. 추가 작업 불필요합니다.src/components/FloatingBox/PriceDisplay.tsx (1)
6-7: 통화 표기 일관성 정비 제안 (옵션)
프로젝트 전반에 걸쳐₩기호와toLocaleString사용 방식(공백 유무·로케일 지정)이 제각각이므로, 통화 포맷 유틸을 도입하여 일관되게 관리할 것을 권장합니다.적용 대상 예시 파일:
- src/components/FloatingBox/PriceDisplay.tsx
- src/components/FloatingBox/TotalPriceDisplay.tsx
- src/app/(with-header)/components/ExperienceCard.tsx
- src/app/(with-header)/components/PopularCard.tsx
- src/app/(with-header)/mypage/activities/components/ActivityCard.tsx
- src/app/(with-header)/mypage/reservations/components/ReservationCard.tsx
- src/app/(with-header)/mypage/reservations/components/ReviewModal.tsx
제안 유틸 예시 (
src/utils/formatCurrency.ts):export const formatCurrencyKR = (value: number) => `₩${value.toLocaleString('ko-KR')}`;사용 예시:
- ₩{price.toLocaleString()} / 인 + {formatCurrencyKR(price)} / 인— 이후
formatCurrencyKR함수를 전역 치환하여 모든 통화 표시를 통일할 수 있습니다.
[optional_refactors_recommended]src/types/activityDetailType.ts (2)
47-56: address 필드의 옵셔널리티 불일치 가능성ActivityDetail.address는 string | undefined(Line 55)인 반면, TitleProps.address(둘 중 유지할 버전) 는 string으로 강제됩니다. 주소가 비어있을 수 있는 흐름이 존재하면, 호출부 강제 변환('' 대입 등) 또는 TitleProps에서도 옵셔널을 허용하는 형태로 정렬이 필요합니다.
옵션 1(도메인 보장): 주소가 항상 존재한다면 ActivityDetail.address를 string으로 고정.
옵션 2(유연성): TitleProps.address를 string | undefined로 완화.Also applies to: 93-99
65-67: 인터페이스명 오타 수정: BookinDateProps → BookingDateProps
현재src/types/activityDetailType.ts에서 선언부 한 곳만 사용되고 있어 안전하게 일괄 변경 가능합니다.
아래와 같이 변경 후 커밋해주세요.-export interface BookinDateProps { +export interface BookingDateProps { schedules: ActivitySchedule[]; }src/hooks/useBooking.ts (1)
15-22: ✅ setToInitial 함수 정의 확인 완료
src/stores/Booking/useBookingStore.ts39줄에setToInitial: () => set({ ...initialState })로 정의되어 있어, destructuring 시 타입 에러나 런타임 에러가 발생하지 않습니다. 해당 리뷰 코멘트는 해결되었습니다.src/components/FloatingBox/BookingButton.tsx (1)
3-3: 공용 타입으로의 전환 좋습니다로컬 인터페이스 제거하고 공용 타입에서 BookingButtonProps를 가져오도록 통합한 점, 유지보수성 향상에 도움이 됩니다.
src/app/(with-header)/myactivity/components/ScheduleSelect.tsx (1)
5-5: 타입 중앙화 전환 적절합니다로컬 인터페이스 제거 후 '@/types/addEditExperienceType'의 ScheduleSelectProps로 일원화한 점, 타입 일관성과 재사용성을 높입니다.
src/app/(with-header)/myactivity/components/MainImageSelect.tsx (1)
5-5: 타입 중앙화 적용 잘 하셨습니다로컬 인터페이스 제거 후 공용 타입으로 일원화해 유지보수성이 좋아졌습니다.
src/app/(with-header)/myactivity/[id]/components/EditActivityForm.tsx (1)
9-9: 타입 분리 일관화 OKSubImageType을 공용 타입에서 가져오는 방향으로 통일된 점 좋습니다.
src/app/(with-header)/myactivity/components/ImageSection.tsx (1)
5-5: 타입 중앙화 적용 좋습니다컴포넌트 간 props 타입이 공용 모듈로 통일되어 재사용성과 일관성이 향상되었습니다.
src/app/(with-header)/activities/[id]/components/Title.tsx (1)
55-57: rating.toFixed(1) 호출 안전
TitleProps에서rating이number로 정의되어 있어 런타임 에러가 발생하지 않습니다. 별도의 방어 로직 추가는 불필요합니다.src/app/(with-header)/myactivity/components/SubImageSelect.tsx (1)
3-3: 타입 중앙화 OKprops 타입 의존성을 공용 모듈로 이동한 방향이 일관되어 좋습니다.
src/app/(with-header)/myactivity/components/FormSection.tsx (1)
2-2: 타입 중앙화 적용 적절로컬 인터페이스 제거 후 공용 타입 모듈로 이동 잘 반영되었습니다. 컴포넌트의 퍼블릭 시그니처가 유지되어 영향 범위도 안전합니다.
src/components/FloatingBox/BookingInterface.tsx (1)
12-12: useBooking 훅 도입 적절로직을 훅으로 분리함으로써 컴포넌트가 UI에 집중하도록 개선된 점 좋습니다. 라우트 의존성과 API 호출이 훅 내부로 캡슐화되어 테스트 용이성과 응집도가 좋아졌습니다.
src/app/(with-header)/activities/[id]/components/ReviewSection.tsx (1)
13-15: 타입 중앙화 일관성 좋음ReviewSectionProps/ReviewProps를 공용 타입 모듈로 분리하여 재사용성과 일관성이 개선되었습니다.
src/app/(with-header)/myactivity/components/ImagePreview.tsx (1)
4-6: 타입 중앙화 반영 적절ImagePreviewProps를 공용 타입으로 이전한 점 좋습니다. 컴포넌트 경량화와 재사용성에 도움이 됩니다.
src/app/(with-header)/myactivity/components/InfoSection.tsx (1)
7-7: 타입 분리 적용 LGTMInfoSectionProps를 공용 타입으로 이전해 타입 일관성이 향상되었습니다.
src/app/(with-header)/myactivity/components/CategoryInput.tsx (1)
46-48: 아이콘 배치/인터랙션 처리 적절
pointer-events-none과 중앙 정렬로 클릭 차단 및 시각적 배치가 적절합니다.src/app/(with-header)/myactivity/hooks/useCreateActivityForm.ts (1)
11-11: DateSlot 타입 중앙화 확인 완료
rg검색 결과,DateSlot은 오직src/types/addEditExperienceType.ts에서만 선언되어 있으며, 나머지 파일들(예:useCreateActivityForm.ts)에서는 모두 해당 타입을 정상적으로 import하여 사용하고 있습니다. 잔여 로컬 선언은 없습니다.src/app/(with-header)/myactivity/components/AddressInput.tsx (2)
8-10: 공용 타입으로의 분리, 적절합니다로컬 인터페이스를 제거하고 공용 타입 모듈에서 AddressInputProps/PostcodeData를 가져오도록 한 리팩터링이 일관성과 재사용성 측면에서 좋습니다.
17-34: DaumPostcode onComplete 매개변수 타입 일치 여부 확인 필요현재
handleComplete의 매개변수를 커스텀PostcodeData로 타이핑했습니다. 라이브러리의 실제onComplete콜백 파라미터와 속성명이 1:1로 일치하지 않으면 향후 업데이트 시 드리프트가 발생할 수 있습니다. 라이브러리 타입을 직접 추론해 쓰는 방식을 고려해 주세요.타입 안전성을 높이려면 아래처럼 컴포넌트 props로부터 실제 콜백 파라미터 타입을 추론하는 방안을 권장합니다(외부 타입 이름에 의존하지 않음).
- const handleComplete = (data: PostcodeData) => { + type PostcodeCompleteData = + Parameters<NonNullable<React.ComponentProps<typeof DaumPostcode>['onComplete']>>[0]; + const handleComplete = (data: PostcodeCompleteData) => { let fullAddress = data.address; let extraAddress = ''; ...이 변경이 부담스럽다면, 최소한 현재
PostcodeData정의가react-daum-postcode의 실제 콜백 데이터와 동일한지 검증 부탁드립니다.src/app/(with-header)/myactivity/components/ScheduleSelectForm.tsx (2)
5-14: 타입/검증 로직 외부화 리팩터링 👍
ScheduleSelectFormProps와 날짜/시간 검증 유틸을 외부로 분리해 의존성을 명확히 했습니다. 컴포넌트가 슬림해져 유지보수성이 좋아졌습니다.
6-7: 유틸 파일명 오탈자(dateValidatoin) 확인확인 결과 실제 파일명도
dateValidatoin.ts로 작성되어 있어 현재 동작에는 문제가 없습니다.
하지만 향후 혼선을 막기 위해 아래 리팩토링을 권장드립니다:
- 파일명 및 경로를
dateValidation.ts로 변경src/app/(with-header)/myactivity/components/ScheduleSelectForm.tsx등에서의 import 경로(../utils/dateValidatoin)를 일괄 업데이트[optional_refactors_recommended]
src/app/(with-header)/activities/[id]/components/ImageGrid.tsx (1)
66-74: Next/Image 최적화 메타데이터 보강 및 설정 확인 필요
fill사용 시sizes속성을 반드시 지정해야 레이아웃 계산 경고를 방지하고 최적의 성능을 낼 수 있습니다.unoptimized는 Next.js의 이미지 최적화를 비활성화하므로, 특별한 사유(허용되지 않은 원격 도메인 사용 등)가 없다면 제거를 권장합니다.- 현재
next.config.ts에images.domains또는images.remotePatterns설정이 없어 외부 이미지 호스트를 사용 중이라면, 도메인 화이트리스트를 추가하거나 계속해서unoptimized를 유지할지 결정해야 합니다.수정 예시:
<Image - src={image[currentIndex]} - alt={`${currentIndex + 1}`} + src={image[currentIndex]} + alt={`이미지 ${currentIndex + 1}`} fill className="rounded-lg object-cover" - priority - unoptimized + priority + sizes="100vw" onError={() => handleImageError(currentIndex)} />•
next.config.ts에 외부 이미지 도메인이 올바르게 설정되어 있는지 확인해 주세요.
• 도메인 설정이 불가능해unoptimized를 유지해야 한다면 그 사유를 알려주시면 좋겠습니다.src/types/addEditExperienceType.ts (1)
1-112: 타입 중앙화 리팩터링 👍산재해 있던 컴포넌트별 인터페이스를 한 곳으로 모아 재사용성과 일관성이 크게 개선되었습니다. 폼/이미지/스케줄 영역 간 계약을 추적하기 쉬워졌습니다.
| const [image, setImage] = useState([mainImage, ...subImages]); | ||
| const [currentIndex, setCurrentIndex] = useState(0); | ||
| const [direction, setDirection] = useState(0); | ||
| const [isOpen, setIsOpen] = useState(false); | ||
| const [selectedImage, setSelectedImage] = useState<string | null>(null); | ||
|
|
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.
🧹 Nitpick (assertive)
props 변경 시 이미지 상태 동기화 필요
초기 렌더에서만 [mainImage, ...subImages]로 state를 설정하므로, props가 갱신되어도 UI가 반영되지 않습니다. 상세 페이지에서 데이터가 비동기로 로드/변경될 수 있다면 동기화 로직을 추가하세요.
아래 수정으로 props 변화에 동기화할 수 있습니다.
-import React, { useState } from 'react';
+import React, { useEffect, useState } from 'react';
...
- const [image, setImage] = useState([mainImage, ...subImages]);
+ const [image, setImage] = useState([mainImage, ...subImages]);
const [currentIndex, setCurrentIndex] = useState(0);
const [direction, setDirection] = useState(0);
const [isOpen, setIsOpen] = useState(false);
const [selectedImage, setSelectedImage] = useState<string | null>(null);
+ useEffect(() => {
+ setImage([mainImage, ...subImages]);
+ }, [mainImage, subImages]);📝 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 [image, setImage] = useState([mainImage, ...subImages]); | |
| const [currentIndex, setCurrentIndex] = useState(0); | |
| const [direction, setDirection] = useState(0); | |
| const [isOpen, setIsOpen] = useState(false); | |
| const [selectedImage, setSelectedImage] = useState<string | null>(null); | |
| import React, { useEffect, useState } from 'react'; | |
| ... | |
| const [image, setImage] = useState([mainImage, ...subImages]); | |
| const [currentIndex, setCurrentIndex] = useState(0); | |
| const [direction, setDirection] = useState(0); | |
| const [isOpen, setIsOpen] = useState(false); | |
| const [selectedImage, setSelectedImage] = useState<string | null>(null); | |
| useEffect(() => { | |
| setImage([mainImage, ...subImages]); | |
| }, [mainImage, subImages]); | |
| ... |
🤖 Prompt for AI Agents
In src/app/(with-header)/activities/[id]/components/ImageGrid.tsx around lines
11 to 16, the image state is initialized only on first render with [mainImage,
...subImages] so updates to mainImage/subImages props are not reflected; add a
useEffect that watches mainImage and subImages and calls setImage([mainImage,
...subImages]) (optionally reset currentIndex and selectedImage as needed) to
synchronize state when props change.
| <div | ||
| onClick={() => handleImageClick(mainImage)} | ||
| className='relative col-span-2 row-span-4 hover:animate-pulse' | ||
| > | ||
| <Image | ||
| src={mainImage} | ||
| src={image[0]} | ||
| alt='메인이미지' | ||
| fill | ||
| className='rounded-lg object-cover' | ||
| onError={() => handleImageError(0)} | ||
| /> | ||
| </div> |
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.
a11y: 클릭 가능한 정적 요소(div) 교체 및 클릭 소스 일관성 수정
- 정적 요소에 onClick을 부여해 접근성 린트 에러가 발생합니다. 버튼 요소로 교체해 키보드 접근성을 확보하세요.
- 메인 타일 클릭 시
mainImage(원본)을 넘기고, 표시 소스는image[0](오류 시 대체)라 불일치합니다. 클릭에도image[0]을 사용해 일관성을 맞추세요. map콜백 파라미터명이 state 변수image와 동일해 가독성이 떨어집니다.src등으로 변경하세요.
아래 diff를 적용하면 접근성 경고를 해결하고, 클릭 동작의 일관성을 맞출 수 있습니다.
- <div
- onClick={() => handleImageClick(mainImage)}
- className='relative col-span-2 row-span-4 hover:animate-pulse'
- >
+ <button
+ type='button'
+ aria-label='메인 이미지 확대 보기'
+ onClick={() => handleImageClick(image[0])}
+ className='relative col-span-2 row-span-4 hover:animate-pulse focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2'
+ >
<Image
src={image[0]}
alt='메인이미지'
fill
className='rounded-lg object-cover'
+ sizes="(min-width: 768px) 50vw, 100vw"
onError={() => handleImageError(0)}
/>
- </div>
+ </button>
- {image.slice(1, 5).map((image, index) => (
- <div
- key={index + 1}
- onClick={() => handleImageClick(image)}
- className='relative col-span-1 row-span-2 h-full hover:animate-pulse'
- >
- <Image
- src={image}
- alt={`서브이미지 ${index + 1}`}
- fill
- className='rounded-lg object-cover'
- onError={() => handleImageError(index + 1)}
- />
- </div>
- ))}
+ {image.slice(1, 5).map((src, index) => (
+ <button
+ type='button'
+ key={index + 1}
+ aria-label={`서브 이미지 ${index + 1} 확대 보기`}
+ onClick={() => handleImageClick(src)}
+ className='relative col-span-1 row-span-2 h-full hover:animate-pulse focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2'
+ >
+ <Image
+ src={src}
+ alt={`서브이미지 ${index + 1}`}
+ fill
+ className='rounded-lg object-cover'
+ sizes="(min-width: 768px) 25vw, 100vw"
+ onError={() => handleImageError(index + 1)}
+ />
+ </button>
+ ))}Also applies to: 120-134
🧰 Tools
🪛 Biome (2.1.2)
[error] 109-113: Static Elements should not be interactive.
To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value.
(lint/a11y/noStaticElementInteractions)
[error] 109-113: Enforce to have the onClick mouse event with the onKeyUp, the onKeyDown, or the onKeyPress keyboard event.
Actions triggered using mouse events should have corresponding keyboard events to account for keyboard-only navigation.
(lint/a11y/useKeyWithClickEvents)
🤖 Prompt for AI Agents
In src/app/(with-header)/activities/[id]/components/ImageGrid.tsx around lines
108-119 (also apply same change to 120-134): replace the clickable static <div>
with a <button> to satisfy accessibility/keyboard interaction, update the click
handler to pass the displayed source (use image[0] / the mapped src) instead of
mainImage so the click payload matches the shown image, and rename the map
callback parameter from image to a neutral name like src to avoid shadowing the
state variable and improve readability; ensure the button keeps the same
classes/visuals and still calls handleImageError with the correct index on image
load error.
|
|
||
| import Star from '@assets/svg/star'; | ||
| import { useState, useEffect } from 'react'; | ||
| import { ReviewTitleProps } from '@/types/activityDetailType'; |
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.
🧹 Nitpick (assertive)
타입 전용 임포트 제안
타입만 사용하므로 import type으로 가져오면 불필요한 번들 영향이 줄어듭니다.
-import { ReviewTitleProps } from '@/types/activityDetailType';
+import type { ReviewTitleProps } from '@/types/activityDetailType';📝 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.
| import { ReviewTitleProps } from '@/types/activityDetailType'; | |
| import type { ReviewTitleProps } from '@/types/activityDetailType'; |
🤖 Prompt for AI Agents
In src/app/(with-header)/activities/[id]/components/ReviewTitle.tsx around line
5, the import of ReviewTitleProps is used only as a type and should be a
type-only import to avoid affecting the runtime bundle; change the import to use
the TypeScript "import type" form for ReviewTitleProps so it is erased at
compile time and does not impact bundle size.
| if (!id) return; | ||
| mutate(id as string); | ||
| setIsPopupOpen(false); | ||
| toast.success('체험이 삭제되었습니다!'); |
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.
삭제 성공 Toast가 성공/실패 구분 없이 노출됩니다 — mutate onSuccess/onError로 이동 권장
현재는 API 결과와 무관하게 성공 토스트가 뜹니다. 성공 시에만 success, 실패 시에는 error 토스트를 띄우도록 변경해 주세요.
아래와 같이 수정하면 안전합니다:
const handleDeleteConfirm = () => {
- if (!id) return;
- mutate(id as string);
- setIsPopupOpen(false);
- toast.success('체험이 삭제되었습니다!');
+ if (!id) return;
+ const idStr = Array.isArray(id) ? id[0] : (id as string);
+ mutate(idStr, {
+ onSuccess: () => {
+ setIsPopupOpen(false);
+ toast.success('체험이 삭제되었습니다!');
+ },
+ onError: () => {
+ 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.
| toast.success('체험이 삭제되었습니다!'); | |
| const handleDeleteConfirm = () => { | |
| if (!id) return; | |
| const idStr = Array.isArray(id) ? id[0] : (id as string); | |
| mutate(idStr, { | |
| onSuccess: () => { | |
| setIsPopupOpen(false); | |
| toast.success('체험이 삭제되었습니다!'); | |
| }, | |
| onError: () => { | |
| toast.error('삭제에 실패했습니다. 다시 시도해주세요.'); | |
| }, | |
| }); | |
| }; |
🤖 Prompt for AI Agents
In src/app/(with-header)/activities/[id]/components/Title.tsx around line 41,
the code currently shows toast.success('체험이 삭제되었습니다!') unconditionally; change
this so the toast is only shown in the mutation callbacks: remove the
unconditional toast call and instead pass onSuccess and onError handlers to the
delete mutation (or to mutateAsync's .then/.catch) that call toast.success(...)
on success and toast.error(...) on failure, and ensure any state updates or
navigation happen inside onSuccess so they run only after a confirmed successful
response.
src/app/(with-header)/activities/[id]/hooks/useDeleteActivity.ts
Outdated
Show resolved
Hide resolved
| export interface TitleProps { | ||
| title: string; | ||
| category: string; | ||
| rating: number; | ||
| reviewCount: number; | ||
| address: string; | ||
| isOwner: boolean; | ||
| } |
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.
중복된 TitleProps 선언으로 컴파일 오류 발생 (중요)
Line 14-21에도 TitleProps가, Line 92-99에도 TitleProps가 다시 선언되어 있습니다. 동일 모듈 내 중복 식별자는 TypeScript 컴파일 오류를 유발합니다. 또한 두 선언의 필드가 불일치합니다(isDropDown vs isOwner). 현재 사용처 리팩터링 방향(공용 타입화)과 요약을 보면 isOwner가 포함된 새 TitleProps(Line 92-99)를 유지하고, 이전 선언(Line 14-21)을 제거하는 게 정합적입니다.
적용 제안(diff):
- export interface TitleProps {
- title: string;
- category: string;
- rating: number;
- reviewCount: number;
- address: string;
- isDropDown?: boolean;
- }만약 드롭다운 UI 여부 플래그가 필요하다면, 별도 UI 전용 타입(예: TitleUIProps)으로 분리하거나, 아래처럼 확장 타입을 도입해 충돌을 피하세요.
// 예시: UI 확장 타입
export interface TitleUIProps extends TitleProps {
isDropDown?: boolean;
}🤖 Prompt for AI Agents
In src/types/activityDetailType.ts around lines 14-21 and 92-99: there are
duplicate TitleProps declarations causing a TypeScript compile error; remove the
older TitleProps declaration at lines 14-21 and keep the newer declaration at
lines 92-99 (which includes isOwner), and if a dropdown UI flag is required
create a separate UI type (e.g., TitleUIProps extends TitleProps with optional
isDropDown) and update any usages/imports to reference the retained type names
accordingly.
src/types/activityDetailType.ts
Outdated
| activityId: string; | ||
| reviewCount: number; | ||
| rating: number; | ||
| } |
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
activityId의 타입 일관성 확인 필요(문자열 vs 숫자)
ReviewSectionProps의 activityId가 string입니다. 반면 ActivityDetail.id(Line 48)는 number입니다. 라우트 파라미터는 string이지만, API/도메인 레벨에서는 number인 경우가 많습니다. 사용처 전반(요청 파라미터, 쿼리 키 등)에서 일관된 타입을 유지하도록 검토가 필요합니다.
일관성 점검 스크립트:
🏁 Script executed:
#!/bin/bash
# ReviewSectionProps 사용처에서 activityId 타입 추적
rg -nP 'ReviewSectionProps' -C3
# activityId 직/간접 사용 시 숫자 변환 여부 확인
rg -nP '\bactivityId\b' -C3Length of output: 36607
ReviewSectionProps.activityId를 숫자형으로 통일하세요
현재 ReviewSectionProps.activityId는 string으로 선언되어 있지만, 도메인·API 레이어와 대부분의 훅에서 number를 사용하고 있습니다. 라우트 파라미터는 문자열이지만, 이를 상위 컴포넌트에서 숫자로 변환해 전달하도록 리팩터링이 필요합니다.
수정할 위치:
- src/types/activityDetailType.ts
- ReviewSectionProps.activityId:
string→number
- ReviewSectionProps.activityId:
- src/app/(with-header)/activities/[id]/components/ActivityDetailForm.tsx
- <ReviewSection
- activityId={id as string}
- <ReviewSection
- activityId={Number(id)} // or parseInt(id, 10)
reviewCount={activityData.reviewCount}
rating={activityData.rating}
/>
- 필요 시 `ActivityDetailForm` 호출부 및 관련 타입 정의 전반 검토
<!-- suggestion_start -->
<details>
<summary>📝 Committable suggestion</summary>
> ‼️ **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.
```suggestion
<ReviewSection
activityId={Number(id)} // or parseInt(id, 10)
reviewCount={activityData.reviewCount}
rating={activityData.rating}
/>
🤖 Prompt for AI Agents
In src/types/activityDetailType.ts around lines 108 to 111,
ReviewSectionProps.activityId is declared as string but the domain/API and most
hooks expect a number; change the type to number and then update
src/app/(with-header)/activities/[id]/components/ActivityDetailForm.tsx to
accept a numeric activityId, ensuring any place that calls ActivityDetailForm
(including route handlers or parent components) converts the route param from
string to number (e.g., parseInt or Number) before passing it; review and adjust
related type definitions and call sites to align with the numeric activityId
type.
| export interface FormSectionProps { | ||
| title: string; | ||
| children: React.ReactNode; | ||
| description?: string; | ||
| } |
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.
🧹 Nitpick (assertive)
React.ReactNode 직접 참조 대신 타입 임포트 사용 권장
types 전용 파일에서 React.ReactNode를 사용하면 전역 타입 설정에 의존합니다. ReactNode를 타입 임포트하여 사용하는 편이 안전합니다.
아래처럼 변경해 주세요.
-import { Schedule } from '@/types/activityDetailType';
+import { Schedule } from '@/types/activityDetailType';
+import type { ReactNode } from 'react';
...
export interface FormSectionProps {
title: string;
- children: React.ReactNode;
+ children: ReactNode;
description?: string;
}
...
export interface ImageUploadProps {
onImageSelect: (file: File) => void;
multiple?: boolean;
className?: string;
- children?: React.ReactNode;
+ children?: ReactNode;
}Also applies to: 49-54
🤖 Prompt for AI Agents
In src/types/addEditExperienceType.ts around lines 27-31 (and also apply the
same change for lines 49-54), the interfaces reference React.ReactNode directly;
update the file to import the type using a type-only import (e.g., import type {
ReactNode } from 'react') at the top and replace React.ReactNode with ReactNode
in the interface properties (and any other occurrences), ensuring you use a
type-only import to avoid runtime React dependency.
| export interface DateSlot { | ||
| date: string; | ||
| startTime: string; | ||
| endTime: string; | ||
| } |
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.
🧹 Nitpick (assertive)
DateSlot은 Schedule 기반의 별칭으로 정의해 중복 제거
DateSlot이 Schedule의 id만 제외한 구조라면 타입 중복을 없애 유지보수성을 높일 수 있습니다.
-export interface DateSlot {
- date: string;
- startTime: string;
- endTime: string;
-}
+export type DateSlot = Omit<Schedule, 'id'>;📝 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 interface DateSlot { | |
| date: string; | |
| startTime: string; | |
| endTime: string; | |
| } | |
| export type DateSlot = Omit<Schedule, 'id'>; |
🤖 Prompt for AI Agents
In src/types/addEditExperienceType.ts around lines 107 to 111, DateSlot
duplicates Schedule except for the id field; replace the explicit DateSlot
interface with a type alias using Omit on Schedule (e.g., type DateSlot =
Omit<Schedule, 'id'>) so the duplicate fields are removed and future changes to
Schedule propagate automatically; ensure imports/exports remain correct and run
type checks.
| export interface BookingButtonProps { | ||
| onClick: () => void; | ||
| children: React.ReactNode; | ||
| disabled?: boolean; | ||
| onBooking?: boolean; | ||
| className?: string; | ||
| } |
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.
🧹 Nitpick (assertive)
불리언 프로퍼티 네이밍 개선: onBooking → isBooking 권장
상태 의미가 명확하도록 isBooking 같은 접두사를 권장합니다. 컴포넌트/훅 반환값과 함께 일괄 정리하면 가독성이 좋아집니다.
예상 변경:
- onBooking?: boolean;
+ isBooking?: boolean;연쇄 영향: BookingButton.tsx, useBooking 훅 반환값 등 동기화 필요.
📝 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 interface BookingButtonProps { | |
| onClick: () => void; | |
| children: React.ReactNode; | |
| disabled?: boolean; | |
| onBooking?: boolean; | |
| className?: string; | |
| } | |
| export interface BookingButtonProps { | |
| onClick: () => void; | |
| children: React.ReactNode; | |
| disabled?: boolean; | |
| isBooking?: boolean; | |
| className?: string; | |
| } |
🤖 Prompt for AI Agents
In src/types/bookingInterfaceType.ts lines 1-7, rename the boolean prop from
onBooking to isBooking to follow boolean naming conventions; update the
interface property name and its optional flag accordingly, then propagate this
change across all usages (e.g., BookingButton.tsx, any consuming components, and
the useBooking hook return type) by renaming parameters, props, and hook return
fields, adjusting imports/types where needed, and running type checks to fix
resulting TypeScript errors.
React.ReactNode 타입 사용 시 import 누락 가능성 — ReactNode로 전환 권장
.ts 파일에서 React.ReactNode를 직접 참조하면 React 네임스페이스가 스코프에 없어서 TS 에러가 날 수 있습니다. import type ReactNode로 치환하는 것을 권장합니다.
적용 제안(diff):
+ import type { ReactNode } from 'react';
export interface BookingButtonProps {
onClick: () => void;
- children: React.ReactNode;
+ children: ReactNode;
disabled?: boolean;
onBooking?: boolean;
className?: string;
}📝 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 interface BookingButtonProps { | |
| onClick: () => void; | |
| children: React.ReactNode; | |
| disabled?: boolean; | |
| onBooking?: boolean; | |
| className?: string; | |
| } | |
| import type { ReactNode } from 'react'; | |
| export interface BookingButtonProps { | |
| onClick: () => void; | |
| children: ReactNode; | |
| disabled?: boolean; | |
| onBooking?: boolean; | |
| className?: string; | |
| } |
🤖 Prompt for AI Agents
In src/types/bookingInterfaceType.ts around lines 1 to 7, the interface uses
React.ReactNode which can cause TS errors if the React namespace isn't in scope;
replace the namespace usage by importing the type and using ReactNode: add
"import type { ReactNode } from 'react'" at the top of the file and change the
children property to "children: ReactNode", leaving other props unchanged.
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: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (15)
src/app/(with-header)/activities/[id]/hooks/useDeleteActivity.ts (1)
16-27: 쿼리 무효화 범위 명시와 네비게이션 UX부분 일치 매칭임을 의도적으로 드러내면 가독성이 좋아집니다. 삭제 후 뒤로가기로 상세로 되돌아오기 어렵게 하려면 replace도 고려해 볼 수 있습니다.
return useMutation({ mutationFn: deleteActivity, onSuccess: (_data) => { - queryClient.invalidateQueries({ queryKey: ['activity'] }); + queryClient.invalidateQueries({ queryKey: ['activity'], exact: false }); queryClient.invalidateQueries({ queryKey: ['experiences'], exact: false, }); - queryClient.invalidateQueries({ queryKey: ['popularExperiences'] }); - router.push(`/`); + queryClient.invalidateQueries({ queryKey: ['popularExperiences'], exact: false }); + router.push(`/`); + // 또는 필요 시 + // router.replace(`/`); toast.success('체험이 삭제되었습니다!'); },src/app/(with-header)/activities/[id]/components/ActivityDetailForm.tsx (6)
39-47: CI 경고 해결: console.log 제거 또는 허용된 레벨로 변경파이프라인 경고(no-console)를 해소해 주세요. 404 처리 자체는 notFound로 충분합니다.
- if (httpStatus === 404) { - console.log('404 에러임'); - notFound(); - } + if (httpStatus === 404) { + notFound(); + // 또는 디버그가 필요하면: + // console.error('Activity 404'); + }
49-54: 불리언 연산 보장현 표현식은 null을 반환할 수 있습니다. 명시적 불리언으로 교체해 타입 일관성을 보장하세요.
- const isOwner = currentUserId && userId && currentUserId === userId; + const isOwner = + currentUserId != null && userId != null && currentUserId === userId;
83-89: 불필요한 setTimeout 제거state 업데이트에 setTimeout(0)은 필요 없습니다. 동기 setState로 단순화하세요.
- const handleMonthChange = useCallback((year: number, month: number) => { - setTimeout(() => { - setYear(year); - setMonth(month); - }); - }, []); + const handleMonthChange = useCallback((y: number, m: number) => { + setYear(y); + setMonth(m); + }, []);
23-24: activityId 숫자형 전달로 일관성 확보
ReviewSection에Number(id)전달로 타입 일관성 맞춘 점 좋습니다. 가능하다면 상단에서id를 한 번 정규화해 전체 파일에서 재사용하면 가독성이 더 좋아집니다.- const { id } = useParams(); + const { id } = useParams(); + const idParam = Array.isArray(id) ? id[0] : id; + const numericId = Number(idParam); ... - queryKey: ['activity', id], + queryKey: ['activity', idParam], ... - activityId={Number(id)} + activityId={numericId}Also applies to: 31-37, 141-144
63-79: allSettled 처리 가독성 개선(선택)타입 단언 없이 fulfilled 결과만 안전히 매핑하면 가독성이 좋아집니다.
- const results = await Promise.allSettled([ + const results = await Promise.allSettled([ privateInstance.get( `/activities/${id}/available-schedule?year=${prevYear}&month=${padMonth(prevMonth)}`, ), privateInstance.get( `/activities/${id}/available-schedule?year=${year}&month=${padMonth(month)}`, ), privateInstance.get( `/activities/${id}/available-schedule?year=${nextYear}&month=${padMonth(nextMonth)}`, ), ]); - // 성공한 것만 합치기 - const data = results - .filter((r) => r.status === 'fulfilled') - .flatMap((r) => (r.status === 'fulfilled' ? r.value.data : [])); + // 성공한 것만 합치기 + const data = results.flatMap((r) => + r.status === 'fulfilled' ? r.value.data : [], + ); return data;
90-92: ActivityDetailSkeleton의userId필수 프로퍼티 타입 불일치
ActivityDetailSkeleton컴포넌트는 다음과 같이userId를number로 명시적으로 요구하고 있습니다.export default function ActivityDetailSkeleton({ userId }: { userId: number }) { … }하지만
ActivityDetailForm.tsx에서 로딩 중이거나activityData가 없는 경우에if (isLoading || !activityData) { return <ActivityDetailSkeleton userId={activityData?.userId} />; }로 작성되어 있어,
activityData가undefined일 때activityData?.userId가undefined로 전달되어 타입 오류 및 런타임 예외가 발생할 수 있습니다.조치가 필요합니다:
- ActivityDetailSkeleton의 프로퍼티 타입을
처럼 선택적(optional)로 변경하거나{ userId?: number }- 해당 분기에서 항상
number가 보장되도록 기본값을 제공하세요.
예시:return <ActivityDetailSkeleton userId={activityData?.userId ?? 0} />;위 두 가지 중 하나를 선택해 반영해 주시면 됩니다.
src/types/activityDetailType.ts (3)
65-67: 네이밍 오타: BookinDateProps → BookingDateProps타입 네이밍 오타를 수정해 검색성·가독성을 높이세요.
-export interface BookinDateProps { +export interface BookingDateProps { schedules: ActivitySchedule[]; }
47-63: address 타입 일관성 검토
ActivityDetail.address는string | undefined인데,TitleProps.address는string입니다. 런타임에서 undefined가 전달될 수 있으면 타입 불일치가 납니다. 한쪽을 맞추거나 호출부에서 기본값을 보장하세요.가능한 정정 중 하나:
-export interface TitleProps { +export interface TitleProps { title: string; category: string; rating: number; reviewCount: number; - address: string; + address?: string; isOwner: boolean; }Also applies to: 14-21, 90-97
6-12:⚠️ 프로퍼티 네이밍 수정 필요:isBlured→isBlurred다음 위치에서 철자 오류를 모두 교정해 주세요:
src/types/activityDetailType.ts (11행)
기존:isBlured?: boolean;
수정:isBlurred?: boolean;src/app/(with-header)/activities/[id]/components/ReviewCard.tsx
• 9행 구조 분해 할당 기본값
기존:isBlured = false,
수정:isBlurred = false,
• 22행 JSX 조건부 렌더링
기존:isBlured ? 'text-gray-300 select-none' : 'text-black'
수정:isBlurred ? 'text-gray-300 select-none' : 'text-black'추가로, 프로젝트 전체에 걸쳐 여전히
isBlured가 남아있는지rg -nP '\bisBlured\b' -n등으로 한 번 더 점검해 보시면 좋겠습니다.src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts (5)
32-42: React Query error 타입을 AxiosError로 지정하고 불필요한 캐스팅 제거현재 useQuery의 Error 제네릭이 Error로 고정되어, 이후 AxiosError로 캐스팅하고 있습니다. 제네릭을 AxiosError로 지정하면 캐스팅과 불필요한 변수 제거가 가능합니다. 또한 404 처리에서 console.log는 제거하세요(아래 diff에 포함).
- const { data, isLoading, status, isError, error } = useQuery< - ActivityDetailEdit, - Error - >({ + const { data, isLoading, status, isError, error } = useQuery< + ActivityDetailEdit, + AxiosError + >({ queryKey: ['edit-activity', id],- if (status === 'error') { - const axiosError = error as AxiosError; - const httpStatus = axiosError.response?.status; - - if (httpStatus === 404) { - console.log('404 에러임'); - notFound(); - } - } + if (status === 'error') { + const httpStatus = error?.response?.status; + if (httpStatus === 404) { + notFound(); + } + }Also applies to: 44-51
80-85: 서브 이미지 개수 초과 시 slice 음수 인덱스 부작용으로 이미지가 추가되는 버그remainingSlots가 음수가 되면 slice(0, 음수)는 "끝에서 N개 제외"로 동작해 원치 않는 이미지가 추가될 수 있습니다. 0으로 클램프하고, 초과 시 빠르게 리턴하세요. 사용자에게 안내 토스트도 권장합니다.
const handleSubImagesAdd = (newFiles: File[]) => { - const remainingSlots = 4 - subImages.length; - const filesToAdd = newFiles.slice(0, remainingSlots); + const remainingSlots = Math.max(0, 4 - subImages.length); + if (remainingSlots === 0) { + toast.error('서브 이미지는 최대 4장까지 업로드할 수 있습니다.'); + return; + } + const filesToAdd = newFiles.slice(0, remainingSlots); const newSubImages = filesToAdd.map((file) => ({ url: file })); setSubImages((prev) => [...prev, ...newSubImages]); };추가로 매직 넘버 4는 상수로 추출해 재사용하세요(예: const MAX_SUB_IMAGES = 4).
127-137: 서브 이미지 업로드 직렬 처리 → 병렬 처리로 성능 개선현재 for-of로 순차 업로드되어 지연이 누적됩니다. Promise.all로 병렬화하면 UX가 개선됩니다.
- const subImageUrlsToAdd: string[] = []; - for (const img of subImages) { - if (!img.id) { - if (img.url instanceof File) { - const uploadedUrl = await uploadImage(img.url); - subImageUrlsToAdd.push(uploadedUrl); - } else if (typeof img.url === 'string') { - subImageUrlsToAdd.push(img.url); - } - } - } + const subImageUrlsToAdd = ( + await Promise.all( + subImages + .filter((img) => !img.id) + .map(async (img) => { + if (img.url instanceof File) return await uploadImage(img.url); + if (typeof img.url === 'string') return img.url; + return null; + }), + ) + ).filter((u): u is string => Boolean(u));
139-162: 스케줄 변경을 “삭제 후 신규 추가”로 처리하는 전략의 원자성/무결성 검토 필요동일 ID의 변경을 remove+add로 처리하면, 네트워크/서버 오류 시 일부만 반영되어 일정 손실이 발생할 수 있습니다. 서버가 원자적 처리(트랜잭션) 또는 일괄 엔드포인트를 제공하는지 확인하고, 가능하다면 schedulesToUpdate( id, date, startTime, endTime ) 형식으로 업데이트를 분리하는 설계를 고려하세요. 최소한 scheduleIdsToRemove 중복 방지(Set 사용)와 newSchedules 중복 방지 처리가 안전합니다.
48-50: 디버그 로그 제거 권장404 처리 시 콘솔 로그는 불필요합니다. 위의 useQuery 에러 타입 개선 diff에 함께 제거했습니다.
♻️ Duplicate comments (3)
src/types/activityDetailType.ts (1)
14-21: 중복된 TitleProps 선언으로 컴파일 오류—이전 선언 제거 필요동일 모듈 내
TitleProps가 두 번 선언되어 충돌합니다. 아래처럼 상단 선언을 제거하고 하단(isOwner 포함)만 유지하세요.export interface TitleProps { title: string; category: string; rating: number; reviewCount: number; address: string; - isDropDown?: boolean; -} + // isOwner 필드는 하단 선언을 유지합니다. +} @@ -export interface TitleProps { - title: string; - category: string; - rating: number; - reviewCount: number; - address: string; - isOwner: boolean; -} +export interface TitleProps { + title: string; + category: string; + rating: number; + reviewCount: number; + address: string; + isOwner: boolean; +}Also applies to: 90-97
src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts (2)
21-21: price 상태 타입 불일치로 인한 타입 오류 및 검증 로직 혼선 (string ↔ number 불일치, 토스트 메시지 불일치 포함)
- Hook 내부 price는 string으로 관리하지만, 사용처(EditActivityForm)에서는 setPrice(Number(price))로 number를 전달하고 있습니다. 이는 TS 타입 오류를 유발합니다.
- 제출 검증에서 '!price'는 string 전제 로직이며, 가격이 0일 때만 false가 됩니다. number로 일원화하고 'price <= 0'로 명확히 검증하세요.
- 지난 코멘트와 동일하게 검증 메시지가 "소개이미지를 제외"라고 되어 있으나 실제로 mainImage도 필수로 체크하고 있어 UX 불일치가 존재합니다. 메시지를 통일하고 스케줄 필드 공란 검증도 포함하세요.
- const [price, setPrice] = useState(''); + const [price, setPrice] = useState<number>(0);- setPrice(data.price.toString()); + setPrice(data.price);- const parsedPrice = parseInt(price, 10); - if (isNaN(parsedPrice) || parsedPrice <= 0) { + if (!Number.isFinite(price) || price <= 0) { throw new Error('유효한 가격을 입력해주세요.'); } const payload = { title, category, description, address, - price: parsedPrice, + price, bannerImageUrl, subImageIdsToRemove, subImageUrlsToAdd, schedulesToAdd: newSchedules, scheduleIdsToRemove, };- if ( - !title || - !category || - !description || - !address || - !price || - !mainImage || - dates.length === 0 - ) { - toast.error('소개이미지를 제외한 모든값은 필수값입니다!'); + if ( + !title || + !category || + !description || + !address || + price <= 0 || + !mainImage || + dates.length === 0 || + dates.some((d) => !d.date || !d.startTime || !d.endTime) + ) { + toast.error('모든 값은 필수값입니다!'); return; }추가 권장: 상수/유효성 로직을 생성/수정 훅에서 공유하는 유틸(예: validateActivityForm)로 분리해 중복을 제거하세요.
Also applies to: 57-58, 164-175, 205-216
12-12: 타입 전용 임포트로 교체하여 불필요한 런타임 의존성 제거SubImageType은 타입 전용이므로 type-only import를 사용하세요. 지난 코멘트와 동일 제안입니다.
- import { SubImageType } from '@/types/addEditExperienceType'; + import type { SubImageType } from '@/types/addEditExperienceType';
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (6)
next.config.ts(1 hunks)src/app/(with-header)/activities/[id]/components/ActivityDetailForm.tsx(1 hunks)src/app/(with-header)/activities/[id]/components/Title.tsx(2 hunks)src/app/(with-header)/activities/[id]/hooks/useDeleteActivity.ts(2 hunks)src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts(2 hunks)src/types/activityDetailType.ts(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
src/app/(with-header)/activities/[id]/components/ActivityDetailForm.tsx (1)
src/app/(with-header)/activities/[id]/components/ReviewSection.tsx (3)
ReviewSectionProps(12-16)ReviewSection(28-106)ReviewProps(18-26)
src/app/(with-header)/activities/[id]/hooks/useDeleteActivity.ts (1)
src/app/api/deleteActivity/[id]/route.ts (1)
DELETE(12-56)
src/types/activityDetailType.ts (3)
src/app/(with-header)/activities/[id]/components/ReviewSection.tsx (2)
ReviewProps(18-26)ReviewSectionProps(12-16)src/app/(with-header)/activities/[id]/components/ReviewTitle.tsx (2)
ReviewTitleProps(6-9)ReviewTitle(10-45)src/types/dashboardTypes.ts (1)
MyActivity(4-16)
src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts (1)
src/app/(with-header)/myactivity/[id]/components/EditActivityForm.tsx (1)
EditActivityForm(14-93)
🪛 GitHub Actions: CI
src/app/(with-header)/activities/[id]/components/ActivityDetailForm.tsx
[warning] 44-44: Unexpected console statement. Only these console methods are allowed: warn, error. (no-console)
src/app/(with-header)/activities/[id]/components/Title.tsx
[error] 14-14: 'toast' is defined but never used. (unused-imports/no-unused-imports)
🔇 Additional comments (7)
src/app/(with-header)/activities/[id]/hooks/useDeleteActivity.ts (2)
26-26: 성공 토스트 문구 정합성 확보됨과거 리뷰의 문구 불일치 이슈가 해결되었고, 성공 시에만 토스트가 노출되도록 onSuccess에 위치해 있는 점 좋습니다.
28-41: 에러 처리 적절 — 사용자 메시지 폴백과 콘솔 로깅 OKAxios 에러 분기와 사용자 메시지 폴백 처리가 적절합니다. 현재 형태 유지에 동의합니다.
src/app/(with-header)/activities/[id]/components/ActivityDetailForm.tsx (1)
141-144: 타입 일치 OK
activityId를 숫자로 전달하도록 수정된 부분이 타입 모듈과 잘 맞습니다.src/app/(with-header)/activities/[id]/components/Title.tsx (2)
17-24: 타입 외부화 및 prop 정규화 적용 LGTM공용 타입으로 전환된
TitleProps사용, 소유자 제어 분기에 이상 없습니다.
14-15: 미사용 import 제거로 CI 오류 해결
toast가 사용되지 않아unused-imports/no-unused-imports오류가 발생합니다. 삭제해 주세요.-import { toast } from 'sonner'; -import { TitleProps } from '@/types/activityDetailType'; +import { TitleProps } from '@/types/activityDetailType';Likely an incorrect or invalid review comment.
src/types/activityDetailType.ts (1)
104-108: activityId 숫자형 통일 확인
ReviewSectionProps.activityId가 number로 정의되어, 상위에서Number(id)로 전달하는 변경과 일치합니다. 타입 정합성 OK.src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts (1)
182-183: RESTful 엔드포인트 경로 일관성 확인 필요
- 조회(GET) 호출
• 파일: src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts
• 코드:await privateInstance.get(/activities/${id});(라인 38)- 수정(PATCH) 호출
• 파일: src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts
• 코드:return await privateInstance.patch(/editActivity/${id}, payload);(라인 182)- Next.js API 라우트에서는
/api/editActivity/[id]만 PATCH로 구현되어 있고,/api/activities/[id]용 GET 핸들러는 정의되어 있지 않습니다.서버 스펙에 따라
editActivity경로가 의도된 것인지 확인해 주세요. 일반적인 REST API 설계라면 PATCH도/activities/${id}로 맞추는 것이 더 자연스럽습니다.
| compiler: { | ||
| removeConsole: process.env.NODE_ENV === 'production', | ||
| }, |
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.
🧹 Nitpick (assertive)
프로덕션 관측성 유지: console 전부 제거 대신 error/warn는 보존 권장
compiler.removeConsole: true는 클라이언트뿐 아니라 서버 번들에서도 모든 console.* 호출을 제거합니다. 운영 장애 파악에 중요한 console.error/console.warn까지 사라질 수 있어 관측성이 급격히 저하됩니다. 아래처럼 예외 목록을 두는 편이 안전합니다.
compiler: {
- removeConsole: process.env.NODE_ENV === 'production',
+ removeConsole:
+ process.env.NODE_ENV === 'production'
+ ? { exclude: ['error', 'warn'] }
+ : false,
},🤖 Prompt for AI Agents
In next.config.ts around lines 21 to 23, the current boolean
compiler.removeConsole removes all console.* (including
console.error/console.warn); change this so production builds do not strip
error/warn. Specifically, stop using a plain boolean removeConsole in production
and instead preserve error/warn by either (A) disabling removeConsole and adding
the babel plugin transform-remove-console configured with exclude:
['error','warn'] for production, or (B) keep removeConsole false and integrate a
build-time transform (e.g., babel plugin or a custom minifier config) that only
removes verbose logs (console.log/debug/info) while leaving console.error and
console.warn intact; implement the chosen approach and ensure it only applies to
production builds.
💡 Verification agent
🧩 Analysis chain
Preview 배포 로그까지 제거되는 부작용 가능성(Vercel 사용 시) — VERCEL_ENV 기준 전환 검토
Vercel의 Preview 환경에서도 NODE_ENV는 production이므로 현재 설정으로는 QA 단계 로그까지 제거됩니다. Vercel을 사용한다면 VERCEL_ENV === 'production'일 때만 제거하도록 전환을 고려하세요. (동시에 error/warn 보존도 적용한 예)
compiler: {
- removeConsole: process.env.NODE_ENV === 'production',
+ removeConsole:
+ process.env.VERCEL_ENV === 'production'
+ ? { exclude: ['error', 'warn'] }
+ : false,
},다음 스크립트로 (1) 코드베이스에서 console.error/console.warn 사용처를 파악하고, (2) Vercel 사용 여부를 대략 확인할 수 있습니다. 결과에 따라 위 옵션 적용을 결정해 주세요.
🏁 Script executed:
#!/bin/bash
# 1) error/warn 사용 현황
rg -nP --type=ts --type=tsx -C2 '\bconsole\.(error|warn)\s*\('
# 2) Vercel 관련 설정/언급 탐색
fd -a -HI 'vercel*' 2>/dev/null || true
rg -nPI 'vercel|VERCEL_ENV|@vercel' -g '!node_modules/**' README* package.json .github 2>/dev/null || trueLength of output: 347
콘솔 에러/경고 호출(console.error/console.warn) 사용 현황과 VERCEL_ENV 참조 여부를 정확히 파악하기 위해 아래 스크립트를 실행해 주세요.
🏁 Script executed:
#!/bin/bash
# 1) console.error/console.warn 사용 현황 (TS/TSX 파일 대상)
rg -nP -C2 '\bconsole\.(error|warn)\s*\(' --glob '*.ts' --glob '*.tsx' -g '!node_modules/**' || echo "(none found)"
# 2) VERCEL_ENV 참조 여부
rg -nP -C2 'VERCEL_ENV' -g '!node_modules/**' || echo "(none found)"Length of output: 5010
콘솔 에러/경고 로그 보존 조건 전환 검토 필요
검증 결과:
console.error/console.warn사용처가 다수(src/components/…,src/app/api/…등 10여 개 파일 이상)에 존재하므로, Preview 배포에서 중요한 에러·경고 로그가 사라질 수 있습니다.- 코드베이스 내에
VERCEL_ENV레퍼런스는 전혀 없습니다.
위 사항을 바탕으로, Vercel Preview 환경에서만 로그를 보존하도록 next.config.ts의 removeConsole 조건을 다음과 같이 전환 적용할 것을 권장드립니다.
compiler: {
- removeConsole: process.env.NODE_ENV === 'production',
+ removeConsole:
+ process.env.VERCEL_ENV === 'production'
+ ? { exclude: ['error', 'warn'] }
+ : false,
},- Vercel 환경변수인
VERCEL_ENV는 배포 시 자동으로production,preview,development중 하나로 설정됩니다. - Preview(검증) 단계에서는
VERCEL_ENV === 'preview'이므로removeConsole: false가 되어 에러/경고 로그가 보존됩니다. - 만약 로컬 개발 환경에서도 동일하게 로그를 유지하려면,
.env.local등에VERCEL_ENV=development를 설정해 주세요. - 위 설정 변경 전후로 빌드·배포 로그를 확인하여 기대 동작이 맞는지 검증해 주시기 바랍니다.
📝 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.
| compiler: { | |
| removeConsole: process.env.NODE_ENV === 'production', | |
| }, | |
| compiler: { | |
| removeConsole: | |
| process.env.VERCEL_ENV === 'production' | |
| ? { exclude: ['error', 'warn'] } | |
| : false, | |
| }, |
🤖 Prompt for AI Agents
next.config.ts around lines 21-23: 현재 removeConsole이 NODE_ENV==='production'로 되어
있어 Vercel Preview에서 console.error/warn가 제거될 수 있으니 조건을 VERCEL_ENV를 우선 사용하도록
변경하세요; 구체적으로 removeConsole이 VERCEL_ENV==='production'일 때만 true가 되도록 하고(단,
VERCEL_ENV가 없을 수 있으니 이전 동작을 보장하려면 fallback으로 NODE_ENV==='production'을 사용) 설정을
변경한 뒤 빌드·배포에서 Preview와 Production 환경에서 로그 보존이 기대대로 동작하는지 검증하세요.
| const handleDeleteConfirm = () => { | ||
| if (!id) return; | ||
| mutate(id as string); | ||
| mutate(Number(id)); | ||
| setIsPopupOpen(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.
🛠️ Refactor suggestion
삭제 팝업은 성공 시에만 닫기
현재는 API 결과와 무관하게 팝업이 닫힙니다. 호출 단에서 mutate 옵션을 사용해 성공 시에만 닫도록 변경하면 UX와 상태 정합성이 좋아집니다.
const handleDeleteConfirm = () => {
if (!id) return;
- mutate(Number(id));
- setIsPopupOpen(false);
+ mutate(Number(id), {
+ onSuccess: () => setIsPopupOpen(false),
+ // 실패 시 팝업 유지(에러 토스트는 훅에서 처리)
+ });
};📝 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 handleDeleteConfirm = () => { | |
| if (!id) return; | |
| mutate(id as string); | |
| mutate(Number(id)); | |
| setIsPopupOpen(false); | |
| }; | |
| const handleDeleteConfirm = () => { | |
| if (!id) return; | |
| mutate(Number(id), { | |
| onSuccess: () => setIsPopupOpen(false), | |
| // 실패 시 팝업 유지(에러 토스트는 훅에서 처리) | |
| }); | |
| }; |
🤖 Prompt for AI Agents
In src/app/(with-header)/activities/[id]/components/Title.tsx around lines 37 to
41, the delete popup is being closed immediately regardless of API outcome;
change mutate(Number(id)); setIsPopupOpen(false); to call mutate with options so
the popup is closed only on success (e.g., mutate(Number(id), { onSuccess: () =>
setIsPopupOpen(false), onError: /* optional error handling */ })), preserving
the early-return check for id.
| const deleteActivity = async (id: number) => { | ||
| const response = await privateInstance.delete(`/deleteActivity/${id}`); | ||
| 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.
🧹 Nitpick (assertive)
숫자형 ID 전환 OK—함수·뮤테이션 제네릭으로 타입 안정성 보강 권장
런타임은 문제없지만 반환·에러·변수 타입을 명시하면 호출부 안전성이 올라갑니다.
-const deleteActivity = async (id: number) => {
+const deleteActivity = async (id: number): Promise<unknown> => {
const response = await privateInstance.delete(`/deleteActivity/${id}`);
return response.data;
};
export const useDeleteActivity = () => {
const queryClient = useQueryClient();
const router = useRouter();
- return useMutation({
+ return useMutation<unknown, AxiosError, number>({
mutationFn: deleteActivity,📝 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 deleteActivity = async (id: number) => { | |
| const response = await privateInstance.delete(`/deleteActivity/${id}`); | |
| return response.data; | |
| }; | |
| const deleteActivity = async (id: number): Promise<unknown> => { | |
| const response = await privateInstance.delete(`/deleteActivity/${id}`); | |
| return response.data; | |
| }; | |
| export const useDeleteActivity = () => { | |
| const queryClient = useQueryClient(); | |
| const router = useRouter(); | |
| return useMutation<unknown, AxiosError, number>({ | |
| mutationFn: deleteActivity, | |
| // …other options (e.g., onSuccess, onError) | |
| }); | |
| }; |
🤖 Prompt for AI Agents
In src/app/(with-header)/activities/[id]/hooks/useDeleteActivity.ts around lines
7 to 10, the deleteActivity function currently lacks explicit return and error
typing; change it to a typed, generic async function (e.g., export async
function deleteActivity<T = void>(id: number): Promise<T>) and annotate the
axios call with the response generic (use the axios delete generic to infer
response data type) so the function returns Promise<T>; ensure callers pass or
infer the correct T and update any mutation typing to accept the same generic
for full type-safety and clearer error/response typing.
|
|
||
| export const useEditActivityForm = () => { | ||
| const { id } = useParams() as { id: string }; | ||
| const { id } = useParams(); |
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
useParams 타입 미지정으로 인한 id의 string | string[] 가능성 (템플릿 리터럴 등에서 타입 오류 위험)
Next.js App Router의 useParams는 제네릭으로 파라미터 스키마를 지정하지 않으면 id가 string | string[]로 추론됩니다. 현재처럼 템플릿 리터럴(/activities/${id})에 바로 사용하면 타입 체커에서 걸리거나 런타임 이상 동작 가능성이 있습니다. 제네릭을 명시해 단일 string으로 고정하세요.
- const { id } = useParams();
+ const params = useParams<{ id: string }>();
+ const id = params.id;📝 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 { id } = useParams(); | |
| const params = useParams<{ id: string }>(); | |
| const id = params.id; |
🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts around
line 15, useParams is inferred as string | string[] which can break
template-literal usages; update the call to useParams with an explicit generic
like useParams<{ id: string }> so id is typed as a single string, and add a
small runtime guard (throw or return early) if id is undefined to satisfy the
typechecker and avoid runtime issues.
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: 8
♻️ Duplicate comments (2)
src/components/FloatingBox/PriceDisplay.tsx (1)
10-15: 시각적 표시의 접근성: 장식 요소 숨기기 + 콜론 제거색상 점은 정보 전달에 기여하지 않는 장식 요소이므로 스크린 리더에서 제외하고, 텍스트의 선행 콜론은 제거하는 편이 자연스럽습니다. 이전 리뷰 의견과 동일한 방향입니다.
- <div className='flex items-center gap-2'> - <div className='h-25 w-25 rounded-full bg-yellow-300' /> - <span className='text-base font-bold text-black'> - : 예약 가능 일자 - </span> - </div> + <div className='flex items-center gap-2' aria-label='예약 가능 일자 표시'> + <div className='h-25 w-25 rounded-full bg-yellow-300' aria-hidden='true' /> + <span className='text-base font-bold text-black'>예약 가능 일자</span> + </div>src/app/(with-header)/activities/[id]/components/Title.tsx (1)
38-40: 삭제 처리 로직에 여전히 문제가 있습니다이전 리뷰에서 지적된 문제가 여전히 해결되지 않았습니다. API 결과와 무관하게 성공 토스트가 표시되고 팝업이 닫히는 문제가 남아있습니다.
다음과 같이 수정해주세요:
const handleDeleteConfirm = () => { if (!id) return; - mutate(Number(id)); - setIsPopupOpen(false); + mutate(Number(id), { + onSuccess: () => { + setIsPopupOpen(false); + toast.success('체험이 삭제되었습니다!'); + }, + onError: () => { + toast.error('삭제에 실패했습니다. 다시 시도해주세요.'); + }, + }); };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
src/app/(with-header)/activities/[id]/components/Title.tsx(2 hunks)src/components/DatePicker/CalendarBody.tsx(2 hunks)src/components/FloatingBox/PriceDisplay.tsx(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/app/(with-header)/activities/[id]/components/Title.tsx (1)
src/types/activityDetailType.ts (1)
TitleProps(14-21)
src/components/FloatingBox/PriceDisplay.tsx (1)
src/components/FloatingBox/BookingInterface.tsx (1)
BookingInterface(18-165)
src/components/DatePicker/CalendarBody.tsx (1)
src/lib/utils.ts (1)
cn(4-6)
🪛 Biome (2.1.2)
src/components/DatePicker/CalendarBody.tsx
[error] 53-91: Provide an explicit type prop for the button element.
The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset
(lint/a11y/useButtonType)
🔇 Additional comments (6)
src/components/FloatingBox/PriceDisplay.tsx (2)
4-4: 레이아웃 구성 LGTM
justify-between + flex-wrap으로 작은 화면 폭에서도 요소가 겹치지 않도록 한 점 좋습니다.
11-11: Tailwind 사이즈 토큰 확인 필요:w-25/h-25기본 스케일에 없음현재 스크립트로는 프로젝트 내
tailwind.config.*파일을 찾지 못해(find명령 결과 없음) 해당 스케일이 확장되었는지 확인할 수 없습니다.
아래 사항을 직접 점검해주세요:
- Tailwind 설정(
tailwind.config.js,tailwind.config.ts등)에서theme.extend.spacing,width,height에25키가 정의되어 있는지- 정의되어 있지 않다면,
w-25/h-25는 0 크기로 렌더링될 수 있으므로 디자인 의도에 맞게 표준 스케일(예:w-4 h-4) 또는 임의 값(w-[10px] h-[10px])으로 교체최소 수정 예시:
- <div className='h-25 w-25 rounded-full bg-yellow-300' /> + <div className='h-4 w-4 rounded-full bg-yellow-300' />접근성 개선(시각적 장식용 요소라면):
- <div className='h-25 w-25 rounded-full bg-yellow-300' /> + <div className='h-4 w-4 rounded-full bg-yellow-300' aria-hidden='true' />src/components/DatePicker/CalendarBody.tsx (3)
44-49: 월 외 날짜 클릭 가능 여부 확인 필요 — 요구사항에 따라 disabled/보조 속성 적용 제안현재 월 외 날짜도 클릭 가능하며 hover 효과가 있습니다. 기대 UX가 “해당 월만 선택”이라면 비활성화/접근성 속성을 권장합니다.
옵션:
- 비활성화:
disabled={!isSameMonth}+aria-disabled={!isSameMonth}+ 클래스disabled:opacity-60 disabled:cursor-not-allowed- 내비게이션 트리거(다음/이전 달로 이동)가 의도라면, 시각적 구분만 유지하고 클릭 시 상위에서 월 전환을 처리
원하시는 방향을 알려주시면 패치 드리겠습니다.
Also applies to: 73-78
91-91: 오늘 점 표시 크기 상향 LGTM시인성이 개선되어 달력 사용성이 좋아졌습니다.
5-5: cn import 검증 완료 – 경로/내보내기 일치
src/lib/cn.ts에export default cn;가 정의되어 있어,import cn from '@/lib/cn';은 올바른 구문입니다. 해당 파일의 캘린더 컴포넌트(import 경로)에는 수정이 필요하지 않습니다.• 확인된 파일
– src/lib/cn.ts (라인 31:export default cn;)
– 여러 컴포넌트(CalendarBody.tsx포함)에서 동일한 방식으로 사용 중Likely an incorrect or invalid review comment.
src/app/(with-header)/activities/[id]/components/Title.tsx (1)
14-14: 타입 분리로 일관성 향상로컬 타입을 공통 타입 모듈로 이동한 것은 좋은 리팩토링입니다. 타입 재사용성과 일관성이 향상됩니다.
| const handleDeleteConfirm = () => { | ||
| if (!id) return; | ||
| mutate(id as string); | ||
| mutate(Number(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.
💡 Verification agent
🧩 Analysis chain
id 타입 변환 검증 필요
useParams()에서 반환되는 id는 string | string[] 타입인데, Number(id)로 변환할 때 배열인 경우 NaN이 될 수 있습니다.
다음 스크립트로 id 사용 패턴을 확인해보겠습니다:
🏁 Script executed:
#!/bin/bash
# useParams의 id 사용 패턴 확인
rg -n -A3 -B3 "useParams.*id" --type=tsx --type=tsLength of output: 80
Ripgrep가 기본적으로 tsx 파일 타입을 인식하지 못해 검색이 실패했습니다. 아래 스크립트로 *.tsx와 *.ts 파일 전체에서 useParams 사용 패턴을 다시 확인해 주세요.
🏁 Script executed:
#!/bin/bash
# useParams의 id 사용 패턴을 tsx/ts 파일에서 재검색
rg -n -C3 "useParams.*id" -g '*.tsx' -g '*.ts'Length of output: 48
useParams 사용 위치와 id 구조분해 할당 패턴을 다시 확인하기 위해 아래 스크립트를 실행해 주세요.
🏁 Script executed:
#!/bin/bash
# 1. useParams 사용 위치 확인
rg -n "useParams" -g '*.tsx' -g '*.ts'
# 2. useParams에서 id를 구조분해 할당하는 패턴 확인
rg -n -C3 "const\s*{\s*id\s*}\s*=\s*useParams" -g '*.tsx' -g '*.ts'Length of output: 1210
Title.tsx 파일에서 id 구조분해 및 mutate(Number(id)) 호출 부분을 정확히 확인하기 위해 아래 스크립트를 실행해 주세요.
🏁 Script executed:
#!/bin/bash
# Title.tsx 상단과 사용 부분 출력
sed -n '1,200p' src/app/\(with-header\)/activities/\[id\]/components/Title.tsxLength of output: 2500
Title.tsx에서 useParams 타입 지정 및 배열 검사 추가 필요
useParams()는 기본적으로 string | string[] 타입을 반환하기 때문에, 현재 코드처럼 바로 Number(id)를 호출하면 배열인 경우 NaN이 될 수 있습니다. 아래와 같이 제네릭으로 id를 string 타입으로 지정하고, 배열 여부를 검사하는 로직을 추가해주세요.
• 파일: src/app/(with-header)/activities/[id]/components/Title.tsx
• 위치: const { id } = useParams(); 및 handleDeleteConfirm 내부
- const { id } = useParams();
+ // 단일 세그먼트인 [id]이므로, 제네릭을 통해 string임을 보장합니다.
+ const { id } = useParams<{ id: string }>();
...
const handleDeleteConfirm = () => {
- if (!id) return;
- mutate(Number(id));
+ // id가 없거나 배열인 경우 처리 중단
+ if (!id || Array.isArray(id)) return;
+ // id는 string이 보장되므로 안전하게 변환
+ mutate(Number(id));
setIsPopupOpen(false);
};위 변경으로
- TypeScript 레벨에서
id가string임을 명시 - 런타임에서 배열일 경우를 방어
두 가지 모두 보완됩니다.
📝 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.
| mutate(Number(id)); | |
| // 단일 세그먼트인 [id]이므로, 제네릭을 통해 string임을 보장합니다. | |
| const { id } = useParams<{ id: string }>(); | |
| const handleDeleteConfirm = () => { | |
| // id가 없거나 배열인 경우 처리 중단 | |
| if (!id || Array.isArray(id)) return; | |
| // id는 string이 보장되므로 안전하게 변환 | |
| mutate(Number(id)); | |
| setIsPopupOpen(false); | |
| }; |
🤖 Prompt for AI Agents
In src/app/(with-header)/activities/[id]/components/Title.tsx around line 38,
useParams is untyped and you call Number(id) directly which can produce NaN if
id is string[]; change const { id } = useParams(); to const { id } = useParams<{
id: string }>(); and in handleDeleteConfirm add a runtime guard: if
(Array.isArray(id) || typeof id !== 'string') { /* handle invalid id: return,
show error, or abort */ } else { mutate(Number(id)); } so TypeScript knows id is
a string and you defend against array values at runtime.
| <button | ||
| key={i} | ||
| onClick={() => onSelectDate(day)} | ||
| className={`relative flex h-30 w-30 items-center justify-center rounded-lg text-sm font-medium transition-all duration-200 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none ${ | ||
| !isSameMonth | ||
| ? 'text-gray-300 hover:text-gray-400' | ||
| : isWeekend | ||
| ? 'text-red-600' | ||
| : 'text-gray-700' | ||
| } ${ | ||
| isToday && !isSelected | ||
| ? 'bg-blue-100 font-bold text-blue-700 ring-2 ring-blue-200' | ||
| : '' | ||
| } ${ | ||
| isSelected | ||
| ? 'scale-105 transform bg-blue-600 font-bold text-white shadow-lg' | ||
| : '' | ||
| } ${ | ||
| !isToday && !isSelected && isSameMonth | ||
| ? 'hover:scale-105 hover:bg-gray-100' | ||
| : '' | ||
| } ${ | ||
| !isToday && !isSelected && !isSameMonth | ||
| ? 'hover:bg-gray-50' | ||
| : '' | ||
| } ${isHighlighted && !isSelected ? 'bg-yellow-200' : ''} `} | ||
| className={cn( |
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.
button 기본 submit 동작 방지: type="button" 명시 필요
폼 내부에서 캘린더를 사용할 수 있으므로 명시적 type 지정이 안전합니다. Biome a11y 규칙(lint/a11y/useButtonType) 경고도 해소됩니다.
아래 패치를 적용하세요:
- <button
- key={i}
- onClick={() => onSelectDate(day)}
+ <button
+ key={i}
+ type="button"
+ onClick={() => onSelectDate(day)}📝 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 | |
| key={i} | |
| onClick={() => onSelectDate(day)} | |
| className={`relative flex h-30 w-30 items-center justify-center rounded-lg text-sm font-medium transition-all duration-200 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none ${ | |
| !isSameMonth | |
| ? 'text-gray-300 hover:text-gray-400' | |
| : isWeekend | |
| ? 'text-red-600' | |
| : 'text-gray-700' | |
| } ${ | |
| isToday && !isSelected | |
| ? 'bg-blue-100 font-bold text-blue-700 ring-2 ring-blue-200' | |
| : '' | |
| } ${ | |
| isSelected | |
| ? 'scale-105 transform bg-blue-600 font-bold text-white shadow-lg' | |
| : '' | |
| } ${ | |
| !isToday && !isSelected && isSameMonth | |
| ? 'hover:scale-105 hover:bg-gray-100' | |
| : '' | |
| } ${ | |
| !isToday && !isSelected && !isSameMonth | |
| ? 'hover:bg-gray-50' | |
| : '' | |
| } ${isHighlighted && !isSelected ? 'bg-yellow-200' : ''} `} | |
| className={cn( | |
| <button | |
| key={i} | |
| type="button" | |
| onClick={() => onSelectDate(day)} | |
| className={cn( | |
| /* existing className logic */ | |
| )} | |
| > | |
| {day.getDate()} | |
| </button> |
🤖 Prompt for AI Agents
In src/components/DatePicker/CalendarBody.tsx around lines 52 to 55, the button
lacks an explicit type which can cause accidental form submission and triggers
the lint/a11y/useButtonType warning; update the button element to include
type="button" so it does not act as a submit control when used inside forms and
resolves the accessibility lint rule.
|
|
||
| return ( | ||
| <button | ||
| key={i} |
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.
🧹 Nitpick (assertive)
key에 배열 인덱스 사용 지양 → 날짜 기반 키로 안정화
인덱스 키는 리렌더/페이지 전환 시 상태 보존 문제가 생길 수 있습니다. 날짜 문자열을 키로 사용하세요.
- key={i}
+ key={day.format('YYYY-MM-DD')}📝 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.
| key={i} | |
| key={day.format('YYYY-MM-DD')} |
🧰 Tools
🪛 Biome (2.1.2)
[error] 53-91: Provide an explicit type prop for the button element.
The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset
(lint/a11y/useButtonType)
🤖 Prompt for AI Agents
In src/components/DatePicker/CalendarBody.tsx around line 53, the component
currently uses the loop index as the React key (key={i}); replace this with a
stable, date-based key such as a normalized date string for each cell (e.g.,
format the Date object to 'YYYY-MM-DD' or use date.toISOString()/toDateString())
so keys are unique and stable across renders and page transitions; ensure the
value is computed from the cell's date variable (not the loop index) and handles
timezone/normalization consistently.
| 'relative flex h-30 w-30 items-center justify-center rounded-xl text-sm font-semibold transition-all duration-200 ease-in-out focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none', | ||
| 'hover:scale-105 hover:shadow-md', |
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.
🧹 Nitpick (assertive)
transition-all 과도 사용, 포커스 가시성 개선, 모션 접근성 고려
- transition-all은 불필요한 속성까지 애니메이션합니다. 필요한 속성으로 한정하세요.
- 포인터 사용자만 링이 보이도록 focus-visible 사용을 권장합니다.
- 모션 민감 사용자 배려를 위해 motion-safe/motion-reduce 활용이 좋습니다.
- 'relative flex h-30 w-30 items-center justify-center rounded-xl text-sm font-semibold transition-all duration-200 ease-in-out focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none',
- 'hover:scale-105 hover:shadow-md',
+ 'relative flex h-30 w-30 items-center justify-center rounded-xl text-sm font-semibold transition-transform transition-colors transition-shadow duration-150 ease-out focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus:outline-none',
+ 'motion-safe:hover:scale-105 hover:shadow-md motion-reduce:transition-none',추가로, 오늘 날짜에는 aria-current="date"를 부여하면 스크린리더 인식이 개선됩니다. 원하시면 버튼 속성에 다음을 추가해 드리겠습니다: aria-current={isToday ? 'date' : undefined}
📝 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.
| 'relative flex h-30 w-30 items-center justify-center rounded-xl text-sm font-semibold transition-all duration-200 ease-in-out focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none', | |
| 'hover:scale-105 hover:shadow-md', | |
| 'relative flex h-30 w-30 items-center justify-center rounded-xl text-sm font-semibold transition-transform transition-colors transition-shadow duration-150 ease-out focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus:outline-none', | |
| 'motion-safe:hover:scale-105 hover:shadow-md motion-reduce:transition-none', |
🤖 Prompt for AI Agents
In src/components/DatePicker/CalendarBody.tsx around lines 56-57, replace the
broad 'transition-all' with focused transition properties (e.g.,
transition-transform transition-shadow or similar) and scope them with
motion-safe/motion-reduce classes (use motion-safe: for enabling transitions and
motion-reduce:transition-none or reduced equivalents) to respect
prefers-reduced-motion; replace focus: ring classes with focus-visible: ring-*
so only keyboard/pointer-visible focus shows the ring; and add
aria-current={isToday ? 'date' : undefined} to the today button element to
improve screen reader recognition.
| isHighlighted && | ||
| !isSelected && | ||
| 'bg-yellow-300 font-bold text-black', | ||
| !isHighlighted && 'opacity-90', | ||
| )} |
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.
비강조 전역 opacity-90 적용으로 ‘오늘/선택’까지 페이드됨 — 가시성/명암 대비 저하
!isHighlighted && 'opacity-90' 조건은 선택일·오늘도 비강조면 흐려집니다. 선택/오늘 상태는 항상 선명해야 합니다.
아래와 같이 조건을 보강하세요:
- !isHighlighted && 'opacity-90',
+ !isHighlighted && !isSelected && !isToday && 'opacity-90',📝 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.
| isHighlighted && | |
| !isSelected && | |
| 'bg-yellow-300 font-bold text-black', | |
| !isHighlighted && 'opacity-90', | |
| )} | |
| isHighlighted && | |
| !isSelected && | |
| 'bg-yellow-300 font-bold text-black', | |
| !isHighlighted && !isSelected && !isToday && 'opacity-90', | |
| )} |
🤖 Prompt for AI Agents
In src/components/DatePicker/CalendarBody.tsx around lines 79 to 83, the class
conditional `!isHighlighted && 'opacity-90'` is causing today/selected cells to
be faded when they are non-highlighted; update the condition so opacity-90 only
applies to cells that are not highlighted, not selected, and not today (e.g.
change to check !isHighlighted && !isSelected && !isToday before adding
'opacity-90'), ensuring selected/today cells remain fully opaque and visually
prominent.
| <div className='mb-1 text-2xl font-bold text-black'> | ||
| ₩{price.toLocaleString('ko-KR')}{' '} | ||
| <span className='text-xl font-bold text-gray-800'>/ 인</span> | ||
| <div className='mt-6 mb-6'> |
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.
🧹 Nitpick (assertive)
여백 클래스 단순화
동일 값의 상·하 마진은 축약형 사용이 가독성에 유리합니다.
- <div className='mt-6 mb-6'>
+ <div className='my-6'>📝 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.
| <div className='mt-6 mb-6'> | |
| <div className='my-6'> |
🤖 Prompt for AI Agents
In src/components/FloatingBox/PriceDisplay.tsx around line 3, the div uses
duplicate margin classes "mt-6 mb-6"; replace them with the shorthand "my-6" to
simplify and improve readability (update the className accordingly).
| <span className='text-xl font-bold text-gray-800'>/ 인</span> | ||
| <div className='mt-6 mb-6'> | ||
| <div className='mb-2 flex flex-wrap items-center justify-between gap-4'> | ||
| <p className='text-2xl font-bold text-black'> |
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.
🧹 Nitpick (assertive)
텍스트 컬러 토큰 일관성
인접 컴포넌트(예: BookingInterface)에서는 text-gray-900를 주로 사용합니다. 본 컴포넌트도 text-black 대신 동일 토큰을 사용하면 UI 톤 앤 매너가 통일됩니다.
- <p className='text-2xl font-bold text-black'>
+ <p className='text-2xl font-bold text-gray-900'>- <span className='text-base font-bold text-black'>
+ <span className='text-base font-bold text-gray-900'>Also applies to: 12-12
🤖 Prompt for AI Agents
In src/components/FloatingBox/PriceDisplay.tsx around lines 5 and 12, the
component uses the Tailwind class text-black which is inconsistent with adjacent
components that use the design token text-gray-900; update both occurrences to
text-gray-900 to align color token usage and maintain UI tone and manner.
| <p className='text-2xl font-bold text-black'> | ||
| ₩{price.toLocaleString('ko-KR')} | ||
| <span className='text-xl font-normal text-gray-600'>/인</span> | ||
| </p> |
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.
🧹 Nitpick (assertive)
가격과 단위 사이 간격 보장 및 통화 포맷 안정화
현재는 가격과 “/인” 사이에 공백이 없어 시각적으로 붙어 보일 수 있습니다. 최소한 ml-1 여백을 권장하며, 가능하면 Intl 통화 포맷을 사용해 통화 기호와 그룹핑을 일관되게 처리하세요.
최소 수정(여백 추가):
- <span className='text-xl font-normal text-gray-600'>/인</span>
+ <span className='ml-1 text-xl font-normal text-gray-600'>/인</span>선택 사항(통화 포맷 사용):
- ₩{price.toLocaleString('ko-KR')}
+ {new Intl.NumberFormat('ko-KR', {
+ style: 'currency',
+ currency: 'KRW',
+ maximumFractionDigits: 0,
+ }).format(price)}📝 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.
| <p className='text-2xl font-bold text-black'> | |
| ₩{price.toLocaleString('ko-KR')} | |
| <span className='text-xl font-normal text-gray-600'>/인</span> | |
| </p> | |
| <p className='text-2xl font-bold text-black'> | |
| {new Intl.NumberFormat('ko-KR', { | |
| style: 'currency', | |
| currency: 'KRW', | |
| maximumFractionDigits: 0, | |
| }).format(price)} | |
| <span className='ml-1 text-xl font-normal text-gray-600'>/인</span> | |
| </p> |
minimo-9
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.
리팩토링하시느라 고생많으셨습니다!
📌 변경 사항 개요
리팩토링을 통한 개선
📝 상세 내용
🔗 관련 이슈
🖼️ 스크린샷(선택사항)
💡 참고 사항
Summary by CodeRabbit