Skip to content

Feat: 입력 필드 상태 변화 수정 - #116

Merged
yiyoonseo merged 1 commit into
developfrom
feature/qa-v1.0
Jul 30, 2026
Merged

Feat: 입력 필드 상태 변화 수정#116
yiyoonseo merged 1 commit into
developfrom
feature/qa-v1.0

Conversation

@yiyoonseo

@yiyoonseo yiyoonseo commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

🔗 관련 이슈

  • Close #

📝 개요

  • 입력 필드 상태 변화를 수정하였습니다.

⌨️ 작업 상세 내용

유효성 검사 타이밍 변경 (Submit-driven Validation)

  • emailTouched, passwordTouched, passwordConfirmTouched 상태를 추가하여 에러 노출 여부를 제어합니다.
  • InputMain에서 사용 중이던 불필요한 이벤트(onBlur 등)를 제거했습니다.
  • handleSignupSubmit 함수(회원가입 버튼 클릭) 내에서 touched 상태들을 모두 true로 변경하여 제출 시점에만 정규식 및 일치 여부 에러가 표시되도록 수정했습니다.

재입력 시 에러 메시지 숨김 처리 (UX 개선)

  • 사용자가 에러를 확인한 후 다시 입력을 시작하면(onChange 발생 시) 해당 입력칸의 touched 상태를 false로 돌려, 다시 제출 버튼을 누르기 전까지 에러 메시지를 숨겨줍니다.

  • handleInputChange, handlePasswordChange, handlePasswordConfirmChange 함수에 관련 상태 초기화 로직을 추가했습니다.

  • InputMain 컴포넌트 Error Prop 조건부 렌더링 수정

    • 에러가 없는 정상적인 상황에서도 하드코딩된 에러 메시지가 노출되던 버그를 수정했습니다.
    • 서버 에러 메시지와 정규식 에러 조건(hasSignupEmailValidationError 등)을 분리하고, 에러가 없을 때는 undefined를 전달하여 UI에서 에러가 깔끔하게 사라지도록 조치했습니다.

📸 스크린샷 (UI 변경 시)

JobDri.-.Chrome.2026-07-30.13-03-10.mp4

🔍 리뷰 요구사항 (Reviewers)

  • [ ]

⚠️ 로컬 실행 시 유의사항

Summary by CodeRabbit

  • New Features

    • Credit purchases now redirect to a secure payment page.
    • Payment results are verified automatically after checkout, with clear success or failure feedback.
    • Successful payments refresh credit information automatically.
  • Bug Fixes

    • Improved login and signup validation by showing field errors after interaction.
    • Reset form validation states when switching between login and signup modes.
    • Added clearer handling for unavailable payment links or failed payment checks.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Credit purchases now redirect through a backend checkout URL and verify results by polling payment status. Login and signup forms add touched-state tracking to control validation feedback.

Changes

Credit payment flow

Layer / File(s) Summary
Payment API contract
jobdri/src/lib/api/credit.ts
Replaces confirmPurchase with checkPaymentStatus(orderId) and preserves the transaction URL behavior.
Checkout page redirection
jobdri/src/components/common/cards/CreditCard.tsx
Requests a backend checkout URL and redirects the browser instead of using the Toss Payments SDK.
Payment status polling
jobdri/src/app/credit/page.tsx
Polls payment status by orderId, handles completion or failure, updates browser history, and clears confirmation state.

Login form validation

Layer / File(s) Summary
Touched-state validation
jobdri/src/components/login/EmailLoginScreen.tsx
Adds field touched state, blur handling, validation gating, and reset behavior.
Form validation wiring
jobdri/src/components/login/EmailLoginScreen.tsx
Connects touched state to login/signup fields and enables all signup validation flags on submit.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CreditCard
  participant Backend
  participant CreditContent
  participant PaymentAPI
  CreditCard->>Backend: preparePurchase(planCode)
  Backend-->>CreditCard: checkoutPage URL
  CreditCard->>Backend: redirect to checkoutPage
  CreditContent->>PaymentAPI: poll orderId
  PaymentAPI-->>CreditContent: payment status
  CreditContent->>CreditContent: handle COMPLETED or FAILED
Loading

Possibly related PRs

Suggested labels: ⭐ Feature, 🎨 UI

Suggested reviewers: minnngo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is related to the input-state and validation updates, though it is broader than the full changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ 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/qa-v1.0

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.

@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: 5

🧹 Nitpick comments (5)
jobdri/src/app/credit/page.tsx (3)

55-55: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace blocking alert() with the existing Toast component used elsewhere in this flow (jobdri/src/components/common/cards/CreditCard.tsx), for consistent UX.

Also applies to: 61-61, 73-73

🤖 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 55, Replace the blocking alert calls
in the credit page completion flows with the existing Toast component pattern
used by CreditCard.tsx. Update all affected success messages, including the
occurrences corresponding to lines 55, 61, and 73, while preserving their
current message text and triggering behavior.

77-81: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The Promise.resolve() wrapper is unnecessary. Calling setIsConfirming(true) synchronously inside an effect is standard and does not trigger a cascading-render warning; the microtask defer only makes the flow harder to follow (and leaves the initial pollStatus un-tracked by the cleanup path).

🤖 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 77 - 81, Remove the
Promise.resolve microtask wrapper around setIsConfirming and pollStatus in the
effect. Invoke setIsConfirming(true) synchronously, then start pollStatus
through the existing cleanup-tracked flow so the initial poll is handled
consistently.

26-27: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

router is initialized but the effect mutates history directly. window.history.replaceState bypasses the App Router, and the COMPLETED path then does a full window.location.reload(). Prefer router.replace(window.location.pathname) plus router.refresh() (or refetching plans/credits) to clear orderId and update data without a hard reload.

Also applies to: 53-56

🤖 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 26 - 27, Update the effect using
the initialized router in the credit page: replace direct
window.history.replaceState calls with router.replace(window.location.pathname),
and replace the COMPLETED branch’s window.location.reload() with
router.refresh() (or the existing plans/credits refetch). Preserve clearing
orderId while updating App Router data without a hard reload.
jobdri/src/components/common/cards/CreditCard.tsx (1)

73-78: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

isLoading stays true when the redirect is slow, but that's the only state left set. Success intentionally keeps the spinner until navigation; fine. Consider validating the redirect target scheme before window.location.assign so a compromised/incorrect backend value can't produce a javascript: navigation.

🛡️ Optional hardening
-      if (checkoutPage) {
+      const target = checkoutPage ? new URL(checkoutPage, window.location.origin) : null;
+      if (target && (target.protocol === "https:" || target.protocol === "http:")) {
         // 백엔드에서 생성한 토스페이 결제 페이지로 이동
-        window.location.assign(checkoutPage);
+        window.location.assign(target.toString());
       } else {
🤖 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/cards/CreditCard.tsx` around lines 73 - 78,
Validate the redirect URL immediately before the window.location.assign call in
the payment flow, allowing only trusted http/https schemes and rejecting
javascript: or other unsafe schemes. On rejection, follow the existing
payment-error cleanup path, while preserving the success behavior that keeps
isLoading true until navigation.
jobdri/src/lib/api/credit.ts (1)

108-122: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the commented-out confirmPurchase block.

Dead code; git history preserves it.

🤖 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/credit.ts` around lines 108 - 122, Remove the
commented-out confirmPurchase function block from the payment API module,
leaving the surrounding code 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/credit/page.tsx`:
- Around line 38-88: Update the polling flow in the useEffect around pollStatus
to track and clear the active timeout during cleanup, and re-check the
cancellation flag after each awaited checkPaymentStatus call before performing
state updates, alerts, history changes, or reloads. Add a finite polling limit
for PENDING, PROCESSING, and unknown statuses; when the limit is reached, stop
polling, remove the URL order state, clear confirmation, and report that payment
status could not be confirmed. Ensure all terminal and error paths clear any
scheduled timeout.

In `@jobdri/src/components/login/EmailLoginScreen.tsx`:
- Line 323: Make touched-state resets field-specific in handleInputChange and
handlePasswordChange: ensure email input changes reset only emailTouched,
password input changes reset passwordTouched, and confirmation input changes
reset passwordConfirmTouched. Pass the appropriate setter through the shared
helper or perform the resets in each dedicated handler, without clearing
unrelated fields.
- Around line 774-780: Wire blur handling for both signup fields: add an onBlur
handler to the signup email input that marks the email as touched, and restore
the commented password-confirmation onBlur handler so it marks that field as
touched. Update the signup form inputs near the signupErrorMessage logic and
preserve the existing validation/error conditions.

In `@jobdri/src/lib/api/credit.ts`:
- Around line 124-129: Update jobdri/src/lib/api/credit.ts lines 124-129 by
giving checkPaymentStatus an explicit response type and returning the unwrapped
result; extend PreparePaymentResult with checkoutPage. In
jobdri/src/components/common/cards/CreditCard.tsx lines 57-72, remove the cast
and fallback, and read response.checkoutPage directly. In
jobdri/src/app/credit/page.tsx lines 45-49, remove the cast and read
response.status directly.
- Around line 124-129: Update checkPaymentStatus to follow the file’s
established API request pattern: build the URL with API_BASE_URL, include
getAuthHeaders(), and pass the response through checkResponse so unauthorized
responses invoke handleUnauthorized(). Unwrap the response’s result field and
declare the returned payment-status type, allowing callers to use it without
casts.

---

Nitpick comments:
In `@jobdri/src/app/credit/page.tsx`:
- Line 55: Replace the blocking alert calls in the credit page completion flows
with the existing Toast component pattern used by CreditCard.tsx. Update all
affected success messages, including the occurrences corresponding to lines 55,
61, and 73, while preserving their current message text and triggering behavior.
- Around line 77-81: Remove the Promise.resolve microtask wrapper around
setIsConfirming and pollStatus in the effect. Invoke setIsConfirming(true)
synchronously, then start pollStatus through the existing cleanup-tracked flow
so the initial poll is handled consistently.
- Around line 26-27: Update the effect using the initialized router in the
credit page: replace direct window.history.replaceState calls with
router.replace(window.location.pathname), and replace the COMPLETED branch’s
window.location.reload() with router.refresh() (or the existing plans/credits
refetch). Preserve clearing orderId while updating App Router data without a
hard reload.

In `@jobdri/src/components/common/cards/CreditCard.tsx`:
- Around line 73-78: Validate the redirect URL immediately before the
window.location.assign call in the payment flow, allowing only trusted
http/https schemes and rejecting javascript: or other unsafe schemes. On
rejection, follow the existing payment-error cleanup path, while preserving the
success behavior that keeps isLoading true until navigation.

In `@jobdri/src/lib/api/credit.ts`:
- Around line 108-122: Remove the commented-out confirmPurchase function block
from the payment API module, leaving the surrounding code 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: dcaa9eae-ac19-40f7-8ccf-bf41a13a5cbd

📥 Commits

Reviewing files that changed from the base of the PR and between 4fac529 and fd33fef.

📒 Files selected for processing (4)
  • jobdri/src/app/credit/page.tsx
  • jobdri/src/components/common/cards/CreditCard.tsx
  • jobdri/src/components/login/EmailLoginScreen.tsx
  • jobdri/src/lib/api/credit.ts

Comment on lines +38 to 88
if (orderId) {
let isPolling = true;

const pollStatus = async () => {
try {
await confirmPurchase(paymentKey, orderId, Number(amount));
window.history.replaceState(null, "", window.location.pathname);
setIsConfirming(false);
setTimeout(() => {
const response = await checkPaymentStatus(orderId);
// API 타입이 지정되지 않은 경우를 대비한 타입 단언
const data = response as unknown as {
status?: string;
result?: { status?: string };
};
const status = data.status || data.result?.status;

if (status === "COMPLETED") {
isPolling = false;
window.history.replaceState(null, "", window.location.pathname);
setIsConfirming(false);
alert("크레딧 충전이 완료되었습니다!");
window.location.reload();
}, 100);
} catch (error) {
console.error("결제 승인 실패:", error);
setIsConfirming(false);
alert("결제 승인에 실패했습니다. 다시 시도해 주세요.");
// 실패 시에도 쿼리 파라미터를 날려 중복 요청 방지
} else if (status === "FAILED") {
isPolling = false;
window.history.replaceState(null, "", window.location.pathname);
setIsConfirming(false);
alert("결제에 실패했거나 취소되었습니다.");
} else {
// PENDING, PROCESSING, UNKNOWN 상태일 경우 계속 폴링
if (isPolling) {
setTimeout(pollStatus, 2000); // 2초 주기로 폴링
}
}
} catch (error: unknown) {
console.error("결제 상태 조회 실패:", error);
isPolling = false;
window.history.replaceState(null, "", window.location.pathname);
setIsConfirming(false);
alert("결제 상태를 확인하는 중 오류가 발생했습니다.");
}
};

processConfirm();
// 동기적 setState 호출 경고(Cascading renders)를 방지하기 위해 Promise로 감싸서 실행
Promise.resolve().then(() => {
setIsConfirming(true);
pollStatus();
});

// 폴링 중단
return () => {
isPolling = false;
};
}
}, [searchParams]);

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 | 🔴 Critical | ⚡ Quick win

Polling is not cancellable and never terminates.

Two defects in this loop:

  1. The cleanup only flips isPolling; the already-scheduled setTimeout (Line 65) is never cleared, and pollStatus does not re-check isPolling after await. After unmount/searchParams change, the in-flight or scheduled call still runs setIsConfirming, alert, and window.location.reload().
  2. If the backend keeps returning PENDING/PROCESSING, the loop polls every 2s forever, with the blocking overlay up indefinitely.
🐛 Proposed fix: cancellable, bounded polling
     if (orderId) {
       let isPolling = true;
+      let timer: ReturnType<typeof setTimeout> | undefined;
+      let attempts = 0;
+      const MAX_ATTEMPTS = 30; // ~60s
+
+      const finish = (message: string, reload = false) => {
+        isPolling = false;
+        window.history.replaceState(null, "", window.location.pathname);
+        setIsConfirming(false);
+        alert(message);
+        if (reload) window.location.reload();
+      };
 
       const pollStatus = async () => {
         try {
           const response = await checkPaymentStatus(orderId);
+          if (!isPolling) return;
           // API 타입이 지정되지 않은 경우를 대비한 타입 단언
           const data = response as unknown as {
             status?: string;
             result?: { status?: string };
           };
           const status = data.status || data.result?.status;
 
           if (status === "COMPLETED") {
-            isPolling = false;
-            window.history.replaceState(null, "", window.location.pathname);
-            setIsConfirming(false);
-            alert("크레딧 충전이 완료되었습니다!");
-            window.location.reload();
+            finish("크레딧 충전이 완료되었습니다!", true);
           } else if (status === "FAILED") {
-            isPolling = false;
-            window.history.replaceState(null, "", window.location.pathname);
-            setIsConfirming(false);
-            alert("결제에 실패했거나 취소되었습니다.");
+            finish("결제에 실패했거나 취소되었습니다.");
+          } else if (++attempts >= MAX_ATTEMPTS) {
+            finish("결제 결과 확인이 지연되고 있습니다. 잠시 후 다시 확인해 주세요.");
           } else {
             // PENDING, PROCESSING, UNKNOWN 상태일 경우 계속 폴링
-            if (isPolling) {
-              setTimeout(pollStatus, 2000); // 2초 주기로 폴링
-            }
+            timer = setTimeout(pollStatus, 2000); // 2초 주기로 폴링
           }
         } catch (error: unknown) {
+          if (!isPolling) return;
           console.error("결제 상태 조회 실패:", error);
-          isPolling = false;
-          window.history.replaceState(null, "", window.location.pathname);
-          setIsConfirming(false);
-          alert("결제 상태를 확인하는 중 오류가 발생했습니다.");
+          finish("결제 상태를 확인하는 중 오류가 발생했습니다.");
         }
       };
@@
       // 폴링 중단
       return () => {
         isPolling = false;
+        if (timer) clearTimeout(timer);
       };
     }
📝 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 (orderId) {
let isPolling = true;
const pollStatus = async () => {
try {
await confirmPurchase(paymentKey, orderId, Number(amount));
window.history.replaceState(null, "", window.location.pathname);
setIsConfirming(false);
setTimeout(() => {
const response = await checkPaymentStatus(orderId);
// API 타입이 지정되지 않은 경우를 대비한 타입 단언
const data = response as unknown as {
status?: string;
result?: { status?: string };
};
const status = data.status || data.result?.status;
if (status === "COMPLETED") {
isPolling = false;
window.history.replaceState(null, "", window.location.pathname);
setIsConfirming(false);
alert("크레딧 충전이 완료되었습니다!");
window.location.reload();
}, 100);
} catch (error) {
console.error("결제 승인 실패:", error);
setIsConfirming(false);
alert("결제 승인에 실패했습니다. 다시 시도해 주세요.");
// 실패 시에도 쿼리 파라미터를 날려 중복 요청 방지
} else if (status === "FAILED") {
isPolling = false;
window.history.replaceState(null, "", window.location.pathname);
setIsConfirming(false);
alert("결제에 실패했거나 취소되었습니다.");
} else {
// PENDING, PROCESSING, UNKNOWN 상태일 경우 계속 폴링
if (isPolling) {
setTimeout(pollStatus, 2000); // 2초 주기로 폴링
}
}
} catch (error: unknown) {
console.error("결제 상태 조회 실패:", error);
isPolling = false;
window.history.replaceState(null, "", window.location.pathname);
setIsConfirming(false);
alert("결제 상태를 확인하는 중 오류가 발생했습니다.");
}
};
processConfirm();
// 동기적 setState 호출 경고(Cascading renders)를 방지하기 위해 Promise로 감싸서 실행
Promise.resolve().then(() => {
setIsConfirming(true);
pollStatus();
});
// 폴링 중단
return () => {
isPolling = false;
};
}
}, [searchParams]);
if (orderId) {
let isPolling = true;
let timer: ReturnType<typeof setTimeout> | undefined;
let attempts = 0;
const MAX_ATTEMPTS = 30; // ~60s
const finish = (message: string, reload = false) => {
isPolling = false;
window.history.replaceState(null, "", window.location.pathname);
setIsConfirming(false);
alert(message);
if (reload) window.location.reload();
};
const pollStatus = async () => {
try {
const response = await checkPaymentStatus(orderId);
if (!isPolling) return;
// API 타입이 지정되지 않은 경우를 대비한 타입 단언
const data = response as unknown as {
status?: string;
result?: { status?: string };
};
const status = data.status || data.result?.status;
if (status === "COMPLETED") {
finish("크레딧 충전이 완료되었습니다!", true);
} else if (status === "FAILED") {
finish("결제에 실패했거나 취소되었습니다.");
} else if (++attempts >= MAX_ATTEMPTS) {
finish("결제 결과 확인이 지연되고 있습니다. 잠시 후 다시 확인해 주세요.");
} else {
// PENDING, PROCESSING, UNKNOWN 상태일 경우 계속 폴링
timer = setTimeout(pollStatus, 2000); // 2초 주기로 폴링
}
} catch (error: unknown) {
if (!isPolling) return;
console.error("결제 상태 조회 실패:", error);
finish("결제 상태를 확인하는 중 오류가 발생했습니다.");
}
};
// 동기적 setState 호출 경고(Cascading renders)를 방지하기 위해 Promise로 감싸서 실행
Promise.resolve().then(() => {
setIsConfirming(true);
pollStatus();
});
// 폴링 중단
return () => {
isPolling = false;
if (timer) clearTimeout(timer);
};
}
}, [searchParams]);
🤖 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 38 - 88, Update the polling flow
in the useEffect around pollStatus to track and clear the active timeout during
cleanup, and re-check the cancellation flag after each awaited
checkPaymentStatus call before performing state updates, alerts, history
changes, or reloads. Add a finite polling limit for PENDING, PROCESSING, and
unknown statuses; when the limit is reached, stop polling, remove the URL order
state, clear confirmation, and report that payment status could not be
confirmed. Ensure all terminal and error paths clear any scheduled timeout.

setter: Dispatch<SetStateAction<string>>,
) => {
setter(value);
setEmailTouched(false);

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

Make touched resets field-specific.

handleInputChange is shared by email, password, and password-confirm inputs, but Line 323 always clears emailTouched. Typing in password or confirmation therefore hides the email error. handlePasswordChange also resets only passwordConfirmTouched, so passwordTouched remains true while the user corrects the password.

Pass the corresponding touched setter into the shared helper, or reset each field’s touched state in its dedicated handler.

Also applies to: 333-333

🤖 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/login/EmailLoginScreen.tsx` at line 323, Make
touched-state resets field-specific in handleInputChange and
handlePasswordChange: ensure email input changes reset only emailTouched,
password input changes reset passwordTouched, and confirmation input changes
reset passwordConfirmTouched. Pass the appropriate setter through the shared
helper or perform the resets in each dedicated handler, without clearing
unrelated fields.

Comment on lines +774 to +780
error={
signupErrorMessage
? signupErrorMessage
: hasSignupEmailValidationError
? "이메일 형식이 맞지 않습니다."
: undefined
}

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

Wire blur handling for both signup fields.

The signup email error is gated by emailTouched, but the signup email input has no onBlur, so format errors appear only after submit. The password-confirm onBlur is commented out, so mismatch errors likewise remain hidden until submit.

Add:

+ onBlur={() => setEmailTouched(true)}

and restore:

- // onBlur={() => setPasswordConfirmTouched(true)}
+ onBlur={() => setPasswordConfirmTouched(true)}

Also applies to: 824-824

🤖 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/login/EmailLoginScreen.tsx` around lines 774 - 780,
Wire blur handling for both signup fields: add an onBlur handler to the signup
email input that marks the email as touched, and restore the commented
password-confirmation onBlur handler so it marks that field as touched. Update
the signup form inputs near the signupErrorMessage logic and preserve the
existing validation/error conditions.

Comment on lines +124 to +129
export async function checkPaymentStatus(orderId: string) {
const response = await fetch(`/api/payments/orders/${orderId}`, {
method: "GET",
});
checkResponse(response, "결제 승인에 실패했습니다.");
if (!response.ok) throw new Error("Failed to fetch payment status");
return response.json();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Both callers use as unknown as casts because the credit API layer doesn't declare its response shapes. Fix the contracts at the source instead of asserting at each call site.

  • jobdri/src/lib/api/credit.ts#L124-L129: give checkPaymentStatus an explicit return type and unwrap result; add checkoutPage to PreparePaymentResult.
  • jobdri/src/components/common/cards/CreditCard.tsx#L57-L72: drop the cast and the unreachable data.result?.checkoutPage fallback, reading response.checkoutPage.
  • jobdri/src/app/credit/page.tsx#L45-L49: drop the cast and read response.status directly.
📍 Affects 3 files
  • jobdri/src/lib/api/credit.ts#L124-L129 (this comment)
  • jobdri/src/components/common/cards/CreditCard.tsx#L57-L72
  • jobdri/src/app/credit/page.tsx#L45-L49
🤖 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/credit.ts` around lines 124 - 129, Update
jobdri/src/lib/api/credit.ts lines 124-129 by giving checkPaymentStatus an
explicit response type and returning the unwrapped result; extend
PreparePaymentResult with checkoutPage. In
jobdri/src/components/common/cards/CreditCard.tsx lines 57-72, remove the cast
and fallback, and read response.checkoutPage directly. In
jobdri/src/app/credit/page.tsx lines 45-49, remove the cast and read
response.status directly.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

checkPaymentStatus breaks the conventions every other call in this file follows: missing API_BASE_URL, auth headers, and checkResponse.

The relative path resolves against the Next.js app origin instead of the API host, and without getAuthHeaders() a user-scoped order lookup will 401. Bypassing checkResponse also skips handleUnauthorized() on 401, so the polling loop in jobdri/src/app/credit/page.tsx (Lines 68-74) surfaces a generic Korean alert instead of redirecting to login. Also unwrap result and type the return so callers don't need as unknown as assertions.

🔧 Proposed fix
-export async function checkPaymentStatus(orderId: string) {
-  const response = await fetch(`/api/payments/orders/${orderId}`, {
-    method: "GET",
-  });
-  if (!response.ok) throw new Error("Failed to fetch payment status");
-  return response.json();
-}
+export type PaymentStatus =
+  | "PENDING"
+  | "PROCESSING"
+  | "COMPLETED"
+  | "FAILED";
+
+export async function checkPaymentStatus(
+  orderId: string,
+): Promise<{ status: PaymentStatus }> {
+  const response = await fetch(
+    `${API_BASE_URL}/api/payments/orders/${encodeURIComponent(orderId)}`,
+    {
+      method: "GET",
+      headers: getAuthHeaders(),
+    },
+  );
+  checkResponse(response, "결제 상태 조회에 실패했습니다.");
+  const { result }: ApiResponse<{ status: PaymentStatus }> =
+    await response.json();
+  return result;
+}
📝 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 async function checkPaymentStatus(orderId: string) {
const response = await fetch(`/api/payments/orders/${orderId}`, {
method: "GET",
});
checkResponse(response, "결제 승인에 실패했습니다.");
if (!response.ok) throw new Error("Failed to fetch payment status");
return response.json();
export type PaymentStatus =
| "PENDING"
| "PROCESSING"
| "COMPLETED"
| "FAILED";
export async function checkPaymentStatus(
orderId: string,
): Promise<{ status: PaymentStatus }> {
const response = await fetch(
`${API_BASE_URL}/api/payments/orders/${encodeURIComponent(orderId)}`,
{
method: "GET",
headers: getAuthHeaders(),
},
);
checkResponse(response, "결제 상태 조회에 실패했습니다.");
const { result }: ApiResponse<{ status: PaymentStatus }> =
await response.json();
return result;
}
🤖 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/credit.ts` around lines 124 - 129, Update
checkPaymentStatus to follow the file’s established API request pattern: build
the URL with API_BASE_URL, include getAuthHeaders(), and pass the response
through checkResponse so unauthorized responses invoke handleUnauthorized().
Unwrap the response’s result field and declare the returned payment-status type,
allowing callers to use it without casts.

@yiyoonseo
yiyoonseo merged commit 439c95d into develop Jul 30, 2026
1 check passed
@yiyoonseo
yiyoonseo deleted the feature/qa-v1.0 branch July 30, 2026 04:12
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.

1 participant