Feat: 모의지원 전체 흐름 연결 및 재도전·삭제 기능 개선 [JDDEV-98] - #103
Hidden character warning
Conversation
…ome_retry—api-feat # Conflicts: # jobdri/src/app/mockApply/[mockApplyId]/page.tsx # jobdri/src/components/common/lnb/Lnb.tsx
📝 WalkthroughWalkthroughThe PR updates mock-apply question persistence, retry and deletion flows, sidebar notifications, modal behavior, viewport layouts, and production host canonicalization. ChangesMock apply workflow
Sidebar and notifications
Modal and layout UI
Canonical-domain routing
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: 🚥 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: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
jobdri/src/app/mockApply/[mockApplyId]/result/page.tsx (1)
103-121: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winRetry failure produces no user feedback — the
catchis unreachable.
reApply(inuseReApply) swallows errors internally and returnsnullrather than throwing, soawait reApply(...)never rejects and thiscatchnever runs. On a failed retry the modal stays open and no error toast is shown. Gate on the return value instead.🐛 Proposed fix
- try { - await reApply(resolvedMockApplyId, { - getRedirectPath: (result) => - `/mockApply/${result.mockApplyId}?retry=1&sequence=${result.sequence}`, - }); - } catch { - setIsRetryModalOpen(false); - showTopToast("재도전을 시작하지 못했어요. 잠시 후 다시 시도해주세요."); - } + const result = await reApply(resolvedMockApplyId, { + getRedirectPath: (retryResult) => + `/mockApply/${retryResult.mockApplyId}?retry=1&sequence=${retryResult.sequence}`, + }); + + 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 103 - 121, Update handleRetryConfirm to inspect the value returned by reApply instead of relying on the unreachable catch block. When reApply returns null or another failure result, close the retry modal and show the existing failure toast; preserve the redirect callback and successful retry behavior.
🤖 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/credit/page.tsx`:
- Around line 48-49: Update the parent content container’s overflow behavior
near the fixed-width main element to allow horizontal scrolling instead of
clipping. Change the `overflow-x-hidden` behavior on the surrounding `<div>` to
`overflow-x-auto`, while preserving the existing vertical scrolling and `main`
width constraints.
In `@jobdri/src/components/common/lnb/Lnb.tsx`:
- Around line 262-273: Guard newNotification.type in the
subscribeToNotificationStream callback before invoking startsWith, preserving
the loadRecentItems refresh when the type is present and starts with
"ANALYSIS_ASYNC_". Treat missing types as non-matching so the notification is
still mapped and stored without throwing.
- Around line 285-308: Update the handleMockApplyDeleted handler in the Lnb
useEffect to select the first remaining recent item when the deleted item was
selected, matching loadRecentItems’s mappedItems[0]?.id ?? "" fallback; retain
the current selection when a different item is deleted and clear it only when no
items remain.
---
Outside diff comments:
In `@jobdri/src/app/mockApply/`[mockApplyId]/result/page.tsx:
- Around line 103-121: Update handleRetryConfirm to inspect the value returned
by reApply instead of relying on the unreachable catch block. When reApply
returns null or another failure result, close the retry modal and show the
existing failure toast; preserve the redirect callback and successful retry
behavior.
🪄 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: 9f625ec5-14b4-4107-999b-7e98b826d8df
📒 Files selected for processing (25)
jobdri/src/app/credit/page.tsxjobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-review/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/job/create/page.tsxjobdri/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/lnb/Lnb.tsxjobdri/src/components/common/lnb/LnbFolded.tsxjobdri/src/components/common/lnb/LnbNotification.tsxjobdri/src/components/common/lnb/LnbShared.tsxjobdri/src/components/common/lnb/index.tsjobdri/src/components/common/modal/ModalNotice.tsxjobdri/src/components/common/searchbar/SearchBar.tsxjobdri/src/components/mockApply/ResumeAnalysisLoading.tsxjobdri/src/components/mockApply/home/ResultApplicationCard.tsxjobdri/src/components/mockApply/home/ResultApplicationList.tsxjobdri/src/components/mockApply/home/applicationHomeUtils.tsjobdri/src/hooks/useReApply.tsjobdri/src/lib/api/mockApplies.tsjobdri/src/lib/api/notification.tsjobdri/src/lib/api/questions.ts
| <div className="min-h-0 min-w-0 flex-1 overflow-x-hidden overflow-y-auto"> | ||
| <main className="mx-auto w-full max-w-[1320px] min-w-[1060px] px-18 pt-12 pb-60"> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Allow horizontal scrolling for the fixed-width content.
main remains min-w-[1060px], but its parent uses overflow-x-hidden. When the viewport is narrower than the LNB plus 1060px, the right side of the credit content is clipped and cannot be reached. Use overflow-x-auto or relax the minimum width at smaller breakpoints.
Proposed fix
- <div className="min-h-0 min-w-0 flex-1 overflow-x-hidden overflow-y-auto">
+ <div className="min-h-0 min-w-0 flex-1 overflow-x-auto overflow-y-auto">📝 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.
| <div className="min-h-0 min-w-0 flex-1 overflow-x-hidden overflow-y-auto"> | |
| <main className="mx-auto w-full max-w-[1320px] min-w-[1060px] px-18 pt-12 pb-60"> | |
| <div className="min-h-0 min-w-0 flex-1 overflow-x-auto overflow-y-auto"> | |
| <main className="mx-auto w-full max-w-[1320px] min-w-[1060px] px-18 pt-12 pb-60"> |
🤖 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/credit/page.tsx` around lines 48 - 49, Update the parent
content container’s overflow behavior near the fixed-width main element to allow
horizontal scrolling instead of clipping. Change the `overflow-x-hidden`
behavior on the surrounding `<div>` to `overflow-x-auto`, while preserving the
existing vertical scrolling and `main` width constraints.
| useEffect(() => { | ||
| const handleMockApplyDeleted = (event: Event) => { | ||
| const mockApplyId = (event as CustomEvent<number>).detail; | ||
|
|
||
| setRecentItems((current) => | ||
| current.filter((item) => item.id !== String(mockApplyId)), | ||
| ); | ||
| setSelectedRecentItemId((current) => | ||
| current === String(mockApplyId) ? "" : current, | ||
| ); | ||
| }; | ||
|
|
||
| window.addEventListener( | ||
| MOCK_APPLY_DELETED_EVENT, | ||
| handleMockApplyDeleted, | ||
| ); | ||
|
|
||
| return () => { | ||
| window.removeEventListener( | ||
| MOCK_APPLY_DELETED_EVENT, | ||
| handleMockApplyDeleted, | ||
| ); | ||
| }; | ||
| }, []); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Reselect fallback item after deleting the selected recent item.
Unlike loadRecentItems (which falls back to mappedItems[0]?.id ?? "" when the current selection is invalid), this handler just clears selectedRecentItemId to "" when the deleted item was selected, leaving nothing highlighted until the next full refresh.
♻️ Proposed fix
setRecentItems((current) =>
current.filter((item) => item.id !== String(mockApplyId)),
);
setSelectedRecentItemId((current) =>
- current === String(mockApplyId) ? "" : current,
+ current === String(mockApplyId)
+ ? (recentItems.find((item) => item.id !== String(mockApplyId))?.id ?? "")
+ : current,
);🤖 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/lnb/Lnb.tsx` around lines 285 - 308, Update the
handleMockApplyDeleted handler in the Lnb useEffect to select the first
remaining recent item when the deleted item was selected, matching
loadRecentItems’s mappedItems[0]?.id ?? "" fallback; retain the current
selection when a different item is deleted and clear it only when no items
remain.
…ome_retry—api-feat # Conflicts: # jobdri/src/app/credit/page.tsx # jobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-review/page.tsx # jobdri/src/app/mockApply/[mockApplyId]/page.tsx # jobdri/src/app/mockApply/[mockApplyId]/result/resume-analysis-loading/ResumeAnalysisLoadingPageClient.tsx # jobdri/src/app/page.tsx # jobdri/src/components/common/lnb/Lnb.tsx # jobdri/src/components/common/lnb/LnbNotification.tsx # jobdri/src/components/mockApply/home/applicationHomeUtils.ts # jobdri/src/lib/api/mockApplies.ts # jobdri/src/lib/api/notification.ts
…ome_retry—api-feat # Conflicts: # jobdri/src/app/credit/page.tsx
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
jobdri/src/constants/policy.ts (2)
1-3: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winReplace all publication placeholders before release.
[YYYY년 MM월 DD일]and[확인 후 기재]remain in the published terms and privacy policy. Populate the actual effective/revision dates and overseas-recipient contact details consistently before exposing these constants to users.Also applies to: 119-121, 125-126, 249-249, 257-257
🤖 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/constants/policy.ts` around lines 1 - 3, Replace every publication placeholder in TERMS_TEXT and the corresponding privacy-policy constants with the approved effective/revision dates and overseas-recipient contact details. Ensure no “[YYYY년 MM월 DD일]” or “[확인 후 기재]” placeholders remain before these constants are exposed to users.
81-83: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftAlign the retention and deletion commitments.
The terms say user-entered data is retained until account deletion, while the JD section promises deletion on request and the privacy table again states retention until account deletion. A user’s deletion request could therefore produce behavior that contradicts one of the published policies.
Also applies to: 95-97, 154-157
🤖 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/constants/policy.ts` around lines 81 - 83, Align the retention and deletion language across the policy entries near the shown text, including the JD section and privacy table, so user-entered data is handled consistently when a user requests deletion. Update the relevant retention commitments to describe deletion on request while preserving legally required retention periods and the existing post-account-deletion behavior.jobdri/src/components/mockApply/home/applicationHomeUtils.ts (1)
86-86: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winNormalize
applyTypebefore casting it.
profileColoris optional, so the fallback toDEFAULTis acceptable as the API default;applyType, however, is required and currently passes any decodedMockApplyHomeItem.applyTypethrough unchecked. If the API can return bad values, coalesce them toMOCK/ACTUAL, or adjustmockApplies.ts’ client-side response typing soapplyTypeis not accepted as-is.🤖 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/mockApply/home/applicationHomeUtils.ts` at line 86, Normalize MockApplyHomeItem.applyType before casting it to the API enum, allowing only the valid MOCK or ACTUAL values and falling back to MOCK for invalid or missing values. Update the relevant mapping alongside profileColor, or tighten the client response typing in mockApplies.ts so unchecked applyType values cannot pass through.jobdri/src/app/credit/page.tsx (2)
48-54: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winPreserve a retry path after confirmation failure.
Because the redirect runs in
finally, any network or server failure immediately removespaymentKey,orderId, andamount. Clear the callback URL only after success, or provide a retry action that can safely reuse the transaction.🤖 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/credit/page.tsx` around lines 48 - 54, Update the confirmation flow around the try/catch/finally block so router.replace("/credit") executes only after successful payment confirmation, not in finally. Preserve the failure state and callback parameters after errors so the user can retry the same transaction safely.
25-25: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winGate the confirmation request while preventing rerun.
isConfirmingonly toggles the overlay; it is checked only afterprocessConfirm()has already started another request and does not handle the React Strict Mode Development-only effect cancellation. Track the in-flight confirmation with a ref/guard beforeconfirmPurchase(), and ensure the backend confirmation forpaymentKey/orderIdis idempotent.🤖 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/credit/page.tsx` at line 25, Update the confirmation flow around processConfirm and confirmPurchase to guard the request with an in-flight ref before starting it, preventing duplicate calls including React Strict Mode development-only reruns. Keep the UI loading state synchronized with the guard, and make backend confirmation for the paymentKey/orderId pair idempotent so repeated attempts produce one effective confirmation.Source: MCP tools
🧹 Nitpick comments (1)
jobdri/src/app/mockApply/[mockApplyId]/result/resume-analysis-loading/ResumeAnalysisLoadingPageClient.tsx (1)
289-315: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse the declared
ModalNoticetypeunion for consistency.
"alertModal"resolves to a variant, butModalNoticeProps.typeis typed asstring, so this literal is not enforced by the component contract. Narrow it to a union that includes"alertModal"alongside the existing un-suffixed names to keep callers from relying on naming/typo-dependent fallback behavior.🤖 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/resume-analysis-loading/ResumeAnalysisLoadingPageClient.tsx around lines 289 - 315, Update the ModalNoticeProps type declaration to use a literal union for type, including "alertModal" and the existing unsuffixed variants, instead of string. Preserve the current usages in ResumeAnalysisLoadingPageClient and ensure invalid variant names are rejected at compile time.
🤖 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/page.tsx`:
- Around line 222-227: Update the currentStep === 1 branch in the targetDraft
switch within the home navigation flow to resume the existing mock apply using
targetDraft.mockApplyId instead of routing through the job-posting review/create
flow. Preserve the existing mockApplyId and navigate directly to the in-progress
mock-apply route without calling the create endpoint.
In `@jobdri/src/components/common/lnb/Lnb.tsx`:
- Around line 217-218: Update the toast scheduling logic around setToastState
and window.setTimeout to retain the active dismissal timer, clear it before
scheduling a replacement, and clear the timer during component cleanup. Ensure
newer notifications remain visible for their full three-second duration.
- Around line 221-242: The fetchAndUpdateNotifications flow must ignore
responses that are older than a newer fetch, preventing stale initial or
SSE-triggered results from replacing current notificationItems and
hasNotification state. Track fetch ordering or request identity when starting
each fetch, and before applying mappedItems or triggering its toast, ensure the
response is still the latest request.
In `@jobdri/src/proxy.ts`:
- Around line 13-15: Update the host normalization logic around the
forwardedHost/request host fallback to parse the value against the canonical
scheme, remove default ports such as :443 for HTTPS while preserving non-default
ports, and use the normalized result consistently for canonical-host comparison
and redirect/header generation.
- Around line 7-15: Update getRequestHost so host-based routing uses an
ingress-overwritten or otherwise immutable host source instead of prioritizing
the client-controlled x-forwarded-host header; preserve normalization and
fallback behavior for the trusted value. Add a production-path integration test
proving a spoofed x-forwarded-host cannot bypass the canonical redirect.
---
Outside diff comments:
In `@jobdri/src/app/credit/page.tsx`:
- Around line 48-54: Update the confirmation flow around the try/catch/finally
block so router.replace("/credit") executes only after successful payment
confirmation, not in finally. Preserve the failure state and callback parameters
after errors so the user can retry the same transaction safely.
- Line 25: Update the confirmation flow around processConfirm and
confirmPurchase to guard the request with an in-flight ref before starting it,
preventing duplicate calls including React Strict Mode development-only reruns.
Keep the UI loading state synchronized with the guard, and make backend
confirmation for the paymentKey/orderId pair idempotent so repeated attempts
produce one effective confirmation.
In `@jobdri/src/components/mockApply/home/applicationHomeUtils.ts`:
- Line 86: Normalize MockApplyHomeItem.applyType before casting it to the API
enum, allowing only the valid MOCK or ACTUAL values and falling back to MOCK for
invalid or missing values. Update the relevant mapping alongside profileColor,
or tighten the client response typing in mockApplies.ts so unchecked applyType
values cannot pass through.
In `@jobdri/src/constants/policy.ts`:
- Around line 1-3: Replace every publication placeholder in TERMS_TEXT and the
corresponding privacy-policy constants with the approved effective/revision
dates and overseas-recipient contact details. Ensure no “[YYYY년 MM월 DD일]” or
“[확인 후 기재]” placeholders remain before these constants are exposed to users.
- Around line 81-83: Align the retention and deletion language across the policy
entries near the shown text, including the JD section and privacy table, so
user-entered data is handled consistently when a user requests deletion. Update
the relevant retention commitments to describe deletion on request while
preserving legally required retention periods and the existing
post-account-deletion behavior.
---
Nitpick comments:
In
`@jobdri/src/app/mockApply/`[mockApplyId]/result/resume-analysis-loading/ResumeAnalysisLoadingPageClient.tsx:
- Around line 289-315: Update the ModalNoticeProps type declaration to use a
literal union for type, including "alertModal" and the existing unsuffixed
variants, instead of string. Preserve the current usages in
ResumeAnalysisLoadingPageClient and ensure invalid variant names are rejected at
compile time.
🪄 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: f4e82f8b-3dba-462d-8c5f-a11703776c7e
📒 Files selected for processing (14)
jobdri/src/app/credit/page.tsxjobdri/src/app/mockApply/[mockApplyId]/page.tsxjobdri/src/app/mockApply/[mockApplyId]/result/resume-analysis-loading/ResumeAnalysisLoadingPageClient.tsxjobdri/src/app/mockApply/job/[jobPostingId]/review/page.tsxjobdri/src/app/mockApply/job/create/page.tsxjobdri/src/app/mockApply/job/loading/page.tsxjobdri/src/app/page.tsxjobdri/src/components/common/lnb/Lnb.tsxjobdri/src/components/common/lnb/LnbNotification.tsxjobdri/src/components/mockApply/home/applicationHomeUtils.tsjobdri/src/constants/policy.tsjobdri/src/lib/api/mockApplies.tsjobdri/src/lib/api/notification.tsjobdri/src/proxy.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- jobdri/src/components/common/lnb/LnbNotification.tsx
- jobdri/src/app/mockApply/[mockApplyId]/page.tsx
| switch (targetDraft.currentStep) { | ||
| case 1: | ||
| case 2: | ||
| router.push( | ||
| `/mockApply/job/${targetDraft.jobPostingId}/review`, | ||
| ); | ||
| break; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check whether the review page resumes an existing mock apply or always creates a new one.
rg -n -A5 -B5 'createMockApplyFromJobPosting|MockApplyFromJobPostingPayload' jobdri/src/app/mockApply/job/\[jobPostingId\]/review/page.tsx jobdri/src/lib/api/mockApplies.tsRepository: JobDri-Developer/FrontEnd
Length of output: 3484
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== candidate files =="
fd -a 'page\.tsx$|mockApplies\.ts$' jobdri/src | sed 's#^\./##' | head -100
echo "== jobdri/src/app/page.tsx relevant section =="
sed -n '180,250p' jobdri/src/app/page.tsx
echo "== review page outline =="
fd 'page\.tsx$' jobdri/src/app/mockApply/job | sed -n '1,20p'
REVIEW=$(fd 'page\.tsx$' 'jobdri/src/app/mockApply/job/[\[jobPostingId\]\]' jobdri/src/app/mockApply/job | sed -n '1p') || true
if [ -n "${REVIEW:-}" ]; then
echo "FILE=$REVIEW"
ast-grep outline "$REVIEW" --view compact || true
echo "== review page relevant sections =="
wc -l "$REVIEW"
sed -n '1,280p' "$REVIEW"
fi
echo "== mockApplies.ts relevant sections =="
sed -n '1,200p' jobdri/src/lib/api/mockApplies.ts
echo "== search for resume-existing mocks/routes =="
rg -n "currentStep|mockApplyId|sequence|mock-from-job-posting|from-job-posting|retry|resume|review" jobdri/src/app jobdri/src/lib -g '*.ts' -g '*.tsx' | head -240Repository: JobDri-Developer/FrontEnd
Length of output: 34137
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== draft creation/status state =="
sed -n '1,130p' jobdri/src/app/page.tsx
echo "== review page create/next section =="
sed -n '50,280p' 'jobdri/src/app/mockApply/job/[jobPostingId]/review/page.tsx'
echo "== precise call sites to createApplyFromJobPosting =="
rg -n -C3 "createApplyFromJobPosting|MockApplyFromJobPostingPayload|sequence" 'jobdri/src/app/mockApply/job/[jobPostingId]/review/page.tsx' jobdri/src/app/mockApply/job/create/page.tsx jobdri/src/app/mockApply/job/loading/page.tsx jobdri/src/lib/api/mockApplies.ts
echo "== review component mock apply ID usages =="
python3 - <<'PY'
from pathlib import Path
import re
fp = Path('jobdri/src/app/mockApply/job/[jobPostingId]/review/page.tsx')
s = fp.read_text()
for pat in [
r"mockApplyId",
r"createApplyFromJobPosting\\(",
r"jobPostingId",
]:
print(f"\nmatches for {pat}:")
for i, line in enumerate(s.splitlines(), 1):
if re.search(pat, line):
print(f"{i}: {line}")
PYRepository: JobDri-Developer/FrontEnd
Length of output: 18853
Resume existing mock applies from currentStep === 1.
Drafts routed from currentStep === 1 already have mockApplyId and status "QUESTION_SELECT", but the home click sends them to the job-posting review page, which creates a new mock apply and pushes /mockApply/${createdApply.mockApplyId}. Keep these on the existing mockApplyId, or preserve and pass the in-progress mockApplyId directly before calling the create endpoint.
🤖 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 222 - 227, Update the currentStep === 1
branch in the targetDraft switch within the home navigation flow to resume the
existing mock apply using targetDraft.mockApplyId instead of routing through the
job-posting review/create flow. Preserve the existing mockApplyId and navigate
directly to the in-progress mock-apply route without calling the create
endpoint.
| setToastState({ message: toastMessage, variant: toastVariant }); | ||
| setTimeout(() => setToastState(null), 3000); | ||
| window.setTimeout(() => setToastState(null), 3000); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Clear the prior toast dismissal timer.
Two notifications within three seconds leave both timers active; the earlier timer clears the newer toast early. Store and clear the previous timeout before scheduling the next one, and clear it during cleanup.
🤖 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/lnb/Lnb.tsx` around lines 217 - 218, Update the
toast scheduling logic around setToastState and window.setTimeout to retain the
active dismissal timer, clear it before scheduling a replacement, and clear the
timer during component cleanup. Ensure newer notifications remain visible for
their full three-second duration.
| const fetchAndUpdateNotifications = async () => { | ||
| try { | ||
| const data = await fetchNotifications(); | ||
| if (data.isSuccess && data.result) { | ||
| const mappedItems = data.result.map(mapApiToLnbItem); | ||
|
|
||
| setNotificationItems((prevItems) => { | ||
| if (prevItems.length > 0 && mappedItems.length > 0) { | ||
| const latestNewItem = mappedItems[0]; | ||
| const prevLatestItem = prevItems[0]; | ||
|
|
||
| if ( | ||
| latestNewItem.id !== prevLatestItem.id && | ||
| !latestNewItem.read | ||
| ) { | ||
| triggerToastBasedOnNotification(latestNewItem); | ||
| } | ||
| } | ||
| return mappedItems; | ||
| }); | ||
|
|
||
| setHasNotification(mappedItems.some((item) => !item.read)); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Prevent stale notification responses from overwriting newer state.
The initial fetch and SSE-triggered fetches can resolve out of order. An older response can replace a newer notification list and unread state.
Proposed fix
+ let notificationRequestId = 0;
+ let disposed = false;
+
const fetchAndUpdateNotifications = async () => {
+ const requestId = ++notificationRequestId;
try {
const data = await fetchNotifications();
+ if (disposed || requestId !== notificationRequestId) return;
+
if (data.isSuccess && data.result) {
// ...
}
} catch (error) {
console.error("알림 목록 갱신 실패:", error);
}
};
@@
return () => {
+ disposed = true;
unsubscribe();
hasSubscribedRef.current = false;
};📝 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.
| const fetchAndUpdateNotifications = async () => { | |
| try { | |
| const data = await fetchNotifications(); | |
| if (data.isSuccess && data.result) { | |
| const mappedItems = data.result.map(mapApiToLnbItem); | |
| setNotificationItems((prevItems) => { | |
| if (prevItems.length > 0 && mappedItems.length > 0) { | |
| const latestNewItem = mappedItems[0]; | |
| const prevLatestItem = prevItems[0]; | |
| if ( | |
| latestNewItem.id !== prevLatestItem.id && | |
| !latestNewItem.read | |
| ) { | |
| triggerToastBasedOnNotification(latestNewItem); | |
| } | |
| } | |
| return mappedItems; | |
| }); | |
| setHasNotification(mappedItems.some((item) => !item.read)); | |
| let notificationRequestId = 0; | |
| let disposed = false; | |
| const fetchAndUpdateNotifications = async () => { | |
| const requestId = ++notificationRequestId; | |
| try { | |
| const data = await fetchNotifications(); | |
| if (disposed || requestId !== notificationRequestId) return; | |
| if (data.isSuccess && data.result) { | |
| const mappedItems = data.result.map(mapApiToLnbItem); | |
| setNotificationItems((prevItems) => { | |
| if (prevItems.length > 0 && mappedItems.length > 0) { | |
| const latestNewItem = mappedItems[0]; | |
| const prevLatestItem = prevItems[0]; | |
| if ( | |
| latestNewItem.id !== prevLatestItem.id && | |
| !latestNewItem.read | |
| ) { | |
| triggerToastBasedOnNotification(latestNewItem); | |
| } | |
| } | |
| return mappedItems; | |
| }); | |
| setHasNotification(mappedItems.some((item) => !item.read)); |
🤖 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/lnb/Lnb.tsx` around lines 221 - 242, The
fetchAndUpdateNotifications flow must ignore responses that are older than a
newer fetch, preventing stale initial or SSE-triggered results from replacing
current notificationItems and hasNotification state. Track fetch ordering or
request identity when starting each fetch, and before applying mappedItems or
triggering its toast, ensure the response is still the latest request.
| function getRequestHost(request: NextRequest) { | ||
| const forwardedHost = request.headers | ||
| .get("x-forwarded-host") | ||
| ?.split(",")[0] | ||
| ?.trim(); | ||
|
|
||
| return (forwardedHost || request.headers.get("host") || request.nextUrl.host) | ||
| .toLowerCase() | ||
| .replace(/\.$/, ""); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Locate proxy.ts"
fd -a 'proxy\.ts$' . || true
echo
echo "Show target file around relevant code"
if [ -f jobdri/src/proxy.ts ]; then
nl -ba jobdri/src/proxy.ts | sed -n '1,120p'
fi
echo
echo "Search for request host parsing/proxies/deployment config hints"
rg -n "getRequestHost|redirectToCanonicalDomain|x-forwarded-host|host|trailingSlash|vercel|middleware|proxy|NextRequest|Rewrite|Redirect" jobdri .vercel 2>/dev/null || true
echo
echo "Show package/deploy config files if present"
for f in jobdri/package.json package.json vercel.json ./.vercelrc ./.vercel rc; do
if [ -e "$f" ]; then
echo "--- $f"
sed -n '1,220p' "$f"
fi
doneRepository: JobDri-Developer/FrontEnd
Length of output: 298
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Show target file around relevant code"
if [ -f jobdri/src/proxy.ts ]; then
cat -n jobdri/src/proxy.ts | sed -n '1,140p'
fi
echo
echo "Search for request host parsing/proxies/deployment config hints"
rg -n "getRequestHost|redirectToCanonicalDomain|x-forwarded-host|host|trailingSlash|vercel|middleware|proxy|NextRequest|Rewrite|Redirect" jobdri .vercel 2>/dev/null || true
echo
echo "List possible config files"
for f in jobdri/package.json package.json vercel.json ./.vercelrc ./.vercel rc; do
if [ -e "$f" ]; then
echo "--- $f"
cat -n "$f" | sed -n '1,220p'
fi
done
echo
echo "List files by top-level repo directories"
git ls-files | sed 's#/.*##' | sort -u | sed -n '1,120p'Repository: JobDri-Developer/FrontEnd
Length of output: 6255
🌐 Web query:
Vercel Next.js middleware request.headers x-forwarded-host client-controlled forwarded headers trusted source
💡 Result:
On the Vercel platform, you should never implicitly trust the x-forwarded-host header (or any client-controllable forwarded headers) for security-critical operations, such as authentication, authorization, or tenant identification [1][2]. When a request reaches a Vercel deployment, the platform manages headers according to its internal routing and edge infrastructure [3][4]. While x-forwarded-host is often present, its values are subject to platform behavior, such as rewrites and internal redirects, which can lead to inconsistencies or unexpected overwrites [5][6][7]. Crucially, because these headers are passed in the HTTP request from the client or an upstream proxy, they are inherently forgeable unless explicitly handled through a verified secure channel [3][8]. Key security considerations include: 1. Do Not Trust Client Input: Any header that a client can manipulate should be treated as untrusted [1][2]. Even if you place Vercel behind your own reverse proxy, a malicious user could spoof these headers before they reach your application [3][8]. 2. Verified Proxy: If you must use a reverse proxy in front of Vercel, use Vercel's Verified Proxy feature [8]. This allows Vercel to trust specific headers (like a custom connecting IP header) only when they originate from your known, static egress IP ranges [3][8]. This mechanism must be configured via Vercel's enterprise or professional support channels [8]. 3. Middleware and Proxy.ts: In Next.js (including the proxy.ts pattern introduced in Next.js 16), code running at the network boundary (formerly middleware) is a useful gateway, but it is not a substitute for cryptographically verifying identity [1][2]. Always verify authentication tokens (like JWTs) directly within your server-side code (e.g., Server Components or API routes) rather than relying on headers passed from the middleware [1][2]. 4. Header Behavior: Vercel documentation confirms that while x-forwarded-host is often provided, it is identical to the host header in most standard contexts [4]. However, users have reported that internal Next.js rewrites can sometimes reset or overwrite this value, leading to issues where the original host is lost [5][6][9]. For robust security, implement defense-in-depth: assume that any header set by a proxy or middleware could have been bypassed if a matcher or route configuration has a gap, and always perform final verification against an immutable source of truth, such as a secure cookie or a cryptographically signed token [1][2].
Citations:
- 1: https://faultlinesec.com/blog/nextjs-middleware-auth
- 2: https://www.devdigest.org/articles/nextjs-16-proxyts-fixing-auth-gaps-from-matcher-to-header-trust
- 3: https://vercel.com/docs/security/reverse-proxy
- 4: https://vercel.com/docs/headers/request-headers
- 5: Rewrite does not set the correct x-forwarded-host in Vercel vercel/next.js#67467
- 6: Nextjs rewrites replaces x-forwarded-host in production vercel/next.js#70019
- 7: fix: set
x-forwarded-hostbased on request vercel/next.js#58500 - 8: https://vercel.com/kb/guide/how-to-setup-verified-proxy
- 9: Rewrite does not set the correct x-forwarded-host in Vercel vercel/next.js#67469
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Node probe for host resolution logic used by getRequestHost and canonical redirect"
node - <<'JS'
const PUBLIC_ROUTES = ["/login", "/oauth2", "/components"];
const PRODUCTION_CANONICAL_ORIGIN = "https://www.jobdri.com";
function getRequestHost(request) {
const forwardedHost = request.headers
.get("x-forwarded-host")
?.split(",")[0]
?.trim();
return (forwardedHost || request.headers.get("host") || request.nextUrl.host)
.toLowerCase()
.replace(/\.$/, "");
}
const canon = new URL(PRODUCTION_CANONICAL_ORIGIN);
function redirectToCanonicalDomain(request) {
if (process.env.VERCEL_ENV !== "production") {
return null;
}
if (getRequestHost(request) === canon.host) {
return null;
}
const resp = { status: 307, url: null, headers: {} };
resp.url = new URL(Object.assign(canon, { pathname: request.nextUrl.pathname, search: request.nextUrl.search }));
return resp;
}
function proxy(request) {
const canonicalRedirect = redirectToCanonicalDomain(request);
if (canonicalRedirect) {
return canonicalRedirect.url.href;
}
const token = request.cookies.get("jobdri_accessToken")?.value;
const { pathname } = request.nextUrl;
const isPublic = PUBLIC_ROUTES.some((route) => pathname.startsWith(route));
if (!token && !isPublic) {
return "Redirect: /login?redirect=...";
}
return "NoRedirect";
}
const cases = [
{
name: "spoofed x-forwarded-host canonical, actual non-canonical host",
get: (headers) => headers.get("host") || requestHeaders.host,
headers: headers => Object.assign(headers, { host: "evil-jobdri.com", "x-forwarded-host": "www.jobdri.com" }),
},
{
name: "spoofed comma-delimited x-forwarded-host canonical first",
get: (headers) => requestHeaders.host,
headers: headers => Object.assign(headers, { host: "evil-jobdri.com", "x-forwarded-host": "www.jobdri.com, bad.example" }),
},
{
name: "canonical actual host and forwarded host",
get: (requestHeaders) => requestHeaders.host,
headers: headers => Object.assign(headers, { host: "www.jobdri.com", "x-forwarded-host": "www.jobdri.com" }),
},
{
name: "canonical with dot trailing in host, removed",
headers: headers => Object.assign(headers, { host: "www.jobdri.com.", "x-forwarded-host": "www.jobdri.com." }),
},
];
for (const c of cases) {
headers = new Headers({ user_agent: "test" });
c.headers(headers);
const req = new Request("https://invalid.invalid/page", { headers });
req.cookies = { get: () => null };
req.nextUrl = { pathname: "/page", search: "" };
Object.defineProperty(req, "headers", { get() { return headers } });
console.log(`${c.name}:`, proxy(req));
}
JSRepository: JobDri-Developer/FrontEnd
Length of output: 575
Treat x-forwarded-host as untrusted for host-based routing.
getRequestHost can let a forged forwarded host make non-canonical traffic skip the canonical redirect, so don’t prioritize or derive security-critical host decisions from it. Use an ingress-overwritten or immutable host value, and add a production-path integration test covering spoofed forwarded-host values.
🤖 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/proxy.ts` around lines 7 - 15, Update getRequestHost so host-based
routing uses an ingress-overwritten or otherwise immutable host source instead
of prioritizing the client-controlled x-forwarded-host header; preserve
normalization and fallback behavior for the trusted value. Add a production-path
integration test proving a spoofed x-forwarded-host cannot bypass the canonical
redirect.
| return (forwardedHost || request.headers.get("host") || request.nextUrl.host) | ||
| .toLowerCase() | ||
| .replace(/\.$/, ""); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Repository files matching proxy.ts:"
fd -a 'proxy\.ts$' . || true
echo
echo "jobdri/src/proxy.ts outline and contents:"
if [ -f jobdri/src/proxy.ts ]; then
wc -l jobdri/src/proxy.ts
cat -n jobdri/src/proxy.ts
fi
echo
echo "Search related terms:"
rg -n 'canonicalUrl|forwardedHost|request\.headers\.get\("host"\)|request\.nextUrl\.host|HostWithPort|URL' jobdri -S || trueRepository: JobDri-Developer/FrontEnd
Length of output: 50382
🏁 Script executed:
#!/bin/bash
set -euo pipefail
node <= 'JS'
const scenarios = [
["jobdri.com:443", "https://www.jobdri.com", "www.jobdri.com"],
["www.jobdri.com:443", "https://www.jobdri.com", "www.jobdri.com"],
["www.jobdri.com:9443", "https://www.jobdri.com", "www.jobdri.com:9443"],
["jobdri.com:8123", "https://www.jobdri.com", "jobdri.com:8123"],
];
for (const [host, canonicalOrigin, expectedDirectHost] of scenarios) {
const canonicalUrl = new URL(canonicalOrigin);
const isMatch = host === canonicalUrl.host;
console.log(JSON.stringify({ host, canonicalUrlHost: canonicalUrl.host, isMatch, redirectToCanonical: !isMatch }));
}
JS
echo
echo "Relevant proxy.ts lines:"
sed -n '1,40p' jobdri/src/proxy.tsRepository: JobDri-Developer/FrontEnd
Length of output: 206
🏁 Script executed:
#!/bin/bash
set -euo pipefail
node - <<'JS'
const canonicalOrigin = 'https://www.jobdri.com';
const cases = [
['jobdri.com:443'],
['www.jobdri.com:443'],
['www.jobdri.com:9443'],
['jobdri.com:8123'],
];
const canonicalUrl = new URL(canonicalOrigin);
console.log('canonicalUrl.host=' + canonicalUrl.host);
for (const [host] of cases) {
console.log(JSON.stringify({ host, canonicalUrlHost: canonicalUrl.host, redirect: host !== canonicalUrl.host }));
}
JS
echo
echo "Relevant proxy.ts lines:"
sed -n '1,40p' jobdri/src/proxy.tsRepository: JobDri-Developer/FrontEnd
Length of output: 1657
Normalize default ports before comparing canonical hosts.
Lines 8-16 normalize hosts to lowercase/strip trailing dots, but they leave ports intact. A forwarded or Host header like www.jobdri.com:443 still redirects to https://www.jobdri.com..., and the canonical host header is generated as www.jobdri.com:443, causing a redirect loop. Compare against a canonical origin parsed with the expected canonical scheme and omit default ports while keeping non-default ports.
🤖 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/proxy.ts` around lines 13 - 15, Update the host normalization
logic around the forwardedHost/request host fallback to parse the value against
the canonical scheme, remove default ports such as :443 for HTTPS while
preserving non-default ports, and use the normalized result consistently for
canonical-host comparison and redirect/header generation.
🔗 관련 이슈
📝 개요
⌨️ 작업 상세 내용
sequence기반 지원 회차 표시ModalNotice컴포넌트로 통일💡 코드 설명 및 참고사항
🔍 리뷰 요구사항 (Reviewers)
Summary by CodeRabbit
New Features
Bug Fixes