diff --git a/package-lock.json b/package-lock.json index 91a06d4a..efc4b632 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,9 +12,11 @@ "@hookform/resolvers": "^3.9.1", "@tanstack/react-query": "^5.61.3", "@tanstack/react-query-devtools": "^5.61.3", + "@types/react-datepicker": "^6.2.0", "axios": "^1.7.8", "next": "15.0.3", "react": "^18.3.1", + "react-datepicker": "^7.5.0", "react-dom": "^18.3.1", "react-hook-form": "^7.53.2", "tailwind-merge": "^2.5.5", @@ -5752,20 +5754,29 @@ "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.12", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", - "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, + "node_modules/@types/react-datepicker": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-6.2.0.tgz", + "integrity": "sha512-+JtO4Fm97WLkJTH8j8/v3Ldh7JCNRwjMYjRaKh4KHH0M3jJoXtwiD3JBCsdlg3tsFIw9eQSqyAPeVDN2H2oM9Q==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.26.2", + "@types/react": "*", + "date-fns": "^3.3.1" + } + }, "node_modules/@types/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", @@ -9069,7 +9080,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -9148,6 +9158,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -14789,7 +14809,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -15924,7 +15943,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -16103,6 +16121,22 @@ "node": ">=0.10.0" } }, + "node_modules/react-datepicker": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-7.5.0.tgz", + "integrity": "sha512-6MzeamV8cWSOcduwePHfGqY40acuGlS1cG//ePHT6bVbLxWyqngaStenfH03n1wbzOibFggF66kWaBTb1SbTtQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.26.23", + "clsx": "^2.1.1", + "date-fns": "^3.6.0", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17 || ^18", + "react-dom": "^16.9.0 || ^17 || ^18" + } + }, "node_modules/react-docgen": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-7.1.0.tgz", @@ -16168,7 +16202,6 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, "node_modules/react-refresh": { diff --git a/package.json b/package.json index 11e07d75..8209897d 100644 --- a/package.json +++ b/package.json @@ -30,12 +30,14 @@ "@hookform/resolvers": "^3.9.1", "@tanstack/react-query": "^5.61.3", "@tanstack/react-query-devtools": "^5.61.3", + "@types/react-datepicker": "^6.2.0", "axios": "^1.7.8", "next": "15.0.3", "react": "^18.3.1", + "react-datepicker": "^7.5.0", "react-dom": "^18.3.1", - "tailwind-merge": "^2.5.5", "react-hook-form": "^7.53.2", + "tailwind-merge": "^2.5.5", "zod": "^3.23.8", "zustand": "^5.0.1" }, diff --git a/src/app/bookclub/create/page.tsx b/src/app/bookclub/create/page.tsx index 7e5086cc..3c91a71e 100644 --- a/src/app/bookclub/create/page.tsx +++ b/src/app/bookclub/create/page.tsx @@ -6,16 +6,22 @@ import { useState } from 'react'; import Button from '@/components/button/Button'; import { CreateClubFormField, + DatePickerContainer, InputField, RadioButtonGroup, } from '@/features/club-create/components'; import { BookClubForm, bookClubSchema } from '@/features/club-create/types'; +import 'react-datepicker/dist/react-datepicker.css'; +import { useCreateBookClub } from '@/features/club-create/hooks/useCreateBookClub'; export default function CreateBookClub() { const [selectedFileName, setSelectedFileName] = useState(''); + const { createBookClub } = useCreateBookClub(); const { register, handleSubmit, + control, + setValue, formState: { errors }, watch, } = useForm({ @@ -25,12 +31,19 @@ export default function CreateBookClub() { // TODO:: 컨테이너별로 비즈니스 로직 작업 후 훅 분리 const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; - setSelectedFileName(file ? file.name : ''); + if (file) { + console.log('선택된 파일:', file); + setSelectedFileName(file.name); + setValue('image', file); + } else { + setSelectedFileName(''); + } }; // TODO: API 연동, 훅 분리 const onSubmit = (data: BookClubForm) => { - console.log(data); + const formData = createBookClub(data); + console.log(formData); }; return ( @@ -62,7 +75,10 @@ export default function CreateBookClub() { /> - +
- - - + error={errors.targetDate?.message} + placeholder="만나는 날짜를 선택해주세요!" + /> - - - + placeholder="모임의 모집 마감 날짜를 선택해주세요!" + /> diff --git a/src/features/club-create/components/DatePickerField.tsx b/src/features/club-create/components/DatePickerField.tsx new file mode 100644 index 00000000..daf95367 --- /dev/null +++ b/src/features/club-create/components/DatePickerField.tsx @@ -0,0 +1,46 @@ +import { Control, Controller } from 'react-hook-form'; +import DatePicker from 'react-datepicker'; +import { ko } from 'date-fns/locale'; +import { BookClubForm } from '../types'; +import CreateClubFormField from '@/features/club-create/components/CreateClubFormField'; +import InputField from '@/features/club-create/components/InputField'; + +interface DatePickerContainerProps { + control: Control; + name: 'targetDate' | 'endDate'; + label: string; + error?: string; + placeholder: string; +} + +export const DatePickerContainer = ({ + control, + name, + label, + error, + placeholder, +}: DatePickerContainerProps) => { + return ( + + ( + } + /> + )} + /> + + ); +}; diff --git a/src/features/club-create/components/index.ts b/src/features/club-create/components/index.ts index f1d0d7c9..b4fff908 100644 --- a/src/features/club-create/components/index.ts +++ b/src/features/club-create/components/index.ts @@ -1,3 +1,4 @@ export { default as CreateClubFormField } from './CreateClubFormField'; export { default as InputField } from './InputField'; export { default as RadioButtonGroup } from './RadioButtonGroup'; +export { DatePickerContainer } from './DatePickerField'; diff --git a/src/features/club-create/hooks/useCreateBookClub.ts b/src/features/club-create/hooks/useCreateBookClub.ts new file mode 100644 index 00000000..1ad4217e --- /dev/null +++ b/src/features/club-create/hooks/useCreateBookClub.ts @@ -0,0 +1,47 @@ +import { toKoreanTime } from '@/lib/utils/dateUtils'; +import { BookClubForm } from '../types'; + +export const useCreateBookClub = () => { + const createBookClub = (data: BookClubForm) => { + const formData = new FormData(); + + const imageFile = data.image instanceof File ? data.image : null; + if (imageFile) { + formData.append('image', imageFile); + } + + const bookClubData = { + title: data.title, + description: data.description, + bookClubType: data.bookClubType, + meetingType: data.meetingType, + town: data.town, + targetDate: toKoreanTime(data.targetDate), + endDate: toKoreanTime(data.endDate), + memberLimit: data.memberLimit, + }; + + formData.append( + 'bookClub', + new Blob([JSON.stringify(bookClubData)], { + type: 'application/json', + }), + ); + + // TODO: API 호출 로직 추가 + console.log('전송될 데이터:', { + 이미지: imageFile + ? { + 이름: imageFile.name, + 타입: imageFile.type, + 크기: `${(imageFile.size / 1024).toFixed(2)}KB`, + } + : null, + 북클럽_데이터: bookClubData, + }); + + return formData; + }; + + return { createBookClub }; +}; diff --git a/src/features/club-create/types/bookClubSchema.ts b/src/features/club-create/types/bookClubSchema.ts index 881ce69a..3b8e4fff 100644 --- a/src/features/club-create/types/bookClubSchema.ts +++ b/src/features/club-create/types/bookClubSchema.ts @@ -9,13 +9,17 @@ export const bookClubSchema = z.object({ .string() .min(1, '상세 설명을 입력해주세요') .max(30, '상세 설명은 최대 30자까지 가능합니다'), - image: z.string().optional(), - bookType: z.enum(['자유책', '지정책']), - location: z.enum(['온라인', '오프라인']), - place: z.string().optional(), - startDate: z.string().min(1, '시작 날짜를 선택해주세요'), - endDate: z.string().min(1, '종료 날짜를 선택해주세요'), - maxParticipants: z + image: z.any().optional(), + bookClubType: z.enum(['FREE', 'FIXED']), + meetingType: z.enum(['ONLINE', 'OFFLINE']), + town: z.string().optional(), + targetDate: z.date().refine((date) => !isNaN(date.getTime()), { + message: '유효한 날짜를 선택해주세요', + }), + endDate: z.date().refine((date) => !isNaN(date.getTime()), { + message: '유효한 날짜를 선택해주세요', + }), + memberLimit: z .number() .min(3, '최소 3명 이상 입력해주세요') .max(20, '최대 20명까지 가능합니다'), diff --git a/src/lib/utils/dateUtils.ts b/src/lib/utils/dateUtils.ts new file mode 100644 index 00000000..34915223 --- /dev/null +++ b/src/lib/utils/dateUtils.ts @@ -0,0 +1,4 @@ +export const toKoreanTime = (date: Date): string => { + const kstDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000); + return kstDate.toISOString().slice(0, 19); +};