Skip to content

Feat: 모의지원 전체 흐름 연결 및 api 연동 [JDDEV-43]#101

Merged
minnngo merged 11 commits into
developfrom
feature/JDDEV-43-jd_parsing—api-feat
Jul 23, 2026

Hidden character warning

The head ref may contain hidden characters: "feature/JDDEV-43-jd_parsing\u2014api-feat"
Merged

Feat: 모의지원 전체 흐름 연결 및 api 연동 [JDDEV-43]#101
minnngo merged 11 commits into
developfrom
feature/JDDEV-43-jd_parsing—api-feat

Conversation

@minnngo

@minnngo minnngo commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

🔗 관련 이슈

  • JDDEV-43

📝 개요

  • 채용 공고 입력부터 공고 확인, 자소서 작성, AI 분석 결과 조회까지 전체 모의지원 흐름을 실제 API와 연결했습니다.
  • 저장된 공고와 지원 데이터를 홈·헤더·결과 화면에 반영하고, 세션 및 UI 문제를 함께 개선했습니다.

⌨️ 작업 상세 내용

  • 텍스트·이미지 공고 분석 및 이미지 presigned URL 업로드 연동
  • 공고 분석 비동기 작업 접수 및 상태 조회 연결
  • 채용 공고 저장·조회·수정·삭제 API 연동
  • 공고 프로필 색상, 공고명, 회사명, 직무명 수동 편집 및 저장
  • 공고 저장 후 모의지원·실제지원 생성 흐름 연결
  • 자소서 문항 조회·선택 및 답변 자동 저장 연결
  • 자소서 분석 요청 후 SSE와 폴링을 이용한 비동기 결과 조회
  • 총점·세부 점수·총평·문항별 분석 결과를 실제 API 데이터로 표시
  • 홈의 이어쓰기·결과 확인·재도전·삭제 흐름 연결
  • 홈 이동 시 로그인 세션 해제 문제와 화면 스크롤·모달·이미지 미리보기 문제 수정
  • 총평 카드가 하나일 때 상단부터 표시되도록 정렬 수정

💡 코드 설명 및 참고사항

공고 분석과 자소서 분석은 비동기 방식으로 동작합니다. 작업 접수 후 받은 taskId로 상태를 조회하며, 자소서 분석은 SSE와 주기적인 폴링을 함께 사용해 연결이 끊겨도 결과를 확인할 수 있도록 구성했습니다.
jobPostingId, mockApplyId, sequence를 화면 간 전달해 동일한 공고와 지원 회차의 데이터를 조회하도록 연결했습니다.
분석 결과 API 관련 타입과 요청 로직은 result.ts로 정리하고 기존 경로는 호환용으로 유지했습니다.

📸 스크린샷 (UI 변경 시)

2026-07-23.6.57.21.mov

Summary by CodeRabbit

  • New Features
    • Added a guided job-posting creation and review flow with editable titles, company details, requirements, profile colors, and posting names.
    • Added automatic job-posting analysis with progress tracking, retry support, and clearer failure handling.
    • Added improved resume analysis loading and result retrieval.
    • Drafts, saved postings, and applications now retain more information and are easier to resume or delete.
  • Bug Fixes
    • Prevented duplicate submissions and improved validation for incomplete questions and job details.
    • Updated navigation and error messages across application creation and review flows.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds job-posting ingestion and review, propagates job-posting identifiers through mock-apply routes, introduces asynchronous analysis polling and streaming, and updates home-page application integration, metadata persistence, profile colors, and unauthorized-response handling.

Changes

Job posting creation and review

Layer / File(s) Summary
Ingestion, review, and persistence flow
jobdri/src/app/mockApply/job/{create,loading,review}/*, jobdri/src/lib/api/jobPostings.ts, jobdri/src/components/mockApply/jd/jdReviewSections.ts
Draft text and images are ingested, analyzed, reviewed with editable fields, saved or updated, and linked to a new mock application.
JD review routing and metadata
jobdri/src/app/mockApply/[mockApplyId]/(jd)/*, jobdri/src/components/mockApply/home/applicationHomeUtils.ts
JD routes no longer use the actual segment, and saved review metadata includes job title, posting name, and profile color.

Question submission and analysis

Layer / File(s) Summary
Question loading and submission
jobdri/src/app/mockApply/[mockApplyId]/page.tsx, jobdri/src/lib/api/questions.ts
Question loading now supports initial selection persistence, while submission saves answers and requests asynchronous analysis with duplicate-submit protection.
API response and authorization handling
jobdri/src/lib/api/client.ts, jobdri/src/lib/api/credit.ts, jobdri/src/lib/api/mockApplies.ts
Unauthorized redirect behavior is configurable for shared API calls, and credit endpoints use the shared API base URL.

Analysis results

Layer / File(s) Summary
Analysis task lifecycle
jobdri/src/app/mockApply/[mockApplyId]/result/resume-analysis-loading/*, jobdri/src/lib/api/result.ts
Analysis requests now expose task status and stream APIs, with polling, retry, timeout, failure handling, and completion navigation.
Result lookup and rendering
jobdri/src/app/mockApply/[mockApplyId]/result/page.tsx, jobdri/src/hooks/useAnalysisResult.ts, jobdri/src/components/mockApply/result/*
Results can be fetched by job posting and sequence, with job-posting header data passed into shared templates and normalized feedback rendered.

Home integration and shared UI

Layer / File(s) Summary
Application home data and navigation
jobdri/src/app/page.tsx, jobdri/src/components/mockApply/home/*
The home page joins applications with saved job postings, supports saved-only drafts, deletion, retry, resume, and job-posting-aware routes.
Profile colors and shared presentation
jobdri/src/components/mockApply/home/Avatar.tsx, jobdri/src/components/mockApply/Question/SidePanel.tsx, jobdri/src/components/common/*
Profile colors are normalized and propagated through avatars and JD side panels; image preview cleanup and navigation presentation are updated.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: ⭐ Feature

Suggested reviewers: yiyoonseo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: connecting the full mock application flow with API integrations.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/JDDEV-43-jd_parsing—api-feat

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@minnngo minnngo closed this Jul 23, 2026
@minnngo
minnngo deleted the feature/JDDEV-43-jd_parsing—api-feat branch July 23, 2026 10:11
@minnngo
minnngo restored the feature/JDDEV-43-jd_parsing—api-feat branch July 23, 2026 10:12
@minnngo minnngo reopened this Jul 23, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
jobdri/src/components/common/input/LLMInput.tsx (1)

97-99: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Deferred revocation logic is correct but non-obvious — consider a clarifying comment.

This defers URL.revokeObjectURL and cancels the pending timer if the effect re-runs shortly after (guards against React Strict Mode's mount→cleanup→remount double-invoke revoking still-in-use blob URLs). It works, but a future refactor could easily "simplify" this back to immediate revocation and reintroduce broken image previews.

💬 Suggested comment
   useEffect(() => {
+    // Cancel any revocation scheduled by a very recent unmount (e.g. React
+    // Strict Mode's mount→cleanup→remount cycle) so still-in-use blob URLs
+    // aren't revoked prematurely.
     if (previewCleanupTimerRef.current) {
       clearTimeout(previewCleanupTimerRef.current);
       previewCleanupTimerRef.current = null;
     }

Also applies to: 155-170

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@jobdri/src/components/common/input/LLMInput.tsx` around lines 97 - 99, Add a
concise comment near previewCleanupTimerRef and the deferred URL.revokeObjectURL
cleanup explaining that the delay and timer cancellation protect blob URL
previews during React Strict Mode’s mount/cleanup/remount cycle. Keep the
existing revocation behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@jobdri/src/app/mockApply/`[mockApplyId]/(jd)/jd-input/JdInputPageClient.tsx:
- Around line 282-287: Update the fallback error message in
moveToJdReviewWithResult to be source-agnostic, since the no-extracted-fields
path handles text, link, and image ingestion. Preserve result?.message when
available and replace only the image-specific default with guidance applicable
to all ingestion sources.

In `@jobdri/src/app/mockApply/`[mockApplyId]/result/page.tsx:
- Around line 119-124: Update the retry flow around reApply to branch on its
returned value rather than relying on the dead catch block, since
useReApply.reApply returns null on failure without throwing. When the result is
null, close the retry modal and show the existing failure toast; preserve the
successful retry behavior for non-null results.

In `@jobdri/src/app/mockApply/job/loading/page.tsx`:
- Around line 123-163: Prevent the in-flight analyzeJobPosting operation from
redirecting after the user cancels via stopAnalysis or navigates home. Add a
cancellation ref that those handlers set before leaving, and check it before
both router.replace calls in analyzeJobPosting, while preserving the existing
hasStartedAnalysisRef behavior and avoiding an effect cleanup flag.

In `@jobdri/src/app/page.tsx`:
- Around line 64-153: Update the completed-application mapping in the
loadMockApplies effect to reuse the exported mapMockApplyToApplication helper,
extending it to accept the joined SavedJobPosting when needed for company,
position, and profileColor fallbacks. Preserve the helper’s canonical status
value of "COMPLETED" and remove the duplicated inline derivation in
mappedResults.
- Around line 162-175: Update the deletePosting handler and the
fetchMyJobPosting error handler in jobdri/src/app/page.tsx at lines 162-175 and
213-231 to show an error toast or notification in each catch block while
preserving the existing console.error logging. Use the page’s established
notification mechanism so failed delete and draft-resume fetch actions provide
immediate user feedback.

In `@jobdri/src/lib/api/result.ts`:
- Around line 105-119: Update normalizeAnalysisResult so the status
normalization inside the questions/analyses mapping safely handles a missing
analysis.status, preserving a valid normalized fallback instead of calling trim
on undefined. Keep the existing trim-and-lowercase behavior when status is
present.

---

Nitpick comments:
In `@jobdri/src/components/common/input/LLMInput.tsx`:
- Around line 97-99: Add a concise comment near previewCleanupTimerRef and the
deferred URL.revokeObjectURL cleanup explaining that the delay and timer
cancellation protect blob URL previews during React Strict Mode’s
mount/cleanup/remount cycle. Keep the existing revocation behavior unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 65f90a44-a4f5-4644-98bf-a6721526c2b7

📥 Commits

Reviewing files that changed from the base of the PR and between 212b319 and ce232ed.

⛔ Files ignored due to path filters (1)
  • .DS_Store is excluded by !**/.DS_Store
📒 Files selected for processing (36)
  • jobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-input/JdInputPageClient.tsx
  • jobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-input/page.tsx
  • jobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-review/page.tsx
  • jobdri/src/app/mockApply/[mockApplyId]/(jd)/page.tsx
  • jobdri/src/app/mockApply/[mockApplyId]/page.tsx
  • jobdri/src/app/mockApply/[mockApplyId]/result/page.tsx
  • jobdri/src/app/mockApply/[mockApplyId]/result/resume-analysis-loading/ResumeAnalysisLoadingPageClient.tsx
  • jobdri/src/app/mockApply/[mockApplyId]/result/resume-analysis-loading/page.tsx
  • jobdri/src/app/mockApply/apply-type/ApplyTypePageClient.tsx
  • jobdri/src/app/mockApply/job/create/page.tsx
  • jobdri/src/app/mockApply/job/jobPostingDraftStore.ts
  • jobdri/src/app/mockApply/job/loading/page.tsx
  • jobdri/src/app/mockApply/job/review/page.tsx
  • jobdri/src/app/page.tsx
  • jobdri/src/components/common/MockApplyTemplate.tsx
  • jobdri/src/components/common/input/LLMInput.tsx
  • jobdri/src/components/common/lnb/Lnb.tsx
  • jobdri/src/components/common/lnb/LnbShared.tsx
  • jobdri/src/components/mockApply/Question/SidePanel.tsx
  • jobdri/src/components/mockApply/home/Avatar.tsx
  • jobdri/src/components/mockApply/home/ResultApplicationCard.tsx
  • jobdri/src/components/mockApply/home/ResultDraftCard.tsx
  • jobdri/src/components/mockApply/home/SavedApplicationsModal.tsx
  • jobdri/src/components/mockApply/home/applicationHomeUtils.ts
  • jobdri/src/components/mockApply/home/types.ts
  • jobdri/src/components/mockApply/jd/jdReviewSections.ts
  • jobdri/src/components/mockApply/result/ResumeAnalysisDetail.tsx
  • jobdri/src/components/mockApply/result/ResumeAnalysisFeedback.tsx
  • jobdri/src/hooks/useAnalysisResult.ts
  • jobdri/src/lib/api/analysis.ts
  • jobdri/src/lib/api/client.ts
  • jobdri/src/lib/api/credit.ts
  • jobdri/src/lib/api/jobPostings.ts
  • jobdri/src/lib/api/mockApplies.ts
  • jobdri/src/lib/api/questions.ts
  • jobdri/src/lib/api/result.ts
💤 Files with no reviewable changes (1)
  • jobdri/src/components/mockApply/result/ResumeAnalysisDetail.tsx

Comment on lines +282 to 287
if (!Object.values(jobPosting).some(Boolean)) {
throw new Error(
result?.message ||
"이미지에서 공고 정보를 추출하지 못했습니다. 더 선명한 이미지를 사용해주세요.",
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fallback error message is image-specific but this path also handles text/link.

moveToJdReviewWithResult is invoked for text, link, and image ingestion. When result?.message is absent and no fields were extracted, text/link users will see the image-only guidance. Consider a source-agnostic default.

💬 Proposed tweak
     if (!Object.values(jobPosting).some(Boolean)) {
       throw new Error(
         result?.message ||
-          "이미지에서 공고 정보를 추출하지 못했습니다. 더 선명한 이미지를 사용해주세요.",
+          "공고 정보를 추출하지 못했습니다. 다른 방법으로 공고 내용을 입력해주세요.",
       );
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!Object.values(jobPosting).some(Boolean)) {
throw new Error(
result?.message ||
"이미지에서 공고 정보를 추출하지 못했습니다. 더 선명한 이미지를 사용해주세요.",
);
}
if (!Object.values(jobPosting).some(Boolean)) {
throw new Error(
result?.message ||
"공고 정보를 추출하지 못했습니다. 다른 방법으로 공고 내용을 입력해주세요.",
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@jobdri/src/app/mockApply/`[mockApplyId]/(jd)/jd-input/JdInputPageClient.tsx
around lines 282 - 287, Update the fallback error message in
moveToJdReviewWithResult to be source-agnostic, since the no-extracted-fields
path handles text, link, and image ingestion. Preserve result?.message when
available and replace only the image-specific default with guidance applicable
to all ingestion sources.

Comment on lines 119 to 124
try {
await reApply(resolvedMockApplyId);
} catch (error) {
} catch {
setIsRetryModalOpen(false);
showTopToast("재도전을 시작하지 못했어요. 잠시 후 다시 시도해주세요.");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Retry failures never surface to the user. useReApply.reApply swallows its own errors and returns null on failure (it never rethrows), so this catch {} is dead code — the failure toast never shows and the modal stays open with no feedback. Branch on the return value instead.

🐛 Proposed fix
-    try {
-      await reApply(resolvedMockApplyId);
-    } catch {
-      setIsRetryModalOpen(false);
-      showTopToast("재도전을 시작하지 못했어요. 잠시 후 다시 시도해주세요.");
-    }
+    const result = await reApply(resolvedMockApplyId);
+    if (!result) {
+      setIsRetryModalOpen(false);
+      showTopToast("재도전을 시작하지 못했어요. 잠시 후 다시 시도해주세요.");
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
await reApply(resolvedMockApplyId);
} catch (error) {
} catch {
setIsRetryModalOpen(false);
showTopToast("재도전을 시작하지 못했어요. 잠시 후 다시 시도해주세요.");
}
const result = await reApply(resolvedMockApplyId);
if (!result) {
setIsRetryModalOpen(false);
showTopToast("재도전을 시작하지 못했어요. 잠시 후 다시 시도해주세요.");
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@jobdri/src/app/mockApply/`[mockApplyId]/result/page.tsx around lines 119 -
124, Update the retry flow around reApply to branch on its returned value rather
than relying on the dead catch block, since useReApply.reApply returns null on
failure without throwing. When the result is null, close the retry modal and
show the existing failure toast; preserve the successful retry behavior for
non-null results.

Comment on lines 123 to +163
useEffect(() => {
if (showStopConfirm) {
if (hasStartedAnalysisRef.current) {
return;
}

const completionTimer = window.setTimeout(() => {
router.replace("/mockApply/job/review");
}, loadingDurationMs);

return () => {
window.clearTimeout(completionTimer);
hasStartedAnalysisRef.current = true;

const analyzeJobPosting = async () => {
try {
const draft = getJobPostingDraft();
const rawText = draft.value.trim() || undefined;
const imageObjectKey = draft.files[0]
? await uploadJobPostingImage(draft.files[0])
: undefined;

if (!rawText && !imageObjectKey) {
throw new Error("분석할 채용 공고가 없습니다.");
}

const accepted = await ingestJobPosting({ rawText, imageObjectKey });
const status = await waitForJobPostingIngest(accepted.taskId);

if (!status.result) {
throw new Error("채용 공고 분석 결과를 확인할 수 없습니다.");
}

saveJobPostingAnalysis(status.result);
router.replace("/mockApply/job/review");
} catch (error) {
const message =
error instanceof Error
? error.message
: "채용 공고 분석에 실패했습니다.";

router.replace(
`/mockApply/job/create?analysisError=${encodeURIComponent(message)}`,
);
}
};
}, [loadingDurationMs, router, showStopConfirm]);

void analyzeJobPosting();
}, [router]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

In-flight analysis has no cancellation guard — user is yanked back after stopping or leaving.

analyzeJobPosting runs to completion regardless of navigation. If the user taps "중단하기" (stopAnalysis, Line 166) or "홈으로" (Line 177) and moves away, this promise still resolves seconds later and calls router.replace("/mockApply/job/review"), navigating them off the page unexpectedly (or, on failure, redirecting to create?analysisError=... over the canceled toast).

Guard both redirects with a cancellation ref set by stop/home. Avoid a useEffect cleanup flag here, since the hasStartedAnalysisRef guard would leave it stuck cancelled under React 19 StrictMode double-invoke.

🔒 Proposed fix
   const [showStopConfirm, setShowStopConfirm] = useState(false);
   const hasStartedAnalysisRef = useRef(false);
+  const isCancelledRef = useRef(false);

   useEffect(() => {
     if (hasStartedAnalysisRef.current) {
       return;
     }
     hasStartedAnalysisRef.current = true;

     const analyzeJobPosting = async () => {
       try {
         ...
         if (!status.result) {
           throw new Error("채용 공고 분석 결과를 확인할 수 없습니다.");
         }

+        if (isCancelledRef.current) {
+          return;
+        }
         saveJobPostingAnalysis(status.result);
         router.replace("/mockApply/job/review");
       } catch (error) {
+        if (isCancelledRef.current) {
+          return;
+        }
         const message =
           error instanceof Error
             ? error.message
             : "채용 공고 분석에 실패했습니다.";

         router.replace(
           `/mockApply/job/create?analysisError=${encodeURIComponent(message)}`,
         );
       }
     };

     void analyzeJobPosting();
   }, [router]);
   const stopAnalysis = () => {
+    isCancelledRef.current = true;
     router.replace("/mockApply/job/create?analysisCanceled=1");
   };
         homeAction={{
           label: "홈으로",
-          onClick: () => clearJobPostingDraft(),
+          onClick: () => {
+            isCancelledRef.current = true;
+            clearJobPostingDraft();
+          },
         }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@jobdri/src/app/mockApply/job/loading/page.tsx` around lines 123 - 163,
Prevent the in-flight analyzeJobPosting operation from redirecting after the
user cancels via stopAnalysis or navigates home. Add a cancellation ref that
those handlers set before leaving, and check it before both router.replace calls
in analyzeJobPosting, while preserving the existing hasStartedAnalysisRef
behavior and avoiding an effect cleanup flag.

Comment thread jobdri/src/app/page.tsx
Comment on lines +64 to +153
const { reApply } = useReApply();
const [drafts, setDrafts] = useState<DraftData[]>([]);
const [results, setResults] = useState<ApplicationCardData[]>([]);

useEffect(() => {
const loadMockApplies = async () => {
try {
const data = await fetchMyMockApplies();

const mappedDrafts = data.inProgress.map((item) => ({
id: String(item.mockApplyId),
companyName: item.companyName,
position:
item.jobTitle || item.detailClassificationName || "직무 미지정",
currentStep: item.status === "ANSWER_WRITE" ? 2 : 1,
updatedAt: formatDate(item.createdAt),
}));

const mappedResults = data.completed.map((item) => ({
id: item.mockApplyId,
jobPostingId: item.jobPostingId,
mockApplyId: item.mockApplyId,
company: item.companyName,
position:
item.jobTitle || item.detailClassificationName || "직무 미지정",
createdAt: formatDate(item.createdAt),
score: item.score || 0,
version: item.version || 1,
status: "completed",
}));
setDrafts(mappedDrafts);
const [data, jobPostings] = await Promise.all([
fetchMyMockApplies({ redirectOnUnauthorized: false }),
fetchMyJobPostings({ redirectOnUnauthorized: false }).catch(
() => [],
),
]);
const jobPostingById = new Map(
jobPostings.map((jobPosting) => [
jobPosting.jobPostingId,
jobPosting,
]),
);

const mappedDrafts = data.inProgress.map((item) => {
const jobPosting = jobPostingById.get(item.jobPostingId);

return {
id: String(item.mockApplyId),
jobPostingId: item.jobPostingId,
mockApplyId: item.mockApplyId,
companyName:
item.companyName ||
jobPosting?.companyName ||
"회사명 미입력",
profileColor: jobPosting?.profileColor ?? "DEFAULT",
position:
item.jobTitle ||
jobPosting?.jobTitle ||
item.detailClassificationName ||
jobPosting?.detailClassificationName ||
"직무 미지정",
currentStep: item.status === "ANSWER_WRITE" ? 2 : 1,
updatedAt: formatDate(item.createdAt),
};
});
const linkedJobPostingIds = new Set(
[...data.inProgress, ...data.completed].map(
(item) => item.jobPostingId,
),
);
const savedOnlyDrafts = jobPostings
.filter(
(jobPosting) =>
!linkedJobPostingIds.has(jobPosting.jobPostingId),
)
.map((jobPosting) => ({
id: `job-posting-${jobPosting.jobPostingId}`,
jobPostingId: jobPosting.jobPostingId,
companyName: jobPosting.companyName || "회사명 미입력",
profileColor: jobPosting.profileColor,
position:
jobPosting.jobTitle ||
jobPosting.detailClassificationName ||
"직무 미지정",
currentStep: 1,
updatedAt: "-",
}));

const mappedResults = data.completed.map((item) => {
const jobPosting = jobPostingById.get(item.jobPostingId);

return {
id: item.mockApplyId,
jobPostingId: item.jobPostingId,
mockApplyId: item.mockApplyId,
company:
item.companyName ||
jobPosting?.companyName ||
"회사명 미입력",
profileColor: jobPosting?.profileColor ?? "DEFAULT",
position:
item.jobTitle ||
jobPosting?.jobTitle ||
item.detailClassificationName ||
jobPosting?.detailClassificationName ||
"직무 미지정",
createdAt: formatDate(item.createdAt),
score: item.score || 0,
version: item.version || 1,
status: "completed",
};
});
setDrafts([...savedOnlyDrafts, ...mappedDrafts]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Mapping logic duplicates (and diverges from) mapMockApplyToApplication in applicationHomeUtils.ts.

This effect re-implements company/position/profileColor derivation inline instead of reusing the shared mapMockApplyToApplication helper (already exported from applicationHomeUtils.ts for this exact purpose). The divergence is concrete: mapMockApplyToApplication sets status: "COMPLETED" for completed items, while mappedResults here sets status: "completed" (lowercase). Since ApplicationCardData.status is a shared field consumed by other home components, this casing mismatch is a real correctness risk if any consumer does a strict "COMPLETED" comparison against data produced here.

Consider extending the shared mapper to accept the joined SavedJobPosting (for the extra company/position/profileColor fallback) rather than maintaining two parallel implementations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@jobdri/src/app/page.tsx` around lines 64 - 153, Update the
completed-application mapping in the loadMockApplies effect to reuse the
exported mapMockApplyToApplication helper, extending it to accept the joined
SavedJobPosting when needed for company, position, and profileColor fallbacks.
Preserve the helper’s canonical status value of "COMPLETED" and remove the
duplicated inline derivation in mappedResults.

Comment thread jobdri/src/app/page.tsx
Comment on lines +162 to +175

const deletePosting = async (jobPostingId: number) => {
try {
await deleteJobPosting(jobPostingId);
setDrafts((current) =>
current.filter((draft) => draft.jobPostingId !== jobPostingId),
);
setResults((current) =>
current.filter((result) => result.jobPostingId !== jobPostingId),
);
} catch (error) {
console.error("채용 공고를 삭제하지 못했습니다.", error);
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Failed deletes / draft-resume fetches are silently swallowed — no user feedback. Both async handlers catch errors with only console.error, so a failed action looks like nothing happened (no toast/retry affordance), leaving users confused whether their click registered.

  • jobdri/src/app/page.tsx#L162-L175: on deleteJobPosting failure, surface an error toast/notification in addition to the console.error log so the user knows the delete didn't succeed.
  • jobdri/src/app/page.tsx#L213-L231: on fetchMyJobPosting failure, surface an error toast/notification instead of only logging, so the click isn't perceived as a dead click.
📍 Affects 1 file
  • jobdri/src/app/page.tsx#L162-L175 (this comment)
  • jobdri/src/app/page.tsx#L213-L231
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@jobdri/src/app/page.tsx` around lines 162 - 175, Update the deletePosting
handler and the fetchMyJobPosting error handler in jobdri/src/app/page.tsx at
lines 162-175 and 213-231 to show an error toast or notification in each catch
block while preserving the existing console.error logging. Use the page’s
established notification mechanism so failed delete and draft-resume fetch
actions provide immediate user feedback.

Comment on lines +105 to +119
export function normalizeAnalysisResult(result: AnalysisResult) {
return {
...result,
keyStrengths: result.keyStrengths ?? [],
keyWeaknesses: result.keyWeaknesses ?? [],
missingKeywords: result.missingKeywords ?? [],
questions: (result.questions ?? []).map((question) => ({
...question,
analyses: (question.analyses ?? []).map((analysis) => ({
...analysis,
status: analysis.status.trim().toLowerCase(),
})),
})),
} satisfies AnalysisResult;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

analysis.status is dereferenced unguarded. The function defensively defaults keyStrengths/keyWeaknesses/missingKeywords/questions to [], but analysis.status.trim().toLowerCase() will throw a TypeError if the backend omits status on any analysis. Given normalize exists precisely to harden against missing fields, guard this too.

🛡️ Proposed fix
       analyses: (question.analyses ?? []).map((analysis) => ({
         ...analysis,
-        status: analysis.status.trim().toLowerCase(),
+        status: (analysis.status ?? "").trim().toLowerCase(),
       })),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function normalizeAnalysisResult(result: AnalysisResult) {
return {
...result,
keyStrengths: result.keyStrengths ?? [],
keyWeaknesses: result.keyWeaknesses ?? [],
missingKeywords: result.missingKeywords ?? [],
questions: (result.questions ?? []).map((question) => ({
...question,
analyses: (question.analyses ?? []).map((analysis) => ({
...analysis,
status: analysis.status.trim().toLowerCase(),
})),
})),
} satisfies AnalysisResult;
}
export function normalizeAnalysisResult(result: AnalysisResult) {
return {
...result,
keyStrengths: result.keyStrengths ?? [],
keyWeaknesses: result.keyWeaknesses ?? [],
missingKeywords: result.missingKeywords ?? [],
questions: (result.questions ?? []).map((question) => ({
...question,
analyses: (question.analyses ?? []).map((analysis) => ({
...analysis,
status: (analysis.status ?? "").trim().toLowerCase(),
})),
})),
} satisfies AnalysisResult;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@jobdri/src/lib/api/result.ts` around lines 105 - 119, Update
normalizeAnalysisResult so the status normalization inside the
questions/analyses mapping safely handles a missing analysis.status, preserving
a valid normalized fallback instead of calling trim on undefined. Keep the
existing trim-and-lowercase behavior when status is present.

@yiyoonseo yiyoonseo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고햇어욥 ㅠ

@minnngo
minnngo merged commit f114a4c into develop Jul 23, 2026
1 check passed
@coderabbitai coderabbitai Bot mentioned this pull request Jul 26, 2026
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants