Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"next": "15.3.5",
"react": "^19.0.0",
"react-datepicker": "^8.4.0",
"react-daum-postcode": "^3.2.0",
"react-dom": "^19.0.0",
"react-kakao-maps-sdk": "^1.2.0",
"tailwind-merge": "^3.3.1",
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

70 changes: 70 additions & 0 deletions src/app/(with-header)/myactivity/components/AddressInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
'use client';

import Modal from '@/components/Modal';
import DaumPostcode from 'react-daum-postcode';
import { useState } from 'react';
import Input from '@/components/Input';
import Button from '@/components/Button';

interface AddressInputProps {
onAddressChange: (address: string) => void;
address: string;
}

export default function AddressInput({
onAddressChange,
address,
}: AddressInputProps) {
const [isOpen, setIsOpen] = useState(false);

const handleComplete = (data: any) => {
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);
};

return (
<div>
<Input
label='주소'
id='address'
value={address}
onClick={() => setIsOpen(true)}
readOnly
/>
Comment on lines +51 to +56
Copy link

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.

<Modal isOpen={isOpen} onOpenChange={setIsOpen}>
<Modal.Content>
<Modal.Header>
<Modal.Title>주소 검색</Modal.Title>
<Modal.Close />
</Modal.Header>
<Modal.Item>
<DaumPostcode onComplete={handleComplete} />
</Modal.Item>
<Modal.Footer>
<Button
variant='primary'
className='py-8'
onClick={() => setIsOpen(false)}
>
닫기
</Button>
</Modal.Footer>
</Modal.Content>
</Modal>
</div>
);
}
37 changes: 37 additions & 0 deletions src/app/(with-header)/myactivity/components/CategoryInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
interface CategoryProps {
category?: string;

onCategoryChange: (value: string) => void;
}

export default function CategoryInput({
category,
onCategoryChange,
}: CategoryProps) {
return (
<div>
<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'
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
<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.

value={category}
onChange={(e) => onCategoryChange(e.target.value)}
>
<option value=''>카테고리를 선택해주세요</option>
<option value='문화예술'>문화/예술</option>
<option value='푸드'>푸드</option>
<option value='스포츠'>스포츠</option>
<option value='투어'>투어</option>
<option value='관광'>관광</option>
<option value='웰빙'>웰빙</option>
</select>
</div>
</div>
);
}
27 changes: 27 additions & 0 deletions src/app/(with-header)/myactivity/components/FormSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type React from 'react';

interface FormSectionProps {
title: string;
children: React.ReactNode;
description?: string;
}

export function FormSection({
title,
children,
description,
}: FormSectionProps) {
return (
<div className='space-y-6'>
<div>
<h2 className='border-b border-gray-200 pb-2 text-xl font-semibold text-gray-900'>
{title}
</h2>
{description && (
<p className='mt-2 text-sm text-gray-600'>{description}</p>
)}
</div>
{children}
</div>
);
}
39 changes: 39 additions & 0 deletions src/app/(with-header)/myactivity/components/ImagePreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use client';

import IconClose from '@assets/svg/close';

interface ImagePreviewProps {
image: File | string;
onRemove: () => void;
alt: string;
className?: string;
}

export function ImagePreview({
image,
onRemove,
alt,
className = '',
}: ImagePreviewProps) {
const src = typeof image === 'string' ? image : URL.createObjectURL(image);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

메모리 누수 방지를 위한 URL 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.

Suggested change
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.


return (
<div className={`group relative ${className}`}>
<div className='aspect-square w-full overflow-hidden rounded-lg'>
<img
Copy link
Contributor

Choose a reason for hiding this comment

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

요 부분은 Image 태그말고 img를 쓰신 이유가 있으신가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

리팩토링 과정에서 변경하도록하겠습니다!

src={src || '/placeholder.svg'}
className='h-full w-full object-cover'
alt={alt}
/>
Comment on lines +23 to +27
Copy link

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.

</div>
<button
type='button'
onClick={onRemove}
className='absolute top-2 right-2 rounded-full bg-gray-600 p-1 text-white transition-colors hover:bg-gray-700'
aria-label='이미지 삭제'
>
<IconClose />
</button>
</div>
);
}
38 changes: 38 additions & 0 deletions src/app/(with-header)/myactivity/components/ImageSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client';

import { MainImageSelect } from './MainImageSelect';
import { SubImageSelect } from './SubImageSelect';

interface ImagesSectionProps {
mainImage: string | File | null;
subImage: (string | File)[];
onMainImageSelect: (file: File) => void;
onMainImageRemove: () => void;
onSubImageAdd: (files: File[]) => void;
onSubImageRemove: (index: number) => void;
}
Comment on lines +6 to +13
Copy link

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.


export function ImageSection({
mainImage,
subImage,
onMainImageSelect,
onMainImageRemove,
onSubImageAdd,
onSubImageRemove,
}: ImagesSectionProps) {
return (
<div className='space-y-8'>
<MainImageSelect
mainImage={mainImage}
onImageSelect={onMainImageSelect}
onImageRemove={onMainImageRemove}
/>

<SubImageSelect
subImage={subImage}
onImagesAdd={onSubImageAdd}
onImageRemove={onSubImageRemove}
/>
</div>
Comment on lines +24 to +36
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

ESLint 경고: prop 정렬
CI 로그에 나온 react/jsx-sort-props 경고(28–34행)는 정렬만으로 해결됩니다. 팀 규칙을 따른다면 즉시 수정해 주세요.

🧰 Tools
🪛 GitHub Actions: CI

[warning] 28-34: react/jsx-sort-props: Props should be sorted alphabetically.

🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/components/ImageSection.tsx between lines 24
and 36, the JSX props in the MainImageSelect and SubImageSelect components are
not sorted alphabetically, causing an ESLint react/jsx-sort-props warning. To
fix this, reorder the props in each component so that their names are in
alphabetical order according to the team's linting rules.

);
}
50 changes: 50 additions & 0 deletions src/app/(with-header)/myactivity/components/ImageUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use client';

import type React from 'react';

interface ImageUploadProps {
onImageSelect: (file: File) => void;
multiple?: boolean;
className?: string;
children?: React.ReactNode;
}

export function ImageUpload({
onImageSelect,
multiple = false,
className = '',
children,
}: ImageUploadProps) {
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
onImageSelect(file);
}
};
Comment on lines +18 to +23
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

multiple prop과 파일 처리 로직 불일치

multiple prop을 받지만 첫 번째 파일만 처리합니다. 일관성을 위해 수정이 필요합니다.

다음 중 하나를 선택하여 수정하세요:

옵션 1: multiple 지원하지 않음 (현재 사용 패턴에 맞음)

 interface ImageUploadProps {
   onImageSelect: (file: File) => void;
-  multiple?: boolean;
   className?: string;
   children?: React.ReactNode;
 }

 export function ImageUpload({
   onImageSelect,
-  multiple = false,
   className = '',
   children,
 }: ImageUploadProps) {

옵션 2: multiple 완전 지원

- onImageSelect: (file: File) => void;
+ onImageSelect: (files: File[]) => void;

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-   const file = e.target.files?.[0];
-   if (file) {
-     onImageSelect(file);
-   }
+   const files = Array.from(e.target.files || []);
+   if (files.length > 0) {
+     onImageSelect(multiple ? files : [files[0]]);
+   }
  };

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

🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/components/ImageUpload.tsx around lines 18
to 23, the handleFileChange function currently only processes the first selected
file despite the component accepting a multiple prop. To fix this, either remove
the multiple prop if multiple file selection is not intended, or update the
function to handle all selected files by iterating over e.target.files and
calling onImageSelect for each file to fully support multiple file uploads.


return (
<label className={`group cursor-pointer ${className}`}>
<div className='flex aspect-square w-full flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 transition-colors hover:border-green-400 hover:bg-green-50'>
{children || (
<>
<h2 className='text-xl text-gray-600 group-hover:text-green-600'>
+
</h2>
<span className='text-center text-xs font-medium text-gray-600 group-hover:text-green-600'>
이미지
<br />
등록
</span>
</>
)}
</div>
<input
type='file'
accept='image/*'
multiple={multiple}
className='hidden'
onChange={handleFileChange}
/>
</label>
);
}
85 changes: 85 additions & 0 deletions src/app/(with-header)/myactivity/components/InfoSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
'use client';

import Input from '@/components/Input';
import AddressInput from './AddressInput';
import CategoryInput from './CategoryInput';

interface InfoSectionProps {
title?: string;
category?: string;
price?: number;
description?: string;
address?: string;
onTitleChange: (value: string) => void;
onCategoryChange: (value: string) => void;
onPriceChange: (value: string) => void;
onDescriptionChange: (value: string) => void;
onAddressChange: (value: string) => void;
}

export function InfoSection({
title = '',
category = '',
price = 0,
description = '',
address = '',
onTitleChange,
onCategoryChange,
onPriceChange,
onDescriptionChange,
onAddressChange,
}: InfoSectionProps) {
return (
<section className='space-y-6'>
<div className='grid grid-cols-1 gap-6 sm:grid-cols-2'>
<div className='sm:col-span-2'>
<Input
label='제목'
id='title'
type='text'
placeholder='체험의 제목을 입력해주세요'
className='w-full'
value={title}
onChange={(e) => onTitleChange(e.target.value)}
/>
</div>

<CategoryInput
category={category}
onCategoryChange={onCategoryChange}
/>

<div>
<div className='relative'>
<Input
label='가격'
type='number'
placeholder='0'
className='w-full appearance-none'
value={price}
onChange={(e) => onPriceChange(e.target.value)}
/>
<span className='absolute top-1/2 right-4 -translate-y-1/2 transform text-gray-500'>
</span>
</div>
</div>
</div>

<div>
<Input
label='설명'
type='textarea'
placeholder='체험에 대한 자세한 설명을 입력해주세요'
className='w-full'
value={description}
onChange={(e) => onDescriptionChange(e.target.value)}
/>
</div>

<div>
<AddressInput onAddressChange={onAddressChange} address={address} />
</div>
</section>
);
}
Loading
Loading