diff --git a/package.json b/package.json
index 3655817..a22c969 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index fa44db3..3748b45 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -35,6 +35,9 @@ importers:
react-datepicker:
specifier: ^8.4.0
version: 8.4.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ react-daum-postcode:
+ specifier: ^3.2.0
+ version: 3.2.0(react@19.1.0)
react-dom:
specifier: ^19.0.0
version: 19.1.0(react@19.1.0)
@@ -1769,6 +1772,11 @@ packages:
react: ^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc
react-dom: ^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc
+ react-daum-postcode@3.2.0:
+ resolution: {integrity: sha512-NHY8TUicZXMqykbKYT8kUo2PEU7xu1DFsdRmyWJrLEUY93Xhd3rEdoJ7vFqrvs+Grl9wIm9Byxh3bI+eZxepMQ==}
+ peerDependencies:
+ react: '>=16.8.0'
+
react-dom@19.1.0:
resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
peerDependencies:
@@ -3782,6 +3790,10 @@ snapshots:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
+ react-daum-postcode@3.2.0(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+
react-dom@19.1.0(react@19.1.0):
dependencies:
react: 19.1.0
diff --git a/src/app/(with-header)/myactivity/components/AddressInput.tsx b/src/app/(with-header)/myactivity/components/AddressInput.tsx
new file mode 100644
index 0000000..261bca0
--- /dev/null
+++ b/src/app/(with-header)/myactivity/components/AddressInput.tsx
@@ -0,0 +1,79 @@
+'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;
+}
+
+interface PostcodeData {
+ address: string;
+ addressType: 'R' | 'J';
+ bname: string;
+ buildingName: string;
+ zonecode: string;
+ userSelectedType: string;
+}
+
+export default function AddressInput({
+ onAddressChange,
+ address,
+}: AddressInputProps) {
+ const [isOpen, setIsOpen] = useState(false);
+
+ const handleComplete = (data: PostcodeData) => {
+ 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 (
+
+ setIsOpen(true)}
+ readOnly
+ />
+
+
+
+ 주소 검색
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(with-header)/myactivity/components/CategoryInput.tsx b/src/app/(with-header)/myactivity/components/CategoryInput.tsx
new file mode 100644
index 0000000..2a82f3b
--- /dev/null
+++ b/src/app/(with-header)/myactivity/components/CategoryInput.tsx
@@ -0,0 +1,34 @@
+interface CategoryProps {
+ category?: string;
+
+ onCategoryChange: (value: string) => void;
+}
+
+export default function CategoryInput({
+ category,
+ onCategoryChange,
+}: CategoryProps) {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(with-header)/myactivity/components/FormSection.tsx b/src/app/(with-header)/myactivity/components/FormSection.tsx
new file mode 100644
index 0000000..cb33e7c
--- /dev/null
+++ b/src/app/(with-header)/myactivity/components/FormSection.tsx
@@ -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 (
+
+
+
+ {title}
+
+ {description && (
+
{description}
+ )}
+
+ {children}
+
+ );
+}
diff --git a/src/app/(with-header)/myactivity/components/ImagePreview.tsx b/src/app/(with-header)/myactivity/components/ImagePreview.tsx
new file mode 100644
index 0000000..8c16632
--- /dev/null
+++ b/src/app/(with-header)/myactivity/components/ImagePreview.tsx
@@ -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);
+
+ return (
+
+
+

+
+
+
+ );
+}
diff --git a/src/app/(with-header)/myactivity/components/ImageSection.tsx b/src/app/(with-header)/myactivity/components/ImageSection.tsx
new file mode 100644
index 0000000..8b41662
--- /dev/null
+++ b/src/app/(with-header)/myactivity/components/ImageSection.tsx
@@ -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;
+}
+
+export function ImageSection({
+ mainImage,
+ subImage,
+ onMainImageSelect,
+ onMainImageRemove,
+ onSubImageAdd,
+ onSubImageRemove,
+}: ImagesSectionProps) {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/app/(with-header)/myactivity/components/ImageUpload.tsx b/src/app/(with-header)/myactivity/components/ImageUpload.tsx
new file mode 100644
index 0000000..12a9bb9
--- /dev/null
+++ b/src/app/(with-header)/myactivity/components/ImageUpload.tsx
@@ -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) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ onImageSelect(file);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/(with-header)/myactivity/components/InfoSection.tsx b/src/app/(with-header)/myactivity/components/InfoSection.tsx
new file mode 100644
index 0000000..509a110
--- /dev/null
+++ b/src/app/(with-header)/myactivity/components/InfoSection.tsx
@@ -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 (
+
+
+
+ onTitleChange(e.target.value)}
+ />
+
+
+
+
+
+
+ onPriceChange(e.target.value)}
+ />
+
+ 원
+
+
+
+
+
+
+ onDescriptionChange(e.target.value)}
+ />
+
+
+
+
+ );
+}
diff --git a/src/app/(with-header)/myactivity/components/MainImageSelect.tsx b/src/app/(with-header)/myactivity/components/MainImageSelect.tsx
new file mode 100644
index 0000000..781fc43
--- /dev/null
+++ b/src/app/(with-header)/myactivity/components/MainImageSelect.tsx
@@ -0,0 +1,41 @@
+'use client';
+
+import { ImageUpload } from './ImageUpload';
+import { ImagePreview } from './ImagePreview';
+
+interface MainImageSelectProps {
+ mainImage: File | string | null;
+ onImageSelect: (file: File) => void;
+ onImageRemove: () => void;
+}
+
+export function MainImageSelect({
+ mainImage,
+ onImageSelect,
+ onImageRemove,
+}: MainImageSelectProps) {
+ return (
+
+
배너 이미지
+
+ 체험을 대표하는 메인 이미지를 등록해주세요.
+
+
+
+
+
+ {mainImage && (
+
+ )}
+
+
+
+ * 메인 이미지는 1장만 등록할 수 있습니다.
+
+
+ );
+}
diff --git a/src/app/(with-header)/myactivity/components/ReservationForm.tsx b/src/app/(with-header)/myactivity/components/ReservationForm.tsx
new file mode 100644
index 0000000..dbe8021
--- /dev/null
+++ b/src/app/(with-header)/myactivity/components/ReservationForm.tsx
@@ -0,0 +1,164 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+
+import type React from 'react';
+import { InfoSection } from './InfoSection';
+import { ScheduleSelectForm } from './ScheduleSelectForm';
+import { ImageSection } from './ImageSection';
+import Button from '@/components/Button';
+
+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([
+ { date: '', startTime: '', endTime: '' },
+ ]);
+ 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: '' }]);
+ };
+
+ const handleRemoveDate = (index: number) => {
+ setDates(dates.filter((_, i) => i !== index));
+ };
+
+ const handleDateChange = (
+ index: number,
+ field: keyof DateSlot,
+ value: string,
+ ) => {
+ const updatedDates = dates.map((date, i) =>
+ i === index ? { ...date, [field]: value } : date,
+ );
+ setDates(updatedDates);
+ };
+
+ const handleMainImageSelect = (file: File) => {
+ setMainImage(file);
+ };
+
+ const handleMainImageRemove = () => {
+ setMainImage(null);
+ };
+
+ const handleSubImagesAdd = (newFiles: File[]) => {
+ const remainingSlots = 4 - subImage.length;
+ const filesToAdd = newFiles.slice(0, remainingSlots);
+ setSubImage([...subImage, ...filesToAdd]);
+ };
+
+ const handleSubImageRemove = (index: number) => {
+ setSubImage(subImage.filter((_, i) => i !== index));
+ };
+
+ const handleSubmit = (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);
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/(with-header)/myactivity/components/ScheduleSelect.tsx b/src/app/(with-header)/myactivity/components/ScheduleSelect.tsx
new file mode 100644
index 0000000..2cf78f9
--- /dev/null
+++ b/src/app/(with-header)/myactivity/components/ScheduleSelect.tsx
@@ -0,0 +1,78 @@
+'use client';
+
+import Input from '@/components/Input';
+import IconClose from '@assets/svg/close';
+
+interface ScheduleSelectProps {
+ index: number;
+ isRemovable: boolean;
+ onAddDate: () => void;
+ onRemove: (index: number) => void;
+ onDateChange: (index: number, value: string) => void;
+ onStartTimeChange: (index: number, value: string) => void;
+ onEndTimeChange: (index: number, value: string) => void;
+ date: string;
+ startTime: string;
+ endTime: string;
+}
+
+export function ScheduleSelect({
+ index,
+ isRemovable,
+
+ onRemove,
+ onDateChange,
+ onStartTimeChange,
+ onEndTimeChange,
+ date,
+ startTime,
+ endTime,
+}: ScheduleSelectProps) {
+ return (
+
+
+
+ onDateChange(index, e.target.value)}
+ />
+
+
+
+ onStartTimeChange(index, e.target.value)}
+ />
+
+
+
+ onEndTimeChange(index, e.target.value)}
+ />
+
+
+
+ {isRemovable && (
+
+ )}
+
+
+
+ );
+}
diff --git a/src/app/(with-header)/myactivity/components/ScheduleSelectForm.tsx b/src/app/(with-header)/myactivity/components/ScheduleSelectForm.tsx
new file mode 100644
index 0000000..af2254e
--- /dev/null
+++ b/src/app/(with-header)/myactivity/components/ScheduleSelectForm.tsx
@@ -0,0 +1,64 @@
+'use client';
+
+import { ScheduleSelect } from './ScheduleSelect';
+
+interface ScheduleType {
+ date: string;
+ startTime: string;
+ endTime: string;
+}
+
+interface ScheduleSelectFormProps {
+ dates: ScheduleType[];
+ onAddDate: () => void;
+ onRemoveDate: (index: number) => void;
+ onDateChange: (
+ index: number,
+ field: keyof ScheduleType,
+ value: string,
+ ) => void;
+}
+
+export function ScheduleSelectForm({
+ dates,
+ onAddDate,
+ onRemoveDate,
+ onDateChange,
+}: ScheduleSelectFormProps) {
+ return (
+
+
+
+ {dates.map((dateSlot, idx) => (
+
+ 1}
+ onAddDate={onAddDate}
+ onRemove={onRemoveDate}
+ onDateChange={(index, value) => onDateChange(index, 'date', value)}
+ onStartTimeChange={(index, value) =>
+ onDateChange(index, 'startTime', value)
+ }
+ onEndTimeChange={(index, value) =>
+ onDateChange(index, 'endTime', value)
+ }
+ date={dateSlot.date}
+ startTime={dateSlot.startTime}
+ endTime={dateSlot.endTime}
+ />
+
+ ))}
+
+ );
+}
diff --git a/src/app/(with-header)/myactivity/components/SubImageSelect.tsx b/src/app/(with-header)/myactivity/components/SubImageSelect.tsx
new file mode 100644
index 0000000..b7e43d7
--- /dev/null
+++ b/src/app/(with-header)/myactivity/components/SubImageSelect.tsx
@@ -0,0 +1,47 @@
+import { ImagePreview } from './ImagePreview';
+import { ImageUpload } from './ImageUpload';
+
+interface SubImageSelectProps {
+ subImage: (string | File)[];
+ onImagesAdd: (files: File[]) => void;
+ onImageRemove: (index: number) => void;
+}
+
+export function SubImageSelect({
+ subImage,
+ onImagesAdd,
+ onImageRemove,
+}: SubImageSelectProps) {
+ const handleImageUpload = (file: File) => {
+ if (subImage.length < 4) {
+ onImagesAdd([file]);
+ }
+ };
+
+ return (
+
+
소개 이미지
+
+ 체험의 상세한 모습을 보여주는 이미지들을 등록해주세요.
+
+
+
+ {subImage.length < 4 && (
+
+ )}
+ {subImage.map((img, idx) => (
+ onImageRemove(idx)}
+ alt={`소개 이미지 ${idx + 1}`}
+ />
+ ))}
+
+
+
+ * 이미지는 최대 4장까지 등록 가능합니다.
+
+
+ );
+}
diff --git a/src/app/(with-header)/myactivity/page.tsx b/src/app/(with-header)/myactivity/page.tsx
new file mode 100644
index 0000000..aba1cad
--- /dev/null
+++ b/src/app/(with-header)/myactivity/page.tsx
@@ -0,0 +1,9 @@
+import ReservationForm from './components/ReservationForm';
+
+export default function Page() {
+ return (
+
+
+
+ );
+}
diff --git a/src/app/globals.css b/src/app/globals.css
index 985b284..38a8ebe 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -67,6 +67,7 @@
html {
font-family: 'Pretendard-Regular', sans-serif;
+ scrollbar-gutter: stable;
}
}