-
Notifications
You must be signed in to change notification settings - Fork 1
Feat/70 체험 등록/수정페이지 로직 및 레이아웃 1차 구현 #73
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
|
Caution Review failedThe pull request is closed. """ Walkthrough이번 변경에서는 새로운 예약(체험 등록) 폼 기능이 추가되었습니다. 이를 위해 다양한 입력 컴포넌트, 이미지 업로드, 일정 선택, 주소 입력 등 여러 React 컴포넌트가 신설되었으며, 관련 의존성( Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Page
participant ReservationForm
participant InfoSection
participant ScheduleSelectForm
participant ImageSection
participant AddressInput
participant CategoryInput
User->>Page: 페이지 접근
Page->>ReservationForm: ReservationForm 렌더링
ReservationForm->>InfoSection: 체험 정보 입력 렌더링
InfoSection->>CategoryInput: 카테고리 선택
InfoSection->>AddressInput: 주소 검색 및 입력
ReservationForm->>ScheduleSelectForm: 일정 입력 폼 렌더링
ReservationForm->>ImageSection: 이미지 업로드/미리보기 렌더링
User->>ReservationForm: 입력값 변경 (제목, 카테고리, 가격, 설명, 주소, 일정, 이미지 등)
User->>ReservationForm: 폼 제출
ReservationForm->>ReservationForm: 입력값 로깅/처리
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20분 Suggested reviewers
Poem
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
src/app/(with-header)/myactivity/components/CategoryInput.tsxOops! Something went wrong! :( ESLint: 9.30.1 ESLint couldn't find the plugin "eslint-plugin-react-hooks". (The package "eslint-plugin-react-hooks" was not found when loaded as a Node module from the directory "".) It's likely that the plugin isn't installed correctly. Try reinstalling by running the following: The plugin "eslint-plugin-react-hooks" was referenced from the config file in " » eslint-config-next/core-web-vitals » /node_modules/.pnpm/[email protected][email protected][email protected][email protected]/node_modules/eslint-config-next/index.js". If you still can't figure out the problem, please see https://eslint.org/docs/latest/use/troubleshooting. src/app/(with-header)/myactivity/components/ScheduleSelect.tsxOops! Something went wrong! :( ESLint: 9.30.1 ESLint couldn't find the plugin "eslint-plugin-react-hooks". (The package "eslint-plugin-react-hooks" was not found when loaded as a Node module from the directory "".) It's likely that the plugin isn't installed correctly. Try reinstalling by running the following: The plugin "eslint-plugin-react-hooks" was referenced from the config file in " » eslint-config-next/core-web-vitals » /node_modules/.pnpm/[email protected][email protected][email protected][email protected]/node_modules/eslint-config-next/index.js". If you still can't figure out the problem, please see https://eslint.org/docs/latest/use/troubleshooting. Note ⚡️ Unit Test Generation is now available in beta!Learn more here, or try it out under "Finishing Touches" below. 📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (2)
✨ 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. 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 (
|
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: 18
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (15)
package.json(1 hunks)src/app/(with-header)/myactivity/components/AddressInput.tsx(1 hunks)src/app/(with-header)/myactivity/components/CategoryInput.tsx(1 hunks)src/app/(with-header)/myactivity/components/FormSection.tsx(1 hunks)src/app/(with-header)/myactivity/components/ImagePreview.tsx(1 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/ReservationForm.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/page.tsx(1 hunks)src/app/globals.css(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (8)
src/app/(with-header)/myactivity/page.tsx (1)
src/app/(with-header)/myactivity/components/ReservationForm.tsx (1)
ReservationForm(37-164)
src/app/(with-header)/myactivity/components/InfoSection.tsx (3)
src/components/Input.tsx (1)
Input(17-61)src/app/(with-header)/myactivity/components/CategoryInput.tsx (1)
CategoryInput(7-37)src/app/(with-header)/myactivity/components/AddressInput.tsx (1)
AddressInput(14-70)
src/app/(with-header)/myactivity/components/ImageSection.tsx (2)
src/app/(with-header)/myactivity/components/MainImageSelect.tsx (1)
MainImageSelect(13-42)src/app/(with-header)/myactivity/components/SubImageSelect.tsx (1)
SubImageSelect(10-47)
src/app/(with-header)/myactivity/components/SubImageSelect.tsx (2)
src/app/(with-header)/myactivity/components/ImageUpload.tsx (1)
ImageUpload(12-50)src/app/(with-header)/myactivity/components/ImagePreview.tsx (1)
ImagePreview(12-39)
src/app/(with-header)/myactivity/components/ScheduleSelect.tsx (1)
src/components/Input.tsx (1)
Input(17-61)
src/app/(with-header)/myactivity/components/MainImageSelect.tsx (2)
src/app/(with-header)/myactivity/components/ImageUpload.tsx (1)
ImageUpload(12-50)src/app/(with-header)/myactivity/components/ImagePreview.tsx (1)
ImagePreview(12-39)
src/app/(with-header)/myactivity/components/ScheduleSelectForm.tsx (1)
src/app/(with-header)/myactivity/components/ScheduleSelect.tsx (1)
ScheduleSelect(19-78)
src/app/(with-header)/myactivity/components/AddressInput.tsx (2)
src/components/Input.tsx (1)
Input(17-61)src/components/Button.tsx (1)
Button(18-49)
🪛 GitHub Actions: CI
src/app/(with-header)/myactivity/components/ImageUpload.tsx
[warning] 43-45: react/jsx-sort-props: Props should be sorted alphabetically.
src/app/(with-header)/myactivity/components/InfoSection.tsx
[warning] 3-81: Multiple react/jsx-sort-props and simple-import-sort/imports warnings: Props should be sorted alphabetically; Callbacks must be listed after all other props; Run autofix to sort these imports.
src/app/(with-header)/myactivity/components/ImageSection.tsx
[warning] 28-34: react/jsx-sort-props: Props should be sorted alphabetically.
src/app/(with-header)/myactivity/components/SubImageSelect.tsx
[warning] 34-36: react/no-array-index-key and react/jsx-sort-props warnings: Do not use Array index in keys; Callbacks must be listed after all other props.
src/app/(with-header)/myactivity/components/CategoryInput.tsx
[warning] 15-22: react/jsx-sort-props: Props should be sorted alphabetically.
src/app/(with-header)/myactivity/components/ScheduleSelect.tsx
[error] 22-22: @typescript-eslint/no-unused-vars: 'onAddDate' is defined but never used. Allowed unused args must match /^_/u.
[warning] 39-68: react/jsx-sort-props: Props should be sorted alphabetically; Callbacks must be listed after all other props.
src/app/(with-header)/myactivity/components/MainImageSelect.tsx
[error] 3-3: unused-imports/no-unused-imports: 'IconClose' is defined but never used.
[warning] 31-31: react/jsx-sort-props: Callbacks must be listed after all other props.
src/app/(with-header)/myactivity/components/ScheduleSelectForm.tsx
[warning] 42-44: react/jsx-key: Missing "key" prop for element in iterator; react/no-array-index-key: Do not use Array index in keys.
[warning] 34-53: Multiple react/jsx-sort-props, react/jsx-key and react/no-array-index-key warnings: Callbacks must be listed after all other props; Missing "key" prop for element in iterator; Do not use Array index in keys; Props should be sorted alphabetically.
src/app/(with-header)/myactivity/components/ReservationForm.tsx
[warning] 104-111: no-console: Unexpected console statement. Only these console methods are allowed: warn, error.
src/app/(with-header)/myactivity/components/AddressInput.tsx
[error] 20-20: @typescript-eslint/no-explicit-any: Unexpected any. Specify a different type.
src/app/(with-header)/myactivity/components/ImagePreview.tsx
[warning] 23-23: @next/next/no-img-element: Using <img> could result in slower LCP and higher bandwidth. Consider using <Image /> from next/image or a custom image loader.
🪛 Biome (2.1.2)
src/app/(with-header)/myactivity/components/ScheduleSelectForm.tsx
[error] 42-43: Missing key property for this element in iterable.
The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.
(lint/correctness/useJsxKeyInIterable)
🔇 Additional comments (13)
package.json (1)
21-21: 새 의존성의 peer-dependency 호환성 확인 필요
react-daum-postcode@^3.2.0는 React 18까지를 peer로 선언하고 있어 React 19(실험 버전)과 함께 사용 시 경고가 발생할 수 있습니다. 실제 실행에 문제는 없지만 CI 출력 또는 브라우저 콘솔에서 peer 경고가 계속 노출될 수 있으니 버전 호환 여부를 한 번 더 확인해 주세요.src/app/(with-header)/myactivity/components/FormSection.tsx (1)
15-26: 구조·스타일 모두 명료, 별도 의견 없습니다
컴포넌트가 단순하고 재사용성도 좋아 보입니다.src/app/(with-header)/myactivity/components/CategoryInput.tsx (1)
27-27: 옵션 값과 표시 텍스트 일관성 검토 필요'문화예술' 값과 '문화/예술' 표시 텍스트가 다릅니다. API나 백엔드에서 어떤 형식을 기대하는지 확인이 필요합니다.
백엔드 API 스펙을 확인하여 카테고리 값의 정확한 형식을 검증해주세요.
src/app/(with-header)/myactivity/components/ReservationForm.tsx (1)
37-164: 우수한 컴포넌트 구조와 상태 관리전체적으로 컴포넌트가 잘 분리되어 있고, 상태 관리가 체계적으로 구현되었습니다. 단방향 데이터 플로우를 잘 따르고 있으며, 각 기능별로 적절히 모듈화되어 있습니다.
src/app/(with-header)/myactivity/components/ScheduleSelectForm.tsx (1)
22-27: 컴포넌트 구조와 인터페이스 설계가 우수함props 인터페이스가 명확하고 컴포넌트의 책임이 잘 분리되어 있습니다. 일정 관리 로직이 적절히 구현되었습니다.
src/app/(with-header)/myactivity/components/InfoSection.tsx (3)
20-31: 컴포넌트 구조와 레이아웃이 잘 설계됨반응형 그리드 레이아웃과 controlled component 패턴이 적절히 적용되었고, 각 입력 필드가 명확한 목적을 가지고 있습니다.
52-66: InfoSection 컴포넌트 검토 결과
- 가격 핸들링 타입 일관성
ReservationForm에서onPriceChange={(value) => setPrice(Number(value))}로 문자열을 숫자로 변환해 상태에 반영하고 있으므로, 타입 불일치 이슈가 없습니다.- 전반적 구조 및 설계
제어 컴포넌트 패턴, Prop 인터페이스, Grid 레이아웃 모두 적절하게 적용되어 있어 컴포넌트 디자인은 문제없습니다.- 추가 확인 필요 사항
Input컴포넌트가type="textarea"를 올바르게 지원하는지 확인이 필요합니다.위 사항 검토 후 머지 진행 부탁드립니다.
70-78: Input 컴포넌트의 textarea 타입 지원 여부 확인 요청
src/app/(with-header)/myactivity/components/InfoSection.tsx72번째 줄에서 아래와 같이<Input type='textarea' />를 사용하고 있습니다:<Input label='설명' type='textarea' placeholder='체험에 대한 자세한 설명을 입력해주세요' className='w-full' value={description} onChange={(e) => onDescriptionChange(e.target.value)} />Input 컴포넌트 정의 파일(
src/components/Input.tsx또는src/components/Input/index.tsx)에type === 'textarea'일 때<textarea>를 렌더링하도록 분기 처리가 구현되어 있는지 확인해 주세요. 지원 로직이 없다면 필요에 따라 해당 분기와 스타일을 추가해 주시기 바랍니다.src/app/(with-header)/myactivity/components/AddressInput.tsx (2)
24-33: 한국 주소 형식 처리 로직이 적절함건물명과 동/읍/면 정보를 포함한 상세 주소 구성 로직이 한국 주소 체계에 맞게 잘 구현되었습니다.
48-67: Modal 통합과 UI 구성이 우수함Modal 컴포넌트와의 통합이 적절하고, 사용자 경험을 고려한 UI 구성이 잘 되어 있습니다.
src/app/(with-header)/myactivity/components/SubImageSelect.tsx (2)
15-19: 이미지 개수 제한 로직이 잘 구현됨4장 제한과 조건부 업로드 허용 로직이 명확하고 사용자 친화적입니다.
28-31: ImageUpload 조건부 렌더링 로직이 우수함최대 이미지 개수에 도달했을 때 업로드 UI를 숨기는 로직이 적절합니다.
src/app/(with-header)/myactivity/components/ImageUpload.tsx (1)
25-49: 접근성과 사용자 경험이 우수함라벨 기반 파일 선택, hover 효과, 시각적 피드백이 잘 구현되어 있고 접근성도 고려되었습니다.
| <label | ||
| htmlFor='category' | ||
| className='font-regular flex flex-col text-lg text-black' | ||
| > | ||
| 카테고리 | ||
| </label> | ||
| <div> | ||
| <select | ||
| id='category' | ||
| className='w-full rounded-md border border-gray-800 bg-white px-20 py-17 placeholder-gray-600' |
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.
Props 정렬 및 옵션 값 일관성 문제 수정 필요
파이프라인에서 지적한 props 정렬 문제를 해결하고, 옵션 값과 표시 텍스트의 일관성을 검토해야 합니다.
다음 diff를 적용하여 문제를 해결하세요:
<label
- htmlFor='category'
- className='font-regular flex flex-col text-lg text-black'
+ className='font-regular flex flex-col text-lg text-black'
+ htmlFor='category'
>
카테고리
</label>
<div>
<select
- id='category'
- className='w-full rounded-md border border-gray-800 bg-white px-20 py-17 placeholder-gray-600'
+ className='w-full rounded-md border border-gray-800 bg-white px-20 py-17 placeholder-gray-600'
+ id='category'
value={category}
onChange={(e) => onCategoryChange(e.target.value)}
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <label | |
| htmlFor='category' | |
| className='font-regular flex flex-col text-lg text-black' | |
| > | |
| 카테고리 | |
| </label> | |
| <div> | |
| <select | |
| id='category' | |
| className='w-full rounded-md border border-gray-800 bg-white px-20 py-17 placeholder-gray-600' | |
| <label | |
| className='font-regular flex flex-col text-lg text-black' | |
| htmlFor='category' | |
| > | |
| 카테고리 | |
| </label> | |
| <div> | |
| <select | |
| className='w-full rounded-md border border-gray-800 bg-white px-20 py-17 placeholder-gray-600' | |
| id='category' | |
| value={category} | |
| onChange={(e) => onCategoryChange(e.target.value)} | |
| > |
🧰 Tools
🪛 GitHub Actions: CI
[warning] 15-22: react/jsx-sort-props: Props should be sorted alphabetically.
🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/components/CategoryInput.tsx around lines 13
to 22, fix the props sorting by arranging the JSX attributes in a consistent
order, typically starting with id, then className, followed by other props.
Also, review the option elements inside the select to ensure that the option
values and their displayed text are consistent and correctly matched. Adjust the
code to apply these changes for better readability and correctness.
| alt, | ||
| className = '', | ||
| }: ImagePreviewProps) { | ||
| const src = typeof image === 'string' ? image : URL.createObjectURL(image); |
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
메모리 누수 방지를 위한 URL cleanup 필요
URL.createObjectURL로 생성된 URL은 메모리 누수를 방지하기 위해 URL.revokeObjectURL로 정리해야 합니다.
다음과 같이 useEffect를 사용하여 cleanup을 구현하세요:
'use client';
+import { useEffect } from 'react';
import IconClose from '@assets/svg/close';
// ... in component
export function ImagePreview({
image,
onRemove,
alt,
className = '',
}: ImagePreviewProps) {
const src = typeof image === 'string' ? image : URL.createObjectURL(image);
+ useEffect(() => {
+ return () => {
+ if (typeof image !== 'string') {
+ URL.revokeObjectURL(src);
+ }
+ };
+ }, [src, image]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const src = typeof image === 'string' ? image : URL.createObjectURL(image); | |
| 'use client'; | |
| import { useEffect } from 'react'; | |
| import IconClose from '@assets/svg/close'; | |
| // ... other imports | |
| export function ImagePreview({ | |
| image, | |
| onRemove, | |
| alt, | |
| className = '', | |
| }: ImagePreviewProps) { | |
| const src = typeof image === 'string' ? image : URL.createObjectURL(image); | |
| useEffect(() => { | |
| return () => { | |
| if (typeof image !== 'string') { | |
| URL.revokeObjectURL(src); | |
| } | |
| }; | |
| }, [src, image]); | |
| return ( | |
| <div className={className}> | |
| <img src={src} alt={alt} /> | |
| <button onClick={onRemove}> | |
| <IconClose /> | |
| </button> | |
| </div> | |
| ); | |
| } |
🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/components/ImagePreview.tsx at line 18, the
URL created by URL.createObjectURL when image is not a string needs to be
revoked to prevent memory leaks. Wrap the URL creation in a useEffect hook that
sets the src state and returns a cleanup function calling URL.revokeObjectURL on
the created URL when the component unmounts or the image changes.
| <img | ||
| src={src || '/placeholder.svg'} | ||
| className='h-full w-full object-cover' | ||
| alt={alt} | ||
| /> |
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)
Next.js Image 컴포넌트 사용 권장
파이프라인에서 지적한 대로 <img> 태그 대신 Next.js의 <Image> 컴포넌트를 사용하면 성능과 LCP를 개선할 수 있습니다.
+import Image from 'next/image';
- <img
+ <Image
src={src || '/placeholder.svg'}
className='h-full w-full object-cover'
alt={alt}
+ fill
+ sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
- />
+ />🧰 Tools
🪛 GitHub Actions: CI
[warning] 23-23: @next/next/no-img-element: Using <img> could result in slower LCP and higher bandwidth. Consider using <Image /> from next/image or a custom image loader.
🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/components/ImagePreview.tsx around lines 23
to 27, replace the standard <img> tag with Next.js's <Image> component to
improve performance and LCP. Import the Image component from 'next/image', then
update the JSX to use <Image> with equivalent props: set src to src or
'/placeholder.svg', provide alt text, and apply the same styling using the
appropriate layout or className props supported by Next.js Image.
| interface ImagesSectionProps { | ||
| mainImage: string | File | null; | ||
| subImage: (string | File)[]; | ||
| onMainImageSelect: (file: File) => void; | ||
| onMainImageRemove: () => void; | ||
| onSubImageAdd: (files: File[]) => void; | ||
| onSubImageRemove: (index: number) => void; | ||
| } |
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)
타입 이름과 컴포넌트 이름 불일치
interface ImagesSectionProps { ... } 와 export function ImageSection 간 복수형/단수형이 달라 가독성이 떨어집니다. 이름을 맞추어 주세요.
-interface ImagesSectionProps {
+interface ImageSectionProps {
...
-}: ImagesSectionProps) {
+}: ImageSectionProps) {Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/components/ImageSection.tsx around lines 6
to 13, the interface name ImagesSectionProps uses a plural form while the
component is named ImageSection in singular, causing inconsistency. Rename the
interface to ImageSectionProps to match the component name for better
readability and consistency.
| {dates.map((dateSlot, idx) => ( | ||
| <div className='flex'> | ||
| <ScheduleSelect | ||
| key={idx} | ||
| index={idx} | ||
| isRemovable={dates.length > 1} | ||
| onAddDate={onAddDate} | ||
| onRemove={onRemoveDate} | ||
| onDateChange={(index, value) => onDateChange(index, 'date', value)} | ||
| onStartTimeChange={(index, value) => | ||
| onDateChange(index, 'startTime', value) | ||
| } | ||
| onEndTimeChange={(index, value) => | ||
| onDateChange(index, 'endTime', value) | ||
| } | ||
| date={dateSlot.date} | ||
| startTime={dateSlot.startTime} | ||
| endTime={dateSlot.endTime} | ||
| /> | ||
| </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.
React key 최적화 및 불필요한 wrapper 제거 필요
배열 인덱스를 key로 사용하면 항목이 재정렬되거나 제거될 때 렌더링 문제가 발생할 수 있습니다. 또한 불필요한 div wrapper가 있습니다.
다음과 같이 수정하세요:
- {dates.map((dateSlot, idx) => (
- <div className='flex'>
- <ScheduleSelect
- key={idx}
+ {dates.map((dateSlot, idx) => (
+ <ScheduleSelect
+ key={`${dateSlot.date}-${dateSlot.startTime}-${idx}`}
index={idx}
isRemovable={dates.length > 1}
onAddDate={onAddDate}
onRemove={onRemoveDate}
onDateChange={(index, value) => onDateChange(index, 'date', value)}
onStartTimeChange={(index, value) =>
onDateChange(index, 'startTime', value)
}
onEndTimeChange={(index, value) =>
onDateChange(index, 'endTime', value)
}
date={dateSlot.date}
startTime={dateSlot.startTime}
endTime={dateSlot.endTime}
/>
- </div>
- ))}
+ ))}📝 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.
| {dates.map((dateSlot, idx) => ( | |
| <div className='flex'> | |
| <ScheduleSelect | |
| key={idx} | |
| index={idx} | |
| isRemovable={dates.length > 1} | |
| onAddDate={onAddDate} | |
| onRemove={onRemoveDate} | |
| onDateChange={(index, value) => onDateChange(index, 'date', value)} | |
| onStartTimeChange={(index, value) => | |
| onDateChange(index, 'startTime', value) | |
| } | |
| onEndTimeChange={(index, value) => | |
| onDateChange(index, 'endTime', value) | |
| } | |
| date={dateSlot.date} | |
| startTime={dateSlot.startTime} | |
| endTime={dateSlot.endTime} | |
| /> | |
| </div> | |
| ))} | |
| {dates.map((dateSlot, idx) => ( | |
| <ScheduleSelect | |
| key={`${dateSlot.date}-${dateSlot.startTime}-${idx}`} | |
| index={idx} | |
| isRemovable={dates.length > 1} | |
| onAddDate={onAddDate} | |
| onRemove={onRemoveDate} | |
| onDateChange={(index, value) => onDateChange(index, 'date', value)} | |
| onStartTimeChange={(index, value) => | |
| onDateChange(index, 'startTime', value) | |
| } | |
| onEndTimeChange={(index, value) => | |
| onDateChange(index, 'endTime', value) | |
| } | |
| date={dateSlot.date} | |
| startTime={dateSlot.startTime} | |
| endTime={dateSlot.endTime} | |
| /> | |
| ))} |
🧰 Tools
🪛 Biome (2.1.2)
[error] 42-43: Missing key property for this element in iterable.
The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.
(lint/correctness/useJsxKeyInIterable)
🪛 GitHub Actions: CI
[warning] 42-44: react/jsx-key: Missing "key" prop for element in iterator; react/no-array-index-key: Do not use Array index in keys.
🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/components/ScheduleSelectForm.tsx between
lines 41 and 61, replace the array index used as the React key with a unique and
stable identifier from the dateSlot object to prevent rendering issues during
reordering or removal. Also, remove the unnecessary div wrapper around the
ScheduleSelect component to simplify the JSX structure.
| {subImage.map((img, idx) => ( | ||
| <ImagePreview | ||
| key={idx} | ||
| image={img} | ||
| onRemove={() => onImageRemove(idx)} | ||
| alt={`소개 이미지 ${idx + 1}`} | ||
| /> | ||
| ))} | ||
| </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.
React key로 배열 인덱스 사용 문제
이미지가 제거될 때 배열 인덱스를 key로 사용하면 렌더링 문제가 발생할 수 있습니다.
File 객체의 고유 속성을 사용하거나 UUID를 생성하여 key로 사용하세요:
{subImage.map((img, idx) => (
<ImagePreview
- key={idx}
+ key={typeof img === 'string' ? img : `${img.name}-${img.size}-${idx}`}
image={img}
onRemove={() => onImageRemove(idx)}
alt={`소개 이미지 ${idx + 1}`}
/>
))}📝 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.
| {subImage.map((img, idx) => ( | |
| <ImagePreview | |
| key={idx} | |
| image={img} | |
| onRemove={() => onImageRemove(idx)} | |
| alt={`소개 이미지 ${idx + 1}`} | |
| /> | |
| ))} | |
| </div> | |
| {subImage.map((img, idx) => ( | |
| <ImagePreview | |
| key={typeof img === 'string' ? img : `${img.name}-${img.size}-${idx}`} | |
| image={img} | |
| onRemove={() => onImageRemove(idx)} | |
| alt={`소개 이미지 ${idx + 1}`} | |
| /> | |
| ))} | |
| </div> |
🧰 Tools
🪛 GitHub Actions: CI
[warning] 34-36: react/no-array-index-key and react/jsx-sort-props warnings: Do not use Array index in keys; Callbacks must be listed after all other props.
🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/components/SubImageSelect.tsx around lines
32 to 40, the React key prop currently uses the array index, which can cause
rendering issues when images are removed. To fix this, use a unique identifier
from the File object if available, or generate a UUID for each image and use
that as the key instead of the index. This ensures stable keys and prevents
rendering problems during updates.
| export default function Page() { | ||
| return ( | ||
| <div> | ||
| <ReservationForm /> | ||
| </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.
🧹 Nitpick (assertive)
페이지 컴포넌트에 'use client' 선언 여부 재검토
부모 페이지가 서버 컴포넌트이고 자식 ReservationForm이 클라이언트 컴포넌트라면 현재 구조로도 동작은 합니다. 다만 페이지 레벨에서도 클라이언트 전용 훅(예: useSearchParams)을 추가할 가능성이 있다면 미리 'use client' 를 선언해 두는 것이 추후 변경 시 혼란을 줄입니다.
🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/page.tsx around lines 3 to 8, the page
component currently lacks a 'use client' directive. If you plan to use
client-only hooks like useSearchParams in this page or want to avoid confusion
when adding such hooks later, add the 'use client' directive at the top of this
file to explicitly mark it as a client component.
| html { | ||
| font-family: 'Pretendard-Regular', sans-serif; | ||
| scrollbar-gutter: stable; | ||
| } |
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)
scrollbar-gutter 브라우저 지원 범위 확인 권장
해당 속성은 Chromium 94+, Safari 16.2+만 지원합니다. 구형 브라우저(특히 Firefox)에서는 무시되므로 레이아웃 점프가 재발할 수 있습니다. 주요 타깃 브라우저가 이 범위를 벗어나면 폴백 CSS(예: padding 보정)를 같이 두는 편이 안전합니다.
🤖 Prompt for AI Agents
In src/app/globals.css around lines 68 to 71, the scrollbar-gutter property is
used but it is only supported in Chromium 94+ and Safari 16.2+, so older
browsers like Firefox will ignore it causing potential layout jumps. To fix
this, add fallback CSS such as padding adjustments to maintain layout stability
in unsupported browsers, ensuring consistent appearance across all target
browsers.
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: 4
♻️ Duplicate comments (5)
src/app/(with-header)/myactivity/components/MainImageSelect.tsx (1)
28-32: Props 순서를 수정해주세요.콜백 함수 props는 다른 props 뒤에 위치해야 합니다.
<ImagePreview + alt='메인 이미지' image={mainImage} - onRemove={onImageRemove} - alt='메인 이미지' + onRemove={onImageRemove} />src/app/(with-header)/myactivity/components/ScheduleSelect.tsx (4)
6-17: 인터페이스에서 미사용 props 제거 필요
onAddDateprops가 여전히 인터페이스에 정의되어 있지만 컴포넌트에서 사용되지 않습니다. 과거 리뷰에서 지적된 문제가 해결되지 않았습니다.다음 diff를 적용하여 미사용 props를 제거하세요:
interface ScheduleSelectProps { index: number; isRemovable: boolean; - onAddDate: () => void; onRemove: (index: number) => void; onDateChange: (index: number, value: string) => void; onStartTimeChange: (index: number, value: string) => void; onEndTimeChange: (index: number, value: string) => void; date: string; startTime: string; endTime: string; }
35-41: Input 컴포넌트의 props 순서 개선 필요과거 리뷰에서 지적된 props 순서 문제가 여전히 존재합니다.
className이onChange콜백 앞에 위치해야 합니다.<Input label='날짜' type='date' + className='w-full' value={date} - className='w-full' onChange={(e) => onDateChange(index, e.target.value)} />
55-61: 종료시간 Input의 props 순서 수정 필요종료시간 Input에도 동일한 props 순서 수정이 필요합니다.
<Input label='종료시간' type='time' + className='w-full' value={endTime} - className='w-full' onChange={(e) => onEndTimeChange(index, e.target.value)} />
45-51: 시작시간 Input의 props 순서 수정 필요동일한 props 순서 문제가 시작시간 Input에도 적용되어야 합니다.
<Input label='시작시간' type='time' + className='w-full' value={startTime} - className='w-full' onChange={(e) => onStartTimeChange(index, e.target.value)} />
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (3)
src/app/(with-header)/myactivity/components/AddressInput.tsx(1 hunks)src/app/(with-header)/myactivity/components/MainImageSelect.tsx(1 hunks)src/app/(with-header)/myactivity/components/ScheduleSelect.tsx(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/app/(with-header)/myactivity/components/ScheduleSelect.tsx (1)
src/components/Input.tsx (1)
Input(17-61)
src/app/(with-header)/myactivity/components/AddressInput.tsx (2)
src/components/Input.tsx (1)
Input(17-61)src/components/Button.tsx (1)
Button(18-49)
src/app/(with-header)/myactivity/components/MainImageSelect.tsx (2)
src/app/(with-header)/myactivity/components/ImageUpload.tsx (1)
ImageUpload(12-50)src/app/(with-header)/myactivity/components/ImagePreview.tsx (1)
ImagePreview(12-39)
🔇 Additional comments (5)
src/app/(with-header)/myactivity/components/MainImageSelect.tsx (3)
1-4: 임포트 정리가 잘 되었습니다.미사용 임포트 문제가 해결되었고, 필요한 컴포넌트들만 임포트하고 있습니다.
6-10: 인터페이스 설계가 우수합니다.다양한 이미지 소스 타입(File, string, null)을 지원하고 콜백 함수들의 시그니처가 명확하게 정의되어 있습니다.
17-41: 컴포넌트 구조와 UX가 잘 설계되었습니다.
- 반응형 그리드 레이아웃으로 다양한 화면 크기를 지원
- 명확한 사용자 안내 메시지와 제약 사항 표시
- 컴포넌트 분리를 통한 관심사 분리가 잘 이루어짐
- 조건부 렌더링으로 적절한 상태 관리
전반적으로 사용자 친화적이고 유지보수하기 좋은 코드입니다.
src/app/(with-header)/myactivity/components/ScheduleSelect.tsx (2)
19-30: 함수 파라미터에서 onAddDate 제거 완료함수 파라미터에서
onAddDate가 제거된 것을 확인했습니다. 22번 라인의 공백이 이를 나타냅니다. 좋은 수정입니다.
64-74: 제거 버튼 로직이 잘 구현됨
isRemovable조건부 렌더링과 제거 버튼의 스타일링이 적절하게 구현되었습니다. 접근성과 사용자 경험을 고려한 hover 효과도 잘 적용되었습니다.
| interface PostcodeData { | ||
| address: string; | ||
| addressType: 'R' | 'J'; | ||
| bname: string; | ||
| buildingName: string; | ||
| zonecode: string; | ||
| userSelectedType: 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)
userSelectedType 타입을 구체화하세요
userSelectedType 역시 'R' | 'J' 로 고정된 값만 들어오므로 string 대신 동일한 유니온 타입을 부여하면 타입 안정성이 한층 올라갑니다.
- userSelectedType: string;
+ userSelectedType: 'R' | 'J';📝 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.
| interface PostcodeData { | |
| address: string; | |
| addressType: 'R' | 'J'; | |
| bname: string; | |
| buildingName: string; | |
| zonecode: string; | |
| userSelectedType: string; | |
| } | |
| interface PostcodeData { | |
| address: string; | |
| addressType: 'R' | 'J'; | |
| bname: string; | |
| buildingName: string; | |
| zonecode: string; | |
| userSelectedType: 'R' | 'J'; | |
| } |
🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/components/AddressInput.tsx between lines 14
and 21, the userSelectedType property in the PostcodeData interface is currently
typed as string, which is too general. Change its type to the union type 'R' |
'J' to match the addressType property and improve type safety by restricting it
to these specific string literals.
| const handleComplete = (data: PostcodeData) => { | ||
| let fullAddress = data.address; | ||
| let extraAddress = ''; | ||
|
|
||
| if (data.addressType === 'R') { | ||
| if (data.bname !== '') { | ||
| extraAddress += data.bname; | ||
| } | ||
| if (data.buildingName !== '') { | ||
| extraAddress += | ||
| extraAddress !== '' ? `, ${data.buildingName}` : data.buildingName; | ||
| } | ||
| fullAddress += extraAddress !== '' ? ` (${extraAddress})` : ''; | ||
| } | ||
|
|
||
| onAddressChange(fullAddress); | ||
| setIsOpen(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.
🧹 Nitpick (assertive)
handleComplete를 useCallback으로 감싸 성능 최적화
매 렌더마다 새로운 함수가 생성되어 DaumPostcode-> 리렌더 트리거 가능성이 있습니다.
- const handleComplete = (data: PostcodeData) => {
+ const handleComplete = useCallback((data: PostcodeData) => {
…
- };
+ }, [onAddressChange]);불필요한 재생성을 줄여 컴포넌트 트리 성능을 개선할 수 있습니다.
📝 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 handleComplete = (data: PostcodeData) => { | |
| let fullAddress = data.address; | |
| let extraAddress = ''; | |
| if (data.addressType === 'R') { | |
| if (data.bname !== '') { | |
| extraAddress += data.bname; | |
| } | |
| if (data.buildingName !== '') { | |
| extraAddress += | |
| extraAddress !== '' ? `, ${data.buildingName}` : data.buildingName; | |
| } | |
| fullAddress += extraAddress !== '' ? ` (${extraAddress})` : ''; | |
| } | |
| onAddressChange(fullAddress); | |
| setIsOpen(false); | |
| }; | |
| const handleComplete = useCallback((data: PostcodeData) => { | |
| let fullAddress = data.address; | |
| let extraAddress = ''; | |
| if (data.addressType === 'R') { | |
| if (data.bname !== '') { | |
| extraAddress += data.bname; | |
| } | |
| if (data.buildingName !== '') { | |
| extraAddress += | |
| extraAddress !== '' ? `, ${data.buildingName}` : data.buildingName; | |
| } | |
| fullAddress += extraAddress !== '' ? ` (${extraAddress})` : ''; | |
| } | |
| onAddressChange(fullAddress); | |
| setIsOpen(false); | |
| }, [onAddressChange]); |
🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/components/AddressInput.tsx around lines 29
to 46, the handleComplete function is recreated on every render, which may cause
unnecessary re-renders of the DaumPostcode component. To optimize performance,
wrap the handleComplete function with React's useCallback hook, providing
appropriate dependencies to prevent unnecessary recreations while ensuring it
updates correctly when needed.
| label='주소' | ||
| id='address' | ||
| value={address} | ||
| onClick={() => setIsOpen(true)} | ||
| readOnly | ||
| /> |
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 prop 전달은 의미 없습니다
Input 컴포넌트 내부에서 useId()로 자체 id를 생성하여 전달된 id를 무시합니다.
id='address' 속성을 제거해 코드 노이즈를 줄여주세요.
🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/components/AddressInput.tsx around lines 51
to 56, the id='address' prop passed to the Input component is ignored because
the component generates its own id internally using useId(). Remove the
id='address' prop from the Input component to eliminate unnecessary code noise.
| <div className='rounded-lg bg-gray-50 p-4'> | ||
| <div className='grid grid-cols-1 items-center gap-10 sm:grid-cols-4'> |
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)
반응형 그리드 레이아웃 개선 제안
현재 그리드 레이아웃이 sm:grid-cols-4로 설정되어 있는데, 제거 버튼이 있을 때와 없을 때의 공간 활용을 더 효율적으로 할 수 있습니다.
- <div className='grid grid-cols-1 items-center gap-10 sm:grid-cols-4'>
+ <div className={`grid grid-cols-1 items-center gap-10 ${isRemovable ? 'sm:grid-cols-4' : 'sm:grid-cols-3'}`}>그리고 각 Input div의 colspan도 조건부로 조정:
- <div className='sm:col-span-1'>
+ <div className={isRemovable ? 'sm:col-span-1' : 'sm:col-span-1'}>Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/components/ScheduleSelect.tsx around lines
32 to 33, the current responsive grid layout uses a fixed sm:grid-cols-4 which
does not optimize space when the remove button is present or absent. Adjust the
grid template columns dynamically based on the presence of the remove button to
better utilize space. Also, update the colspan of each input div conditionally
to reflect these layout changes for improved responsiveness and alignment.
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.
만드시느라 고생많으셨습니다! 양이 굉장히 많은데 금방 구현하시고 대단합니다!
| return ( | ||
| <div className={`group relative ${className}`}> | ||
| <div className='aspect-square w-full overflow-hidden rounded-lg'> | ||
| <img |
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.
요 부분은 Image 태그말고 img를 쓰신 이유가 있으신가요?
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.
리팩토링 과정에서 변경하도록하겠습니다!
LeeCh0129
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
신규 기능
스타일
기타