From 78e5553ae68460ee8dd52f5770527bd9d6c87139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9C=A4=EC=84=9C?= Date: Tue, 28 Jul 2026 13:09:07 +0900 Subject: [PATCH 1/5] =?UTF-8?q?Feat:=20=EB=A1=9C=EA=B3=A0=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EC=BD=98=20=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20=ED=99=88?= =?UTF-8?q?=20=EC=9D=B4=EB=8F=99=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/common/lnb/LnbShared.tsx | 227 +++++++++--------- 1 file changed, 116 insertions(+), 111 deletions(-) diff --git a/jobdri/src/components/common/lnb/LnbShared.tsx b/jobdri/src/components/common/lnb/LnbShared.tsx index 3966181..bd56657 100644 --- a/jobdri/src/components/common/lnb/LnbShared.tsx +++ b/jobdri/src/components/common/lnb/LnbShared.tsx @@ -47,116 +47,116 @@ export const navItems: LnbNavItem[] = [ }, ]; -export const defaultRecentItems: LnbRecentItem[] = [ - { - id: "toss-product-designer", - companyName: "토스", - jobTitle: "프로덕트 디자이너", - version: 1, - }, - { - id: "kakao-ux-researcher", - companyName: "카카오", - jobTitle: "UX 리서처", - version: 1, - }, - { - id: "naver-ui-designer", - companyName: "네이버", - jobTitle: "UI 디자이너", - version: 1, - }, - { - id: "sandbox-grading-criteria-refinement", - companyName: "SK하이닉스", - jobTitle: "채점 평가 요소 고도화 및 결과 리포트 개선", - version: 2, - }, - { - id: "coupang-mobile-designer", - companyName: "쿠팡", - jobTitle: "모바일 디자이너", - version: 6, - }, - { - id: "coupang-contract-interview-5", - companyName: "쿠팡", - jobTitle: "모바일 디자이너 계약직 인턴십", - version: 5, - }, - { - id: "coupang-contract-interview-4", - companyName: "쿠팡", - jobTitle: "모바일 디자이너 계약직 인턴십", - version: 4, - }, - { - id: "coupang-contract-interview-3", - companyName: "쿠팡", - jobTitle: "모바일 디자이너 계약직 인턴십", - version: 3, - }, - { - id: "coupang-contract-interview-2", - companyName: "쿠팡", - jobTitle: "모바일 디자이너 계약직 인턴십", - version: 2, - }, - { - id: "coupang-contract-interview-1", - companyName: "쿠팡", - jobTitle: "모바일 디자이너 계약직 인턴십", - version: 1, - }, - { - id: "line-product-manager", - companyName: "라인", - jobTitle: "프로덕트 매니저", - version: 3, - }, - { - id: "baemin-brand-designer", - companyName: "배달의민족", - jobTitle: "브랜드 디자이너", - version: 2, - }, - { - id: "danggeun-content-marketer", - companyName: "당근", - jobTitle: "콘텐츠 마케터", - version: 1, - }, - { - id: "hyundai-data-analyst", - companyName: "현대자동차", - jobTitle: "서비스 데이터 분석가", - version: 4, - }, - { - id: "musinsa-product-designer", - companyName: "무신사", - jobTitle: "커머스 프로덕트 디자이너", - version: 2, - }, - { - id: "zigbang-frontend-engineer", - companyName: "직방", - jobTitle: "프론트엔드 엔지니어", - version: 5, - }, - { - id: "bucketplace-ux-writer", - companyName: "오늘의집", - jobTitle: "UX 라이터", - version: 1, - }, - { - id: "yanolja-growth-manager", - companyName: "야놀자", - jobTitle: "그로스 매니저", - version: 2, - }, -]; +// export const defaultRecentItems: LnbRecentItem[] = [ +// { +// id: "toss-product-designer", +// companyName: "토스", +// jobTitle: "프로덕트 디자이너", +// version: 1, +// }, +// { +// id: "kakao-ux-researcher", +// companyName: "카카오", +// jobTitle: "UX 리서처", +// version: 1, +// }, +// { +// id: "naver-ui-designer", +// companyName: "네이버", +// jobTitle: "UI 디자이너", +// version: 1, +// }, +// { +// id: "sandbox-grading-criteria-refinement", +// companyName: "SK하이닉스", +// jobTitle: "채점 평가 요소 고도화 및 결과 리포트 개선", +// version: 2, +// }, +// { +// id: "coupang-mobile-designer", +// companyName: "쿠팡", +// jobTitle: "모바일 디자이너", +// version: 6, +// }, +// { +// id: "coupang-contract-interview-5", +// companyName: "쿠팡", +// jobTitle: "모바일 디자이너 계약직 인턴십", +// version: 5, +// }, +// { +// id: "coupang-contract-interview-4", +// companyName: "쿠팡", +// jobTitle: "모바일 디자이너 계약직 인턴십", +// version: 4, +// }, +// { +// id: "coupang-contract-interview-3", +// companyName: "쿠팡", +// jobTitle: "모바일 디자이너 계약직 인턴십", +// version: 3, +// }, +// { +// id: "coupang-contract-interview-2", +// companyName: "쿠팡", +// jobTitle: "모바일 디자이너 계약직 인턴십", +// version: 2, +// }, +// { +// id: "coupang-contract-interview-1", +// companyName: "쿠팡", +// jobTitle: "모바일 디자이너 계약직 인턴십", +// version: 1, +// }, +// { +// id: "line-product-manager", +// companyName: "라인", +// jobTitle: "프로덕트 매니저", +// version: 3, +// }, +// { +// id: "baemin-brand-designer", +// companyName: "배달의민족", +// jobTitle: "브랜드 디자이너", +// version: 2, +// }, +// { +// id: "danggeun-content-marketer", +// companyName: "당근", +// jobTitle: "콘텐츠 마케터", +// version: 1, +// }, +// { +// id: "hyundai-data-analyst", +// companyName: "현대자동차", +// jobTitle: "서비스 데이터 분석가", +// version: 4, +// }, +// { +// id: "musinsa-product-designer", +// companyName: "무신사", +// jobTitle: "커머스 프로덕트 디자이너", +// version: 2, +// }, +// { +// id: "zigbang-frontend-engineer", +// companyName: "직방", +// jobTitle: "프론트엔드 엔지니어", +// version: 5, +// }, +// { +// id: "bucketplace-ux-writer", +// companyName: "오늘의집", +// jobTitle: "UX 라이터", +// version: 1, +// }, +// { +// id: "yanolja-growth-manager", +// companyName: "야놀자", +// jobTitle: "그로스 매니저", +// version: 2, +// }, +// ]; export function LnbDivider() { return ( @@ -171,9 +171,14 @@ export function LnbDefaultHeader({ }: { onToggleFold: () => void; }) { + const router = useRouter(); + return (
- + router.push("/")} + > From 173ed48075fc0bb78bd36f6cab1891051b26c238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9C=A4=EC=84=9C?= Date: Tue, 28 Jul 2026 13:16:49 +0900 Subject: [PATCH 2/5] =?UTF-8?q?Feat:=20HeaderPanel=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jobdri/src/components/common/header/Header.tsx | 14 ++++++++++++-- .../src/components/common/header/HeaderPanel.tsx | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/jobdri/src/components/common/header/Header.tsx b/jobdri/src/components/common/header/Header.tsx index b25b55b..e999af0 100644 --- a/jobdri/src/components/common/header/Header.tsx +++ b/jobdri/src/components/common/header/Header.tsx @@ -5,6 +5,7 @@ import { useRouter } from "next/navigation"; import clsx from "clsx"; import { Button } from "../buttons"; import type { IconType } from "../icons/Icon"; +import { HeaderPanel } from "./HeaderPanel"; interface HeaderActionProps extends Omit< ButtonHTMLAttributes, @@ -37,7 +38,7 @@ interface HeaderProps { } const defaultSteps: ProgressStep[] = [ - { label: "JD 확인" }, + { label: "공고 확인" }, { label: "자소서 입력" }, { label: "첨삭 결과" }, ]; @@ -267,7 +268,7 @@ export default function Header({
- + {/*
    {steps.map((step, index) => { @@ -304,6 +305,15 @@ export default function Header({ ); })}
+
*/} +
+ ({ + id: index + 1, + label: step.label, + }))} + currentStepId={activeStep} + />
{lastSavedAt && ( diff --git a/jobdri/src/components/common/header/HeaderPanel.tsx b/jobdri/src/components/common/header/HeaderPanel.tsx index 5061b8b..10f0409 100644 --- a/jobdri/src/components/common/header/HeaderPanel.tsx +++ b/jobdri/src/components/common/header/HeaderPanel.tsx @@ -25,7 +25,7 @@ const Step: React.FC = ({ step, state }) => { return (
{/* 숫자 또는 체크 아이콘 영역 */} From 1f506eb7e44ba49ab7bf46b8134738f823a80c61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9C=A4=EC=84=9C?= Date: Tue, 28 Jul 2026 13:29:35 +0900 Subject: [PATCH 3/5] =?UTF-8?q?Feat:=20=EA=B3=B5=EA=B3=A0=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EC=9D=B8=ED=84=B0=EB=9E=99=EC=85=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jobdri/src/app/mockApply/job/create/page.tsx | 89 ++++++++++++-------- 1 file changed, 56 insertions(+), 33 deletions(-) diff --git a/jobdri/src/app/mockApply/job/create/page.tsx b/jobdri/src/app/mockApply/job/create/page.tsx index cefa0f5..1b0e762 100644 --- a/jobdri/src/app/mockApply/job/create/page.tsx +++ b/jobdri/src/app/mockApply/job/create/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState, type MouseEvent } from "react"; +import { useEffect, useState, useRef, type MouseEvent } from "react"; import { useRouter } from "next/navigation"; import Header from "@/components/common/header/Header"; import { LLMInput } from "@/components/common/input"; @@ -90,6 +90,24 @@ export default function JobPostingCreatePage() { string | null >(null); + const inputContainerRef = useRef(null); + + useEffect(() => { + function handleClickOutside(event: globalThis.MouseEvent) { + if ( + inputContainerRef.current && + !inputContainerRef.current.contains(event.target as Node) + ) { + setIsInputActive(false); + } + } + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + useEffect(() => { const searchParams = new URLSearchParams(window.location.search); const analysisError = searchParams.get("analysisError"); @@ -172,44 +190,49 @@ export default function JobPostingCreatePage() { />
-
-
+ {/* 1. 타이틀 + 인풋 영역 (활성화 시 중앙으로 이동) */} +
-
-
-

- 지원하고자하는 기업의 공고를 붙여넣으세요 -

-

- 첨부한 내용을 바탕으로 모의 공고가 생성되고 내가 작성한 - 자소서를 채점받을 수 있습니다. -

-
+
+

+ 지원하고자하는 기업의 공고를 붙여넣으세요 +

+

+ 첨부한 내용을 바탕으로 모의 공고가 생성되고 내가 작성한 자소서를 + 채점받을 수 있습니다. +

+
- setIsInputActive(true)} - onSubmit={handleSubmit} - defaultFiles={initialDraft.files} - /> -
+ setIsInputActive(true)} + onSubmit={handleSubmit} + defaultFiles={initialDraft.files} + /> +
- {!isInputActive && ( -
- {INTRO_STEPS.map((step) => ( - - ))} -
+ {/* 2. 카드 영역 (활성화 시 투명해지며 사라짐) */} +
+ > + {INTRO_STEPS.map((step) => ( + + ))} +
From 247c3ed35055b9216f805f57cde28f788c54ce52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9C=A4=EC=84=9C?= Date: Tue, 28 Jul 2026 14:10:47 +0900 Subject: [PATCH 4/5] =?UTF-8?q?Feat:=20=EC=9D=B8=ED=84=B0=EB=9E=99?= =?UTF-8?q?=EC=85=98=20=EC=A1=B0=EA=B1=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jobdri/src/app/mockApply/job/create/page.tsx | 13 +++--- .../src/components/common/input/LLMInput.tsx | 42 ++++++++++++------- 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/jobdri/src/app/mockApply/job/create/page.tsx b/jobdri/src/app/mockApply/job/create/page.tsx index 1b0e762..c3c3c6f 100644 --- a/jobdri/src/app/mockApply/job/create/page.tsx +++ b/jobdri/src/app/mockApply/job/create/page.tsx @@ -98,7 +98,13 @@ export default function JobPostingCreatePage() { inputContainerRef.current && !inputContainerRef.current.contains(event.target as Node) ) { - setIsInputActive(false); + const hasInput = + jobPostingInputValue.trim().length > 0 || attachedFiles.length > 0; + + // 아무것도 입력되지 않았을 때만 원래대로 돌아감 + if (!hasInput) { + setIsInputActive(false); + } } } @@ -106,7 +112,7 @@ export default function JobPostingCreatePage() { return () => { document.removeEventListener("mousedown", handleClickOutside); }; - }, []); + }, [jobPostingInputValue, attachedFiles]); // 의존성 배열에 상태 추가 useEffect(() => { const searchParams = new URLSearchParams(window.location.search); @@ -190,9 +196,7 @@ export default function JobPostingCreatePage() { />
- {/* 컨테이너의 flex를 활용해 중앙 기준을 잡고 내부 요소들을 translate로 상하 이동시킵니다. */}
- {/* 1. 타이틀 + 인풋 영역 (활성화 시 중앙으로 이동) */}
- {/* 2. 카드 영역 (활성화 시 투명해지며 사라짐) */}
, - "className" | "defaultValue" | "onChange" | "onSubmit" | "value" - > { +export interface LLMInputProps extends Omit< + TextareaHTMLAttributes, + "className" | "defaultValue" | "onChange" | "onSubmit" | "value" +> { value?: string; defaultValue?: string; defaultFiles?: File[]; @@ -94,9 +93,9 @@ export function LLMInput({ const rootRef = useRef(null); const fileInputRef = useRef(null); const imagePreviewsRef = useRef([]); - const previewCleanupTimerRef = useRef | null>(null); + const previewCleanupTimerRef = useRef | null>( + null, + ); const imageLimitToastTimerRef = useRef | null>( null, ); @@ -251,17 +250,27 @@ export function LLMInput({ const imageFiles = incomingFiles.filter((file) => file.type.startsWith("image/"), ); - const remainingImageCount = Math.max(maxImages - imagePreviews.length, 0); - const files = imageFiles.slice(0, remainingImageCount); + // 중복 방지 로직 + const uniqueFiles = imageFiles.filter( + (newFile) => + !imagePreviews.some( + (preview) => + preview.file.name === newFile.name && + preview.file.size === newFile.size, + ), + ); + + const remainingImageCount = Math.max(maxImages - imagePreviews.length, 0); + const files = uniqueFiles.slice(0, remainingImageCount); if (files.length === 0) { - if (imageFiles.length > 0 && hasReachedMaxImages) { + if (incomingFiles.length > 0 && hasReachedMaxImages) { openImageLimitToast(); } return; } - if (imageFiles.length > files.length) { + if (uniqueFiles.length > files.length) { openImageLimitToast(); } @@ -315,7 +324,10 @@ export function LLMInput({ const handleDragLeave = (event: DragEvent) => { const nextTarget = event.relatedTarget; - if (nextTarget instanceof Node && event.currentTarget.contains(nextTarget)) { + if ( + nextTarget instanceof Node && + event.currentTarget.contains(nextTarget) + ) { return; } @@ -343,7 +355,9 @@ export function LLMInput({ const handleImageLoad = (id: string) => { setImagePreviews((currentImagePreviews) => currentImagePreviews.map((imagePreview) => - imagePreview.id === id ? { ...imagePreview, loaded: true } : imagePreview, + imagePreview.id === id + ? { ...imagePreview, loaded: true } + : imagePreview, ), ); }; From 6518d177168c4452e67f54605eb5be85846d43e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9C=A4=EC=84=9C?= Date: Tue, 28 Jul 2026 14:40:32 +0900 Subject: [PATCH 5/5] =?UTF-8?q?Feat:=20=EA=B3=B5=EA=B3=A0=20=EB=B6=84?= =?UTF-8?q?=EC=84=9D=20=EC=97=90=EB=9F=AC=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jobdri/src/app/mockApply/job/create/page.tsx | 61 +++++---- jobdri/src/app/mockApply/job/loading/page.tsx | 121 ++++++++++++++++-- jobdri/src/lib/api/jobPostings.ts | 22 ++-- 3 files changed, 161 insertions(+), 43 deletions(-) diff --git a/jobdri/src/app/mockApply/job/create/page.tsx b/jobdri/src/app/mockApply/job/create/page.tsx index c3c3c6f..3b255c9 100644 --- a/jobdri/src/app/mockApply/job/create/page.tsx +++ b/jobdri/src/app/mockApply/job/create/page.tsx @@ -86,9 +86,7 @@ export default function JobPostingCreatePage() { const [showExitConfirm, setShowExitConfirm] = useState(false); const [showInvalidDataModal, setShowInvalidDataModal] = useState(false); - const [jobPostingToastMessage, setJobPostingToastMessage] = useState< - string | null - >(null); + const [toastMessages, setToastMessages] = useState([]); const inputContainerRef = useRef(null); @@ -114,37 +112,45 @@ export default function JobPostingCreatePage() { }; }, [jobPostingInputValue, attachedFiles]); // 의존성 배열에 상태 추가 + // ✅ 교체할 부분: URL에서 여러 개의 에러를 가져오도록 useEffect 수정 useEffect(() => { const searchParams = new URLSearchParams(window.location.search); - const analysisError = searchParams.get("analysisError"); - if (analysisError === "not_saved") { + // getAll을 사용하여 배열 형태로 모두 가져옵니다 + const analysisErrors = searchParams.getAll("analysisError"); + const isCanceled = searchParams.get("analysisCanceled"); + + if (analysisErrors.includes("not_saved")) { const modalTimer = window.setTimeout(() => { setShowInvalidDataModal(true); window.history.replaceState(null, "", window.location.pathname); }, 0); - return () => window.clearTimeout(modalTimer); } - const toastMessage = - searchParams.get("analysisCanceled") === "1" - ? "공고 분석을 중단했습니다." - : analysisError === "1" - ? "업로드에 실패했습니다." - : analysisError; + const nextToastMessages: string[] = []; + + if (isCanceled === "1") { + nextToastMessages.push("공고 분석을 중단했습니다."); + } + + analysisErrors.forEach((err) => { + if (err === "1") nextToastMessages.push("업로드에 실패했습니다."); + else if (err !== "not_saved") nextToastMessages.push(err); + }); - if (!toastMessage) { + if (nextToastMessages.length === 0) { return; } const openToastTimer = window.setTimeout(() => { - setJobPostingToastMessage(toastMessage); + setToastMessages(nextToastMessages); window.history.replaceState(null, "", window.location.pathname); }, 0); + const toastTimer = window.setTimeout(() => { - setJobPostingToastMessage(null); - }, 3000); + setToastMessages([]); + }, 4000); return () => { window.clearTimeout(openToastTimer); @@ -152,6 +158,10 @@ export default function JobPostingCreatePage() { }; }, []); + const removeToast = (indexToRemove: number) => { + setToastMessages((prev) => prev.filter((_, i) => i !== indexToRemove)); + }; + const handleSubmit = () => { saveJobPostingDraft({ files: attachedFiles, @@ -273,13 +283,18 @@ export default function JobPostingCreatePage() {
)} - {jobPostingToastMessage && ( - setJobPostingToastMessage(null)} - className="!right-7 !bottom-7 !max-w-none !rounded-card" - /> + {toastMessages.length > 0 && ( +
+ {toastMessages.map((msg, index) => ( + removeToast(index)} + className="!static !max-w-none !rounded-card" + /> + ))} +
)}
); diff --git a/jobdri/src/app/mockApply/job/loading/page.tsx b/jobdri/src/app/mockApply/job/loading/page.tsx index 6076284..dc8882c 100644 --- a/jobdri/src/app/mockApply/job/loading/page.tsx +++ b/jobdri/src/app/mockApply/job/loading/page.tsx @@ -32,6 +32,17 @@ const loadingStatusMessages = [ type LoadingStepStatus = "done" | "active" | "pending"; +interface InvalidField { + message?: string; + [key: string]: unknown; +} + +interface ParsedError { + message?: string; + invalidFields?: InvalidField[]; + [key: string]: unknown; +} + function createRandomLoadingDurationMs() { return ( MIN_LOADING_DURATION_MS + @@ -134,14 +145,93 @@ export default function JobPostingLoadingPage() { : undefined; if (!rawText && !imageObjectKey) { - throw new Error("분석할 채용 공고가 없습니다."); + throw new Error(JSON.stringify(["분석할 채용 공고가 없습니다."])); } const accepted = await ingestJobPosting({ rawText, imageObjectKey }); - const status = await waitForJobPostingIngest(accepted.taskId); + + let status; + try { + status = await waitForJobPostingIngest(accepted.taskId); + } catch (err: unknown) { + if (err instanceof Error) { + try { + const parsedStatus = JSON.parse(err.message); + if ( + parsedStatus && + (parsedStatus.error || + parsedStatus.failureReason || + parsedStatus.status === "FAILED") + ) { + status = parsedStatus; + } else { + throw err; + } + } catch { + throw err; + } + } else { + throw err; + } + } + + if ( + status && + (status.status === "FAILED" || status.error || status.failureReason) + ) { + const errorMessages: string[] = []; + const rawErrorData = + status.error || status.failureReason || status.message; + + let parsedError: ParsedError | null = null; + + if (rawErrorData) { + if (typeof rawErrorData === "object") { + parsedError = rawErrorData as ParsedError; + } else if (typeof rawErrorData === "string") { + try { + parsedError = JSON.parse(rawErrorData) as ParsedError; + } catch { + try { + const fixedJsonStr = rawErrorData.replace(/'/g, '"'); + parsedError = JSON.parse(fixedJsonStr) as ParsedError; + } catch { + errorMessages.push(rawErrorData); + } + } + } + } + + if (parsedError) { + if ( + Array.isArray(parsedError.invalidFields) && + parsedError.invalidFields.length > 0 + ) { + parsedError.invalidFields.forEach((field: InvalidField) => { + if (field.message) { + errorMessages.push(field.message); + } + }); + } + + if (errorMessages.length === 0 && parsedError.message) { + errorMessages.push(parsedError.message); + } + } + + if (errorMessages.length === 0) { + errorMessages.push( + status.message || "채용 공고 분석에 실패했습니다.", + ); + } + + throw new Error(JSON.stringify(errorMessages)); + } if (!status.result) { - throw new Error("채용 공고 분석 결과를 확인할 수 없습니다."); + throw new Error( + JSON.stringify(["채용 공고 분석 결과를 확인할 수 없습니다."]), + ); } saveJobPostingAnalysis(status.result); @@ -149,7 +239,6 @@ export default function JobPostingLoadingPage() { const resultJobPostingId = status.result.saved?.jobPostingId; const isSavedToDb = status.result.savedToDatabase ?? true; - // ID가 존재하고 DB 저장이 정상적으로 완료되었을 때만 리뷰 페이지로 이동 return resultJobPostingId && isSavedToDb ? `/mockApply/job/${resultJobPostingId}/review` : "/mockApply/job/create?analysisError=not_saved"; @@ -157,16 +246,28 @@ export default function JobPostingLoadingPage() { if (!analysisPromiseRef.current) { analysisPromiseRef.current = analyzeJobPosting().catch((error) => { - const message = - error instanceof Error - ? error.message - : "채용 공고 분석에 실패했습니다."; + let params = ""; + try { + const rawMessage = error instanceof Error ? error.message : ""; - return `/mockApply/job/create?analysisError=${encodeURIComponent(message)}`; + if (rawMessage.startsWith("[") && rawMessage.endsWith("]")) { + const messages = JSON.parse(rawMessage) as string[]; + if (messages.length > 0) { + params = messages + .map((msg) => `analysisError=${encodeURIComponent(msg)}`) + .join("&"); + } + } else { + params = `analysisError=${encodeURIComponent(rawMessage || "채용 공고 분석에 실패했습니다.")}`; + } + } catch { + params = `analysisError=${encodeURIComponent("채용 공고 분석에 실패했습니다.")}`; + } + return `/mockApply/job/create?${params}`; }); } - void analysisPromiseRef.current.then((destination) => { + void analysisPromiseRef.current?.then((destination) => { if (isActive) { router.replace(destination); } diff --git a/jobdri/src/lib/api/jobPostings.ts b/jobdri/src/lib/api/jobPostings.ts index 01978bc..0c7e20c 100644 --- a/jobdri/src/lib/api/jobPostings.ts +++ b/jobdri/src/lib/api/jobPostings.ts @@ -455,17 +455,17 @@ export async function waitForJobPostingIngest( for (let attempt = 0; attempt < maxAttempts; attempt += 1) { const status = await fetchJobPostingIngestStatus(taskId); - if (status.result) { - return status; + if ( + status.error || + status.failureReason || + isFailedStatus(status.status) || + status.status === "FAILED" + ) { + throw new Error(JSON.stringify(status)); } - if (status.error || status.failureReason || isFailedStatus(status.status)) { - throw new Error( - status.failureReason || - status.error || - status.message || - "채용 공고 처리에 실패했습니다.", - ); + if (status.result) { + return status; } if (isCompletedStatus(status.status)) { @@ -477,5 +477,7 @@ export async function waitForJobPostingIngest( await delay(intervalMs); } - throw new Error("채용 공고 처리 시간이 초과되었습니다."); + throw new Error( + JSON.stringify({ failureReason: "채용 공고 처리 시간이 초과되었습니다." }), + ); }