Skip to content

[FIX] 홈 새로고침 시 로그인 이탈 수정 및 인증 가드 클라이언트 전환 - #174

Closed
ehye1 wants to merge 6 commits into
developfrom
feat/web/166-middleware-route-guard
Closed

[FIX] 홈 새로고침 시 로그인 이탈 수정 및 인증 가드 클라이언트 전환#174
ehye1 wants to merge 6 commits into
developfrom
feat/web/166-middleware-route-guard

Conversation

@ehye1

@ehye1 ehye1 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

ISSUE 🔗

close #166



What is this PR? 🔍

로그인 후 홈에서 새로고침하면 /login으로 튕기는 문제를 수정하고, 미들웨어 기반 라우트 가드를 클라이언트 AuthGuardProvider로 전환했습니다.

배경

  • 기존 구조: 서버 미들웨어(proxy.ts)가 쿠키를 읽어 인증 여부를 판단하고, 비인증 상태면 /login으로 리다이렉트하는 구조였습니다.
  • 발생 문제: 두 가지 원인이 맞물려 로그인 성공 후에도 홈에서 계속 /login으로 리다이렉트됐습니다.
    1. next.config.jsrewrites()가 백엔드 응답의 Set-Cookie 헤더 여러 개를 하나의 문자열로 합쳐버려 브라우저가 파싱하지 못해 refreshToken/sessionId 쿠키가 모두 유실됐습니다.
    2. 쿠키에 Path=/auth가 지정돼 있어 /api 경로 요청에는 쿠키가 실리지 않았습니다.
  • 해결 방향: Set-Cookie 유실은 Route Handler로 직접 프록시해 해결했습니다. 라우트 가드는 쿠키 기반 미들웨어 대신 클라이언트 AuthGuardProvider로 전환했습니다.

Set-Cookie 프록시 — app/api/api/v1/auth/token/route.ts, app/auth/reissue/route.ts

  • 변경 요약: 토큰 교환과 재발급 요청을 Next.js Route Handler로 직접 프록시하도록 추가했습니다.
  • 이유: rewrites()는 내부적으로 Node fetch를 사용하는데, Headers.get('set-cookie')가 여러 값을 콤마로 이어붙인 단일 문자열로 반환합니다. 이를 그대로 클라이언트에 내려보내면 브라우저가 쿠키 2개를 모두 파싱하지 못하고 유실합니다.
  • 구현 방식: backendResponse.headers.getSetCookie()로 각 Set-Cookie 값을 배열로 분리한 뒤, response.headers.append('set-cookie', cookie)로 하나씩 추가해 각각이 독립적인 헤더로 내려가도록 합니다.
for (const cookie of backendResponse.headers.getSetCookie()) {
  response.headers.append("set-cookie", cookie);
}

next.config.js / proxy.ts — 미들웨어 및 rewrite 재구성

  • 변경 요약: API 요청을 /api/:path*${apiBaseUrl}/:path*, 인증 경로를 /auth/:path*${apiBaseUrl}/api/v1/auth/:path* rewrite로 전환하고, 미들웨어의 쿠키 기반 인증 가드를 전부 제거했습니다.
  • 이유: 쿠키가 브라우저에 정상 저장되지 않는 상황에서 미들웨어가 항상 비인증으로 판단했습니다. 인증 상태 관리를 클라이언트 AuthGuardProvider로 일원화해 역할을 분리했습니다.
  • 경계 · 제약: /auth/reissue 요청이 미들웨어(next-intl)를 타면 로케일 리다이렉트가 발생할 수 있어 proxy.ts matcher에서 auth를 제외했습니다.

useAuthStore — 토큰 저장 전략

  • 변경 요약: accessToken은 Zustand 메모리에만 저장하고, onboardingCompleted만 localStorage에 persist하도록 유지했습니다.
  • 이유: 과거 버전에서 localStorage에 accessToken까지 저장하던 사용자가 남아 있을 수 있어, 기본 merge 대신 onboardingCompleted만 명시적으로 복원하는 커스텀 merge를 유지했습니다.

AuthGuardProvider — 가드 로직 정비

  • 변경 요약: isReady state를 제거했습니다.
  • 이유: isReady!!accessToken과 항상 동일한 값을 추적했습니다. 별도 state를 유지하면 reissue 완료 시점에 두 곳을 동시에 갱신해야 하는 불일치 위험이 있었습니다.
  • 구현 방식: 토큰이 없을 때 reissue를 호출하고, 실패하거나 토큰이 없으면 /login으로 리다이렉트합니다.

온보딩 / 로그인 가드

  • 변경 요약: AuthGuardProviderrequireOnboardingCompleted / redirectIfOnboardingCompleted prop을 추가하고 각 페이지에 적용했습니다.
  • 이유: OauthCallbackContainer가 로그인 응답의 user.onboardingCompleted를 store에 반영하지 않아 온보딩 완료 사용자도 온보딩으로 재진입했습니다. /login은 인증된 상태에서 직접 접근하면 폼이 먼저 보이고 이후에 리다이렉트되는 문제가 있었습니다.
  • 구현 방식: OauthCallbackContainer에서 setOnboardingCompleted(Boolean(data.user?.onboardingCompleted))를 호출하고, LoginContainer는 token이 없을 때만 렌더링하도록 변경했습니다.



To Reviewers

axios.ts의 reissue 싱글턴 패턴(중복 호출 방지)은 유지했습니다. AuthGuardProvider의 reissue 경로와 axios interceptor의 401 → reissue 경로가 동시에 실행되는 케이스를 한 번 봐주세요.

쿠키 Path=/auth와 로컬 Secure 쿠키 제약은 백엔드 협의가 필요한 영역으로 이번 PR 범위 밖입니다.



Screenshot 📷



Test Checklist ✔

  • 구글 로그인 → /home 진입 확인
  • 온보딩 완료 유저가 /onboarding 직접 접근 시 /home으로 리다이렉트 확인
  • 미인증 상태에서 /home 직접 접근 시 /login으로 리다이렉트 확인
  • pnpm build — CI에서 확인 예정
  • 토큰 만료 후 reissue → 재시도 정상 동작 — 배포 환경에서 확인 필요

ehye1 and others added 2 commits July 13, 2026 19:58
next.config.js의 rewrites()는 백엔드 응답의 Set-Cookie 헤더 여러 개를
Node fetch의 Headers.get('set-cookie')가 콤마로 합쳐 단일 문자열로 반환해
브라우저가 refreshToken/sessionId를 모두 파싱하지 못하고 유실시켰습니다.

토큰 교환(/api/v1/auth/token)과 재발급(/auth/reissue) 요청을 Route Handler로
직접 프록시하고 getSetCookie()로 각 Set-Cookie를 분리해 독립적인 헤더로
내려주도록 변경했습니다.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
[문제] 홈 새로고침 시 /login으로 이탈

accessToken이 Zustand 메모리에만 있어 새로고침 시 사라지고, 개발 환경(HTTP)에서는
Secure 쿠키 제약으로 reissue도 동작하지 않아 AuthGuardProvider가 /login으로
리다이렉트했습니다.

[변경]

proxy.ts: 쿠키 기반 인증 가드 전체 제거, next-intl 미들웨어만 남김.
  /auth 경로가 로케일 리다이렉트를 타지 않도록 matcher에서 제외했습니다.

axios.ts: reissueAccessToken을 export하고 baseURL: ""를 명시해
  재발급 요청이 정확히 /auth/reissue로 나가도록 수정했습니다.

useAuthStore: setAccessToken/clearAccessToken 호출 시 sessionStorage에도
  저장하도록 변경했습니다. sessionStorage는 새로고침엔 살아남고 탭을 닫으면
  삭제되어 localStorage보다 보안적으로 낫습니다. 스토어 초기값은 SSR/CSR
  hydration mismatch를 방지하기 위해 null로 유지하고 getStoredAccessToken을
  export했습니다.

AuthGuardProvider: isReady state를 제거(!!accessToken과 중복)하고
  sessionStorage 폴백을 추가했습니다. 토큰이 없을 때 sessionStorage를 먼저
  확인해 복원하고, 없을 때만 reissue를 호출합니다.

OauthCallbackContainer: 로그인 응답의 user.onboardingCompleted를 store에
  반영했습니다.

OnboardingFunnelContainer: 온보딩 완료 시 setOnboardingCompleted(true) 호출
  추가했습니다.

onboarding/page.tsx, (main)/layout.tsx: AuthGuardProvider에 온보딩 완료 여부
  기반 가드 prop을 적용했습니다.

LoginContainer: token이 없을 때만 렌더링하도록 변경했습니다.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
timo Ready Ready Preview, Comment Jul 13, 2026 11:31am

Request Review

@github-actions
github-actions Bot requested review from kimminna and yumin-kim2 July 13, 2026 11:02
@github-actions github-actions Bot added ⏰ Timo-web Timo 웹 서비스 🐛 Bug 기능이 정상적으로 작동하지 않는 문제 수정 ♥️ 혜원 혜원양 labels Jul 13, 2026
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ehye1, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 32 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b56c7735-bf49-4247-bb86-4a16e6d40649

📥 Commits

Reviewing files that changed from the base of the PR and between e52b4e4 and 9bf7140.

📒 Files selected for processing (5)
  • apps/timo-web/app/[locale]/login/_containers/LoginContainer.tsx
  • apps/timo-web/global.d.ts
  • apps/timo-web/next.config.js
  • apps/timo-web/providers/auth/AuthGuardProvider.tsx
  • apps/timo-web/stores/auth/useAuthStore.ts

Walkthrough

API 요청을 상대 경로 프록시로 전환하고 인증 쿠키 전달 라우트를 추가했습니다. 인증 스토어에 세션 토큰과 온보딩 상태를 저장하며, 인증 가드가 토큰 복원·재발급과 온보딩별 라우팅을 처리하도록 변경했습니다.

Changes

인증 및 라우팅 흐름

Layer / File(s) Summary
인증 상태 저장소 확장
apps/timo-web/stores/auth/useAuthStore.ts
onboardingCompleted를 persist하고 access token은 sessionStorage에 저장·복원하도록 인증 상태 구조를 확장했습니다.
인증 API 프록시 연결
apps/timo-web/api/client/axios.ts, apps/timo-web/app/api/api/v1/auth/token/route.ts, apps/timo-web/app/auth/reissue/route.ts, apps/timo-web/next.config.js, apps/timo-web/proxy.ts
Axios와 rewrite 경로를 상대 경로 기반으로 변경하고, 토큰·재발급 응답의 다중 Set-Cookie를 개별 헤더로 전달합니다.
인증 가드와 온보딩 분기
apps/timo-web/providers/auth/AuthGuardProvider.tsx, apps/timo-web/app/[locale]/(main)/layout.tsx
저장 토큰 복원 또는 토큰 재발급 후 인증 실패, 온보딩 미완료, 온보딩 완료 상태에 따라 라우팅합니다.
로그인·OAuth·온보딩 상태 반영
apps/timo-web/app/[locale]/login/_containers/LoginContainer.tsx, apps/timo-web/app/[locale]/oauth/callback/_containers/OauthCallbackContainer.tsx, apps/timo-web/app/[locale]/onboarding/_containers/OnboardingFunnelContainer.tsx, apps/timo-web/app/[locale]/onboarding/page.tsx
인증된 사용자의 로그인 화면 접근을 차단하고 OAuth 및 온보딩 성공 시 완료 상태를 갱신합니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant AuthGuardProvider
  participant AuthStore
  participant reissueAccessToken
  participant Router
  Browser->>AuthGuardProvider: 보호된 라우트 접근
  AuthGuardProvider->>AuthStore: sessionStorage 토큰 조회
  AuthGuardProvider->>reissueAccessToken: 저장 토큰이 없으면 재발급 요청
  reissueAccessToken-->>AuthGuardProvider: accessToken 반환
  AuthGuardProvider->>AuthStore: 인증 상태 설정
  AuthGuardProvider->>Router: 온보딩 또는 홈으로 이동
Loading

Possibly related PRs

Suggested labels: ✨ Feature, ♦️ 민아

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning 핵심 요구인 proxy.ts의 쿠키 기반 1차 리다이렉트와 accessToken의 localStorage 유지가 구현되지 않았습니다. proxy.ts에 세션/리프레시 쿠키 기반 인증 리다이렉트를 복원하고, 저장소 전략이 localStorage 요구와 일치하는지 맞춰주세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed 인증 프록시, 스토어, 가드, 리다이렉트 변경만 포함되어 보이는 범위 밖 수정은 없습니다.
Title check ✅ Passed 홈 새로고침 시 로그인 이탈 수정과 서버 미들웨어에서 클라이언트 인증 가드로 전환한 핵심 변경을 잘 요약합니다.
Description check ✅ Passed 세트쿠키 프록시, rewrite 재구성, AuthGuardProvider 전환, 스토어/온보딩 처리 등 변경 내용과 목적이 일관되게 설명됩니다.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/web/166-middleware-route-guard

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.

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown

Timo Performance Report

Bundle Size — timo-web
라우트 크기 First Load JS
/[locale]/home 198.43 kB 🔴 407.11 kB
/[locale]/today 153.71 kB 🔴 362.38 kB
/[locale]/focus 150.76 kB 🔴 359.44 kB
/[locale]/settings/account 0 B 🟡 208.68 kB
/[locale]/settings 160.77 kB 🔴 369.45 kB
/[locale]/statistics 149.23 kB 🔴 357.91 kB
/[locale]/[...rest] 0 B 🟡 208.68 kB
/[locale]/login 121.81 kB 🟡 330.49 kB
/[locale]/oauth/callback 117.03 kB 🟡 325.70 kB
/[locale]/onboarding 229.06 kB 🔴 437.73 kB
/[locale] 0 B 🟡 208.68 kB
/api/api/v1/auth/token/route 0 B 🟡 208.68 kB
/auth/reissue/route 0 B 🟡 208.68 kB

공유 번들: 208.68 kB
🟢 < 200kB  |  🟡 < 350kB  |  🔴 ≥ 350kB (First Load JS · gzip)

Lighthouse — timo-web
URL Perf A11y LCP CLS TBT
/en/home 🔴 59 🟢 95 🔴 15.0s 🟢 0.000 🟡 587ms
/en/today 🔴 58 🟢 95 🔴 14.6s 🟢 0.000 🔴 630ms
/en/focus 🔴 58 🟢 95 🔴 14.6s 🟢 0.000 🔴 631ms
/en/statistics 🔴 57 🟢 95 🔴 14.6s 🟢 0.000 🔴 640ms

Perf ≥ 70 / A11y ≥ 85 목표
LCP 🟢 < 2.5s 🟡 < 4s 🔴 ≥ 4s  |  CLS 🟢 < 0.1 🟡 < 0.25 🔴 ≥ 0.25  |  TBT 🟢 < 200ms 🟡 < 600ms 🔴 ≥ 600ms

Image Optimization — timo-web
파일 크기 포맷 상태
images/google-calendar.png 36.20 kB PNG ⚠️ 🟢
images/google-logo.png 26.79 kB PNG ⚠️ 🟢

총 2개 · 63.00 kB  |  🟢 < 200KB  |  🟡 < 500KB  |  🔴 ≥ 500KB
⚠️ 2개 파일 WebP/AVIF 변환 권장

측정 커밋: b093350

@repo/timo-design-system/assets/*.svg 임포트에 대한 타입 선언이 없어
tsc --noEmit 실행 시 TS2307 에러가 발생했습니다.
global.d.ts에 declare module "*.svg"를 추가했습니다.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 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 `@apps/timo-web/app/`[locale]/login/_containers/LoginContainer.tsx:
- Around line 21-32: Update LoginContainer’s authentication initialization so
getStoredAccessToken() is not called during render. Store the token in useState
with an SSR-safe initial value, then read sessionStorage via a mount-time
useEffect and update the state before applying the existing redirect logic;
ensure SSR and initial hydration render the same login UI while authenticated
users are redirected after mount.

In
`@apps/timo-web/app/`[locale]/oauth/callback/_containers/OauthCallbackContainer.tsx:
- Around line 18-20: OauthCallbackContainer에서 onboardingCompleted를 Zustand에 저장하지
말고, queryClient의 사용자 프로필 캐시를 단일 데이터 소스로 사용하도록 setOnboardingCompleted 호출과 관련 상태
의존성을 제거하세요. 라우팅 가드가 새로고침 후에도 값을 사용할 수 있도록 React Query 캐시에서 파생된 값을 사용하고, 필요한 경우
React Query persist 설정을 재사용하세요.

In `@apps/timo-web/app/api/api/v1/auth/token/route.ts`:
- Around line 10-33: Extract the duplicated backend proxy flow from POST in the
token route into a shared proxyToBackend helper, including request forwarding,
response body/status/content-type copying, and Set-Cookie propagation. Update
both the token and reissue route handlers to delegate to this helper with their
respective backend paths and forwarding options, preserving their current
behavior.
- Around line 10-33: Update the POST route handler’s backend fetch to use an
AbortController with a finite timeout, clearing the timeout after completion.
Wrap the fetch and response forwarding flow in try/catch, returning a 504
response when the request is aborted by the timeout and a 502 response for other
backend connection or proxy errors, while preserving successful response and
cookie forwarding.

In `@apps/timo-web/providers/auth/AuthGuardProvider.tsx`:
- Around line 21-27: Update the AuthGuardProvider session-restoration flow so
reissueAccessToken() also refreshes onboardingCompleted from the server via
getMyProfile, rather than restoring only accessToken. Ensure the login and
token-reissue paths persist the returned profile onboarding state before
routing, preventing completed users from being redirected to /onboarding.

In `@apps/timo-web/stores/auth/useAuthStore.ts`:
- Around line 22-62: Restrict the sessionStorage persistence in setAccessToken
to development-only execution, while continuing to update the in-memory
accessToken state in every environment. Apply the same environment guard to
sessionStorage removal in clearAccessToken so production does not interact with
client storage; leave the onboardingCompleted persistence and merge behavior
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0d52ab16-8143-4048-93c0-7c284519c703

📥 Commits

Reviewing files that changed from the base of the PR and between 219c3c2 and e52b4e4.

📒 Files selected for processing (12)
  • apps/timo-web/api/client/axios.ts
  • apps/timo-web/app/[locale]/(main)/layout.tsx
  • apps/timo-web/app/[locale]/login/_containers/LoginContainer.tsx
  • apps/timo-web/app/[locale]/oauth/callback/_containers/OauthCallbackContainer.tsx
  • apps/timo-web/app/[locale]/onboarding/_containers/OnboardingFunnelContainer.tsx
  • apps/timo-web/app/[locale]/onboarding/page.tsx
  • apps/timo-web/app/api/api/v1/auth/token/route.ts
  • apps/timo-web/app/auth/reissue/route.ts
  • apps/timo-web/next.config.js
  • apps/timo-web/providers/auth/AuthGuardProvider.tsx
  • apps/timo-web/proxy.ts
  • apps/timo-web/stores/auth/useAuthStore.ts

Comment thread apps/timo-web/app/[locale]/login/_containers/LoginContainer.tsx Outdated
Comment on lines +18 to +20
const setOnboardingCompleted = useAuthStore(
(state) => state.setOnboardingCompleted,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔵 Trivial | 🏗️ Heavy lift

onboardingCompleted를 Zustand에 저장하는 것은 경로 지침에 위배됩니다.

onboardingCompleted는 백엔드 사용자 프로필 응답에서 파생된 API 서버 데이터입니다. 경로 지침에 따르면 "API 서버 데이터를 store에 저장 금지 (React Query가 담당)"해야 합니다. 현재 OauthCallbackContainer에서 queryClient.setQueryData로 React Query에도 저장하므로, Zustand와 React Query 간 상태 불일치 가능성이 있습니다.

새로고침 시 라우팅 가드가 즉시 온보딩 상태를 알아야 하는 실용적 필요성은 이해합니다. @tanstack/react-query-persist-client로 React Query 캐시를 영속화하거나, onboardingCompleted 대신 React Query 캐시에서 파생된 값을 사용하는 방안을 검토해 보세요.

As per path instructions, "API 서버 데이터를 store에 저장 금지 (React Query가 담당)" 및 "파생 값은 store에 저장하지 않고 selector로 계산" 규칙이 apps/timo-web/stores/** 경로에 적용됩니다.

Also applies to: 41-41, 50-57

🤖 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
`@apps/timo-web/app/`[locale]/oauth/callback/_containers/OauthCallbackContainer.tsx
around lines 18 - 20, OauthCallbackContainer에서 onboardingCompleted를 Zustand에
저장하지 말고, queryClient의 사용자 프로필 캐시를 단일 데이터 소스로 사용하도록 setOnboardingCompleted 호출과 관련
상태 의존성을 제거하세요. 라우팅 가드가 새로고침 후에도 값을 사용할 수 있도록 React Query 캐시에서 파생된 값을 사용하고, 필요한
경우 React Query persist 설정을 재사용하세요.

Source: Path instructions

Comment on lines +10 to +33
export async function POST(request: NextRequest) {
const body = await request.text();

const backendResponse = await fetch(`${apiBaseUrl}/api/v1/auth/token`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body,
});

const responseBody = await backendResponse.text();
const response = new NextResponse(responseBody, {
status: backendResponse.status,
headers: {
"Content-Type":
backendResponse.headers.get("content-type") ?? "application/json",
},
});

for (const cookie of backendResponse.headers.getSetCookie()) {
response.headers.append("set-cookie", cookie);
}

return response;
}

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 | 🔵 Trivial | ⚡ Quick win

reissue/route.ts와 동일한 프록시 로직이 중복됩니다.

두 Route Handler가 fetch → body 복사 → getSetCookie() 반복 패턴을 공유합니다. 공유 헬퍼로 추출하면 에러 처리·타임아웃 추가도 한 곳에서 해결됩니다.

♻️ 공유 프록시 헬퍼 추출 제안
// lib/proxyBackend.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL;

export async function proxyToBackend(
  request: NextRequest,
  backendPath: string,
  options?: { forwardBody?: boolean; forwardCookie?: boolean },
) {
  const fetchOptions: RequestInit = {
    method: request.method,
    headers: {
      ...(options?.forwardBody
        ? { "Content-Type": "application/json" }
        : {}),
      ...(options?.forwardCookie
        ? { cookie: request.headers.get("cookie") ?? "" }
        : {}),
    },
  };

  if (options?.forwardBody) {
    fetchOptions.body = await request.text();
  }

  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 10000);

  try {
    const backendResponse = await fetch(
      `${API_BASE_URL}${backendPath}`,
      { ...fetchOptions, signal: controller.signal },
    );

    const responseBody = await backendResponse.text();
    const response = new NextResponse(responseBody, {
      status: backendResponse.status,
      headers: {
        "Content-Type":
          backendResponse.headers.get("content-type") ?? "application/json",
      },
    });

    for (const cookie of backendResponse.headers.getSetCookie()) {
      response.headers.append("set-cookie", cookie);
    }

    return response;
  } catch (error) {
    /* 에러 처리 */
  } finally {
    clearTimeout(timeout);
  }
}

이 헬퍼를 두 Route Handler에서 사용:

// token/route.ts
export const POST = (request: NextRequest) =>
  proxyToBackend(request, "/api/v1/auth/token", { forwardBody: true });

// reissue/route.ts
export const POST = (request: NextRequest) =>
  proxyToBackend(request, "/api/v1/auth/reissue", { forwardCookie: true });
🤖 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 `@apps/timo-web/app/api/api/v1/auth/token/route.ts` around lines 10 - 33,
Extract the duplicated backend proxy flow from POST in the token route into a
shared proxyToBackend helper, including request forwarding, response
body/status/content-type copying, and Set-Cookie propagation. Update both the
token and reissue route handlers to delegate to this helper with their
respective backend paths and forwarding options, preserving their current
behavior.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target route file and nearby context
git ls-files 'apps/timo-web/app/api/api/v1/auth/token/route.ts'
wc -l apps/timo-web/app/api/api/v1/auth/token/route.ts
cat -n apps/timo-web/app/api/api/v1/auth/token/route.ts

# Look for similar route handlers and any shared timeout/error handling patterns
rg -n "AbortController|setTimeout\\(|getSetCookie\\(|new NextResponse\\(|fetch\\(" apps/timo-web/app/api -S

Repository: Team-Timo/Timo-client

Length of output: 1939


백엔드 프록시 호출에 타임아웃과 예외 처리를 추가하세요

백엔드가 지연되거나 연결 실패하면 이 Route Handler가 오래 대기하거나 예외로 끝날 수 있습니다. AbortController로 제한 시간을 두고 try/catch에서 502/504로 응답을 정리하세요.
Next.js Route Handler의 에러 처리: https://nextjs.org/docs/app/building-your-application/routing/route-handlers

🤖 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 `@apps/timo-web/app/api/api/v1/auth/token/route.ts` around lines 10 - 33,
Update the POST route handler’s backend fetch to use an AbortController with a
finite timeout, clearing the timeout after completion. Wrap the fetch and
response forwarding flow in try/catch, returning a 504 response when the request
is aborted by the timeout and a 502 response for other backend connection or
proxy errors, while preserving successful response and cookie forwarding.

Comment on lines 21 to 27
const accessToken = useAuthStore((state) => state.accessToken);
const setAccessToken = useAuthStore((state) => state.setAccessToken);
const onboardingCompleted = useAuthStore(
(state) => state.onboardingCompleted,
);
const router = useRouter();
const { mutate } = useReissue();
const hasChecked = useRef(false);
const [isReady, setIsReady] = useState(!!accessToken);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "setOnboardingCompleted|onboardingCompleted" apps/timo-web -g '*.ts*' -B2 -A5

Repository: Team-Timo/Timo-client

Length of output: 11812


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== useAuthStore =="
sed -n '1,220p' apps/timo-web/stores/auth/useAuthStore.ts

echo
echo "== AuthGuardProvider =="
sed -n '1,220p' apps/timo-web/providers/auth/AuthGuardProvider.tsx

echo
echo "== search reissue / profile restore flows =="
rg -n "reissueAccessToken|refreshToken|getMyProfile|setOnboardingCompleted|onboardingCompleted|setAccessToken|getStoredAccessToken" apps/timo-web -g '*.ts*' -B2 -A5

Repository: Team-Timo/Timo-client

Length of output: 33871


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== AuthGuardProvider usages =="
rg -n "AuthGuardProvider|requireOnboardingCompleted|redirectIfOnboardingCompleted" apps/timo-web -g '*.ts*' -B2 -A4

echo
echo "== files around AuthGuardProvider call sites =="
for f in $(rg -l "requireOnboardingCompleted|redirectIfOnboardingCompleted" apps/timo-web -g '*.ts*'); do
  echo "--- $f ---"
  sed -n '1,220p' "$f"
  echo
done

Repository: Team-Timo/Timo-client

Length of output: 10065


세션 복원 시 onboardingCompleted도 다시 채워주세요. 상태 분리는 깔끔한데, reissueAccessToken()은 accessToken만 복원해서 onboardingCompleted가 비어 있으면 이미 온보딩을 끝낸 사용자도 /onboarding으로 잘못 보낼 수 있습니다. 로그인/재발급 시 getMyProfile로 서버 값을 다시 반영하는 흐름이 필요해요. 참고: zustand persist middleware

🤖 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 `@apps/timo-web/providers/auth/AuthGuardProvider.tsx` around lines 21 - 27,
Update the AuthGuardProvider session-restoration flow so reissueAccessToken()
also refreshes onboardingCompleted from the server via getMyProfile, rather than
restoring only accessToken. Ensure the login and token-reissue paths persist the
returned profile onboarding state before routing, preventing completed users
from being redirected to /onboarding.

Comment thread apps/timo-web/stores/auth/useAuthStore.ts
next typegen 등 빌드 외 명령도 next.config.js를 모듈 로드 시 실행하므로,
NEXT_PUBLIC_API_BASE_URL이 없는 CI type-check 환경에서 최상단 throw가 발생해
next typegen이 크래시나고 tsc가 SVG 모듈을 찾지 못하는 문제가 있었습니다.
env 체크를 rewrites() 호출 시점으로 미뤄 빌드/서버 시작에서만 검증합니다.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
reissue가 배포 환경에서만 동작하므로 sessionStorage 폴백은 불필요합니다.
setAccessToken/clearAccessToken을 원래 메모리 전용 방식으로 되돌립니다.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LoginContainer에 남아있던 getStoredAccessToken 참조를 제거하고
accessToken만으로 인증 여부를 판단하도록 단순화했습니다.

next.config.js의 rewrites()에서 NEXT_PUBLIC_API_BASE_URL이 없을 때
throw 대신 빈 배열을 반환하도록 변경했습니다.
next typegen이 rewrites()를 호출할 때 env가 없어도 크래시나지 않습니다.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@kimminna kimminna closed this Jul 13, 2026
@kimminna
kimminna deleted the feat/web/166-middleware-route-guard branch July 14, 2026 18:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⏰ Timo-web Timo 웹 서비스 ♥️ 혜원 혜원양 🐛 Bug 기능이 정상적으로 작동하지 않는 문제 수정

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] API 프록시·토큰 저장소 전환으로 미들웨어 라우트 가드 구현

2 participants