diff --git a/jobdri/components/common/LoadMotion.tsx b/jobdri/components/common/LoadMotion.tsx deleted file mode 100644 index ed88097..0000000 --- a/jobdri/components/common/LoadMotion.tsx +++ /dev/null @@ -1,56 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import clsx from "clsx"; - -interface LoadMotionProps { - className?: string; - dotFrameClassName?: string; - dotClassName?: string; - activeDotClassName?: string; - inactiveDotClassName?: string; - activeMotionClassName?: string; - inactiveMotionClassName?: string; -} - -export default function LoadMotion({ - className, - dotFrameClassName, - dotClassName, - activeDotClassName = "bg-icon-neutral-heavy", - inactiveDotClassName = "bg-icon-neutral-assistive", - activeMotionClassName = "-translate-y-0.5", - inactiveMotionClassName = "translate-y-0", -}: LoadMotionProps) { - const [active, setActive] = useState(0); - - useEffect(() => { - const interval = setInterval(() => { - setActive((prev) => (prev + 1) % 3); - }, 400); - return () => clearInterval(interval); - }, []); - return ( -
- {[0, 1, 2].map((i) => ( -
- -
- ))} -
- ); -} diff --git a/jobdri/src/app/layout.tsx b/jobdri/src/app/layout.tsx index 5be80ed..839463a 100644 --- a/jobdri/src/app/layout.tsx +++ b/jobdri/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import "./globals.css"; +import QueryProvider from "@/components/providers/QueryProvider"; // import LayoutShell from "./LayoutShell"; export const metadata: Metadata = { @@ -18,7 +19,7 @@ export default function RootLayout({ - {children} + {children} ); diff --git a/jobdri/src/app/mockApply/[mockApplyId]/page.tsx b/jobdri/src/app/mockApply/[mockApplyId]/page.tsx index af67be5..b00fb2b 100644 --- a/jobdri/src/app/mockApply/[mockApplyId]/page.tsx +++ b/jobdri/src/app/mockApply/[mockApplyId]/page.tsx @@ -7,7 +7,7 @@ import JDSidePanel from "@/components/mockApply/Question/SidePanel"; import SideHeaderContainer from "@/components/common/header/SideHeaderContainer"; import WritingForm from "@/components/mockApply/Question/WritingForm"; import clsx from "clsx"; -import { scrollbarClass } from "@/components/common/scrollbar/scrollbarStyles"; +import { scrollbarClassS } from "@/components/common/scrollbar/scrollbarStyles"; import { fetchSelectedQuestions, saveQuestions, @@ -18,7 +18,8 @@ import { ModalCard } from "@/components/common/modal/ModalCard"; import { Toast, type ToastVariant } from "@/components/common/toast"; import { CtaFooter } from "@/components/common/cta"; import { fetchCreditBalance } from "@/lib/api/credit"; -import { formatApplicationSequenceLabel } from "@/lib/mockApply/applicationLabel"; +import { requestAnalysis } from "@/lib/api/analysis"; +import MockApplyTemplate from "@/components/common/MockApplyTemplate"; const ModalOverlay = ({ children }: { children: React.ReactNode }) => (
@@ -39,14 +40,20 @@ export default function MockApplyPage({ const [selectedId, setSelectedId] = useState(null); const [lastSavedTime, setLastSavedTime] = useState("저장 전"); - const [toast, setToast] = useState<{ open: boolean; message: string }>({ + const [toast, setToast] = useState<{ + open: boolean; + message: string; + variant?: string; + }>({ open: false, message: "", + variant: "normal", }); const [modalTarget, setModalTarget] = useState(null); const [isLeaveModalOpen, setIsLeaveModalOpen] = useState(false); const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false); const [isCreditShortModalOpen, setIsCreditShortModalOpen] = useState(false); + const [applicationLabel, setApplicationLabel] = useState(""); // 데이터 포맷팅 함수 const getSubmitPayload = (questionsData: QuestionItem[]) => { @@ -93,7 +100,7 @@ export default function MockApplyPage({ const now = new Date(); const timeString = `${now.getHours()}:${String(now.getMinutes()).padStart(2, "0")}`; setLastSavedTime(timeString); - console.log("✅ 자동 저장 완료!", timeString); + console.log("자동 저장 완료!", timeString); } catch (error) { console.error("자동 저장 실패:", error); } @@ -117,6 +124,21 @@ export default function MockApplyPage({ setTimeout(() => setToast({ open: false, message: "" }), 3000); }; + const handleConfirm = async () => { + try { + await saveApply(Number(mockApplyId), getSubmitPayload(questions)); + const response = await requestAnalysis(Number(mockApplyId)); + const taskId = response.taskId; + + router.push( + `/mockApply/${mockApplyId}/result/resume-analysis-loading?taskId=${taskId}`, + ); + } catch (error) { + console.error("제출 실패:", error); + alert("채점 요청 중 오류가 발생했습니다."); + } + }; + const handleDeleteQuestion = (targetId: string) => { const targetQ = questions.find((q) => q.id === targetId); const hasContent = @@ -149,7 +171,7 @@ export default function MockApplyPage({ const newQuestion: QuestionItem = { id: `temp-${Date.now()}`, questionId: 0, - question: "새 문항", + question: "새로운 문항", answer: "", maxLength: 1000, custom: true, @@ -176,15 +198,12 @@ export default function MockApplyPage({ }; const handleTrySubmit = async () => { - let hasCredit = false; - try { const currentCredit = await fetchCreditBalance(); setIsConfirmModalOpen(false); if (currentCredit > 0) { - await saveApply(Number(mockApplyId), getSubmitPayload(questions)); - router.push(`/mockApply/${mockApplyId}/result/`); + await handleConfirm(); } else { setIsCreditShortModalOpen(true); } @@ -199,16 +218,6 @@ export default function MockApplyPage({ 3000, ); } - - if (!hasCredit) { - return; - } - - try { - await handleConfirm(); - } catch { - alert("답변 저장에 실패했습니다."); - } }; // 현재 선택된 문항 폼 데이터 @@ -222,142 +231,142 @@ export default function MockApplyPage({ : null; return ( -
-
- -
-
- - } - /> -
- -
+
+
-
- {mappedQuestionForForm ? ( - - ) : ( -
- 문항을 불러오는 중입니다... -
+
+ + } + /> +
+ +
+
+ {mappedQuestionForForm ? ( + + ) : ( +
+ 문항을 불러오는 중입니다... +
+ )} +
-
-
- - setIsLeaveModalOpen(true) }} - nextAction={{ - label: "채점하기", - onClick: () => setIsConfirmModalOpen(true), - disabled: - !mappedQuestionForForm || - questions.some((q) => !(q.answer || "").trim()), - iconType: "SPARKLE", - }} - /> - - setIsPanelOpen(false)} - onOpen={() => setIsPanelOpen(true)} - /> - - {toast.open && ( - setToast({ ...toast, open: false })} - className="absolute top-6" +
+ + setIsLeaveModalOpen(true) }} + nextAction={{ + label: "채점하기", + onClick: () => setIsConfirmModalOpen(true), + disabled: + !mappedQuestionForForm || + questions.some((q) => !(q.answer || "").trim()), + iconType: "SPARKLE", + }} /> - )} - - {modalTarget && ( - - setModalTarget(null)} - onErrorClick={() => { - performDelete(modalTarget); - setModalTarget(null); - }} - /> - - )} - - {isLeaveModalOpen && ( - - setIsLeaveModalOpen(false)} - onPrimaryClick={() => { - setIsLeaveModalOpen(false); - router.push(`/mockApply/actual/${mockApplyId}/jd-review`); - }} - /> - - )} - - {isConfirmModalOpen && ( - - setIsConfirmModalOpen(false)} - onPrimaryClick={handleTrySubmit} - /> - - )} - - {isCreditShortModalOpen && ( - - setIsCreditShortModalOpen(false)} - onPrimaryClick={() => setIsCreditShortModalOpen(false)} + + setIsPanelOpen(false)} + onOpen={() => setIsPanelOpen(true)} + /> + + {toast.open && ( + setToast({ ...toast, open: false })} + className="absolute top-6" /> - - )} -
+ )} + + {modalTarget && ( + + setModalTarget(null)} + onErrorClick={() => { + performDelete(modalTarget); + setModalTarget(null); + }} + /> + + )} + + {isLeaveModalOpen && ( + + setIsLeaveModalOpen(false)} + onPrimaryClick={() => { + setIsLeaveModalOpen(false); + router.push(`/mockApply/actual/${mockApplyId}/jd-review`); + }} + /> + + )} + + {isConfirmModalOpen && ( + + setIsConfirmModalOpen(false)} + onPrimaryClick={handleTrySubmit} + /> + + )} + + {isCreditShortModalOpen && ( + + setIsCreditShortModalOpen(false)} + onPrimaryClick={() => setIsCreditShortModalOpen(false)} + /> + + )} +
+ ); } diff --git a/jobdri/src/app/mockApply/[mockApplyId]/result/[jobPostingId]/ResultPageClient.tsx b/jobdri/src/app/mockApply/[mockApplyId]/result/[jobPostingId]/ResultPageClient.tsx deleted file mode 100644 index a5757d4..0000000 --- a/jobdri/src/app/mockApply/[mockApplyId]/result/[jobPostingId]/ResultPageClient.tsx +++ /dev/null @@ -1,70 +0,0 @@ -"use client"; - -import { useCallback, useState } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; -import Header from "@/components/common/header/Header"; -import ApplyResult from "@/components/mockApply/result/ApplyResult"; -import { ModalNotice } from "@/components/common/modal"; -import { useReApply } from "@/hooks/useReApply"; - -interface ResultPageClientProps { - id: string; -} - -export default function ResultPageClient({ id }: ResultPageClientProps) { - const router = useRouter(); - const searchParams = useSearchParams(); - const sequence = Number(searchParams.get("sequence") ?? "1"); - const totalCount = Number(searchParams.get("totalCount") ?? sequence); - const jobPostingId = Number(id); - const [showAnalysisErrorModal, setShowAnalysisErrorModal] = useState(false); - const [mockApplyId, setMockApplyId] = useState(0); - const { reApply, isSaving } = useReApply(); - - const closeAnalysisErrorModal = () => { - setShowAnalysisErrorModal(false); - router.push("/mockApply"); - }; - const openAnalysisErrorModal = useCallback(() => { - setShowAnalysisErrorModal(true); - }, []); - - return ( -
-
reApply(mockApplyId), - disabled: isSaving || !mockApplyId, - }} - /> -
- -
- - {showAnalysisErrorModal && ( -
- -
- )} -
- ); -} diff --git a/jobdri/src/app/mockApply/[mockApplyId]/result/[jobPostingId]/page.tsx b/jobdri/src/app/mockApply/[mockApplyId]/result/[jobPostingId]/page.tsx deleted file mode 100644 index 9647ac7..0000000 --- a/jobdri/src/app/mockApply/[mockApplyId]/result/[jobPostingId]/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Suspense } from "react"; -import ResultPageClient from "./ResultPageClient"; - -interface ResultPageProps { - params: Promise<{ jobPostingId: string }>; -} - -export default async function ResultPage({ params }: ResultPageProps) { - const { jobPostingId } = await params; - return ( - - - - ); -} diff --git a/jobdri/src/app/mockApply/[mockApplyId]/result/page.tsx b/jobdri/src/app/mockApply/[mockApplyId]/result/page.tsx new file mode 100644 index 0000000..79d1280 --- /dev/null +++ b/jobdri/src/app/mockApply/[mockApplyId]/result/page.tsx @@ -0,0 +1,154 @@ +"use client"; + +import { use, useState } from "react"; +import { useRouter } from "next/navigation"; +import ResumeAnalysisFeedback from "@/components/mockApply/result/ResumeAnalysisFeedback"; +import ResumeAnalysisDetail from "@/components/mockApply/result/ResumeAnalysisDetail"; +import AnalysisHeader from "@/components/mockApply/result/AnalysisHeader"; +import { formatApplicationSequenceLabel } from "@/lib/mockApply/applicationLabel"; +import { ModalNotice } from "@/components/common/modal"; +import { Toast } from "@/components/common/toast"; +import { useReApply } from "@/hooks/useReApply"; +import { getMockApplyResumeRecords } from "@/lib/api/mockApplies"; +import { useAnalysisResult } from "@/hooks/useAnalysisResult"; +import { AnalysisResultResponse } from "@/lib/api/analysis"; +import MockApplyTemplate from "@/components/common/MockApplyTemplate"; + +interface ResultPageProps { + params: Promise<{ + mockApplyId: string; + }>; + searchParams: Promise<{ + sequence?: string; + tab?: string; + }>; +} + +function parsePositiveNumber(value?: string) { + const parsedValue = Number(value); + return Number.isFinite(parsedValue) && parsedValue > 0 + ? parsedValue + : undefined; +} + +export default function ResultPage({ params, searchParams }: ResultPageProps) { + const { mockApplyId } = use(params); + const { sequence, tab } = use(searchParams); + const router = useRouter(); + const { reApply, isSaving } = useReApply(); + const [isRetryModalOpen, setIsRetryModalOpen] = useState(false); + const [toast, setToast] = useState<{ open: boolean; message: string }>({ + open: false, + message: "", + }); + + const parsedSequence = parsePositiveNumber(sequence); + const parsedMockApplyId = parsePositiveNumber(mockApplyId); + const applicationLabel = formatApplicationSequenceLabel(parsedSequence); + + const { + data: analysisData, + isPending, + isError, + } = useAnalysisResult(parsedMockApplyId); + + const activeTabId = tab === "score-detail" ? "score-detail" : "ai-feedback"; + const headerComponent = ; + + const closeToast = () => setToast({ open: false, message: "" }); + const showTopToast = (message: string) => { + setToast({ open: true, message }); + window.setTimeout(closeToast, 3000); + }; + + const handleRetryConfirm = async () => { + const resolvedMockApplyId = + parsedMockApplyId ?? getMockApplyResumeRecords()[0]?.mockApplyId; + + if (!resolvedMockApplyId) { + setIsRetryModalOpen(false); + showTopToast("재도전할 지원 정보를 찾지 못했어요."); + return; + } + try { + await reApply(resolvedMockApplyId); + } catch (error) { + setIsRetryModalOpen(false); + showTopToast("재도전을 시작하지 못했어요. 잠시 후 다시 시도해주세요."); + } + }; + + return ( + <> + setIsRetryModalOpen(true)} + onSaveAndExitClick={() => router.push("/")} + > + {/* 🌟 3. 이 안쪽은 온전히 '컨텐츠(main)' 영역입니다! */} +
+ {isPending ? ( +
+ + 분석 결과를 불러오는 중... + +
+ ) : isError ? ( +
+ 데이터를 불러오지 못했습니다. 잠시 후 다시 시도해주세요. +
+ ) : analysisData ? ( + activeTabId === "ai-feedback" ? ( + + {headerComponent} + + ) : ( + + {headerComponent} + + ) + ) : null} +
+
+ + {isRetryModalOpen && ( +
+ setIsRetryModalOpen(false)} + secondaryAction={{ + label: "취소", + onClick: () => setIsRetryModalOpen(false), + }} + primaryAction={{ + label: "재도전 하기", + onClick: handleRetryConfirm, + disabled: isSaving, + }} + /> +
+ )} + + {toast.open && ( + + )} + + ); +} diff --git a/jobdri/src/app/mockApply/resume-analysis-loading/ResumeAnalysisLoadingPageClient.tsx b/jobdri/src/app/mockApply/[mockApplyId]/result/resume-analysis-loading/ResumeAnalysisLoadingPageClient.tsx similarity index 72% rename from jobdri/src/app/mockApply/resume-analysis-loading/ResumeAnalysisLoadingPageClient.tsx rename to jobdri/src/app/mockApply/[mockApplyId]/result/resume-analysis-loading/ResumeAnalysisLoadingPageClient.tsx index e9e44df..d6a09df 100644 --- a/jobdri/src/app/mockApply/resume-analysis-loading/ResumeAnalysisLoadingPageClient.tsx +++ b/jobdri/src/app/mockApply/[mockApplyId]/result/resume-analysis-loading/ResumeAnalysisLoadingPageClient.tsx @@ -1,7 +1,7 @@ "use client"; import { useCallback } from "react"; -import { useRouter } from "next/navigation"; +import { useRouter, useParams } from "next/navigation"; import ResumeAnalysisLoading from "@/components/mockApply/ResumeAnalysisLoading"; const RESUME_ANALYSIS_LOADING_DURATION_MS = 316_000; @@ -14,10 +14,13 @@ export default function ResumeAnalysisLoadingPageClient({ applicationLabel, }: ResumeAnalysisLoadingPageClientProps) { const router = useRouter(); + const params = useParams(); + const mockApplyId = params.mockApplyId; + const handleComplete = useCallback(() => { - const queryString = window.location.search; - router.replace(`/mockApply/resume-analysis-feedback${queryString}`); - }, [router]); + if (!mockApplyId) return; + router.replace(`/mockApply/${mockApplyId}/result`); + }, [router, mockApplyId]); return ( 0; + const hasAttachedFiles = attachedFiles.length > 0; + + if (!hasInputContent && !hasAttachedFiles) { + clearJobPostingDraft(); + router.push("/"); + return; + } + setShowExitConfirm(true); }; diff --git a/jobdri/src/app/mockApply/job/review/page.tsx b/jobdri/src/app/mockApply/job/review/page.tsx index d987727..39ae36a 100644 --- a/jobdri/src/app/mockApply/job/review/page.tsx +++ b/jobdri/src/app/mockApply/job/review/page.tsx @@ -13,6 +13,7 @@ import { CtaFooter } from "@/components/common/cta"; import { JDInput } from "@/components/common/input"; import { ModalNotice } from "@/components/common/modal"; import Avatar from "@/components/mockApply/home/Avatar"; +import MockApplyTemplate from "@/components/common/MockApplyTemplate"; const wizardSteps = [ { label: "공고 확인" }, @@ -85,8 +86,13 @@ function JobProfileRow({ avatarName }: { avatarName: string }) { ); } -export default function JobPostingReviewPage() { +export default function JobPostingReviewPage({ + params, +}: { + params: { mockApplyId: string }; +}) { const router = useRouter(); + const { mockApplyId } = params; const [jobPostingName, setJobPostingName] = useState(""); const [companyName, setCompanyName] = useState(""); const [roleName, setRoleName] = useState(""); @@ -112,22 +118,14 @@ export default function JobPostingReviewPage() { }; return ( -
-
-
- + <> + setShowBackConfirm(true)} + onNextClick={() => router.push(`/mockApply/${mockApplyId}`)} + isNextDisabled={!isNextEnabled} + >
- + @@ -204,20 +195,7 @@ export default function JobPostingReviewPage() { className="!top-[10px] !right-1 !bottom-[10px] z-10" />
- - setShowBackConfirm(true), - }} - nextAction={{ - label: "다음으로", - disabled: !isNextEnabled, - }} - /> -
+
{showBackConfirm && (
@@ -257,6 +235,6 @@ export default function JobPostingReviewPage() { />
)} -
+ ); } diff --git a/jobdri/src/app/mockApply/resume-analysis-feedback/page.tsx b/jobdri/src/app/mockApply/resume-analysis-feedback/page.tsx deleted file mode 100644 index e16bdc4..0000000 --- a/jobdri/src/app/mockApply/resume-analysis-feedback/page.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import ResumeAnalysisFeedback from "@/components/mockApply/ResumeAnalysisFeedback"; - -interface ResumeAnalysisFeedbackPageProps { - searchParams: Promise<{ - mockApplyId?: string; - sequence?: string; - }>; -} - -function parsePositiveNumber(value?: string) { - const parsedValue = Number(value); - - return Number.isFinite(parsedValue) && parsedValue > 0 - ? parsedValue - : undefined; -} - -export default async function ResumeAnalysisFeedbackPage({ - searchParams, -}: ResumeAnalysisFeedbackPageProps) { - const { mockApplyId, sequence } = await searchParams; - - return ( - - ); -} diff --git a/jobdri/src/app/page.tsx b/jobdri/src/app/page.tsx index 7922b98..d52c7b2 100644 --- a/jobdri/src/app/page.tsx +++ b/jobdri/src/app/page.tsx @@ -152,7 +152,7 @@ export default function Home() { }} onResume={(app) => { // 예시: 결과 상세 페이지로 이동! - router.push(`/mockApply/result/${app.mockApplyId}`); + router.push(`/mockApply/${app.mockApplyId}/result`); }} />
diff --git a/jobdri/src/assets/ic_Empty.svg b/jobdri/src/assets/ic_Empty.svg new file mode 100644 index 0000000..9f46dcd --- /dev/null +++ b/jobdri/src/assets/ic_Empty.svg @@ -0,0 +1,4 @@ + + + + diff --git a/jobdri/src/components/common/AppShell.tsx b/jobdri/src/components/common/AppShell.tsx deleted file mode 100644 index f5a4c07..0000000 --- a/jobdri/src/components/common/AppShell.tsx +++ /dev/null @@ -1,30 +0,0 @@ -"use client"; - -import type { ReactNode } from "react"; -import { usePathname } from "next/navigation"; -import Lnb from "@/components/common/lnb/Lnb"; -import PageHeader from "@/components/common/PageHeader"; - -const standaloneRoutes = new Set(["/login"]); - -export default function AppShell({ children }: { children: ReactNode }) { - const pathname = usePathname(); - - if (standaloneRoutes.has(pathname)) { - return <>{children}; - } - - return ( -
- -
-
- - {children} -
-
-
- ); -} diff --git a/jobdri/src/components/common/MockApplyTemplate.tsx b/jobdri/src/components/common/MockApplyTemplate.tsx new file mode 100644 index 0000000..194951d --- /dev/null +++ b/jobdri/src/components/common/MockApplyTemplate.tsx @@ -0,0 +1,111 @@ +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { usePathname } from "next/navigation"; +import Header from "@/components/common/header/Header"; +import CtaFooter from "@/components/common/cta/CtaFooter"; +import { fetchSequence } from "@/lib/api/result"; +import { LEAVE_MODAL_CONFIG } from "@/constants/modalConfig"; +import { ModalCard } from "./modal/ModalCard"; + +interface MockApplyTemplateProps { + mockApplyId: number; + currentStep: number; + + children: React.ReactNode; + customHomeModal?: (typeof LEAVE_MODAL_CONFIG)[keyof typeof LEAVE_MODAL_CONFIG]; + onHomeLeave?: () => void; + + lastSavedAt?: string; + onHomeClick?: () => void; + onBackClick?: () => void; + onNextClick?: () => void; + isNextDisabled?: boolean; + onRetryClick?: () => void; + onSaveAndExitClick?: () => void; +} + +export default function MockApplyTemplate({ + mockApplyId, + children, + currentStep, + customHomeModal = LEAVE_MODAL_CONFIG.COMMON_HOME, + onHomeLeave, + + lastSavedAt, + onBackClick, + onNextClick, + isNextDisabled, + onRetryClick, + onSaveAndExitClick, +}: MockApplyTemplateProps) { + const router = useRouter(); + const [showHomeConfirm, setShowHomeConfirm] = useState(false); + const pathname = usePathname(); + const isResultPage = pathname.includes("/result"); + + const handleHomeLeave = () => { + if (onHomeLeave) onHomeLeave(); + else router.push("/"); + }; + + const [applicationLabel, setApplicationLabel] = + useState("첫 번째 지원"); + + useEffect(() => { + const loadSequence = async () => { + if (!mockApplyId) return; + try { + const data = await fetchSequence(mockApplyId); + setApplicationLabel(`${data.sequence}번째 지원`); + } catch (error) { + console.error("순번을 불러오지 못했습니다.", error); + } + }; + + loadSequence(); + }, [mockApplyId]); + + return ( +
+
{ + e.preventDefault(); + setShowHomeConfirm(true); + }, + }} + /> + +
{children}
+ + + {showHomeConfirm && ( +
+ setShowHomeConfirm(false)} + /> +
+ )} +
+ ); +} diff --git a/jobdri/src/components/common/chips/ChipTag.tsx b/jobdri/src/components/common/chips/ChipTag.tsx index 156074c..91919e1 100644 --- a/jobdri/src/components/common/chips/ChipTag.tsx +++ b/jobdri/src/components/common/chips/ChipTag.tsx @@ -24,8 +24,8 @@ const stateColorMap: Record< > = { default: "default", proven: "green", - mentioned: "pink", - fabricated: "red", + mentioned: "red", + fabricated: "pink", }; export default function ChipTag({ diff --git a/jobdri/src/components/common/header/Header.tsx b/jobdri/src/components/common/header/Header.tsx index 44e2fe6..b25b55b 100644 --- a/jobdri/src/components/common/header/Header.tsx +++ b/jobdri/src/components/common/header/Header.tsx @@ -146,7 +146,7 @@ export default function Header({ jobTitle = "프로덕트 디자이너", applicationLabel, version, - lastSavedAt = "17:00", + lastSavedAt, lastSavedLabel = "마지막 저장", homeAction = { label: "홈으로" }, currentStep = 1, @@ -305,16 +305,17 @@ export default function Header({ })} -
-
- - {lastSavedLabel} - - -
+ {lastSavedAt && ( +
+ + {lastSavedLabel} + + +
+ )} {homeActionButton}
diff --git a/jobdri/src/components/common/icons/Icon.tsx b/jobdri/src/components/common/icons/Icon.tsx index 7f961e1..163bdcb 100644 --- a/jobdri/src/components/common/icons/Icon.tsx +++ b/jobdri/src/components/common/icons/Icon.tsx @@ -78,6 +78,7 @@ import IC_LOGOUT from "@/assets/ic_Logout.svg"; import IC_INFO from "@/assets/ic_Info.svg"; import IC_REVIEW_20 from "@/assets/ic_Review_20.svg"; import IC_SCORE_20 from "@/assets/ic_Score_20.svg"; +import IC_EMPTY from "@/assets/ic_Empty.svg"; const iconMap = { EX_L: IC_EX_L, @@ -163,6 +164,7 @@ const iconMap = { INFO: IC_INFO, REVIEW_20: IC_REVIEW_20, SCORE_20: IC_SCORE_20, + EMPTY: IC_EMPTY, } as const; export type IconType = keyof typeof iconMap; diff --git a/jobdri/src/components/common/scrollbar/scrollbarStyles.ts b/jobdri/src/components/common/scrollbar/scrollbarStyles.ts index 245109c..0e4c172 100644 --- a/jobdri/src/components/common/scrollbar/scrollbarStyles.ts +++ b/jobdri/src/components/common/scrollbar/scrollbarStyles.ts @@ -1,2 +1,20 @@ -export const scrollbarClass = - "[&::-webkit-scrollbar]:w-1 [&::-webkit-scrollbar-track-piece]:bg-transparent [&::-webkit-scrollbar-thumb]:bg-icon-neutral-weak [&::-webkit-scrollbar-thumb]:rounded-full"; +const scrollbarBase = + "[&::-webkit-scrollbar-track]:bg-transparent " + + "[&::-webkit-scrollbar-thumb]:bg-icon-neutral-weak " + + "[&::-webkit-scrollbar-thumb]:rounded-full " + + "[&::-webkit-scrollbar-thumb]:border-solid " + + "[&::-webkit-scrollbar-thumb]:border-transparent " + + "[&::-webkit-scrollbar-thumb]:bg-clip-padding " + + "[&::-webkit-scrollbar-track]:![margin-block:4px] "; + +// 텍스트 인풋 등 넓은 영역에 사용 +export const scrollbarClassL = + scrollbarBase + + "[&::-webkit-scrollbar]:w-4 [&::-webkit-scrollbar]:h-4 " + // 전체 너비 16px + "[&::-webkit-scrollbar-thumb]:border-[4px]"; // 상하좌우 4px 투명 여백 (실제 보이는 두께 8px) + +// 리스트 내부, 드롭다운 등 좁은 영역에 사용 +export const scrollbarClassS = + scrollbarBase + + "[&::-webkit-scrollbar]:w-3 [&::-webkit-scrollbar]:h-3 " + // 전체 너비 12px + "[&::-webkit-scrollbar-thumb]:border-[4px]"; // 상하좌우 4px 투명 여백 (실제 보이는 두께 4px) diff --git a/jobdri/src/components/mockApply/Question/QuestionList.tsx b/jobdri/src/components/mockApply/Question/QuestionList.tsx index d159741..09a951d 100644 --- a/jobdri/src/components/mockApply/Question/QuestionList.tsx +++ b/jobdri/src/components/mockApply/Question/QuestionList.tsx @@ -7,8 +7,9 @@ interface QuestionListProps { questions: ApiQuestionItem[]; // API에서 받아온 전체 문항 배열 selectedId: string | null; // 현재 선택된 문항 ID onSelect: (id: string) => void; // 문항 클릭 시 부모에게 알림 - onAdd: () => void; // 문항 추가 시 부모에게 알림 - onDelete: (id: string) => void; // 문항 삭제 시 부모에게 알림 + onAdd?: () => void; // 문항 추가 시 부모에게 알림 + onDelete?: (id: string) => void; // 문항 삭제 시 부모에게 알림 + type?: "result" | "apply"; } export const QuestionList = ({ @@ -17,15 +18,22 @@ export const QuestionList = ({ onSelect, onAdd, onDelete, + type = "apply", }: QuestionListProps) => { const MAX_QUESTIONS = 5; return (
{/* 헤더 영역 */} -
+

- 자소서 문항 {questions.length}/{MAX_QUESTIONS} + {type === "apply" ? ( + <> + 자소서 문항 {questions.length}/{MAX_QUESTIONS} + + ) : ( + <>문항별 피드백 + )}

@@ -38,18 +46,21 @@ export const QuestionList = ({ data={ { id: Number(q.id), - title: (q.question === "새 문항" ? "" : q.question) || "", + title: (q.question === "새로운 문항" ? "" : q.question) || "", content: q.answer || "", } as QuestionData } isActive={q.id === selectedId} onClick={() => onSelect(q.id)} - onDelete={questions.length > 1 ? () => onDelete(q.id) : undefined} + onDelete={ + onDelete && type === "apply" && questions.length > 1 + ? () => onDelete(q.id) + : undefined + } /> ))} - {/* 문항 추가 버튼 */} - {questions.length < MAX_QUESTIONS && ( + {type === "apply" && questions.length < MAX_QUESTIONS && (
diff --git a/jobdri/src/components/mockApply/result/ApplyResult.tsx b/jobdri/src/components/mockApply/result/ApplyResult.tsx deleted file mode 100644 index 0d923d4..0000000 --- a/jobdri/src/components/mockApply/result/ApplyResult.tsx +++ /dev/null @@ -1,142 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { - fetchAnalysisByJobPosting, - type AnalysisResult, -} from "@/lib/api/result"; -import Sidebar from "@/components/mockApply/result/Sidebar"; -import Trybar from "./Trybar"; -import OverviewSection from "./OverviewSection"; -import DetailSection from "./DetailSection"; - -interface ApplyResultProps { - jobPostingId: number; - sequence?: number; - totalCount?: number; - onAnalysisError?: () => void; - onMockApplyIdChange?: (mockApplyId: number) => void; -} - -interface Question { - id: string; - question: string; -} - -const FALLBACK_QUESTIONS: Question[] = [ - { id: "f0", question: "지원 동기를 500자 이내로 작성해주세요." }, - { - id: "f1", - question: - "데이터를 기반으로 문제점을 파악하고 성과를 개선해 본 경험을 서술해 주세요.", - }, - { - id: "f2", - question: "프로젝트에서 마주친 기술적 어려움과 해결 방법을 설명해주세요.", - }, - { - id: "f3", - question: "프로젝트에서 마주친 기술적 어려움과 해결 방법을 설명해주세요.", - }, - { - id: "f4", - question: "프로젝트에서 마주친 기술적 어려움과 해결 방법을 설명해주세요.", - }, -]; - -function ResultLoadingState({ message }: { message: string }) { - return ( -
- {message} -
- ); -} - -export default function ApplyResult({ - jobPostingId, - sequence: initialSequence = 1, - totalCount: initialTotalCount, - onAnalysisError, - onMockApplyIdChange, -}: ApplyResultProps) { - const [selectedSequence, setSelectedSequence] = useState(initialSequence); - const [questions, setQuestions] = useState(FALLBACK_QUESTIONS); - const [analysis, setAnalysis] = useState(null); - const [analysisErrorMessage, setAnalysisErrorMessage] = useState(""); - const [isOverview, setIsOverview] = useState(true); - const [activeId, setActiveId] = useState("0"); - - useEffect(() => { - const controller = new AbortController(); - - fetchAnalysisByJobPosting(jobPostingId, selectedSequence, controller.signal) - .then((fetchedAnalysis) => { - if (controller.signal.aborted) return; - setAnalysis(fetchedAnalysis); - setAnalysisErrorMessage(""); - onMockApplyIdChange?.(fetchedAnalysis.mockApplyId); - // 분석 결과에서 문항 목록 추출 - const fetchedQuestions = fetchedAnalysis.questions.map((q, i) => ({ - id: String(i), - question: q.questionContent, - })); - if (fetchedQuestions.length > 0) { - setQuestions(fetchedQuestions); - setActiveId("0"); - } - }) - .catch((error) => { - if (controller.signal.aborted) return; - setAnalysisErrorMessage( - error instanceof Error - ? error.message - : "분석 결과를 불러오지 못했습니다.", - ); - onAnalysisError?.(); - }); - - return () => controller.abort(); - }, [jobPostingId, selectedSequence, onAnalysisError, onMockApplyIdChange]); - - const handleOverview = () => { - setIsOverview(true); - setActiveId(""); - }; - - const handleSelect = (id: string) => { - setIsOverview(false); - setActiveId(id); - }; - - const currentAnalysis = - analysis?.sequence === selectedSequence ? analysis : null; - const activeAnalysisQuestion = currentAnalysis?.questions[Number(activeId)]; - const loadingMessage = - analysisErrorMessage || "분석 결과를 불러오는 중입니다."; - - return ( -
- - -
- {!currentAnalysis ? ( - - ) : isOverview ? ( - - ) : ( - - )} -
-
- ); -} diff --git a/jobdri/src/components/mockApply/result/DetailAnotationPannel.tsx b/jobdri/src/components/mockApply/result/DetailAnotationPannel.tsx index 1fb51ef..f22b304 100644 --- a/jobdri/src/components/mockApply/result/DetailAnotationPannel.tsx +++ b/jobdri/src/components/mockApply/result/DetailAnotationPannel.tsx @@ -1,11 +1,10 @@ "use client"; -import { useState } from "react"; // 💡 useState 추가 +import { useEffect, useRef } from "react"; import { type QuestionAnalysis } from "@/lib/api/result"; import ChipTag from "@/components/common/chips/ChipTag"; import { type HighlightStatus } from "./highlightStyles"; -import { scrollbarClass } from "@/components/common/scrollbar/scrollbarStyles"; -import Icon from "@/components/common/icons/Icon"; // 새로 추가된 Icon 유지 +import Icon from "@/components/common/icons/Icon"; const statusLabel: Record = { proven: "적절함", @@ -13,34 +12,56 @@ const statusLabel: Record = { mentioned: "신뢰성 부족", }; -interface DetailAnnotationPanelProps { +export interface DetailAnnotationPanelProps { analyses: QuestionAnalysis[]; + selectedAnalysisId: number | null; + hoveredAnalysisId: number | null; + onAnalysisClick: (id: number) => void; + onAnalysisHover: (id: number | null) => void; } export default function DetailAnnotationPanel({ analyses, + selectedAnalysisId, + hoveredAnalysisId, + onAnalysisClick, + onAnalysisHover, }: DetailAnnotationPanelProps) { - const [clickedId, setClickedId] = useState(null); + const cardRefs = useRef>({}); + + useEffect(() => { + if (selectedAnalysisId && cardRefs.current[selectedAnalysisId]) { + cardRefs.current[selectedAnalysisId]?.scrollIntoView({ + behavior: "smooth", + block: "start", // 최상단으로 올리되, 공간이 부족한 마지막 카드는 알아서 바닥에 맞춤 + }); + } + }, [selectedAnalysisId]); return ( -
+
{analyses.map((analysis) => { - const isClicked = clickedId === analysis.questionAnalysisId; + const isSelected = selectedAnalysisId === analysis.questionAnalysisId; + const isHovered = hoveredAnalysisId === analysis.questionAnalysisId; return (
setClickedId(analysis.questionAnalysisId)} + ref={(el) => { + cardRefs.current[analysis.questionAnalysisId] = el; + }} + onClick={() => onAnalysisClick(analysis.questionAnalysisId)} + onMouseEnter={() => onAnalysisHover(analysis.questionAnalysisId)} + onMouseLeave={() => onAnalysisHover(null)} className={`flex flex-col gap-4 p-1 rounded-card cursor-pointer border transition-colors ${ - isClicked - ? "border-line-primary-default bg-fill-quaternary-default-hover hover:shadow-hover" - : "border-transparent hover:bg-fill-quaternary-default-hover" + isSelected + ? "border-line-primary-default bg-fill-quaternary-default-hover shadow-hover" + : isHovered + ? "border-transparent bg-fill-quaternary-default-hover" + : "border-transparent hover:bg-fill-quaternary-default-hover" }`} >
- {/* 칩 태그 */} - {/* 타이틀 */}

{analysis.reason}

- {/* 인용 문장 */}

{analysis.sentence}

diff --git a/jobdri/src/components/mockApply/result/DetailSection.tsx b/jobdri/src/components/mockApply/result/DetailSection.tsx deleted file mode 100644 index bccb8c1..0000000 --- a/jobdri/src/components/mockApply/result/DetailSection.tsx +++ /dev/null @@ -1,97 +0,0 @@ -"use client"; - -import { useState } from "react"; -import clsx from "clsx"; -import { type AnalysisQuestion, type QuestionAnalysis } from "@/lib/api/result"; -import DetailAnnotationPanel from "./DetailAnotationPannel"; -import { highlightStyles, type HighlightStatus } from "./highlightStyles"; - -interface DetailSectionProps { - question: AnalysisQuestion | undefined; -} - -function buildSegments(answer: string, analyses: QuestionAnalysis[]) { - const sorted = [...analyses].sort((a, b) => a.start - b.start); - const segments: { - text: string; - status: HighlightStatus | null; - analysisId: number | null; - }[] = []; - let cursor = 0; - - for (const { start, end, status, questionAnalysisId } of sorted) { - if (start > cursor) { - segments.push({ - text: answer.slice(cursor, start), - status: null, - analysisId: null, - }); - } - segments.push({ - text: answer.slice(start, end), - status: status as HighlightStatus, - analysisId: questionAnalysisId, - }); - cursor = end; - } - - if (cursor < answer.length) { - segments.push({ - text: answer.slice(cursor), - status: null, - analysisId: null, - }); - } - - return segments; -} - -export default function DetailSection({ question }: DetailSectionProps) { - const [selectedId, setSelectedId] = useState(null); - - if (!question) return null; - - const segments = buildSegments(question.answer, question.analyses); - - const visibleAnalyses = - selectedId !== null - ? question.analyses.filter((a) => a.questionAnalysisId === selectedId) - : question.analyses; - - const handleSpanClick = (analysisId: number) => { - setSelectedId((prev) => (prev === analysisId ? null : analysisId)); - }; - - return ( -
- {/* 왼쪽: 문항 + 답변 */} -
-

- {question.questionContent} -

-
-

- {segments.map((seg, i) => - seg.status && seg.analysisId !== null ? ( - handleSpanClick(seg.analysisId!)} - className={clsx( - selectedId === seg.analysisId - ? highlightStyles[seg.status].selected - : highlightStyles[seg.status].default, - )} - > - {seg.text} - - ) : ( - {seg.text} - ), - )} -

-
- {/* 오른쪽: 개선안 목록 */} - -
- ); -} diff --git a/jobdri/src/components/mockApply/result/OverviewSection.tsx b/jobdri/src/components/mockApply/result/OverviewSection.tsx deleted file mode 100644 index b561cbf..0000000 --- a/jobdri/src/components/mockApply/result/OverviewSection.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"use client"; - -import imgSuit from "@/assets/img_result_Suit.png"; -import imgPerformance from "@/assets/img_result_Performance.png"; -import imgCompleteness from "@/assets/img_result_Completeness.png"; -import { type AnalysisResult } from "@/lib/api/result"; -import ScoreCircle from "./ScoreCircle"; -import Alret from "./Alret"; -import SummaryCard from "./SummaryCard"; - -const SCORE_MAP: { - key: keyof Pick; - label: string; - img: typeof imgSuit; -}[] = [ - { key: "jobFit", label: "직무 적합도", img: imgSuit }, - { key: "impact", label: "성과 구체성", img: imgPerformance }, - { key: "completeness", label: "완성도", img: imgCompleteness }, -]; - -interface OverviewSectionProps { - analysis: AnalysisResult; -} - -export default function OverviewSection({ analysis }: OverviewSectionProps) { - return ( -
-
- -
- -

- {analysis.feedback} -

-
-
-
- {SCORE_MAP.map(({ key, label, img }) => ( - - ))} -
-
- ); -} diff --git a/jobdri/src/components/mockApply/result/ResumeAnalysisDetail.tsx b/jobdri/src/components/mockApply/result/ResumeAnalysisDetail.tsx new file mode 100644 index 0000000..f29d500 --- /dev/null +++ b/jobdri/src/components/mockApply/result/ResumeAnalysisDetail.tsx @@ -0,0 +1,255 @@ +"use client"; + +import React, { useState, useRef, useEffect } from "react"; +import { QuestionList } from "../Question/QuestionList"; +import { type QuestionItem as ApiQuestionItem } from "@/lib/api/questions"; +import { lnbHiddenScrollbarClass } from "@/components/common/lnb/LnbScrollbar"; +import DetailAnnotationPanel from "./DetailAnotationPannel"; +import { + scrollbarClassL, + scrollbarClassS, +} from "@/components/common/scrollbar/scrollbarStyles"; +import { QuestionAnalysis, type AnalysisResult } from "@/lib/api/result"; +import { HighlightStatus, HighlightStyles } from "./highlightStyles"; +import Icon from "@/components/common/icons/Icon"; + +interface ResumeAnalysisDetailProps { + mockApplyId?: number; + sequence?: number; + analysisData: AnalysisResult; + children?: React.ReactNode; +} + +interface QuestionViewerProps { + questionNumber?: number; + questionText?: string; + answerText?: string; + analyses?: QuestionAnalysis[]; + selectedAnalysisId?: number | null; + hoveredAnalysisId?: number | null; + onAnalysisClick?: (analysisId: number) => void; +} + +function QuestionViewer({ + questionNumber = 1, + questionText = "문항이 선택되지 않았거나 등록된 내용이 없습니다.", + answerText = "", + analyses = [], + hoveredAnalysisId, + selectedAnalysisId, + onAnalysisClick, +}: QuestionViewerProps) { + const renderHighlightedText = () => { + if (!answerText) return null; + if (!analyses || analyses.length === 0) return answerText; + + const sortedAnalyses = [...analyses].sort((a, b) => a.start - b.start); + const result = []; + let currentIndex = 0; + console.log("들어온 분석 데이터:", analyses); + + sortedAnalyses.forEach((analysis, idx) => { + if (analysis.start > currentIndex) { + result.push( + + {answerText.slice(currentIndex, analysis.start)} + , + ); + } + + const status = analysis.status as HighlightStatus; + const isSelected = selectedAnalysisId === analysis.questionAnalysisId; + const isHovered = hoveredAnalysisId === analysis.questionAnalysisId; + + const stateKey: "selected" | "hover" | "default" = isSelected + ? "selected" + : isHovered + ? "hover" + : "default"; + const styleClass = HighlightStyles[status]?.[stateKey] || ""; + + result.push( + onAnalysisClick?.(analysis.questionAnalysisId)} + > + {answerText.slice(analysis.start, analysis.end)} + , + ); + + currentIndex = analysis.end; + }); + + if (currentIndex < answerText.length) { + result.push( + {answerText.slice(currentIndex)}, + ); + } + + return result; + }; + + return ( +
+
+ + {questionNumber}번 문항 + +

+ {questionText} +

+
+ +
+ {renderHighlightedText()} +
+
+ ); +} + +export default function ResumeAnalysisDetail({ + // mockApplyId, + // sequence, + analysisData, + children, +}: ResumeAnalysisDetailProps) { + const [selectedAnalysisId, setSelectedAnalysisId] = useState( + null, + ); + const [hoveredAnalysisId, setHoveredAnalysisId] = useState( + null, + ); + const initialQuestions = analysisData.questions.map((q) => ({ + id: String(q.questionId), + question: q.questionContent, + answer: q.answer, + })); + + const [questions, setQuestions] = + useState(initialQuestions); + const [selectedId, setSelectedId] = useState( + initialQuestions[0]?.id || null, + ); + + const scrollRef = useRef(null); + const [showGradient, setShowGradient] = useState(false); + + const checkScroll = () => { + if (scrollRef.current) { + const { scrollHeight, clientHeight, scrollTop } = scrollRef.current; + const isScrollable = scrollHeight - clientHeight > 2; + const isNotAtBottom = + Math.ceil(scrollTop + clientHeight) < scrollHeight - 2; + setShowGradient(isScrollable && isNotAtBottom); + } + }; + + useEffect(() => { + const timer = setTimeout(checkScroll, 50); + const observer = new ResizeObserver(() => checkScroll()); + if (scrollRef.current) { + observer.observe(scrollRef.current); + } + window.addEventListener("resize", checkScroll); + + return () => { + clearTimeout(timer); + observer.disconnect(); + window.removeEventListener("resize", checkScroll); + }; + }, [selectedId, questions]); + + const currentQuestionIdx = questions.findIndex((q) => q.id === selectedId); + const currentQuestion = questions[currentQuestionIdx]; + + const currentAnalyses = + analysisData.questions.find((q) => String(q.questionId) === selectedId) + ?.analyses || []; + + const handleAnalysisClick = (id: number) => { + setSelectedAnalysisId((prevId) => (prevId === id ? null : id)); + }; + + return ( +
+
+
+
+ {children} +
+
+
+ +
+ + {/* 중앙 QuestionViewer는 그대로 둠! */} + + +
+
+

+ 피드백 +

+ + {currentAnalyses.length > 0 && currentAnalyses.length} + +
+ +
+ {currentAnalyses.length > 0 ? ( + + ) : ( +
+ +
+

+ 모두 잘 작성되었어요! +

+

+ 다른 문항들보다 완성도가 높아요. +

+
+
+ )} +
+ + {showGradient && ( +
+ )} +
+
+
+
+
+
+
+ ); +} diff --git a/jobdri/src/components/mockApply/ResumeAnalysisFeedback.tsx b/jobdri/src/components/mockApply/result/ResumeAnalysisFeedback.tsx similarity index 52% rename from jobdri/src/components/mockApply/ResumeAnalysisFeedback.tsx rename to jobdri/src/components/mockApply/result/ResumeAnalysisFeedback.tsx index a54cd09..c2f90a3 100644 --- a/jobdri/src/components/mockApply/ResumeAnalysisFeedback.tsx +++ b/jobdri/src/components/mockApply/result/ResumeAnalysisFeedback.tsx @@ -4,27 +4,23 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; import { CtaFooter } from "@/components/common/cta"; import Divider from "@/components/common/Divider"; -import Header from "@/components/common/header/Header"; import Icon from "@/components/common/icons/Icon"; import { LnbScrollbar, lnbHiddenScrollbarClass, useLnbScrollMetrics, } from "@/components/common/lnb/LnbScrollbar"; -import { ModalNotice } from "@/components/common/modal"; import TabMenu from "@/components/common/tabs/TabMenu"; -import { Toast } from "@/components/common/toast"; -import AnalysisHeader from "@/components/mockApply/result/AnalysisHeader"; import Evaluation from "@/components/mockApply/result/Evaluation"; import ScoreBar from "@/components/mockApply/result/ScoreBar"; import ScoreCircle from "@/components/mockApply/result/ScoreCircle"; -import { useReApply } from "@/hooks/useReApply"; -import { getMockApplyResumeRecords } from "@/lib/api/mockApplies"; -import { formatApplicationSequenceLabel } from "@/lib/mockApply/applicationLabel"; +import type { AnalysisResult } from "@/lib/api/result"; interface ResumeAnalysisFeedbackProps { mockApplyId?: number; sequence?: number; + children?: React.ReactNode; + analysisData: AnalysisResult; } const scoreItems = [ @@ -111,7 +107,26 @@ function ScoreMetricRow({ ); } -function ScoreSummaryCard() { +function ScoreSummaryCard({ data }: { data: AnalysisResult }) { + // API 데이터를 기반으로 점수 항목 생성 (80점 미만이면 danger 톤으로 처리) + const scoreItems = [ + { + label: "직무 적합성", + score: data.jobFit, + tone: data.jobFit >= 80 ? "primary" : "danger", + }, + { + label: "정량적 성과", + score: data.impact, + tone: data.impact >= 80 ? "primary" : "danger", + }, + { + label: "논리 구조", + score: data.completeness, + tone: data.completeness >= 80 ? "primary" : "danger", + }, + ] as const; + return (
- + 총점
@@ -129,7 +144,7 @@ function ScoreSummaryCard() {
- +
@@ -150,17 +165,16 @@ function ScoreSummaryCard() { type="SPARKLE" className="h-6 w-6 shrink-0 text-icon-primary-default" /> -

- 강점이 잘 드러나고 있어요 +

+ {data.score >= 80 + ? "강점이 잘 드러나고 있어요" + : "조금 더 보완이 필요해요"}

-

- 직무 적합성과 정량적 성과는 강하지만, 회사·직무에 대한 - 구체적 이해와 본인만의 차별점이 더 드러나면 통과 가능성이 한 - 단계 올라가요. 지원 동기와 입사 후 기여 방향을 좀 더 구체적으로 - 서술하면 설득력이 높아집니다. +

+ {data.feedback}

@@ -169,19 +183,19 @@ function ScoreSummaryCard() { ); } -function ReviewSummaryCard() { +function ReviewSummaryCard({ data }: { data: AnalysisResult }) { const [activeTabId, setActiveTabId] = useState(reviewTabs[0].id); const isStrengthTab = activeTabId === "strengths"; - const evaluations = isStrengthTab ? strengthEvaluations : weaknessEvaluations; + + const evaluations = isStrengthTab + ? data.keyStrengths || [] + : data.keyWeaknesses || []; return ( -