Feat: 모의지원 전체 흐름 연결 및 api 연동 [JDDEV-43]#101
Hidden character warning
Conversation
…d_parsing—api-feat # Conflicts: # jobdri/src/app/mockApply/job/create/page.tsx # jobdri/src/app/mockApply/job/review/page.tsx
📝 WalkthroughWalkthroughThis 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. ChangesJob posting creation and review
Question submission and analysis
Analysis results
Home integration and shared UI
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
jobdri/src/components/common/input/LLMInput.tsx (1)
97-99: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDeferred revocation logic is correct but non-obvious — consider a clarifying comment.
This defers
URL.revokeObjectURLand 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
⛔ Files ignored due to path filters (1)
.DS_Storeis excluded by!**/.DS_Store
📒 Files selected for processing (36)
jobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-input/JdInputPageClient.tsxjobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-input/page.tsxjobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-review/page.tsxjobdri/src/app/mockApply/[mockApplyId]/(jd)/page.tsxjobdri/src/app/mockApply/[mockApplyId]/page.tsxjobdri/src/app/mockApply/[mockApplyId]/result/page.tsxjobdri/src/app/mockApply/[mockApplyId]/result/resume-analysis-loading/ResumeAnalysisLoadingPageClient.tsxjobdri/src/app/mockApply/[mockApplyId]/result/resume-analysis-loading/page.tsxjobdri/src/app/mockApply/apply-type/ApplyTypePageClient.tsxjobdri/src/app/mockApply/job/create/page.tsxjobdri/src/app/mockApply/job/jobPostingDraftStore.tsjobdri/src/app/mockApply/job/loading/page.tsxjobdri/src/app/mockApply/job/review/page.tsxjobdri/src/app/page.tsxjobdri/src/components/common/MockApplyTemplate.tsxjobdri/src/components/common/input/LLMInput.tsxjobdri/src/components/common/lnb/Lnb.tsxjobdri/src/components/common/lnb/LnbShared.tsxjobdri/src/components/mockApply/Question/SidePanel.tsxjobdri/src/components/mockApply/home/Avatar.tsxjobdri/src/components/mockApply/home/ResultApplicationCard.tsxjobdri/src/components/mockApply/home/ResultDraftCard.tsxjobdri/src/components/mockApply/home/SavedApplicationsModal.tsxjobdri/src/components/mockApply/home/applicationHomeUtils.tsjobdri/src/components/mockApply/home/types.tsjobdri/src/components/mockApply/jd/jdReviewSections.tsjobdri/src/components/mockApply/result/ResumeAnalysisDetail.tsxjobdri/src/components/mockApply/result/ResumeAnalysisFeedback.tsxjobdri/src/hooks/useAnalysisResult.tsjobdri/src/lib/api/analysis.tsjobdri/src/lib/api/client.tsjobdri/src/lib/api/credit.tsjobdri/src/lib/api/jobPostings.tsjobdri/src/lib/api/mockApplies.tsjobdri/src/lib/api/questions.tsjobdri/src/lib/api/result.ts
💤 Files with no reviewable changes (1)
- jobdri/src/components/mockApply/result/ResumeAnalysisDetail.tsx
| if (!Object.values(jobPosting).some(Boolean)) { | ||
| throw new Error( | ||
| result?.message || | ||
| "이미지에서 공고 정보를 추출하지 못했습니다. 더 선명한 이미지를 사용해주세요.", | ||
| ); | ||
| } |
There was a problem hiding this comment.
🎯 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.
| 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.
| try { | ||
| await reApply(resolvedMockApplyId); | ||
| } catch (error) { | ||
| } catch { | ||
| setIsRetryModalOpen(false); | ||
| showTopToast("재도전을 시작하지 못했어요. 잠시 후 다시 시도해주세요."); | ||
| } |
There was a problem hiding this comment.
🎯 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.
| 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.
| 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]); |
There was a problem hiding this comment.
🩺 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.
| 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]); |
There was a problem hiding this comment.
🎯 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.
|
|
||
| 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); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🩺 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: ondeleteJobPostingfailure, surface an error toast/notification in addition to theconsole.errorlog so the user knows the delete didn't succeed.jobdri/src/app/page.tsx#L213-L231: onfetchMyJobPostingfailure, 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.
| 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; | ||
| } |
There was a problem hiding this comment.
🩺 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.
| 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.
🔗 관련 이슈
📝 개요
⌨️ 작업 상세 내용
💡 코드 설명 및 참고사항
📸 스크린샷 (UI 변경 시)
2026-07-23.6.57.21.mov
Summary by CodeRabbit