diff --git a/package-lock.json b/package-lock.json index 731f13d..781a044 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "clsx": "^2.1.1", "postcss": "^8.5.3", "react": "^19.0.0", + "react-datepicker": "^8.3.0", "react-dom": "^19.0.0", "react-router-dom": "^7.5.1", "tailwind-merge": "^3.2.0", @@ -849,6 +850,54 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz", + "integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.0.tgz", + "integrity": "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==", + "dependencies": { + "@floating-ui/core": "^1.7.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.8.tgz", + "integrity": "sha512-EQJ4Th328y2wyHR3KzOUOoTW2UKjFk53fmyahfwExnFQ8vnsMYqKc+fFPOkeYtj5tcp1DUMiNJ7BFhed7e9ONw==", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.9", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2774,6 +2823,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -5622,6 +5680,20 @@ "node": ">=0.10.0" } }, + "node_modules/react-datepicker": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-8.3.0.tgz", + "integrity": "sha512-DhfrIJnTPJTUVRtXU7c7zooug40rD6q+Fc8UTCt19dYEotLpDQgTN98MfocY6Rc4S99oOFFEoxyanOM/TKauuw==", + "dependencies": { + "@floating-ui/react": "^0.27.3", + "clsx": "^2.1.1", + "date-fns": "^4.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/react-dom": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", @@ -6355,6 +6427,11 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "node_modules/tailwind-merge": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.2.0.tgz", diff --git a/package.json b/package.json index 8f5ea13..b36c913 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "clsx": "^2.1.1", "postcss": "^8.5.3", "react": "^19.0.0", + "react-datepicker": "^8.3.0", "react-dom": "^19.0.0", "react-router-dom": "^7.5.1", "tailwind-merge": "^3.2.0", diff --git a/src/pages/NoticeRegisterPage.tsx b/src/pages/NoticeRegisterPage.tsx index e489be6..84900cc 100644 --- a/src/pages/NoticeRegisterPage.tsx +++ b/src/pages/NoticeRegisterPage.tsx @@ -1,3 +1,189 @@ +import { useState } from "react"; + +import DatePicker from "react-datepicker"; +import { useNavigate } from "react-router-dom"; + +import "react-datepicker/dist/react-datepicker.css"; + +import { postNotice } from "@/apis/services/noticeService"; +import { Close } from "@/assets/icon"; +import Button from "@/components/Button"; +import TextField from "@/components/TextField"; +import { ROUTES } from "@/constants/router"; +import { useUserStore } from "@/hooks/useUserStore"; +import { extractDigits, numberCommaFormatter } from "@/utils/number"; + +type FormType = { + hourlyPay: string; + startsAt: Date | null; + workhour: string; + description: string; +}; + +const FIELD_LABELS: Record = { + hourlyPay: "시급", + startsAt: "시작 일시", + workhour: "업무 시간", + description: "공고 설명", +}; + export default function NoticeRegisterPage() { - return
NoticeRegisterPage
; + const navigate = useNavigate(); + const { user } = useUserStore(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [form, setForm] = useState({ + hourlyPay: "", + startsAt: null, + workhour: "", + description: "", + }); + + const handleChange = (key: keyof FormType, value: string | null | Date) => { + setForm((prev) => ({ ...prev, [key]: value })); + }; + + const handleSubmit = async () => { + if (!user?.id) { + alert("로그인 정보가 없습니다."); + return; + } + + if (!user?.shopId) { + alert("가게 정보가 없습니다."); + return; + } + + if (isSubmitting) return; + + const requiredFields: Array = [ + "hourlyPay", + "startsAt", + "workhour", + ]; + + const missingField = requiredFields.find((key) => { + const value = form[key]; + return ( + value === null || (typeof value === "string" && value.trim() === "") + ); + }); + + if (missingField) { + alert(`${FIELD_LABELS[missingField]}을(를) 입력해 주세요.`); + return; + } + + const hourlyPay = Number(extractDigits(form.hourlyPay)); + if (isNaN(hourlyPay) || hourlyPay <= 0) { + alert("유효한 시급을 입력해 주세요."); + return; + } + + const workhour = Number(form.workhour); + if (isNaN(workhour) || workhour <= 0) { + alert("유효한 업무 시간을 입력해 주세요."); + return; + } + + setIsSubmitting(true); + + const payload = { + hourlyPay: hourlyPay, + startsAt: form.startsAt!.toISOString(), + workhour: workhour, + description: form.description.trim(), + }; + + try { + await postNotice(user.shopId, payload); + navigate(ROUTES.SHOP.ROOT); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
{ + e.preventDefault(); + handleSubmit(); + }} + > +
+

+ 공고 등록 +

+ +
+ +
+ { + const rawValue = e.target.value; + const digitsOnly = extractDigits(rawValue); + const formatted = digitsOnly + ? numberCommaFormatter(Number(digitsOnly)) + : ""; + handleChange("hourlyPay", formatted); + }} + postfix={} + /> +
+ + handleChange("startsAt", date)} + showTimeSelect + timeFormat="HH:mm" + timeIntervals={10} + dateFormat="yyyy-MM-dd HH:mm" + placeholderText="선택" + className="w-full border border-gray-30 focus-within:border-blue-20 rounded-[0.375rem] py-4 px-5 text-[1rem]" + /> +
+ handleChange("workhour", e.target.value)} + postfix={ + 시간 + } + /> +
+
+ handleChange("description", e.target.value)} + /> +
+
+ +
+
+ ); }