Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ export default function ActivityDetailForm() {
enabled: !!id && !!year && !!month,
});



if (isLoading || !activityData) {
return <div>로딩 중...</div>;
Copy link
Contributor

Choose a reason for hiding this comment

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

요부분은 나중에 로딩스피너로 바꾸셔도 좋을 것 같습니다!

}
Expand Down
14 changes: 13 additions & 1 deletion src/app/(with-header)/activities/[id]/components/Title.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import ActivityDropdown from '@/components/ActivityDropdown';
import Menu from '@/components/ActivityDropdown/menu';
import Item from '@/components/ActivityDropdown/Item';
import Trigger from '@/components/ActivityDropdown/trigger';
import { useParams } from 'next/navigation';
import { useRouter } from 'next/navigation';
import { useQueryClient } from '@tanstack/react-query';

export default function Title({
title,
Expand All @@ -15,6 +18,15 @@ export default function Title({
address,
isOwner,
}: ActivityDetail) {
const { id } = useParams();
const router = useRouter();
const queryClient = useQueryClient();

const handleEdit = () => {
queryClient.invalidateQueries({ queryKey: ['edit-activity', id] });
router.push(`/myactivity/${id}`);
};

return (
<div className='mb-6 flex items-start justify-between'>
<div className='flex flex-col gap-8'>
Expand All @@ -39,7 +51,7 @@ export default function Title({
<IconDropdown />
</Trigger>
<Menu>
<Item onClick={() => alert('수정')}>수정하기</Item>
<Item onClick={handleEdit}>수정하기</Item>
<Item onClick={() => alert('삭제')}>삭제하기</Item>
</Menu>
</ActivityDropdown>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use client';

import Button from '@/components/Button';
import { InfoSection } from '../../components/InfoSection';
import { ScheduleSelectForm } from '../../components/ScheduleSelectForm';
import { ImageSection } from '../../components/ImageSection';
import { useEditActivityForm } from '../hooks/useEditActivityForm';

interface SubImageType {
id?: number;
url: string | File;
}

export default function EditActivityForm() {
const {
title,
category,
price,
description,
address,
mainImage,
subImages,
dates,
isLoading,
error,
setTitle,
setCategory,
setPrice,
setDescription,
setAddress,
handleSubImageRemove,
handleSubImagesAdd,
handleAddDate,
handleRemoveDate,
handleDateChange,
handleMainImageSelect,
handleMainImageRemove,
handleSubmit,
} = useEditActivityForm();

if (isLoading) return <div>로딩 중...</div>;
if (error) return <div>오류가 발생했습니다: {error.message}</div>;
Comment on lines +41 to +42
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

로딩 및 에러 처리 로직은 기능적으로 올바릅니다.

Early return 패턴을 사용한 것이 좋습니다. 향후 개선을 위해 로딩 스피너나 더 상세한 에러 메시지를 고려해볼 수 있겠습니다.

🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/[id]/components/EditActivityForm.tsx around
lines 41 to 42, the current loading and error handling uses simple text
messages. To improve user experience, replace the plain text with a loading
spinner component for the loading state and enhance the error display by showing
more detailed error information or a styled error message component. This will
make the UI more user-friendly and informative.


return (
<div className='min-h-screen bg-gray-50 px-4 py-8 sm:px-6 lg:px-8'>
<div className='mx-auto max-w-1200 p-4 sm:px-20 lg:p-8'>
<form onSubmit={handleSubmit} className='space-y-8'>
<div className='mb-8 flex items-center justify-between'>
<h1 className='text-3xl font-bold'>체험 수정</h1>
<Button type='submit' variant='primary' className='px-6 py-3'>
수정 완료
</Button>
</div>

<InfoSection
title={title}
category={category}
price={price}
description={description}
address={address}
onTitleChange={setTitle}
onCategoryChange={setCategory}
onPriceChange={(price) => setPrice(Number(price))}
onDescriptionChange={setDescription}
onAddressChange={setAddress}
/>

<ScheduleSelectForm
dates={dates}
onAddDate={handleAddDate}
onRemoveDate={handleRemoveDate}
onDateChange={handleDateChange}
/>

<ImageSection
mainImage={mainImage}
subImage={subImages.map((img: SubImageType) => img.url)}
onMainImageSelect={handleMainImageSelect}
onMainImageRemove={handleMainImageRemove}
onSubImageAdd={handleSubImagesAdd}
onSubImageRemove={handleSubImageRemove}
/>
</form>
</div>
</div>
);
}
198 changes: 198 additions & 0 deletions src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
'use client';

import { useState, useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { privateInstance } from '@/apis/privateInstance';
import { uploadImage } from '../../utils/uploadImage';
import { ActivityDetailEdit, Schedule } from '@/types/activityDetailType';
import { AxiosError } from 'axios';
Comment on lines +3 to +9
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

임포트 순서를 수정해주세요.

파이프라인에서 simple-import-sort/imports 경고가 발생했습니다. 임포트 순서를 정렬해주세요.

🧰 Tools
🪛 GitHub Actions: CI

[warning] 3-3: simple-import-sort/imports: Run autofix to sort these imports!

🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts around
lines 3 to 9, the import statements are not sorted according to the
simple-import-sort/imports rule. Reorder the imports by grouping them into
external packages first (like 'react', 'next/navigation',
'@tanstack/react-query', 'axios'), then absolute imports (starting with '@/'),
and finally relative imports (starting with './' or '../'), sorting each group
alphabetically to fix the warning.


interface SubImageType {
id?: number;
url: string | File;
}

export const useEditActivityForm = () => {
const { id } = useParams() as { id: string };
const router = useRouter();
const queryClient = useQueryClient();

const [title, setTitle] = useState('');
const [category, setCategory] = useState('');
const [price, setPrice] = useState(0);
const [description, setDescription] = useState('');
const [address, setAddress] = useState('');
const [mainImage, setMainImage] = useState<File | string | null>(null);
const [originalSubImages, setOriginalSubImages] = useState<SubImageType[]>(
[],
);
const [subImages, setSubImages] = useState<SubImageType[]>([]);
const [originalSchedules, setOriginalSchedules] = useState<Schedule[]>([]);
const [dates, setDates] = useState<Schedule[]>([]);

const { data, isLoading, error } = useQuery<ActivityDetailEdit, Error>({
queryKey: ['edit-activity', id],
queryFn: async () => {
const res = await privateInstance.get(`/activities/${id}`);
return res.data;
},
enabled: !!id,
});

useEffect(() => {
if (data) {
setTitle(data.title);
setCategory(data.category);
setPrice(data.price);
setDescription(data.description);
setAddress(data.address);
setMainImage(data.bannerImageUrl);

const mappedSubImages: SubImageType[] =
data.subImages?.map((img) => ({
id: img.id,
url: img.imageUrl,
})) ?? [];

setOriginalSubImages(mappedSubImages);
setSubImages(mappedSubImages);

setOriginalSchedules(data.schedules ?? []);
setDates(data.schedules ?? []);
}
}, [data]);

const handleSubImageRemove = (index: number) => {
setSubImages((prev) => prev.filter((_, i) => i !== index));
};

const handleSubImagesAdd = (newFiles: File[]) => {
const remainingSlots = 4 - subImages.length;
const filesToAdd = newFiles.slice(0, remainingSlots);
const newSubImages = filesToAdd.map((file) => ({ url: file }));
setSubImages((prev) => [...prev, ...newSubImages]);
};

const handleAddDate = () => {
setDates([...dates, { date: '', startTime: '', endTime: '' }]);
};

const handleRemoveDate = (index: number) => {
setDates(dates.filter((_, i) => i !== index));
};

const handleDateChange = (
index: number,
field: keyof Schedule,
value: string,
) => {
setDates((prev) =>
prev.map((item, i) => (i === index ? { ...item, [field]: value } : item)),
);
};

const handleMainImageSelect = (file: File) => {
setMainImage(file);
};

const handleMainImageRemove = () => {
setMainImage(null);
};

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

try {
let bannerImageUrl = '';
if (typeof mainImage === 'string') {
bannerImageUrl = mainImage;
} else if (mainImage instanceof File) {
bannerImageUrl = await uploadImage(mainImage);
}

const subImageIdsToRemove = originalSubImages
.filter((orig) => !subImages.some((img) => img.id === orig.id))
.map((img) => img.id)
.filter((id): id is number => id !== undefined);

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);
}
}
}
Comment on lines +111 to +130
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

이미지 업로드 실패에 대한 에러 처리를 추가해보세요.

uploadImage 함수 호출 시 실패할 경우에 대한 에러 처리가 없습니다. 네트워크 오류나 서버 문제로 이미지 업로드가 실패하면 전체 폼 제출이 실패할 수 있습니다.

       } else if (mainImage instanceof File) {
-        bannerImageUrl = await uploadImage(mainImage);
+        try {
+          bannerImageUrl = await uploadImage(mainImage);
+        } catch (uploadError) {
+          console.error('메인 이미지 업로드 실패:', uploadError);
+          throw new Error('메인 이미지 업로드에 실패했습니다.');
+        }
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
bannerImageUrl = await uploadImage(mainImage);
}
const subImageIdsToRemove = originalSubImages
.filter((orig) => !subImages.some((img) => img.id === orig.id))
.map((img) => img.id)
.filter((id): id is number => id !== undefined);
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);
}
}
}
} else if (mainImage instanceof File) {
try {
bannerImageUrl = await uploadImage(mainImage);
} catch (uploadError) {
console.error('메인 이미지 업로드 실패:', uploadError);
throw new Error('메인 이미지 업로드에 실패했습니다.');
}
}
const subImageIdsToRemove = originalSubImages
.filter((orig) => !subImages.some((img) => img.id === orig.id))
.map((img) => img.id)
.filter((id): id is number => id !== undefined);
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);
}
}
}
🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts around
lines 111 to 130, the calls to uploadImage lack error handling, which can cause
the entire form submission to fail silently if an upload fails. Wrap each
uploadImage call in a try-catch block to catch any errors during image upload,
handle or log the error appropriately, and prevent the failure from stopping the
entire process.


const newSchedules = dates.filter((d) => !d.id);

const scheduleIdsToRemove = originalSchedules
.filter((orig) => !dates.some((d) => d.id === orig.id))
.map((d) => d.id)
.filter((id): id is number => id !== undefined);
Comment on lines +114 to +137
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

복잡한 배열 비교 로직을 단순화하는 것을 고려해보세요.

서브 이미지와 일정의 추가/제거를 판단하는 로직이 복잡하여 버그가 발생할 가능성이 있습니다. 헬퍼 함수로 분리하거나 더 명확한 로직으로 리팩토링하는 것을 권장합니다.

예시:

const getSubImageChanges = (original: SubImageType[], current: SubImageType[]) => {
  const idsToRemove = original
    .filter(orig => !current.some(img => img.id === orig.id))
    .map(img => img.id)
    .filter((id): id is number => id !== undefined);
    
  const urlsToAdd = current
    .filter(img => !img.id)
    .map(img => img.url);
    
  return { idsToRemove, urlsToAdd };
};
🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts between
lines 114 and 137, the logic for determining which sub-images and schedules to
add or remove is complex and repetitive. Refactor this by extracting the array
comparison logic into reusable helper functions that clearly separate the
concerns of identifying items to remove and items to add. For example, create a
function that takes original and current arrays and returns ids to remove and
urls or new items to add, simplifying the main code and reducing potential bugs.


const payload = {
title,
category,
description,
address,
price,
bannerImageUrl,
subImageIdsToRemove,
subImageUrlsToAdd,
schedulesToAdd: newSchedules,
scheduleIdsToRemove,
};

await privateInstance.patch(`/editActivity/${id}`, payload);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

API 엔드포인트 경로를 확인해주세요.

현재 /editActivity/${id}로 요청하고 있지만, 실제 API 라우트는 /api/editActivity/${id}에 정의되어 있습니다. 이로 인해 404 에러가 발생할 수 있습니다.

-      await privateInstance.patch(`/editActivity/${id}`, payload);
+      await privateInstance.patch(`/api/editActivity/${id}`, payload);
📝 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
await privateInstance.patch(`/editActivity/${id}`, payload);
await privateInstance.patch(`/api/editActivity/${id}`, payload);
🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts at line
152, the API endpoint used in the patch request is missing the '/api' prefix.
Update the endpoint from `/editActivity/${id}` to `/api/editActivity/${id}` to
match the actual API route and prevent 404 errors.


alert('수정이 완료되었습니다.'); //토스트로 대체
queryClient.invalidateQueries({ queryKey: ['activity', id] });
router.push(`/activities/${id}`);
} catch (err) {
const error = err as AxiosError;
const responseData = error.response?.data as
| { error?: string; message?: string }
| undefined;
console.error('전체 에러:', error);
alert(
//토스트로대체
responseData?.error ||
responseData?.message ||
error.message ||
'수정에 실패했습니다.',
);
Comment on lines +154 to +169
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

사용자 피드백을 토스트 알림으로 개선해보세요.

코드 주석에서 언급했듯이 alert() 대신 토스트 알림을 사용하는 것이 더 나은 UX를 제공합니다. 모던 웹 애플리케이션에서 브라우저 기본 alert은 권장되지 않습니다.

🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts around
lines 154 to 169, replace the alert() calls used for success and error messages
with a toast notification system to improve user experience. Identify the
existing toast utility or library in the project and use it to show success and
error messages instead of alert(), ensuring non-blocking, user-friendly
feedback.

}
};
Comment on lines +103 to +171
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

폼 제출 중 로딩 상태 관리를 추가해보세요.

현재 폼 제출 중에 사용자가 중복 제출을 할 수 있고, 진행 상황을 알 수 없습니다. 로딩 상태를 관리하여 UX를 개선하는 것을 권장합니다.

+  const [isSubmitting, setIsSubmitting] = useState(false);
+
   const handleSubmit = async (e: React.FormEvent) => {
     e.preventDefault();
+    setIsSubmitting(true);

     try {
       // ... 기존 로직
     } catch (err) {
       // ... 에러 처리
+    } finally {
+      setIsSubmitting(false);
     }
   };

그리고 반환 객체에 isSubmitting을 추가해주세요.

🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts between
lines 103 and 171, the form submission handler lacks loading state management,
allowing users to submit multiple times and not see progress. Add a state
variable, e.g., isSubmitting, initialized to false. Set isSubmitting to true at
the start of handleSubmit and reset it to false in both success and error paths
to prevent duplicate submissions. Also, return isSubmitting from the hook so the
component can use it to disable the submit button and show loading indicators.


return {
title,
category,
price,
description,
address,
mainImage,
subImages,
dates,
isLoading,
error,
setTitle,
setCategory,
setPrice,
setDescription,
setAddress,
handleSubImageRemove,
handleSubImagesAdd,
handleAddDate,
handleRemoveDate,
handleDateChange,
handleMainImageSelect,
handleMainImageRemove,
handleSubmit,
};
};
9 changes: 9 additions & 0 deletions src/app/(with-header)/myactivity/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import EditActivityForm from './components/EditActivityForm';

export default function Page() {
return (
<div>
<EditActivityForm />
</div>
);
}
Comment on lines +1 to +9
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 페이지 컴포넌트로서 적절한 구조를 가지고 있습니다. EditActivityForm 컴포넌트를 렌더링하는 간단한 래퍼 역할을 수행합니다.

선택적 개선사항: 불필요한 래퍼 div 제거

EditActivityForm이 자체 컨테이너 스타일링을 가지고 있다면 래퍼 div는 제거할 수 있습니다:

 export default function Page() {
-  return (
-    <div>
-      <EditActivityForm />
-    </div>
-  );
+  return <EditActivityForm />;
 }
📝 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
import EditActivityForm from './components/EditActivityForm';
export default function Page() {
return (
<div>
<EditActivityForm />
</div>
);
}
import EditActivityForm from './components/EditActivityForm';
export default function Page() {
return <EditActivityForm />;
}
🤖 Prompt for AI Agents
In src/app/(with-header)/myactivity/[id]/page.tsx lines 1 to 9, the wrapper div
around EditActivityForm is unnecessary if EditActivityForm already handles its
own container styling. Remove the outer div and return EditActivityForm directly
from the Page component to simplify the structure.

14 changes: 4 additions & 10 deletions src/app/(with-header)/myactivity/components/ScheduleSelectForm.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
'use client';

import { ScheduleSelect } from './ScheduleSelect';

interface ScheduleType {
date: string;
startTime: string;
endTime: string;
}
import { Schedule } from '@/types/activityDetailType';

interface ScheduleSelectFormProps {
dates: ScheduleType[];
dates: Schedule[];
onAddDate: () => void;
onRemoveDate: (index: number) => void;
onDateChange: (
index: number,
field: keyof ScheduleType,
field: keyof Omit<Schedule, 'id'>,
value: string,
) => void;
}
Expand All @@ -39,9 +34,8 @@ export function ScheduleSelectForm({
</div>

{dates.map((dateSlot, idx) => (
<div className='flex'>
<div key={dateSlot.id ?? idx} className='flex'>
<ScheduleSelect
key={idx}
index={idx}
isRemovable={dates.length > 1}
onAddDate={onAddDate}
Expand Down
Loading