diff --git a/src/app/(with-header)/myactivity/components/ImagePreview.tsx b/src/app/(with-header)/myactivity/components/ImagePreview.tsx index 8c16632..e824386 100644 --- a/src/app/(with-header)/myactivity/components/ImagePreview.tsx +++ b/src/app/(with-header)/myactivity/components/ImagePreview.tsx @@ -1,6 +1,7 @@ 'use client'; import IconClose from '@assets/svg/close'; +import { useState,useEffect } from 'react'; interface ImagePreviewProps { image: File | string; @@ -15,7 +16,20 @@ export function ImagePreview({ alt, className = '', }: ImagePreviewProps) { - const src = typeof image === 'string' ? image : URL.createObjectURL(image); + const [src, setSrc] = useState(''); + + useEffect(() => { + if (typeof image === 'string') { + setSrc(image); + return; + } + const objectUrl = URL.createObjectURL(image); + setSrc(objectUrl); + + return () => { + URL.revokeObjectURL(objectUrl); + }; + }, [image]); return (
diff --git a/src/app/(with-header)/myactivity/components/ReservationForm.tsx b/src/app/(with-header)/myactivity/components/ReservationForm.tsx index dbe8021..0591407 100644 --- a/src/app/(with-header)/myactivity/components/ReservationForm.tsx +++ b/src/app/(with-header)/myactivity/components/ReservationForm.tsx @@ -1,38 +1,21 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState } from 'react'; +import axios from 'axios'; import type React from 'react'; import { InfoSection } from './InfoSection'; import { ScheduleSelectForm } from './ScheduleSelectForm'; import { ImageSection } from './ImageSection'; import Button from '@/components/Button'; +import { uploadImage } from '../utils/uploadImage'; +import { privateInstance } from '@/apis/privateInstance'; interface DateSlot { date: string; startTime: string; endTime: string; } -const mockData = { - title: '함께 배우면 즐거운 스트릿댄스', - category: '투어', - description: '둠칫 둠칫 두둠칫', - address: '서울특별시 강남구 테헤란로 427', - price: 10000, - schedules: [ - { date: '2023-12-01', startTime: '12:00', endTime: '13:00' }, - { date: '2023-12-05', startTime: '12:00', endTime: '13:00' }, - { date: '2023-12-05', startTime: '13:00', endTime: '14:00' }, - { date: '2023-12-05', startTime: '14:00', endTime: '15:00' }, - ], - bannerImageUrl: '/test/image1.png', - subImageUrls: [ - '/test/image2.png', - '/test/image3.png', - '/test/image4.png', - '/test/image5.png', - ], -}; export default function ReservationForm() { const [dates, setDates] = useState([ @@ -40,27 +23,12 @@ export default function ReservationForm() { ]); const [mainImage, setMainImage] = useState(null); const [subImage, setSubImage] = useState<(File | string)[]>([]); - const [title, setTitle] = useState(''); const [category, setCategory] = useState(''); const [price, setPrice] = useState(0); const [description, setDescription] = useState(''); const [address, setAddress] = useState(''); - useEffect(() => { - // mock데이터로 수정페이지용 테스트 - setTimeout(() => { - setTitle(mockData.title); - setCategory(mockData.category); - setPrice(mockData.price); - setDescription(mockData.description); - setAddress(mockData.address); - setDates(mockData.schedules); - setMainImage(mockData.bannerImageUrl); - setSubImage(mockData.subImageUrls); - }, 500); - }, []); - const handleAddDate = () => { setDates([...dates, { date: '', startTime: '', endTime: '' }]); }; @@ -80,37 +48,88 @@ export default function ReservationForm() { setDates(updatedDates); }; - const handleMainImageSelect = (file: File) => { - setMainImage(file); + const handleMainImageSelect = async (file: File) => { + try { + const url = await uploadImage(file); + setMainImage(url); + } catch (err) { + console.error(err); + alert('메인 이미지 업로드에 실패했습니다.'); + } }; const handleMainImageRemove = () => { setMainImage(null); }; - const handleSubImagesAdd = (newFiles: File[]) => { + const handleSubImagesAdd = async (newFiles: File[]) => { const remainingSlots = 4 - subImage.length; const filesToAdd = newFiles.slice(0, remainingSlots); - setSubImage([...subImage, ...filesToAdd]); + + try { + const uploadPromises = filesToAdd.map((file) => uploadImage(file)); + const uploadedUrls = await Promise.all(uploadPromises); + setSubImage([...subImage, ...uploadedUrls]); + } catch (err) { + console.error('서브 이미지 업로드 실패', err); + alert('서브 이미지 업로드 중 문제가 발생.'); + } }; const handleSubImageRemove = (index: number) => { setSubImage(subImage.filter((_, i) => i !== index)); }; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - console.log('title:', title); - console.log('category:', category); - console.log('price:', price); - console.log('description:', description); - console.log('address:', address); - console.log('dates:', dates); - console.log('mainImage:', mainImage); - console.log('subImage:', subImage); + if (!mainImage) { + alert('메인 이미지를 업로드해주세요.'); //추후 토스트나 팝업으로 대체 + return; + } + + if ( + !title || + !category || + !description || + !address || + !price || + dates.length === 0 + ) { + alert('모든 필드를 입력해주세요.'); //추후 토스트나 팝업으로 대체 + return; + } + + const payload = { + title, + category, + description, + address, + price, + schedules: dates, + bannerImageUrl: mainImage, + subImageUrls: subImage, + }; + + try { + const response = await privateInstance.post('/addActivity', payload); + console.log('등록 성공:', response.data); + alert('체험이 성공적으로 등록되었습니다!'); //추후 토스트나 팝업으로 대체 + } catch (err) { + console.error('체험 등록 실패:', err); + + if (axios.isAxiosError(err)) { + const detailMessage = + err.response?.data?.detail?.message || + err.response?.data?.message || + '체험 등록 중 오류가 발생했습니다.'; + + alert(detailMessage); //추후 토스트나 팝업으로 대체 + } else { + alert('알 수 없는 오류가 발생했습니다.'); //추후 토스트나 팝업으로 대체 + } + } }; - return (
diff --git a/src/app/(with-header)/myactivity/utils/uploadImage.ts b/src/app/(with-header)/myactivity/utils/uploadImage.ts new file mode 100644 index 0000000..dc4d69e --- /dev/null +++ b/src/app/(with-header)/myactivity/utils/uploadImage.ts @@ -0,0 +1,14 @@ +import { privateInstance } from '@/apis/privateInstance'; + +export async function uploadImage(file: File): Promise { + const formData = new FormData(); + formData.append('image', file); + + const res = await privateInstance.post(`/image`, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + return res.data.replace(/^"+|"+$/g, ''); +} diff --git a/src/app/api/addActivity/route.ts b/src/app/api/addActivity/route.ts new file mode 100644 index 0000000..a193cc5 --- /dev/null +++ b/src/app/api/addActivity/route.ts @@ -0,0 +1,69 @@ +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 POST(req: NextRequest) { + const cookieStore = await cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + + if (!accessToken) { + return NextResponse.json({ message: '액세스 토큰 없음' }, { status: 401 }); + } + + try { + const body = await req.json(); + const { + title, + category, + description, + address, + price, + schedules, + bannerImageUrl, + subImageUrls, + } = body; + + const response = await axios.post( + `${BACKEND_BASE_URL}/activities`, + { + title, + category, + description, + address, + price, + schedules, + bannerImageUrl, + subImageUrls, + }, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + return NextResponse.json(response.data, { status: 201 }); + } 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/app/api/image/route.ts b/src/app/api/image/route.ts new file mode 100644 index 0000000..0a8c2a2 --- /dev/null +++ b/src/app/api/image/route.ts @@ -0,0 +1,65 @@ +import { cookies } from 'next/headers'; +import { NextRequest, NextResponse } from 'next/server'; + +export const config = { + api: { + bodyParser: false, + }, +}; + +const BACKEND_BASE_URL = process.env.NEXT_PUBLIC_API_SERVER_URL; + +export async function POST(req: NextRequest) { + const cookieStore = await cookies(); + const accessToken = cookieStore.get('accessToken')?.value; + + if (!accessToken) { + return NextResponse.json({ message: '액세스 토큰 없음' }, { status: 401 }); + } + + try { + const formData = await req.formData(); + const file = formData.get('image'); + + if (!(file instanceof File)) { + return NextResponse.json( + { message: '이미지 파일 없음' }, + { status: 400 }, + ); + } + + const uploadForm = new FormData(); + uploadForm.append('image', file); + + const backendRes = await fetch(`${BACKEND_BASE_URL}/activities/image`, { + method: 'POST', + body: uploadForm, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + + if (!backendRes.ok) { + return NextResponse.json( + { message: '백엔드 이미지 업로드 실패' }, + { status: backendRes.status }, + ); + } + + const result = await backendRes.json(); + + if (!result.activityImageUrl) { + return NextResponse.json( + { message: '이미지 URL이 백엔드 응답에 없습니다.' }, + { status: 500 }, + ); + } + + const Image = JSON.stringify(result.activityImageUrl); + + return NextResponse.json(Image, { status: 200 }); + } catch (error) { + console.error('업로드 중 에러:', error); + return NextResponse.json({ message: '서버 내부 오류' }, { status: 500 }); + } +}