diff --git a/src/app/(home)/page.tsx b/src/app/(home)/page.tsx index 1d3dcd9b..8f4f15ea 100644 --- a/src/app/(home)/page.tsx +++ b/src/app/(home)/page.tsx @@ -60,7 +60,7 @@ export default function Home() { 한 곳에서 관리하는 알바 구인 플랫폼

{user ? ( - +

알바 둘러보기

diff --git a/src/app/(pages)/(albaform)/addform/page.tsx b/src/app/(pages)/(albaform)/addform/page.tsx index 09113d8e..e6b399f0 100644 --- a/src/app/(pages)/(albaform)/addform/page.tsx +++ b/src/app/(pages)/(albaform)/addform/page.tsx @@ -13,11 +13,12 @@ import RecruitConditionSection from "./section/RecruitConditionSection"; import WorkConditionSection from "./section/WorkConditionSection"; import useEditing from "@/hooks/useEditing"; import { SubmitFormDataType } from "@/types/addform"; +import CustomFormModal from "@/app/components/modal/modals/confirm/CustomFormModal"; export default function AddFormPage() { const router = useRouter(); const formId = useParams().formId; - // 리액트 훅폼에서 관리할 데이터 타입 지정 및 메서드 호출 (상위 컴포넌트 = useForm 사용) + // 리액트 훅폼에서 관리할 데이터 타입 지정 및 메서드 호출 (상위 컴���트 = useForm 사용) const methods = useForm({ mode: "onChange", defaultValues: { @@ -60,23 +61,47 @@ export default function AddFormPage() { // 폼 제출 리액트쿼리 const mutation = useMutation({ mutationFn: async () => { + // 이미지 필수 체크 + if (!imageFiles || imageFiles.length === 0) { + toast.error("이미지를 첨부해주세요."); + throw new Error("이미지는 필수입니다."); + } + + // 이미지 업로드 처리 + let uploadedUrls: string[] = []; + try { + uploadedUrls = await uploadImages(Array.from(imageFiles)); + if (!uploadedUrls.length) { + toast.error("이미지 업로드에 실패했습니다."); + throw new Error("이미지 업로드 실패"); + } + setValue("imageUrls", uploadedUrls); + } catch (error) { + console.error("이미지 업로드 중 오류 발생:", error); + toast.error("이미지 업로드 중 오류가 발생했습니다."); + throw error; + } + const excludedKeys = ["displayDate", "workDateRange", "recruitDateRange", "imageFiles"]; // 원하는 필드만 포함된 새로운 객체 만들기 const filteredData = Object.entries(currentValues) - .filter(([key]) => !excludedKeys.includes(key)) // 제외할 키를 필터링 + .filter(([key]) => !excludedKeys.includes(key)) .reduce((acc: Partial, [key, value]) => { if (key === "numberOfPositions") { - // numberOfPositions는 숫자형으로 변환 acc[key] = Number(value); } else if (key === "hourlyWage") { - // hourlyWage는 쉼표를 제거하고 숫자형으로 변환 - if (value.includes(",")) acc[key] = Number(value.replaceAll(/,/g, "")); // 쉼표 제거 후 숫자형 변환 + // 문자열이면 콤마 제거 후 숫자로 변환 + acc[key] = typeof value === "string" ? Number(value.replace(/,/g, "")) : Number(value); + } else if (key === "imageUrls") { + // 업로드된 이미지 URL 사용 + acc[key] = uploadedUrls; } else { - acc[key as keyof SubmitFormDataType] = value; // 나머지 값은 그대로 추가 + acc[key as keyof SubmitFormDataType] = value; } return acc; }, {}); + await axios.post("/api/forms", filteredData); }, onSuccess: () => { @@ -175,21 +200,6 @@ export default function AddFormPage() { // 폼데이터 임시 저장 함수 const onTempSave = async () => { - // 이미지 처리 로직 - if (imageFiles && imageFiles.length > 0) { - try { - const uploadedUrls = await uploadImages(Array.from(imageFiles)); - if (uploadedUrls && uploadedUrls.length > 0) { - setValue("imageUrls", [...uploadedUrls]); - } else { - setValue("imageUrls", [...currentValues.imageUrls]); - } - } catch (error) { - console.error("임시저장 - 이미지 업로드 중 오류 발생:", error); - toast.error("이미지 업로드 중 오류가 발생했습니다."); - setValue("imageUrls", []); - } - } // 임시저장 if (typeof window !== "undefined") { window.localStorage.setItem("tempAddFormData", JSON.stringify(currentValues)); @@ -201,6 +211,64 @@ export default function AddFormPage() { // 각각의 탭 작성중 여부 const { isEditingRecruitContent, isEditingRecruitCondition, isEditingWorkCondition } = useEditing(currentValues); + const [showTempDataModal, setShowTempDataModal] = useState(false); + + // 임시저장 데이터 로드 함수 + const loadTempData = () => { + const tempData = localStorage.getItem("tempAddFormData"); + if (tempData) { + const parsedData: SubmitFormDataType = JSON.parse(tempData); + + // 기본 필드들 설정 + Object.entries(parsedData).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + setValue(key as keyof SubmitFormDataType, value); + } + }); + + // 날짜 관련 필드들은 Date 객체로 변환 + if (parsedData.recruitmentStartDate && parsedData.recruitmentEndDate) { + setValue("recruitmentStartDate", parsedData.recruitmentStartDate); + setValue("recruitmentEndDate", parsedData.recruitmentEndDate); + } + + if (parsedData.workStartDate && parsedData.workEndDate) { + setValue("workStartDate", parsedData.workStartDate); + setValue("workEndDate", parsedData.workEndDate); + } + + // 이미지 URL 설정 + if (parsedData.imageUrls?.length > 0) { + setValue("imageUrls", parsedData.imageUrls); + } + } + }; + + // 임시저장 데이터 초기화 + const clearTempData = () => { + localStorage.removeItem("tempAddFormData"); + }; + + // 임시저장 데이터 확인 및 모달 표시 + useEffect(() => { + const tempData = localStorage.getItem("tempAddFormData"); + if (tempData) { + setShowTempDataModal(true); + } + }, []); + + // 모달 확인 버튼 핸들러 + const handleConfirmTemp = () => { + loadTempData(); + setShowTempDataModal(false); + }; + + // 모달 취소 버튼 핸들러 + const handleCancelTemp = () => { + clearTempData(); + setShowTempDataModal(false); + }; + return (
@@ -225,7 +293,6 @@ export default function AddFormPage() { color="orange" className="h-[58px] border bg-background-100 lg:h-[72px] lg:text-xl lg:leading-8" onClick={() => onTempSave()} - disabled={!isDirty} > 임시 저장 @@ -243,6 +310,17 @@ export default function AddFormPage() {
{renderChildren()} + {/* 임시저장 데이터 확인 모달 */} +
); diff --git a/src/app/(pages)/(albaform)/addform/section/RecruitContentSection.tsx b/src/app/(pages)/(albaform)/addform/section/RecruitContentSection.tsx index 94349f20..a1d35ae1 100644 --- a/src/app/(pages)/(albaform)/addform/section/RecruitContentSection.tsx +++ b/src/app/(pages)/(albaform)/addform/section/RecruitContentSection.tsx @@ -5,7 +5,7 @@ import ImageInput from "@/app/components/input/file/ImageInput/ImageInput"; import DatePickerInput from "@/app/components/input/dateTimeDaypicker/DatePickerInput"; import { cn } from "@/lib/tailwindUtil"; import { useFormContext } from "react-hook-form"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import Label from "../../component/Label"; // 알바폼 만들기 - 사장님 - 1-모집내용 @@ -19,34 +19,9 @@ export default function RecruitContentSection() { register, setValue, getValues, - formState: { errors, isDirty }, + formState: { errors }, } = useFormContext(); - const currentValue = getValues(); - - // 이미지 파일 change핸들러 - const handleChangeImages = (files: File[]) => { - // 훅폼 데이터에 추가-> 상위 페이지에서 "imageFiles" data를 관리할 수 있음 - setValue("imageFiles", files); - - // 기존 이미지 리스트와 새로운 이미지를 합침 - setInitialImageList((prevList) => [ - ...prevList, - ...files.map((file: File) => ({ - file, - url: URL.createObjectURL(file), - id: crypto.randomUUID(), - })), - ]); - }; - - // 컴포넌트가 마운트될 때 이미지 초기값 설정 (초기로딩 제외) - useEffect(() => { - if (currentValue.imageFiles?.length > 0) { - handleChangeImages(currentValue.imageFiles); - } - }, []); - // 날짜 선택 const [recruitmentDateRange, setRecruitmentDateRange] = useState<[Date | null, Date | null]>([null, null]); const handleRecruitmentDateChange = (dates: [Date | null, Date | null]) => { @@ -55,6 +30,20 @@ export default function RecruitContentSection() { if (start) setValue("recruitmentStartDate", start.toISOString()); if (end) setValue("recruitmentEndDate", end.toISOString()); }; + + // 이미지 파일 change핸들러 + const handleChangeImages = (files: File[]) => { + setValue("imageFiles", files); + + const newImages = files.map((file: File) => ({ + file, + url: URL.createObjectURL(file), + id: crypto.randomUUID(), + })); + + setInitialImageList(newImages); + }; + const errorTextStyle = "absolute -bottom-[26px] right-1 text-[13px] text-sm font-medium leading-[22px] text-state-error lg:text-base lg:leading-[26px]"; @@ -90,7 +79,7 @@ export default function RecruitContentSection() { endDate={recruitmentDateRange[1] || undefined} onChange={handleRecruitmentDateChange} required={true} - errormessage={isDirty && (!recruitmentDateRange[0] || !recruitmentDateRange[1])} + errormessage={!recruitmentDateRange[0] || !recruitmentDateRange[1]} displayValue="recruitDateRange" /> {!recruitmentDateRange[0] || @@ -98,14 +87,16 @@ export default function RecruitContentSection() { - { - handleChangeImages(files); - }} - initialImageList={initialImageList || []} - /> - {errors.imageUrls &&

{errors.imageUrls.message as string}

} +
+ { + handleChangeImages(files); + }} + initialImageList={initialImageList} + /> + {errors.imageUrls &&

{errors.imageUrls.message as string}

} +
); diff --git a/src/app/(pages)/(albaform)/addform/section/WorkConditionSection.tsx b/src/app/(pages)/(albaform)/addform/section/WorkConditionSection.tsx index a2b3f474..83fadbd5 100644 --- a/src/app/(pages)/(albaform)/addform/section/WorkConditionSection.tsx +++ b/src/app/(pages)/(albaform)/addform/section/WorkConditionSection.tsx @@ -1,16 +1,18 @@ "use client"; import { useFormContext } from "react-hook-form"; -import { useState, ChangeEvent, MouseEvent, useEffect } from "react"; +import { useState, ChangeEvent, MouseEvent, useCallback, useEffect } from "react"; import { cn } from "@/lib/tailwindUtil"; import DatePickerInput from "@/app/components/input/dateTimeDaypicker/DatePickerInput"; -import LocationInput from "@/app/components/input/text/LocationInput"; import TimePickerInput from "@/app/components/input/dateTimeDaypicker/TimePickerInput"; import DayPickerList from "@/app/components/input/dateTimeDaypicker/DayPickerList"; import BaseInput from "@/app/components/input/text/BaseInput"; import CheckBtn from "@/app/components/button/default/CheckBtn"; import formatMoney from "@/utils/formatMoney"; import Label from "../../component/Label"; +import Script from "next/script"; +import LocationInput from "@/app/components/input/text/LocationInput"; +import { toast } from "react-hot-toast"; // 알바폼 만들기 - 사장님 - 3-근무조건 export default function WorkConditionSection() { @@ -20,7 +22,7 @@ export default function WorkConditionSection() { getValues, trigger, watch, - formState: { errors, isDirty }, + formState: { errors }, } = useFormContext(); // 근무 날짜 지정 @@ -54,26 +56,60 @@ export default function WorkConditionSection() { } }; + // 최저시급 상수 수정 (2025년 기준) + const MINIMUM_WAGE = 10030; + // 시급 상태 추가 - const [displayWage, setDisplayWage] = useState(""); + const [displayWage, setDisplayWage] = useState(formatMoney(MINIMUM_WAGE.toString())); - // 리액트 훅폼 데이터를 가져와서 렌더링 + // 컴포넌트 마운트 시 최저시급으로 초기화 useEffect(() => { - const selectedDays = getValues("workDays") || []; - setSelectedWorkDays(selectedDays); - const wage = getValues("hourlyWage") || 0; - setDisplayWage(wage); - }, [getValues]); + setValue("hourlyWage", MINIMUM_WAGE); + }, [setValue]); + + // 시급 변경 핸들러 수정 + const handleWageChange = (e: ChangeEvent) => { + const value = e.target.value.replace(/,/g, ""); + const numericValue = Number(value); + + // 최저시급 미만으로 설정 시도할 경우 + if (numericValue < MINIMUM_WAGE) { + toast.error(`최저시급(${formatMoney(MINIMUM_WAGE.toString())}원) 이상을 입력해주세요.`); + setDisplayWage(formatMoney(MINIMUM_WAGE.toString())); + setValue("hourlyWage", MINIMUM_WAGE); + return; + } + + setValue("hourlyWage", numericValue); + setDisplayWage(formatMoney(value)); + }; const errorTextStyle = "absolute -bottom-[26px] right-1 text-[13px] text-sm font-medium leading-[22px] text-state-error lg:text-base lg:leading-[26px]"; + // 주소 변경 핸들러만 유지 + const handleAddressChange = useCallback( + (fullAddress: string) => { + setValue("location", fullAddress); + trigger("location"); + }, + [setValue, trigger] + ); + return (
+