diff --git a/src/app/(pages)/(albaform)/addform/page.tsx b/src/app/(pages)/(albaform)/addform/page.tsx index d367bfeb..e9269d06 100644 --- a/src/app/(pages)/(albaform)/addform/page.tsx +++ b/src/app/(pages)/(albaform)/addform/page.tsx @@ -7,14 +7,15 @@ import TabMenuDropdown from "@/app/components/button/dropdown/TabMenuDropdown"; import Button from "@/app/components/button/default/Button"; import { toast } from "react-hot-toast"; import { useMutation } from "@tanstack/react-query"; -import { useUpdateProfile } from "@/hooks/queries/user/me/useUpdateProfile"; import RecruitContentSection from "./section/RecruitContentSection"; 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"; +import tempSave from "@/utils/tempSave"; import DotLoadingSpinner from "@/app/components/loading-spinner/DotLoadingSpinner"; +import useUploadImages from "@/hooks/queries/user/me/useImageUpload"; export default function AddFormPage() { const router = useRouter(); @@ -58,6 +59,19 @@ export default function AddFormPage() { // 이미지 업로드 api 처리를 위해 별도 변수에 할당 const imageFiles = currentValues.imageFiles; const [, setSelectedOption] = useState(""); + const [showTempDataModal, setShowTempDataModal] = useState(false); + + const { uploadImages } = useUploadImages(); + + // 각각의 탭 작성중 여부 + const { isEditingRecruitContent, isEditingRecruitCondition, isEditingWorkCondition } = useEditing(currentValues); + + // tab 선택 시 Url params 수정 & 하위 폼 데이터 임시저장 + const searchParams = useSearchParams(); + const currentParam = searchParams.get("tab"); + const [prevOption, setPrevOption] = useState(null); + const initialLoad = currentParam === null; // 초기 로딩 여부 확인 + const [loading, setLoading] = useState(false); // 폼 제출 리액트쿼리 const mutation = useMutation({ @@ -72,7 +86,11 @@ export default function AddFormPage() { // 이미지 업로드 처리 let uploadedUrls: string[] = []; try { - uploadedUrls = await uploadImages(Array.from(imageFiles)); + if (currentValues.imageUrls.length !== currentValues.imageFiles.length) { + uploadedUrls = await uploadImages(Array.from(imageFiles)); + } else { + uploadedUrls = currentValues.imageUrls; + } if (!uploadedUrls.length) { toast.error("이미지 업로드에 실패했습니다."); throw new Error("이미지 업로드 실패"); @@ -114,11 +132,11 @@ export default function AddFormPage() { } setLoading(false); toast.success("알바폼을 등록했습니다."); - // if (formId) router.push(`/alba/${formId}`); }, onError: (error) => { setLoading(false); toast.error("에러가 발생했습니다."); + console.error(error); onTempSave(); }, }); @@ -129,12 +147,24 @@ export default function AddFormPage() { } }, [formId]); - // tab 선택 시 Url params 수정 & 하위 폼 데이터 임시저장 - const searchParams = useSearchParams(); - const currentParam = searchParams.get("tab"); - const [prevOption, setPrevOption] = useState(null); - const initialLoad = currentParam === null; // 초기 로딩 여부 확인 - const [loading, setLoading] = useState(false); + // 폼데이터 임시 저장 함수 + const onTempSave = async () => { + // 이미지 처리 로직 + if (currentValues.imageUrls.length !== currentValues.imageFiles.length) { + 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); + setValue("imageUrls", []); + } + } + tempSave("addformData", currentValues); + }; const handleOptionChange = async (option: string) => { setSelectedOption(option); @@ -178,52 +208,6 @@ export default function AddFormPage() { return ; } }; - const { uploadImageMutation } = useUpdateProfile(); - - // 이미지 업로드 api - const uploadImages = async (files: File[]) => { - if (currentValues.imageUrls.length !== currentValues.imageFiles.length) { - const uploadedUrls: string[] = []; - - // 전체 파일 배열을 순회하면서 업로드 로직 진행 - for (const file of files) { - // 파일 크기 체크 - const maxSize = 5 * 1024 * 1024; // 5MB - if (file.size > maxSize) { - toast.error(`5MB 이상의 파일은 업로드할 수 없습니다.`); - continue; - } - const formData = new FormData(); - formData.append("image", file); - try { - const uploadResponse = await uploadImageMutation.mutateAsync(file); - if (uploadResponse?.url) { - uploadedUrls.push(uploadResponse.url); - } - } catch (uploadError) { - console.error(`파일 ${file.name} 업로드 실패:`, uploadError); - } - } - return uploadedUrls; - } else { - return currentValues.imageUrls; - } - }; - - // 폼데이터 임시 저장 함수 - const onTempSave = async () => { - // 임시저장 - if (typeof window !== "undefined") { - window.localStorage.setItem("tempAddFormData", JSON.stringify(currentValues)); - } - toast.success("임시 저장되었습니다."); - console.log("임시저장 데이터", currentValues); - }; - - // 각각의 탭 작성중 여부 - const { isEditingRecruitContent, isEditingRecruitCondition, isEditingWorkCondition } = useEditing(currentValues); - - const [showTempDataModal, setShowTempDataModal] = useState(false); // 임시저장 데이터 로드 함수 const loadTempData = () => { diff --git a/src/app/(pages)/(albaform)/addform/section/WorkConditionSection.tsx b/src/app/(pages)/(albaform)/addform/section/WorkConditionSection.tsx index c1382921..4b5c2ea9 100644 --- a/src/app/(pages)/(albaform)/addform/section/WorkConditionSection.tsx +++ b/src/app/(pages)/(albaform)/addform/section/WorkConditionSection.tsx @@ -137,7 +137,7 @@ export default function WorkConditionSection() { value={workStartTime} {...register("workStartTime", { required: "근무 시작 시간을 선택해주세요" })} onChange={(e: ChangeEvent) => { - setValue("workStartTime", e.target.value); + setValue("workStartTime", e.target.value, { shouldDirty: true }); }} /> id, name 반환 - const uploadResume = async (file: FileList) => { - const uploadedFile: { resumeName: string; resumeId: number } = { - resumeName: "", - resumeId: 0, - }; - const formData = new FormData(); - formData.append("file", file[0]); - try { - const response = await axios.post(`/api/resume/upload`, formData, { - headers: { - "Content-Type": "multipart/form-data", - }, - timeout: 5000, // 5초 타임아웃 설정 - }); - console.log("이력서 업로드", response.data); - return { - resumeName: response.data.resumeName, - resumeId: response.data.resumeId, - }; - } catch (error) { - console.error("Error uploading resume:", error); - toast.error("이력서 업로드에 실패했습니다."); - } - return uploadedFile; - }; - // 폼 제출 리액트쿼리 const mutation = useMutation({ mutationFn: async () => { @@ -103,11 +78,7 @@ export default function Apply() { const uploadedResume = await uploadResume(currentValues.resume); setValue("resumeId", uploadedResume.resumeId); setValue("resumeName", uploadedResume.resumeName); - - window.localStorage.setItem("tempApplyData", JSON.stringify(currentValues)); - toast.success("임시 저장되었습니다."); - console.log("임시저장 currentData", currentValues); - console.log("임시저장 submitData", submitData); + tempSave("applyData", currentValues); } catch (error) { console.error("Error uploading resume:", error); toast.error("이력서 업로드에 실패했습니다."); diff --git a/src/app/api/(file)/images/upload/route.ts b/src/app/api/(file)/images/upload/route.ts index c9ea437b..bc922c44 100644 --- a/src/app/api/(file)/images/upload/route.ts +++ b/src/app/api/(file)/images/upload/route.ts @@ -12,7 +12,6 @@ export async function POST(req: NextRequest) { try { const formData = await req.formData(); - try { const response = await apiClient.post("/images/upload", formData, { headers: { @@ -21,7 +20,6 @@ export async function POST(req: NextRequest) { "Content-Type": "multipart/form-data", }, }); - if (response.status === 201 && response.data?.url) { return NextResponse.json(response.data, { status: 201 }); } diff --git a/src/app/api/forms/[formId]/applications/route.ts b/src/app/api/forms/[formId]/applications/route.ts index 8bf78d63..4937ee5e 100644 --- a/src/app/api/forms/[formId]/applications/route.ts +++ b/src/app/api/forms/[formId]/applications/route.ts @@ -17,7 +17,6 @@ export async function POST(req: NextRequest, { params }: { params: { formId: str "Content-Type": "application/json", }, }); - console.log("apply 라우터에서 response.data출력 ", response.data); return NextResponse.json(response.data); } else { const response = await apiClient.post(`/forms/${params.formId}/applications`, body, { @@ -26,7 +25,6 @@ export async function POST(req: NextRequest, { params }: { params: { formId: str "Content-Type": "application/json", }, }); - console.log("apply 라우터에서 response.data출력 ", response.data); return NextResponse.json(response.data); } } catch (error: unknown) { diff --git a/src/hooks/queries/user/me/useImageUpload.ts b/src/hooks/queries/user/me/useImageUpload.ts new file mode 100644 index 00000000..3c7943dd --- /dev/null +++ b/src/hooks/queries/user/me/useImageUpload.ts @@ -0,0 +1,34 @@ +import { useUpdateProfile } from "@/hooks/queries/user/me/useUpdateProfile"; +import renameFile from "@/utils/renameFile"; +import toast from "react-hot-toast"; + +const useUploadImages = () => { + const { uploadImageMutation } = useUpdateProfile(); + + const uploadImages = async (files: File[]) => { + const uploadedUrls: string[] = []; + + for (const file of files) { + const maxSize = 5 * 1024 * 1024; // 5MB + if (file.size > maxSize) { + toast.error(`5MB 이상의 파일은 업로드할 수 없습니다.`); + continue; + } + + try { + const uploadResponse = await uploadImageMutation.mutateAsync(renameFile(file)); + if (uploadResponse?.url) { + uploadedUrls.push(uploadResponse.url); + } + } catch (uploadError) { + console.error(`파일 ${file.name} 업로드 실패:`, uploadError); + } + } + + return uploadedUrls; + }; + + return { uploadImages }; +}; + +export default useUploadImages; diff --git a/src/utils/renameFile.ts b/src/utils/renameFile.ts new file mode 100644 index 00000000..1354d16a --- /dev/null +++ b/src/utils/renameFile.ts @@ -0,0 +1,12 @@ +// 파일 이름을 임의의 값으로 수정하는 함수(한글 파일명 인코딩 방지) + +const renameFile = (file: File) => { + const now = new Date(); + const timestamp = now.toISOString().replace(/[-:T]/g, "").slice(0, 14); + const fileExtension = file.name.split(".").pop(); // 확장자 추출 + const newFileName = `${timestamp}.${fileExtension}`; // 새로운 이름 생성 + + const renamedFile = new File([file], newFileName, { type: file.type }); + return renamedFile; +}; +export default renameFile; diff --git a/src/utils/tempSave.ts b/src/utils/tempSave.ts new file mode 100644 index 00000000..9c2cb885 --- /dev/null +++ b/src/utils/tempSave.ts @@ -0,0 +1,11 @@ +import toast from "react-hot-toast"; + +// 폼데이터 임시 저장 함수 +const tempSave = async (name: string, data: any) => { + if (typeof window !== "undefined") { + window.localStorage.setItem(name, JSON.stringify(data)); + } + toast.success("임시 저장되었습니다."); + console.log("임시저장 데이터", data); +}; +export default tempSave; diff --git a/src/utils/uploadResume.ts b/src/utils/uploadResume.ts new file mode 100644 index 00000000..1e810788 --- /dev/null +++ b/src/utils/uploadResume.ts @@ -0,0 +1,31 @@ +import axios from "axios"; +import toast from "react-hot-toast"; + +// 이력서 업로드 api -> id, name 반환 +const uploadResume = async (file: FileList) => { + const uploadedFile: { resumeName: string; resumeId: number } = { + resumeName: "", + resumeId: 0, + }; + const formData = new FormData(); + formData.append("file", file[0]); + try { + const response = await axios.post(`/api/resume/upload`, formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + timeout: 5000, // 5초 타임아웃 설정 + }); + console.log("이력서 업로드", response.data); + return { + resumeName: response.data.resumeName, + resumeId: response.data.resumeId, + }; + } catch (error) { + console.error("Error uploading resume:", error); + toast.error("이력서 업로드에 실패했습니다."); + } + return uploadedFile; +}; + +export default uploadResume;