diff --git a/jobdri/src/app/credit/page.tsx b/jobdri/src/app/credit/page.tsx index 3ba1fe2..8508ce7 100644 --- a/jobdri/src/app/credit/page.tsx +++ b/jobdri/src/app/credit/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState, Suspense } from "react"; import { useSearchParams } from "next/navigation"; import { CreditCard } from "@/components/common/cards"; import Useage from "@/components/common/credit/Useage"; @@ -9,8 +9,7 @@ import { confirmPurchase, type CreditPlan, } from "@/lib/api/credit"; -import { Lnb } from "@/components/common/lnb"; -import Header from "@/components/common/header/Header"; +import Lnb from "@/components/common/lnb/Lnb"; import PageHeader from "@/components/common/PageHeader"; function calcDiscountRate(plan: CreditPlan, basePricePerUnit: number): string { @@ -20,7 +19,7 @@ function calcDiscountRate(plan: CreditPlan, basePricePerUnit: number): string { return `${rate}%`; } -export default function CreditPage() { +function CreditContent() { const [plans, setPlans] = useState([]); const searchParams = useSearchParams(); @@ -43,28 +42,38 @@ export default function CreditPage() { const basePricePerUnit = plans.find((p) => p.planCode === "ONE_TIME")?.price ?? 2500; + return ( + <> + +
+ {plans.map((plan) => ( + + ))} +
+
+ +
+ + ); +} + +export default function CreditPage() { return (
- -
- {plans.map((plan) => ( - - ))} -
-
- -
+ 로딩 중...
}> + + ); diff --git a/jobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-input/JdInputPageClient.tsx b/jobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-input/JdInputPageClient.tsx deleted file mode 100644 index 1b8e190..0000000 --- a/jobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-input/JdInputPageClient.tsx +++ /dev/null @@ -1,705 +0,0 @@ -"use client"; - -import { forwardRef, useImperativeHandle, useRef, useState } from "react"; -import { useParams, useRouter } from "next/navigation"; -import Header from "@/components/common/header/Header"; -import { Button } from "@/components/common/buttons"; -import { Method1Card, Method2Card } from "@/components/common/cards"; -import Icon from "@/components/common/icons/Icon"; -import { InputTextAreaAutoGrowS } from "@/components/common/input"; -import { ModalFileUpload, ModalInput } from "@/components/common/modal"; -import useOutsideClick from "@/hooks/useOutsideClick"; -import { - ingestJobPosting, - ingestJobPostingImage, - waitForJobPostingIngest, - type JobPostingIngestStatus, -} from "@/lib/api/jobPostings"; -import { - createJdReviewSectionsFromJobPosting, - getJdReviewMetadataStorageKey, - getJdReviewSavedStorageKey, - getJdReviewStorageKey, -} from "@/components/mockApply/jd/jdReviewSections"; - -type JdInputMethod = "text" | "link" | "image" | "manual"; -type TextModalStep = "input" | "reading" | "failed"; -type LinkModalStep = "input" | "reading" | "failed"; -type ImageModalStep = "upload" | "reading" | "failed"; - -function isUrlFormat(value: string) { - const normalizedUrl = normalizeUrl(value); - - if (!normalizedUrl || /\s/.test(value.trim())) { - return false; - } - - try { - const url = new URL(normalizedUrl); - - return ( - ["http:", "https:"].includes(url.protocol) && url.hostname.includes(".") - ); - } catch { - return false; - } -} - -function normalizeUrl(value: string) { - const trimmedValue = value.trim(); - - if (!trimmedValue) { - return ""; - } - - return /^[a-zA-Z][a-zA-Z\d+.-]*:\/\//.test(trimmedValue) - ? trimmedValue - : `https://${trimmedValue}`; -} - -export interface JdInputPageClientHandle { - handleCtaClick: () => void; -} - -interface JdInputPageClientProps { - selectedMethod: JdInputMethod | null; - onMethodChange: (method: JdInputMethod | null) => void; -} - -interface TextJobPostingInputModalProps { - value: string; - onChange: (value: string) => void; - onSubmit: () => void; - onClose: () => void; - submitDisabled: boolean; -} - -function TextJobPostingInputModal({ - value, - onChange, - onSubmit, - onClose, - submitDisabled, -}: TextJobPostingInputModalProps) { - const modalRef = useRef(null); - - useOutsideClick(modalRef, onClose); - - return ( -
-
-
- -
- -
-
-

- 공고 내용을 입력해주세요. -

- - -
-
- -
-
-
-
- ); -} - -const JdInputPageClient = forwardRef< - JdInputPageClientHandle, - JdInputPageClientProps ->(function JdInputPageClient({ selectedMethod, onMethodChange }, ref) { - const { mockApplyId: id } = useParams<{ mockApplyId: string }>(); - const router = useRouter(); - const manualJdReviewPath = `/mockApply/${id}/jd-review?mode=manual`; - const activeRequestIdRef = useRef(0); - - const [isTextModalOpen, setIsTextModalOpen] = useState(false); - const [isLinkModalOpen, setIsLinkModalOpen] = useState(false); - const [isImageModalOpen, setIsImageModalOpen] = useState(false); - const [textModalStep, setTextModalStep] = useState("input"); - const [linkModalStep, setLinkModalStep] = useState("input"); - const [imageModalStep, setImageModalStep] = - useState("upload"); - const [jdRawText, setJdRawText] = useState(""); - const [jdLink, setJdLink] = useState(""); - const [selectedImageFile, setSelectedImageFile] = useState(null); - const [processingErrorMessage, setProcessingErrorMessage] = useState(""); - const hasRawText = jdRawText.trim().length > 0; - const hasLinkText = jdLink.trim().length > 0; - - const handleCtaClick = () => { - if (selectedMethod === "text") { - setTextModalStep("input"); - setIsTextModalOpen(true); - return; - } - - if (selectedMethod === "link") { - setLinkModalStep("input"); - setIsLinkModalOpen(true); - return; - } - - if (selectedMethod === "image") { - setSelectedImageFile(null); - setImageModalStep("upload"); - setIsImageModalOpen(true); - return; - } - - if (selectedMethod === "manual") { - router.push(manualJdReviewPath); - } - }; - - useImperativeHandle(ref, () => ({ handleCtaClick })); - - const closeTextModal = () => { - setIsTextModalOpen(false); - }; - - const closeLinkModal = () => { - setIsLinkModalOpen(false); - }; - - const closeImageModal = () => { - setIsImageModalOpen(false); - }; - - const resetToUploadStart = () => { - activeRequestIdRef.current += 1; - setIsTextModalOpen(false); - setIsLinkModalOpen(false); - setIsImageModalOpen(false); - setTextModalStep("input"); - setLinkModalStep("input"); - setImageModalStep("upload"); - onMethodChange(null); - setJdRawText(""); - setJdLink(""); - setSelectedImageFile(null); - setProcessingErrorMessage(""); - }; - - const cancelTextReading = () => { - activeRequestIdRef.current += 1; - setIsTextModalOpen(false); - setTextModalStep("input"); - setProcessingErrorMessage(""); - }; - - const cancelImageReading = () => { - activeRequestIdRef.current += 1; - setIsTextModalOpen(false); - setIsLinkModalOpen(false); - setIsImageModalOpen(false); - setImageModalStep("upload"); - setProcessingErrorMessage(""); - - if (document.fullscreenElement) { - void document.exitFullscreen(); - } - }; - - const restartImageUpload = () => { - activeRequestIdRef.current += 1; - setIsTextModalOpen(false); - setIsLinkModalOpen(false); - setIsImageModalOpen(true); - onMethodChange("image"); - setSelectedImageFile(null); - setImageModalStep("upload"); - setProcessingErrorMessage(""); - }; - - const moveToJdReviewWithResult = (status: JobPostingIngestStatus) => { - const result = status.result; - const generated = result?.generated; - const extracted = result?.extracted; - const saved = result?.saved; - const classification = result?.classification; - const firstNonEmpty = (...values: Array) => - values.find((value) => value?.trim())?.trim() ?? ""; - const jobPosting = { - companyName: firstNonEmpty( - generated?.companyName, - extracted?.companyName, - saved?.companyName, - ), - jobTitle: firstNonEmpty( - saved?.jobTitle, - generated?.jobTitle, - extracted?.jobTitle, - classification?.detailClassificationName, - ), - task: firstNonEmpty(generated?.task, extracted?.task, saved?.task), - requirements: firstNonEmpty( - generated?.requirements, - extracted?.requirements, - saved?.requirement, - ), - preferredQualifications: firstNonEmpty( - generated?.preferredQualifications, - extracted?.preferredQualifications, - saved?.preferred, - ), - }; - - if (!Object.values(jobPosting).some(Boolean)) { - throw new Error( - result?.message || - "이미지에서 공고 정보를 추출하지 못했습니다. 더 선명한 이미지를 사용해주세요.", - ); - } - - const detailClassificationId = - saved?.detailClassificationId ?? - classification?.detailClassificationId ?? - result?.candidates?.[0]?.detailClassificationId ?? - 0; - - window.sessionStorage.setItem( - getJdReviewStorageKey(id), - JSON.stringify(createJdReviewSectionsFromJobPosting(jobPosting)), - ); - window.sessionStorage.setItem( - getJdReviewMetadataStorageKey(id), - JSON.stringify({ - companySize: saved?.companySize ?? "STARTUP", - detailClassificationId, - profileColor: saved?.profileColor ?? "DEFAULT", - postingName: firstNonEmpty(saved?.postingName, jobPosting.jobTitle), - jobTitle: jobPosting.jobTitle, - }), - ); - - if (saved) { - window.sessionStorage.setItem( - getJdReviewSavedStorageKey(id), - JSON.stringify(saved), - ); - } else { - window.sessionStorage.removeItem(getJdReviewSavedStorageKey(id)); - } - - router.push(`/mockApply/${id}/jd-review`); - }; - - const processJobPosting = async ({ - rawText, - sourceUrl, - image, - }: { - rawText?: string; - sourceUrl?: string; - image?: File | null; - }) => { - const requestId = activeRequestIdRef.current + 1; - activeRequestIdRef.current = requestId; - setProcessingErrorMessage(""); - - try { - const accepted = image - ? await ingestJobPostingImage(image) - : await ingestJobPosting({ rawText, sourceUrl }); - const status = await waitForJobPostingIngest(accepted.taskId); - - if (activeRequestIdRef.current !== requestId) { - return; - } - - moveToJdReviewWithResult(status); - } catch (error) { - if (activeRequestIdRef.current !== requestId) { - return; - } - - setProcessingErrorMessage( - error instanceof Error ? error.message : "공고 입력에 실패했습니다.", - ); - - if (rawText) { - setTextModalStep("failed"); - return; - } - - if (sourceUrl) { - setLinkModalStep("failed"); - return; - } - - setImageModalStep("failed"); - } - }; - - const submitTextInput = () => { - if (!hasRawText) { - return; - } - - setTextModalStep("reading"); - void processJobPosting({ rawText: jdRawText }); - }; - - const submitLinkInput = () => { - if (!hasLinkText) { - return; - } - - if (!isUrlFormat(jdLink)) { - setProcessingErrorMessage("올바른 공고 링크를 입력해주세요."); - setLinkModalStep("failed"); - return; - } - - setLinkModalStep("reading"); - void processJobPosting({ sourceUrl: normalizeUrl(jdLink) }); - }; - - const submitImageInput = () => { - if (!selectedImageFile) { - return; - } - - setImageModalStep("reading"); - void processJobPosting({ image: selectedImageFile }); - }; - - const selectMethodFromFailure = (method: JdInputMethod) => { - onMethodChange(method); - - if (method === "text") { - setJdRawText(""); - setTextModalStep("input"); - setIsLinkModalOpen(false); - setIsImageModalOpen(false); - setIsTextModalOpen(true); - setProcessingErrorMessage(""); - return; - } - - if (method === "link") { - setJdLink(""); - setLinkModalStep("input"); - setIsTextModalOpen(false); - setIsImageModalOpen(false); - setIsLinkModalOpen(true); - setProcessingErrorMessage(""); - return; - } - - if (method === "image") { - setSelectedImageFile(null); - setImageModalStep("upload"); - setIsTextModalOpen(false); - setIsLinkModalOpen(false); - setIsImageModalOpen(true); - setProcessingErrorMessage(""); - return; - } - - router.push(manualJdReviewPath); - }; - - return ( - <> -
-
-
- -
-
-
-
-

- 공고 내용 입력 방식으로 선택해 주세요 -

-
-
- -
-
- - onMethodChange(selectedMethod === "text" ? null : "text") - } - /> - - onMethodChange( - selectedMethod === "image" ? null : "image", - ) - } - /> -
- - - onMethodChange( - selectedMethod === "manual" ? null : "manual", - ) - } - /> -
-
-
-
-
- - {isTextModalOpen && ( - <> - {textModalStep === "input" ? ( - - ) : textModalStep === "reading" ? ( - undefined} - onSubmit={() => { - activeRequestIdRef.current += 1; - setTextModalStep("input"); - }} - onCancel={cancelTextReading} - onClose={cancelTextReading} - title="텍스트를 읽고 있습니다" - description="텍스트 내용이 부적절한 경우 제대로 추출되지 않을 수 있습니다." - submitLabel="다시 입력하기" - showInputField={false} - showDescription - showLoadMotion - /> - ) : ( - undefined} - onSubmit={() => undefined} - onClose={resetToUploadStart} - title="공고 입력에 실패했습니다" - description={ - processingErrorMessage || "다른 방법으로 공고 내용을 입력해주세요" - } - showInputField={false} - showDescription - showLoadMotion={false} - statusIconType="WARN" - methodActions={[ - { - label: "직접 입력하기", - iconType: "EDIT", - onClick: () => selectMethodFromFailure("manual"), - }, - { - label: "텍스트 붙여넣기", - iconType: "TEXT", - onClick: () => selectMethodFromFailure("text"), - }, - { - label: "이미지 업로드", - iconType: "UPLOAD_M", - onClick: () => selectMethodFromFailure("image"), - }, - ]} - /> - )} - - )} - - {isLinkModalOpen && ( - <> - {linkModalStep === "input" ? ( - - ) : linkModalStep === "reading" ? ( - { - activeRequestIdRef.current += 1; - setLinkModalStep("input"); - }} - onCancel={resetToUploadStart} - onClose={resetToUploadStart} - title="링크를 읽고 있습니다" - description="링크 내용이 부적절한 경우 제대로 추출되지 않을 수 있습니다." - placeholder="https://www.com" - showInputField - showDescription - showLoadMotion - /> - ) : ( - undefined} - onClose={resetToUploadStart} - title="공고 입력에 실패했습니다" - description={ - processingErrorMessage || - "다른 방법으로 공고 내용을 입력해주세요" - } - showInputField={false} - showDescription - showLoadMotion={false} - statusIconType="WARN" - methodActions={[ - { - label: "직접 입력하기", - iconType: "EDIT", - onClick: () => selectMethodFromFailure("manual"), - }, - { - label: "텍스트 붙여넣기", - iconType: "TEXT", - onClick: () => selectMethodFromFailure("text"), - }, - { - label: "이미지 업로드", - iconType: "UPLOAD_M", - onClick: () => selectMethodFromFailure("image"), - }, - ]} - /> - )} - - )} - - {isImageModalOpen && - (imageModalStep === "upload" ? ( - - ) : imageModalStep === "reading" ? ( - undefined} - onSubmit={restartImageUpload} - onCancel={cancelImageReading} - onClose={cancelImageReading} - title="이미지를 읽고 있습니다" - description="이미지가 부적절한 경우 제대로 추출되지 않을 수 있습니다" - submitLabel="다시 입력하기" - showInputField={false} - showDescription - showLoadMotion - > - {selectedImageFile && ( -
- - - {selectedImageFile.name} - -
- )} -
- ) : ( - undefined} - onSubmit={() => undefined} - onClose={resetToUploadStart} - title="공고 입력에 실패했습니다" - description={ - processingErrorMessage || "다른 방법으로 공고 내용을 입력해주세요" - } - showInputField={false} - showDescription - showLoadMotion={false} - statusIconType="WARN" - methodActions={[ - { - label: "직접 입력하기", - iconType: "EDIT", - onClick: () => selectMethodFromFailure("manual"), - }, - { - label: "텍스트 붙여넣기", - iconType: "TEXT", - onClick: () => selectMethodFromFailure("text"), - }, - { - label: "이미지 업로드", - iconType: "UPLOAD_M", - onClick: () => selectMethodFromFailure("image"), - }, - ]} - /> - ))} - - ); -}); - -export default JdInputPageClient; diff --git a/jobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-input/page.tsx b/jobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-input/page.tsx deleted file mode 100644 index 4c6dcc7..0000000 --- a/jobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-input/page.tsx +++ /dev/null @@ -1,36 +0,0 @@ -"use client"; - -import { useRef, useState } from "react"; -import { useRouter } from "next/navigation"; -import { Footer } from "@/components/common/footer"; -import JdInputPageClient, { - type JdInputPageClientHandle, -} from "./JdInputPageClient"; - -type JdInputMethod = "text" | "link" | "image" | "manual"; - -export default function MockApplicationJdInputPage() { - const router = useRouter(); - const clientRef = useRef(null); - const [selectedMethod, setSelectedMethod] = useState( - null, - ); - - return ( -
- -
router.push("/") }} - ctaAction={{ - label: "선택하기", - disabled: selectedMethod === null, - onClick: () => clientRef.current?.handleCtaClick(), - }} - /> -
- ); -} diff --git a/jobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-review/JdReviewPageClient.tsx b/jobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-review/JdReviewPageClient.tsx deleted file mode 100644 index 49f932d..0000000 --- a/jobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-review/JdReviewPageClient.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client"; - -import Header from "@/components/common/header/Header"; -import JdReviewMain from "@/components/mockApply/jd/JdReviewMain"; -import type { JdReviewSection } from "@/components/mockApply/jd/jdReviewSections"; - -interface JdReviewPageClientProps { - sections?: JdReviewSection[]; - onSectionsChange?: (sections: JdReviewSection[]) => void; -} - -export default function JdReviewPageClient({ - sections, - onSectionsChange, -}: JdReviewPageClientProps) { - const sectionsKey = sections - ? JSON.stringify(sections.map(({ id, value }) => [id, value])) - : "mock"; - - return ( -
-
-
- -
-
-
-

- 공고 내용을 확인하고 수정해주세요 -

-
- -
-
-
-
- ); -} diff --git a/jobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-review/page.tsx b/jobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-review/page.tsx deleted file mode 100644 index c9ed16b..0000000 --- a/jobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-review/page.tsx +++ /dev/null @@ -1,440 +0,0 @@ -"use client"; - -import { useMemo, useState, useSyncExternalStore } from "react"; -import { useParams, useRouter, useSearchParams } from "next/navigation"; -import { Footer } from "@/components/common/footer"; -import { ModalNotice } from "@/components/common/modal"; -import JdReviewPageClient from "./JdReviewPageClient"; -import QuestionGenerationLoading from "@/components/mockApply/QuestionGenerationLoading"; -import { - ingestJobPosting, - saveJobPosting, - waitForJobPostingIngest, - type SavedJobPosting, - type JobPostingSavePayload, -} from "@/lib/api/jobPostings"; -import { - createApplyFromJobPosting, - getSelectedApplyType, - saveMockApplyResumeRecord, -} from "@/lib/api/mockApplies"; -import { - emptyJdSections, - getJdReviewMetadataStorageKey, - getJdReviewSavedStorageKey, - getJdReviewStorageKey, - mockJdSections, - type JdReviewMetadata, - type JdReviewSection, -} from "@/components/mockApply/jd/jdReviewSections"; - -export function subscribeToNotificationStream( - onMessage: (data: ApiNotificationItem) => void, - onError?: (error: unknown) => void, -) { - const headers = getAuthHeaders(); - const ctrl = new AbortController(); - - if (!headers.Authorization) { - console.warn("로그인 토큰이 없어 실시간 알림을 연결하지 않습니다."); - return () => ctrl.abort(); - } - - const tokenOnly = headers.Authorization.replace("Bearer ", ""); - - console.log( - "🔌 SSE 연결 시도 중... URL:", - `${API_BASE_URL}/api/notifications/stream`, - ); - - fetchEventSource( - `${API_BASE_URL}/api/notifications/stream?accessToken=${tokenOnly}`, - { - method: "GET", - headers: { - ...headers, - Accept: "text/event-stream", - }, - signal: ctrl.signal, - - onopen(response) { - if (response.ok) { - console.log("🟢 SSE 연결 성공!! 서버와 스트림 연결됨."); - } else { - console.error( - "🔴 SSE 연결 응답 이상:", - response.status, - response.statusText, - ); - } - }, - - onmessage(event) { - console.log("📥 SSE raw 메시지 수신:", event.data); // 👈 서버가 뭐라도 보내면 무조건 찍힘! - - if (!event.data || !event.data.trim().startsWith("{")) { - return; - } - - try { - const parsedData = JSON.parse(event.data) as ApiNotificationItem; - onMessage(parsedData); - } catch (e: unknown) { - console.error("SSE 데이터 파싱 실패:", e); - } - }, - - onerror(err: unknown) { - console.error("❌ SSE 스트림 에러 발생:", err); - if (onError) onError(err); - }, - }, - ).catch((err) => { - console.error("❌ SSE 연결 래퍼 예외 발생:", err); - }); - - return () => ctrl.abort(); -} - -function parseStoredSections(value: string | null) { - if (!value) { - return undefined; - } - - try { - const parsedSections = JSON.parse(value) as JdReviewSection[]; - return Array.isArray(parsedSections) && parsedSections.length > 0 - ? parsedSections - : undefined; - } catch { - return undefined; - } -} - -function parseStoredMetadata(value: string | null) { - if (!value) { - return undefined; - } - - try { - return JSON.parse(value) as JdReviewMetadata; - } catch { - return undefined; - } -} - -function getSectionValue(sections: JdReviewSection[], id: string) { - return sections.find((section) => section.id === id)?.value.trim() ?? ""; -} - -function createSavePayload( - sections: JdReviewSection[], - metadata?: JdReviewMetadata, -): JobPostingSavePayload { - const companyName = getSectionValue(sections, "company"); - const jobTitle = getSectionValue(sections, "job"); - const detailClassificationId = metadata?.detailClassificationId; - - if (!companyName) { - throw new Error("회사명을 입력해주세요."); - } - - if (!detailClassificationId || detailClassificationId <= 0) { - throw new Error( - "직무 분류 정보가 없어 저장할 수 없습니다. 링크 또는 이미지로 공고를 입력하거나, 직접 작성용 소분류 선택 기능이 필요합니다.", - ); - } - - if (!jobTitle) { - throw new Error("직무명을 입력해주세요."); - } - - return { - profileColor: metadata?.profileColor ?? "DEFAULT", - postingName: metadata?.postingName?.trim() || jobTitle, - companyName, - companySize: metadata?.companySize?.trim() || "STARTUP", - jobTitle, - detailClassificationId, - task: getSectionValue(sections, "main-task"), - requirement: getSectionValue(sections, "qualification"), - preferred: getSectionValue(sections, "preference"), - }; -} - -function createQuestionLoadingDurationMs() { - return 40000 + Math.floor(Math.random() * 20001); -} - -function delay(ms: number) { - return new Promise((resolve) => { - window.setTimeout(resolve, ms); - }); -} - -interface QuestionLoadingInfo { - companyName: string; - jobName: string; -} - -function createManualRawText(sections: JdReviewSection[]) { - const rawText = sections - .map(({ label, value }) => { - const trimmedValue = value.trim(); - - return trimmedValue ? `${label}\n${trimmedValue}` : ""; - }) - .filter(Boolean) - .join("\n\n"); - - if (!rawText) { - throw new Error("공고 내용을 입력해주세요."); - } - - return rawText; -} - -async function createSavedJobPostingFromRawText(rawText: string) { - const accepted = await ingestJobPosting({ rawText }); - const status = await waitForJobPostingIngest(accepted.taskId); - const savedJobPosting = status.result?.saved; - - if (!savedJobPosting) { - throw new Error( - status.result?.message || "채용 공고 저장 결과를 확인할 수 없습니다.", - ); - } - - return savedJobPosting; -} - -function saveJdReviewSessionData({ - applyId, - sections, - savedJobPosting, -}: { - applyId: string; - sections: JdReviewSection[]; - savedJobPosting: SavedJobPosting; -}) { - window.sessionStorage.setItem( - getJdReviewStorageKey(applyId), - JSON.stringify(sections), - ); - window.sessionStorage.setItem( - getJdReviewSavedStorageKey(applyId), - JSON.stringify(savedJobPosting), - ); - window.sessionStorage.setItem( - getJdReviewMetadataStorageKey(applyId), - JSON.stringify({ - companySize: savedJobPosting.companySize, - detailClassificationId: savedJobPosting.detailClassificationId, - profileColor: savedJobPosting.profileColor, - postingName: savedJobPosting.postingName, - jobTitle: savedJobPosting.jobTitle, - }), - ); -} - -export default function MockApplicationJdReviewPage() { - const { mockApplyId: id } = useParams<{ mockApplyId: string }>(); - const router = useRouter(); - const searchParams = useSearchParams(); - const mode = searchParams.get("mode"); - const storageKey = getJdReviewStorageKey(id); - const storedSectionsValue = useSyncExternalStore( - subscribeToSessionStorage, - () => window.sessionStorage.getItem(storageKey) ?? "", - () => "", - ); - - const reviewSections = useMemo( - () => - mode === "manual" - ? emptyJdSections - : parseStoredSections(storedSectionsValue), - [mode, storedSectionsValue], - ); - const [currentSections, setCurrentSections] = useState( - reviewSections ?? mockJdSections, - ); - const [showBackConfirm, setShowBackConfirm] = useState(false); - const [saveErrorMessage, setSaveErrorMessage] = useState(""); - const [isSaving, setIsSaving] = useState(false); - const [questionLoadingDurationMs, setQuestionLoadingDurationMs] = - useState(50000); - const [questionLoadingInfo, setQuestionLoadingInfo] = - useState(null); - - const openBackConfirm = () => setShowBackConfirm(true); - const closeBackConfirm = () => setShowBackConfirm(false); - const goToJdInput = () => router.replace(`/mockApply/${id}/jd-input`); - const closeSaveError = () => setSaveErrorMessage(""); - - const handleConfirm = async () => { - if (isSaving) { - return; - } - - setSaveErrorMessage(""); - const isManualMode = mode === "manual"; - let createSavedJobPosting: () => Promise; - let loadingCompanyName = getSectionValue(currentSections, "company"); - - try { - if (isManualMode) { - const rawText = createManualRawText(currentSections); - createSavedJobPosting = () => createSavedJobPostingFromRawText(rawText); - } else { - const metadata = parseStoredMetadata( - window.sessionStorage.getItem(getJdReviewMetadataStorageKey(id)), - ); - const savePayload: JobPostingSavePayload = createSavePayload( - currentSections, - metadata, - ); - - loadingCompanyName = savePayload.companyName; - createSavedJobPosting = () => saveJobPosting(savePayload); - } - } catch (error) { - setSaveErrorMessage( - error instanceof Error - ? error.message - : "채용 공고 저장에 실패했습니다.", - ); - return; - } - - const loadingDurationMs = createQuestionLoadingDurationMs(); - const nextQuestionLoadingInfo = { - companyName: loadingCompanyName, - jobName: getSectionValue(currentSections, "job"), - }; - let shouldKeepLoading = false; - - setQuestionLoadingInfo(nextQuestionLoadingInfo); - setQuestionLoadingDurationMs(loadingDurationMs); - setIsSaving(true); - - try { - const createNextApply = async () => { - const savedJobPosting = await createSavedJobPosting(); - const createdApply = await createApplyFromJobPosting({ - jobPostingId: savedJobPosting.jobPostingId, - applyType: getSelectedApplyType(), - }); - const nextApplyId = String(createdApply.mockApplyId); - - saveMockApplyResumeRecord({ - jobPostingId: savedJobPosting.jobPostingId, - mockApplyId: createdApply.mockApplyId, - status: "APPLICATION_CREATED", - }); - - saveJdReviewSessionData({ - applyId: id, - sections: currentSections, - savedJobPosting, - }); - saveJdReviewSessionData({ - applyId: nextApplyId, - sections: currentSections, - savedJobPosting, - }); - - return { - jobPostingId: savedJobPosting.jobPostingId, - mockApplyId: nextApplyId, - }; - }; - - const [nextApply] = await Promise.all([ - createNextApply(), - delay(loadingDurationMs), - ]); - - shouldKeepLoading = true; - router.push( - `/mockApply/${nextApply.mockApplyId}?jobPostingId=${nextApply.jobPostingId}`, - ); - } catch (error) { - setSaveErrorMessage( - error instanceof Error - ? error.message - : "채용 공고 저장에 실패했습니다.", - ); - } finally { - if (!shouldKeepLoading) { - setIsSaving(false); - } - } - }; - - const loadingCompanyName = getSectionValue(currentSections, "company"); - const loadingJobName = getSectionValue(currentSections, "job"); - const questionLoadingCompanyName = - questionLoadingInfo?.companyName || loadingCompanyName; - const questionLoadingJobName = questionLoadingInfo?.jobName || loadingJobName; - - return ( -
- {isSaving ? ( - - ) : ( - <> - -
- - )} - - {showBackConfirm && ( -
- -
- )} - - {saveErrorMessage && ( -
- -
- )} -
- ); -} diff --git a/jobdri/src/app/mockApply/[mockApplyId]/(jd)/page.tsx b/jobdri/src/app/mockApply/[mockApplyId]/(jd)/page.tsx deleted file mode 100644 index 12cdfda..0000000 --- a/jobdri/src/app/mockApply/[mockApplyId]/(jd)/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -"use client"; - -import { use } from "react"; -import { Footer } from "@/components/common/footer"; -import Header from "@/components/common/header/Header"; - -interface JdPageProps { - params: Promise<{ mockApplyId: string }>; -} - -export default function JdPage({ params }: JdPageProps) { - const { mockApplyId } = use(params); - - return ( - <> -
-
{/* JD 입력 UI */}
-