Skip to content

feat(web): 인증 refresh 및 라우트 보호 연결 - #76

Merged
meteorqz6 merged 20 commits into
developfrom
feature/68-auth-route-refresh
Jun 24, 2026
Merged

feat(web): 인증 refresh 및 라우트 보호 연결#76
meteorqz6 merged 20 commits into
developfrom
feature/68-auth-route-refresh

Conversation

@meteorqz6

@meteorqz6 meteorqz6 commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

작업 내용

  • access token을 AuthProvider Context에서 메모리 상태로 관리하도록 변경
  • apiClient에 401 refresh/retry 흐름과 unauthorized redirect hook 연결
  • login/signup 성공 시 AuthProvider session 갱신으로 전환
  • 게스트 라우트에서 로그인 사용자는 dashboard 또는 organization/new로 이동
  • Pretendard CDN 로딩을 next/font/local 기반으로 변경

확인

  • pnpm --filter @fragment/web typecheck
  • pnpm --filter @fragment/web lint
  • 브라우저 확인: /login 화면 정지 없음
  • 브라우저 확인: 회원가입 후 조직 없으면 /organization/new 이동
  • 브라우저 확인: access token 메모리 소실 후 refresh cookie로 /auth/refresh, /auth/me 복구
  • 브라우저 확인: 로그아웃 후 보호 라우트 접근 시 /login 이동

참고

Closes #68

Summary by CodeRabbit

  • New Features

    • 로그인 상태를 더 안정적으로 관리하는 인증 흐름이 추가되었습니다.
    • 비로그인 전용 화면에서는 인증된 사용자를 자동으로 적절한 화면으로 안내합니다.
    • 앱 전반에 새 폰트가 적용되었습니다.
  • Bug Fixes

    • 세션 복구 및 로그아웃 후 화면 상태가 더 일관되게 정리됩니다.
    • 인증이 필요한 요청의 재시도 및 오류 처리 방식이 개선되었습니다.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@meteorqz6, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 58 minutes and 7 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

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 credits.

🚦 How do rate 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 see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e32392d5-e211-4d68-ac42-e12259a38fae

📥 Commits

Reviewing files that changed from the base of the PR and between 8fdbb7f and 8a09261.

⛔ Files ignored due to path filters (1)
  • apps/web/src/app/fonts/PretendardVariable.woff2 is excluded by !**/*.woff2
📒 Files selected for processing (5)
  • apps/web/src/app/layout.tsx
  • apps/web/src/features/auth/components/auth-provider.tsx
  • apps/web/src/features/auth/components/require-guest-auth.tsx
  • apps/web/src/lib/api-client.ts
  • apps/web/tsconfig.json

Walkthrough

모듈 스코프 액세스 토큰 유틸(access-token.ts)을 삭제하고 AuthProvider/useAuth React 컨텍스트로 세션 상태를 이전한다. apiClient에 외부 주입형 인증 핸들러와 401 재시도 흐름을 추가하고, 게스트 전용 라우트 보호를 위해 RequireGuestAuth를 도입한다. 아울러 Pretendard 폰트 로딩을 CSS @font-face에서 next/font/local로 전환한다.

Changes

Auth Context 기반 세션 관리 리팩터

Layer / File(s) Summary
apiClient 인증 핸들러 타입 및 401 재시도 흐름
apps/web/src/lib/api-client.ts
ApiRequestOptionsauth/redirectOnUnauthorized 옵션을 추가하고, ApiClientAuthHandlers 타입과 authHandlers 변수를 도입한다. configureApiClientAuth/resetApiClientAuth로 핸들러를 주입/해제하며, 401 응답 시 refreshAccessToken() 후 재시도 → 실패 시 onUnauthorized() 분기를 구현한다.
AuthProvider 컨텍스트 및 useAuth 훅
apps/web/src/features/auth/components/auth-provider.tsx
AuthContextValue/AuthContext를 정의하고 AuthProvider에서 액세스 토큰 상태·ref, clearSession, redirectToLogin을 구성한다. refreshPromiseRef로 동시 리프레시를 단일 Promise로 제어하고, useEffect에서 configureApiClientAuth로 API 클라이언트 인증을 주입한다. configured 플래그로 렌더를 차단하며 useAuth는 Provider 외부 사용 시 에러를 던진다.
auth-api.ts 옵션 확장 및 쿼리 키 분리
apps/web/src/features/auth/api/auth-api.ts, apps/web/src/features/auth/queries/auth-query-keys.ts
signup/login/logout/refreshSession 호출에 auth: false를 추가하고 getMeredirectOnUnauthorized 옵션을 전달하도록 변경한다. authMeQueryKey를 독립 파일로 분리한다.
useAuthMeQuery 및 useLogoutMutation 컨텍스트 기반 전환
apps/web/src/features/auth/queries/auth-session.ts
useAuthMeQueryaccessToken 없을 때 refreshAccessToken()getMe를 호출하도록 교체하고, useLogoutMutationonSettledclearSession() + removeQueries로 변경한다. authMeQueryKeyauth-query-keys에서 재export한다.
RequireGuestAuth 게스트 라우트 보호 및 (auth) 레이아웃
apps/web/src/features/auth/components/require-guest-auth.tsx, apps/web/src/app/(auth)/layout.tsx
RequireGuestAuth가 마운트 시 refreshAccessToken()getMe(redirectOnUnauthorized: false) 순으로 인증 복원을 시도하고, 인증된 경우 /dashboard 또는 /organization/new로 리다이렉트한다. (auth) 레이아웃에서 children을 해당 컴포넌트로 래핑한다.
로그인/회원가입 폼 및 Providers 연결
apps/web/src/features/auth/components/login-form.tsx, apps/web/src/features/auth/components/signup-form.tsx, apps/web/src/app/providers.tsx
LoginFormSignupForm에서 setAccessToken() 직접 호출을 useAuth().setSession(session)으로 교체한다. ProvidersAuthProvider 래퍼를 추가한다.

Pretendard 폰트 로딩 방식 변경

Layer / File(s) Summary
next/font/local 기반 Pretendard 로딩 및 CSS 정리
apps/web/package.json, apps/web/src/app/globals.css, apps/web/src/app/layout.tsx
pretendard 패키지를 추가하고, globals.css에서 @font-face--font-pretendard 정의를 제거한다. layout.tsx에서 localFont로 Pretendard를 로드하고 bodyclassNamepretendard.variable을 주입한다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

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.
Description check ⚠️ Warning 핵심 변경과 이슈 링크는 있으나, 템플릿의 개요·변경 유형·영향 범위 등 필수 섹션이 대부분 누락되었습니다. 개요, 변경 사항, 변경 유형, 영향 범위, 테스트 방법, 체크리스트, 스크린샷/영상, 리뷰 포인트를 템플릿대로 보완하세요.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed 인증 refresh와 라우트 보호 연결을 요약해 변경사항의 핵심을 잘 반영합니다.
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.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/68-auth-route-refresh

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.

@meteorqz6 meteorqz6 self-assigned this Jun 24, 2026
Base automatically changed from feature/67-auth-session-me to develop June 24, 2026 12:23
…ute-refresh

# Conflicts:
#	apps/web/src/app/layout.tsx
#	apps/web/src/app/providers.tsx
#	apps/web/src/features/auth/components/login-form.tsx
#	apps/web/src/features/auth/components/signup-form.tsx
#	apps/web/src/features/auth/queries/auth-session.ts
#	apps/web/src/lib/api-client.ts

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

🤖 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/web/src/app/providers.tsx`:
- Around line 26-29: The app-wide wrapping in `AuthProvider` is causing the
initial render to return null and blank the whole page until configuration
completes. Update `AuthProvider` so its initialization does not block rendering
for all routes, or limit the provider to only the minimal subtree that actually
needs it. Use the `AuthProvider` component and its `if (!configured) return
null;` behavior as the main place to adjust so first paint/SSR is preserved.

In `@apps/web/src/features/auth/components/auth-provider.tsx`:
- Around line 52-75: The session-clearing flow in auth-provider.tsx allows an
in-flight refreshAccessToken() started via refreshPromiseRef to still resolve
and call setAccessToken after clearSession() has run. Update clearSession() and
refreshAccessToken() so that logout/401 cleanup invalidates any pending refresh
(for example by bumping a generation/abort flag or resetting the ref state) and
make the refresh resolution path verify it is still current before writing
session state. Use the existing clearSession, refreshAccessToken, and
refreshPromiseRef symbols to ensure stale refresh results cannot restore
accessToken after session cleanup.

In `@apps/web/src/features/auth/components/require-guest-auth.tsx`:
- Around line 19-42: The guest-auth guard in require-guest-auth.tsx currently
returns children immediately, which lets `/login` and `/signup` render before
refreshAccessToken() and getMe() finish. Update RequireGuestAuth so it tracks an
authentication-checking state in the useEffect flow around
redirectIfAuthenticated, and render null or a loading shell until the
restoration attempt completes. Only render children once the user is confirmed
unauthenticated; keep the existing router.replace logic and ignore flag behavior
in place.

In `@apps/web/src/lib/api-client.ts`:
- Around line 156-159: The refresh retry path in `sendRequest` is bypassing the
same network-error normalization as the initial request, so a failed retry can
leak a raw TypeError instead of ApiError(0). Update the retry logic inside
`api-client.ts` so the second `sendRequest()` after
`authHandlers.refreshAccessToken()` goes through the same try/catch or shared
error-handling path as the first request, keeping the error contract consistent
in the 401 refresh flow.
🪄 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: 7cd7c763-1dbc-4bc2-9f13-0a227a2fbc10

📥 Commits

Reviewing files that changed from the base of the PR and between b502445 and 8fdbb7f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (14)
  • apps/web/package.json
  • apps/web/src/app/(auth)/layout.tsx
  • apps/web/src/app/globals.css
  • apps/web/src/app/layout.tsx
  • apps/web/src/app/providers.tsx
  • apps/web/src/features/auth/api/auth-api.ts
  • apps/web/src/features/auth/components/auth-provider.tsx
  • apps/web/src/features/auth/components/login-form.tsx
  • apps/web/src/features/auth/components/require-guest-auth.tsx
  • apps/web/src/features/auth/components/signup-form.tsx
  • apps/web/src/features/auth/queries/auth-query-keys.ts
  • apps/web/src/features/auth/queries/auth-session.ts
  • apps/web/src/lib/access-token.ts
  • apps/web/src/lib/api-client.ts
💤 Files with no reviewable changes (2)
  • apps/web/src/lib/access-token.ts
  • apps/web/src/app/globals.css

Comment thread apps/web/src/app/providers.tsx
Comment thread apps/web/src/features/auth/components/auth-provider.tsx
Comment thread apps/web/src/features/auth/components/require-guest-auth.tsx
Comment thread apps/web/src/lib/api-client.ts Outdated
@meteorqz6 meteorqz6 mentioned this pull request Jun 24, 2026
9 tasks
Comment thread apps/web/src/app/layout.tsx Outdated
Comment on lines +8 to +10
const pretendard = localFont({
display: "swap",
src: "../../../../node_modules/pretendard/dist/web/variable/woff2/PretendardVariable.woff2",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[제안]
next build에서 Pretendard 폰트 파일을 resolve하지 못해 production build가 실패합니다. pretendardapps/web/package.json의 dependency라서 root node_modules를 가정하는 ../../../../node_modules/... 경로 대신 apps/web package 기준으로 resolve되는 경로를 사용하거나, 폰트 파일을 앱 내부 asset으로 두는 방식은 어떠신가요?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

폰트 파일을 앱 내부 asset으로 두는 방식으로 수정하겠습니다!

Comment on lines +20 to +40
useEffect(() => {
let ignore = false;

async function redirectIfAuthenticated() {
try {
await refreshAccessToken();
const authMe = await getMe({ redirectOnUnauthorized: false });

if (!ignore) {
router.replace(authMe.organization ? "/dashboard" : "/organization/new");
}
} catch {
// Guest routes should stay usable when no session can be restored.
} finally {
if (!ignore) {
setIsCheckingSession(false);
}
}
}

redirectIfAuthenticated();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[제안]
현재 guest/auth 라우트와 admin/onboarding 라우트의 redirect가 모두 client guard에서 처리되고 있습니다. 조직 유무처럼 /auth/me 조회가 필요한 분기는 현재 guard에 남기는 게 자연스럽지만, refresh token cookie 존재 여부만으로 판단 가능한 1차 접근 제어는 Next middleware에서 처리하는 것도 검토해볼 수 있을 것 같습니다.

예를 들어 미인증 사용자의 admin/onboarding 접근은 렌더링 전에 /login으로 보내고, 이미 세션 쿠키가 있는 사용자의 /login//signup 접근은 auth 화면 렌더링 전에 한 번 걸러낼 수 있습니다. 이렇게 나누면 초기 렌더링 전 redirect가 가능하고 불필요한 client-side refresh 시도를 줄일 수 있어 프로젝트 인증 컨벤션과도 더 잘 맞습니다.

다만 조직 유무에 따른 /dashboard vs /organization/new 분기는 서버 사용자 정보가 필요하므로 지금처럼 client guard에서 처리해도 괜찮아 보입니다.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

cookie 존재 여부만으로 판단 가능한 1차 redirect를 middleware로 분리하는 방향에는 동의합니다.

다만 이번 PR은 AuthProvider/useAuth 기반 세션 복원과 guest guard 연결을 범위로 잡고 있어서, middleware 도입은 admin/onboarding 보호까지 함께 정리하는 후속 작업으로 분리하겠습니다.

throw createInvalidJsonResponseError(response);
}

return parsedResponse as TResponse;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[질문]
성공 응답은 현재 JSON 존재 여부만 확인한 뒤 TResponse로 타입 단언하고 있는 것으로 보입니다. 에러 응답은 errorResponseSchema.safeParse()로 런타임 검증을 하고 있는데, 성공 응답은 shared schema 검증 없이 호출부의 제네릭 타입에만 의존하고 있어서요. 특히 auth 흐름에서는 accessToken, hasOrganization, organization 값이 토큰 저장과 라우팅에 바로 사용되다 보니 서버 응답 계약이 깨졌을 때 감지가 늦어질 수 있을 것 같습니다.

authSessionResponseSchema, authMeResponseSchema 같은 shared schema를 API wrapper에서 parse하거나 apiClient가 선택적으로 response schema를 받아 검증하는 방식도 가능해 보이는데, 현재처럼 성공 응답 검증을 생략한 이유가 있을까요?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

API 응답 검증 정책을 어디에 둘지 정한 후에 후속 반영하겠습니다!

선택지는 각 API 함수에서 검증하는 것과 apiClient가 optional schema를 받게 하는 방법이 있을 것 같습니다.
지금은 각 API 함수에서 검증하는 것이 더 나아보여요. 공통 apiClient를 키우는 것보다는 auth API wrapper에서만 먼저 shared schema로 parse하는 것이 안전할 것 같다고 생각했습니다.

@meteorqz6
meteorqz6 merged commit 73e059c into develop Jun 24, 2026
2 checks passed
@meteorqz6
meteorqz6 deleted the feature/68-auth-route-refresh branch June 24, 2026 17:25
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.

feat(web): 보호 라우트 및 토큰 재발급 처리

2 participants