@@ -39,7 +51,7 @@ export default function Title({
diff --git a/src/app/(with-header)/myactivity/[id]/components/EditActivityForm.tsx b/src/app/(with-header)/myactivity/[id]/components/EditActivityForm.tsx
new file mode 100644
index 0000000..fd61910
--- /dev/null
+++ b/src/app/(with-header)/myactivity/[id]/components/EditActivityForm.tsx
@@ -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
로딩 중...
;
+ if (error) return
오류가 발생했습니다: {error.message}
;
+
+ return (
+
+ );
+}
diff --git a/src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts b/src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts
new file mode 100644
index 0000000..d30c454
--- /dev/null
+++ b/src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts
@@ -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';
+
+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
(null);
+ const [originalSubImages, setOriginalSubImages] = useState(
+ [],
+ );
+ const [subImages, setSubImages] = useState([]);
+ const [originalSchedules, setOriginalSchedules] = useState([]);
+ const [dates, setDates] = useState([]);
+
+ const { data, isLoading, error } = useQuery({
+ 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);
+ }
+ }
+ }
+
+ 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);
+
+ const payload = {
+ title,
+ category,
+ description,
+ address,
+ price,
+ bannerImageUrl,
+ subImageIdsToRemove,
+ subImageUrlsToAdd,
+ schedulesToAdd: newSchedules,
+ scheduleIdsToRemove,
+ };
+
+ await privateInstance.patch(`/editActivity/${id}`, payload);
+
+ 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 ||
+ '수정에 실패했습니다.',
+ );
+ }
+ };
+
+ return {
+ title,
+ category,
+ price,
+ description,
+ address,
+ mainImage,
+ subImages,
+ dates,
+ isLoading,
+ error,
+ setTitle,
+ setCategory,
+ setPrice,
+ setDescription,
+ setAddress,
+ handleSubImageRemove,
+ handleSubImagesAdd,
+ handleAddDate,
+ handleRemoveDate,
+ handleDateChange,
+ handleMainImageSelect,
+ handleMainImageRemove,
+ handleSubmit,
+ };
+};
diff --git a/src/app/(with-header)/myactivity/[id]/page.tsx b/src/app/(with-header)/myactivity/[id]/page.tsx
new file mode 100644
index 0000000..3c2d7f0
--- /dev/null
+++ b/src/app/(with-header)/myactivity/[id]/page.tsx
@@ -0,0 +1,9 @@
+import EditActivityForm from './components/EditActivityForm';
+
+export default function Page() {
+ return (
+
+
+
+ );
+}
diff --git a/src/app/(with-header)/myactivity/components/ScheduleSelectForm.tsx b/src/app/(with-header)/myactivity/components/ScheduleSelectForm.tsx
index af2254e..4976aef 100644
--- a/src/app/(with-header)/myactivity/components/ScheduleSelectForm.tsx
+++ b/src/app/(with-header)/myactivity/components/ScheduleSelectForm.tsx
@@ -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,
value: string,
) => void;
}
@@ -39,9 +34,8 @@ export function ScheduleSelectForm({
+
1}
onAddDate={onAddDate}
diff --git a/src/app/api/editActivity/[id]/route.ts b/src/app/api/editActivity/[id]/route.ts
new file mode 100644
index 0000000..4eb2526
--- /dev/null
+++ b/src/app/api/editActivity/[id]/route.ts
@@ -0,0 +1,60 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { cookies } from 'next/headers';
+import axios, { AxiosError } from 'axios';
+
+const BACKEND_BASE_URL = process.env.NEXT_PUBLIC_API_SERVER_URL;
+
+interface ErrorResponse {
+ message?: string;
+ error?: string;
+}
+
+export async function PATCH(req: NextRequest) {
+ const cookieStore = await cookies();
+ const accessToken = cookieStore.get('accessToken')?.value;
+
+ const url = new URL(req.url); // <- 여기서 전체 URL 접근
+ const segments = url.pathname.split('/');
+ const id = segments[segments.indexOf('editActivity') + 1]; // editActivity 다음 segment가 id임
+
+ if (!id) {
+ return NextResponse.json(
+ { error: '유효하지 않은 요청입니다.' },
+ { status: 400 },
+ );
+ }
+
+ if (!accessToken) {
+ return NextResponse.json({ message: '액세스 토큰 없음' }, { status: 401 });
+ }
+
+ try {
+ const body = await req.json();
+
+ const response = await axios.patch(
+ `${BACKEND_BASE_URL}/my-activities/${id}`,
+ body,
+ {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+
+ return NextResponse.json(response.data, { status: 200 });
+ } catch (error: unknown) {
+ console.error('체험 등록 에러:', error);
+
+ if (axios.isAxiosError(error)) {
+ const axiosError = error as AxiosError;
+ const status = axiosError.response?.status || 500;
+ const detail = axiosError.response?.data;
+ const errorMessage = detail?.message || detail?.error || '체험 등록 실패';
+
+ return NextResponse.json({ message: errorMessage }, { status });
+ }
+
+ return NextResponse.json({ message: '서버 내부 오류' }, { status: 500 });
+ }
+}
diff --git a/src/types/activityDetailType.ts b/src/types/activityDetailType.ts
index cf1a6b9..e19870e 100644
--- a/src/types/activityDetailType.ts
+++ b/src/types/activityDetailType.ts
@@ -45,7 +45,7 @@ export interface ActivitySubImage {
export interface ActivityDetail {
id: number;
- isOwner : boolean;
+ isOwner: boolean;
userId: number;
title: string;
description: string;
@@ -64,3 +64,24 @@ export interface ActivityDetail {
export interface BookinDateProps {
schedules: ActivitySchedule[];
}
+
+export interface Schedule {
+ id?: number;
+ date: string;
+ startTime: string;
+ endTime: string;
+}
+
+export interface ActivityDetailEdit {
+ title: string;
+ category: string;
+ description: string;
+ address: string;
+ price: number;
+ bannerImageUrl: string;
+ subImages: {
+ id: number;
+ imageUrl: string;
+ }[];
+ schedules: Schedule[];
+}