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
16 changes: 15 additions & 1 deletion src/app/(with-header)/myactivity/components/ImagePreview.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import IconClose from '@assets/svg/close';
import { useState,useEffect } from 'react';

interface ImagePreviewProps {
image: File | string;
Expand All @@ -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 (
<div className={`group relative ${className}`}>
Expand Down
119 changes: 69 additions & 50 deletions src/app/(with-header)/myactivity/components/ReservationForm.tsx
Original file line number Diff line number Diff line change
@@ -1,66 +1,34 @@
'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<DateSlot[]>([
{ date: '', startTime: '', endTime: '' },
]);
const [mainImage, setMainImage] = useState<File | string | null>(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: '' }]);
};
Expand All @@ -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 (
<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'>
Expand Down
14 changes: 14 additions & 0 deletions src/app/(with-header)/myactivity/utils/uploadImage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { privateInstance } from '@/apis/privateInstance';

export async function uploadImage(file: File): Promise<string> {
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, '');
}
69 changes: 69 additions & 0 deletions src/app/api/addActivity/route.ts
Original file line number Diff line number Diff line change
@@ -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<ErrorResponse>;
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 });
}
}
65 changes: 65 additions & 0 deletions src/app/api/image/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
}