Skip to content

Fix: #78 리다이렉트 로직 수정#79

Open
yiyoonseo wants to merge 1 commit into
developfrom
feature/#78-auth_barrier-fix
Open

Fix: #78 리다이렉트 로직 수정#79
yiyoonseo wants to merge 1 commit into
developfrom
feature/#78-auth_barrier-fix

Conversation

@yiyoonseo
Copy link
Copy Markdown
Contributor

@yiyoonseo yiyoonseo commented May 30, 2026

🔗 관련 이슈

#78

📝 개요

  • 쿠키 만료 시 리다이렉트 로직 수정

⌨️ 작업 상세 내용

기존에는 401에러 인터셉터가 없어서 추가하였습니다.

  • src/lib/api/client.ts 생성 — 공통 API 클라이언트 (401 인터셉터, parseApiResponse, parseApiResponseAllowNull)
  • 401 응답 시 clearAuthTokens() 호출 후 /login?redirect=<현재경로> 로 리다이렉트
  • 기존 API 파일(mockApplies, jobPostings, result, questions, credit)에서 중복 코드 제거 후 공통 클라이언트로 교체

💡 코드 설명 및 참고사항

  • 기존에는 각 API 파일마다 ApiResponse 인터페이스와 parseApiResponse 함수가 중복 정의되어 있었습니다. client.ts로 통합하여 이후 공통 처리가 필요할 때 한 곳만 수정하면 됩니다.
  • 402 예외 처리: result.ts의 크레딧 부족(402) 에러는 해당 파일 특화 로직이므로 client.ts를 래핑하는 방식으로 로컬에 유지했습니다.
  • redirect 파라미터: 로그인 후 원래 페이지로 돌아올 수 있도록 현재 경로를 redirect 쿼리파라미터로 전달합니다. (로그인 화면에서 redirect 처리는 별도 작업 필요)

📸 스크린샷 (UI 변경 시)

🔍 리뷰 요구사항 (Reviewers)

  • [ ]

⚠️ 로컬 실행 시 유의사항

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of expired authentication—users are now automatically redirected to login while preserving their current page location.
  • Refactor

    • Consolidated API response parsing and error handling across the application for consistency.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 30, 2026

📝 Walkthrough

Walkthrough

This PR introduces a centralized API client module that standardizes response parsing and unauthorized (401) error handling across the application. It then refactors five existing API modules to remove duplicate logic and import shared utilities, eliminating code duplication in ApiResponse types, getAuthHeaders(), and response parsing functions.

Changes

API Response Handling Centralization

Layer / File(s) Summary
API Client Foundation
jobdri/src/lib/api/client.ts
Exports ApiResponse<T> interface, UnauthorizedError class, handleUnauthorized() to clear tokens and redirect on 401, parseApiResponse<T>() for strict parsing, and parseApiResponseAllowNull<T>() for optional responses. Re-exports API_BASE_URL and getAuthHeaders from auth module.
Credit API Module Refactoring
jobdri/src/lib/api/credit.ts
Imports centralized error handling from client module. Adds internal checkResponse() helper that validates HTTP status and triggers unauthorized handling. All five credit functions (fetchCreditBalance, fetchCreditTransactions, fetchCreditPlans, preparePurchase, confirmPurchase) use the shared helper before JSON parsing.
Job Postings, Mock Applies, Questions, and Result API Modules
jobdri/src/lib/api/jobPostings.ts, jobdri/src/lib/api/mockApplies.ts, jobdri/src/lib/api/questions.ts, jobdri/src/lib/api/result.ts
Each module removes local ApiResponse<T> types and response parsing implementations. All four modules import getAuthHeaders and parseApiResponse helpers from the shared client module. Exported function signatures and behavior remain unchanged.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • JobDri-Developer/FrontEnd#59: Introduces the lib/auth.ts token persistence infrastructure that this PR's handleUnauthorized() depends on for clearing stored auth tokens.
  • JobDri-Developer/FrontEnd#66: Adds the initial credit/payment API functions in credit.ts that this PR refactors to use centralized error handling.
  • JobDri-Developer/FrontEnd#62: Introduces the initial job-postings API client in jobPostings.ts that this PR refactors to remove duplicate response parsing logic.

Suggested reviewers

  • minnngo

Poem

🐰 A rabbit hops through scattered API calls,
Gathers lost response-helpers from the halls,
One client module, clean and bright,
No more duplication, all feels right!
Now auth-errors flow through one shared way,
The code's more tidy every day! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically addresses the main objective: implementing redirect logic when authentication expires, directly referencing issue #78.
Linked Issues check ✅ Passed All code changes successfully implement the core requirement from issue #78: redirecting to login page on auth expiration (401 responses), with additional redirect parameter to restore user context.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the authentication redirect logic. While the PR includes refactoring of duplicate code across API modules, this consolidation is necessary infrastructure to support the new centralized 401 handling.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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/#78-auth_barrier-fix

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
jobdri/src/lib/api/questions.ts (1)

131-135: 💤 Low value

Non-null assertion is inconsistent with other usages in this file.

Lines 50 and 99 use defensive handling (result ?? [], result?.questions ?? []), but this line uses a non-null assertion. If parseApiResponse can legitimately return null, this could cause a runtime error. If it guarantees non-null for valid responses, the assertion is unnecessary.

Consider either removing the assertion if the base function guarantees non-null, or adding explicit handling:

♻️ Suggested fix
   const result = await parseApiResponse<SaveApplyResult>(
     response,
     "답변 제출에 실패했습니다.",
   );
-  return result!;
+  if (!result) {
+    throw new Error("답변 제출에 실패했습니다.");
+  }
+  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/questions.ts` around lines 131 - 135, The code uses a
non-null assertion on result from parseApiResponse<SaveApplyResult> which is
inconsistent with other defensive patterns in this file; update the save/apply
handling in questions.ts by removing the `!` and explicitly handle a
null/undefined result from parseApiResponse (for example, return a safe default,
throw a descriptive error, or map to `{ success: false }`), referencing the
result variable and the parseApiResponse<SaveApplyResult> call so the fix is
applied where the response is processed.
jobdri/src/lib/api/credit.ts (1)

22-34: ⚡ Quick win

Consider using parseApiResponse for consistency with other modules.

The other refactored modules (jobPostings.ts, mockApplies.ts, questions.ts) all delegate to parseApiResponse from the shared client, which handles 401 checks, JSON parsing, and error handling uniformly. This file introduces a separate checkResponse + manual JSON parsing pattern that diverges from the established convention.

Unifying to parseApiResponse would reduce maintenance burden and ensure consistent error handling behavior across all API modules.

♻️ Suggested refactor to align with other modules
-import { getAuthHeaders, handleUnauthorized } from "`@/lib/api/client`";
+import { getAuthHeaders, parseApiResponse } from "`@/lib/api/client`";
 
 const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL;
 
 // ... types remain the same ...
 
-function checkResponse(response: Response, fallbackMessage: string): void {
-  if (response.status === 401) handleUnauthorized();
-  if (!response.ok) throw new Error(fallbackMessage);
-}
-
 export async function fetchCreditBalance(): Promise<number> {
   const response = await fetch(`${BASE_URL}/api/payments/credits/me`, {
     headers: getAuthHeaders(),
   });
-  checkResponse(response, "크레딧 잔액 조회에 실패했습니다.");
-  const { result }: ApiResponse<{ creditBalance: number }> =
-    await response.json();
-  return result.creditBalance;
+  const result = await parseApiResponse<{ creditBalance: number }>(
+    response,
+    "크레딧 잔액 조회에 실패했습니다.",
+  );
+  return result.creditBalance;
 }

Apply the same pattern to fetchCreditTransactions, fetchCreditPlans, preparePurchase, and confirmPurchase.

🤖 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 22 - 34, The current checkResponse
+ manual JSON parsing in fetchCreditBalance (and sibling functions
fetchCreditTransactions, fetchCreditPlans, preparePurchase, confirmPurchase)
diverges from other modules; replace those patterns to call parseApiResponse
from the shared client instead: remove checkResponse usage and direct
response.json() calls in the listed functions and delegate to
parseApiResponse<ResponseType>(response, "fallback message") so 401 handling,
parsing, and error messages are consistent; ensure you import parseApiResponse
and keep the same return types (e.g., ApiResponse<{ creditBalance: number }>)
and use the returned .result field as other modules do.
🤖 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/lib/api/client.ts`:
- Around line 81-82: The conditional can throw if data is null (JSON.parse can
return null); update the guard to use optional chaining — change the check from
if (!response.ok || !data.isSuccess) to if (!response.ok || !data?.isSuccess)
and keep using data?.error || data?.message || fallbackMessage when throwing so
the code safely handles data === null; locate this in client.ts around the
response/data handling (same area as parseApiResponse logic) and apply the
optional chaining fix.
- Around line 48-49: The current validation wrongly treats legitimate falsy
results as failures and can throw when data is null; update the check in the
response handling so it uses safe null checks and explicit undefined/null
checks: replace the condition `if (!response.ok || !data.isSuccess ||
!data.result)` with something like `if (!response.ok || data == null ||
data.isSuccess === false || data.result == null)` (use optional chaining where
appropriate, e.g., `data == null` and `data.isSuccess === false`, and check
`data.result == null` instead of `!data.result`) so that `response`, `data`,
`isSuccess`, and `result` are validated correctly in the function that parses
`response.json()` in client.ts.
- Around line 21-30: handleUnauthorized currently builds redirectPath using
window.location.pathname which drops ?query and `#hash`; change how redirectPath
is composed in the handleUnauthorized function to include window.location.search
and window.location.hash (e.g. const redirectPath =
encodeURIComponent(window.location.pathname + window.location.search +
window.location.hash)) before calling
window.location.replace(`${ROUTES.LOGIN}?redirect=${redirectPath}`) so the full
original URL (path, query, and hash) is preserved when redirecting to
ROUTES.LOGIN.

---

Nitpick comments:
In `@jobdri/src/lib/api/credit.ts`:
- Around line 22-34: The current checkResponse + manual JSON parsing in
fetchCreditBalance (and sibling functions fetchCreditTransactions,
fetchCreditPlans, preparePurchase, confirmPurchase) diverges from other modules;
replace those patterns to call parseApiResponse from the shared client instead:
remove checkResponse usage and direct response.json() calls in the listed
functions and delegate to parseApiResponse<ResponseType>(response, "fallback
message") so 401 handling, parsing, and error messages are consistent; ensure
you import parseApiResponse and keep the same return types (e.g., ApiResponse<{
creditBalance: number }>) and use the returned .result field as other modules
do.

In `@jobdri/src/lib/api/questions.ts`:
- Around line 131-135: The code uses a non-null assertion on result from
parseApiResponse<SaveApplyResult> which is inconsistent with other defensive
patterns in this file; update the save/apply handling in questions.ts by
removing the `!` and explicitly handle a null/undefined result from
parseApiResponse (for example, return a safe default, throw a descriptive error,
or map to `{ success: false }`), referencing the result variable and the
parseApiResponse<SaveApplyResult> call so the fix is applied where the response
is processed.
🪄 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: 34dad631-610e-4012-b5f5-e485620fba82

📥 Commits

Reviewing files that changed from the base of the PR and between d4b18d1 and 35a33ce.

📒 Files selected for processing (6)
  • jobdri/src/lib/api/client.ts
  • jobdri/src/lib/api/credit.ts
  • jobdri/src/lib/api/jobPostings.ts
  • jobdri/src/lib/api/mockApplies.ts
  • jobdri/src/lib/api/questions.ts
  • jobdri/src/lib/api/result.ts

Comment on lines +21 to +30
export function handleUnauthorized(): never {
clearAuthTokens();

if (typeof window !== "undefined") {
const redirectPath = encodeURIComponent(window.location.pathname);
window.location.replace(`${ROUTES.LOGIN}?redirect=${redirectPath}`);
}

throw new UnauthorizedError();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve query string and hash in the redirect path.

window.location.pathname drops the current ?query and #hash. Since the goal is to return the user to the exact page after login, pages that rely on query params will lose state on redirect-back.

🔧 Proposed fix
   if (typeof window !== "undefined") {
-    const redirectPath = encodeURIComponent(window.location.pathname);
+    const redirectPath = encodeURIComponent(
+      window.location.pathname + window.location.search + window.location.hash,
+    );
     window.location.replace(`${ROUTES.LOGIN}?redirect=${redirectPath}`);
   }
📝 Committable suggestion

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

Suggested change
export function handleUnauthorized(): never {
clearAuthTokens();
if (typeof window !== "undefined") {
const redirectPath = encodeURIComponent(window.location.pathname);
window.location.replace(`${ROUTES.LOGIN}?redirect=${redirectPath}`);
}
throw new UnauthorizedError();
}
export function handleUnauthorized(): never {
clearAuthTokens();
if (typeof window !== "undefined") {
const redirectPath = encodeURIComponent(
window.location.pathname + window.location.search + window.location.hash,
);
window.location.replace(`${ROUTES.LOGIN}?redirect=${redirectPath}`);
}
throw new UnauthorizedError();
}
🤖 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/client.ts` around lines 21 - 30, handleUnauthorized
currently builds redirectPath using window.location.pathname which drops ?query
and `#hash`; change how redirectPath is composed in the handleUnauthorized
function to include window.location.search and window.location.hash (e.g. const
redirectPath = encodeURIComponent(window.location.pathname +
window.location.search + window.location.hash)) before calling
window.location.replace(`${ROUTES.LOGIN}?redirect=${redirectPath}`) so the full
original URL (path, query, and hash) is preserved when redirecting to
ROUTES.LOGIN.

Comment on lines +48 to +49
if (!response.ok || !data.isSuccess || !data.result) {
throw new Error(data?.error || data?.message || fallbackMessage);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

!data.result rejects valid falsy results, and data may be null here.

Two concerns on the validation check:

  1. !data.result treats legitimate falsy values (0, false, "") as failures and throws even when isSuccess is true. Use an explicit null/undefined check instead.
  2. response.json() can resolve to null (literal JSON null body), so data may be null; !data.isSuccess would then throw a TypeError. The optional chaining on line 49 (data?.error) suggests this guard was intended but is missing on line 48.
🔧 Proposed fix
-  if (!response.ok || !data.isSuccess || !data.result) {
+  if (!response.ok || !data?.isSuccess || data.result == null) {
     throw new Error(data?.error || data?.message || fallbackMessage);
   }
🤖 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/client.ts` around lines 48 - 49, The current validation
wrongly treats legitimate falsy results as failures and can throw when data is
null; update the check in the response handling so it uses safe null checks and
explicit undefined/null checks: replace the condition `if (!response.ok ||
!data.isSuccess || !data.result)` with something like `if (!response.ok || data
== null || data.isSuccess === false || data.result == null)` (use optional
chaining where appropriate, e.g., `data == null` and `data.isSuccess === false`,
and check `data.result == null` instead of `!data.result`) so that `response`,
`data`, `isSuccess`, and `result` are validated correctly in the function that
parses `response.json()` in client.ts.

Comment on lines +81 to +82
if (!response.ok || !data.isSuccess) {
throw new Error(data?.error || data?.message || fallbackMessage);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Same potential null dereference as parseApiResponse.

JSON.parse("null") returns null, so data can be null here while responseText is non-empty. !data.isSuccess on line 81 would throw a TypeError, despite line 82 using data?.. Apply the same optional-chaining guard.

🔧 Proposed fix
-  if (!response.ok || !data.isSuccess) {
+  if (!response.ok || !data?.isSuccess) {
     throw new Error(data?.error || data?.message || fallbackMessage);
   }
📝 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 (!response.ok || !data.isSuccess) {
throw new Error(data?.error || data?.message || fallbackMessage);
if (!response.ok || !data?.isSuccess) {
throw new Error(data?.error || data?.message || fallbackMessage);
🤖 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/client.ts` around lines 81 - 82, The conditional can throw
if data is null (JSON.parse can return null); update the guard to use optional
chaining — change the check from if (!response.ok || !data.isSuccess) to if
(!response.ok || !data?.isSuccess) and keep using data?.error || data?.message
|| fallbackMessage when throwing so the code safely handles data === null;
locate this in client.ts around the response/data handling (same area as
parseApiResponse logic) and apply the optional chaining fix.

Copy link
Copy Markdown
Collaborator

@minnngo minnngo left a comment

Choose a reason for hiding this comment

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

너무 고생 많았어ㅠㅠ!!

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.

[Fix] 로그인 페이지 리다이렉트

2 participants