[FEAT] 인증 세션·라우트 가드 개선 및 언어 동기화 추가 - #181
Conversation
…179) - accessToken을 zustand 인메모리에서 localStorage 기반으로 저장하도록 변경했습니다 - 로그인 사용자의 /login 접근과 온보딩 완료 사용자의 /onboarding 접근을 차단하는 가드를 추가했습니다 - 세션 초기화(reissue)를 루트 AuthProvider에서 1회만 수행하도록 통합해 하이드레이션 깜빡임과 중복 호출을 제거했습니다
- 홈 접근 시 유저 프로필을 조회해 서버에 저장된 언어와 현재 로케일이 다르면 자동으로 전환하도록 했습니다
- duration 변환 유틸 3개를 utils/duration/으로, date/시간 유틸 3개를 utils/date/로 이동했습니다 - 이동한 파일을 참조하던 import 경로를 전체 업데이트했습니다 - 더 이상 필요 없는 .gitkeep 플레이스홀더를 정리했습니다
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 48 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (9)
Walkthrough인증 상태를 localStorage와 auth store에 연동하고, 루트 인증 초기화 및 로그인·온보딩 라우트 가드를 추가했습니다. 홈에서는 프로필 언어에 맞춰 로케일을 동기화하며, 날짜·duration 관련 유틸리티를 도메인별 경로로 재구성했습니다. Changes인증 및 유틸리티 개편
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant RootLayout
participant AuthProvider
participant AuthStore
participant AuthGuardProvider
participant LoginPage
RootLayout->>AuthProvider: initialize authentication
AuthProvider->>AuthStore: restore token and set initialized
AuthProvider->>AuthGuardProvider: render initialized state
AuthGuardProvider->>LoginPage: redirect when token is absent
Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Timo Performance ReportBundle Size — timo-web
Lighthouse — timo-web
Image Optimization — timo-web
측정 커밋: |
There was a problem hiding this comment.
Actionable comments posted: 10
🤖 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]/(main)/(with-time-sidebar)/home/_containers/HomeLanguageSyncContainer.tsx:
- Around line 9-10: Update isSupportedLocale to derive its accepted locale
values from the shared i18n/routing configuration or its inferred locale type
instead of hardcoding "ko" and "en"; preserve the existing type-guard behavior
and reuse the routing source of truth.
In `@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/page.tsx:
- Around line 9-11: Update the AsyncBoundary wrapping HomeLanguageSyncContainer
to provide errorFallback={null}, ensuring profile query failures are contained
and the non-critical language synchronization does not interrupt homepage
rendering.
In `@apps/timo-web/providers/auth/AuthProvider.tsx`:
- Around line 19-24: AuthProvider의 useEffect에서 hasChecked.current = true를
accessToken 검사보다 먼저 설정해 reissue 시도를 한 번만 수행하도록 수정하세요. 이후 accessToken이 없어져
effect가 재실행되어도 hasChecked 가드가 먼저 동작해 useReissue가 다시 호출되지 않도록 기존 초기화 흐름은 유지하세요.
In `@apps/timo-web/stores/auth/useAuthStore.ts`:
- Around line 30-34: Rename clearAccessToken to clearAuth or clearSession
throughout the auth store and its call sites, preserving its existing behavior
of clearing both the access token and onboarding completion state.
- Line 14: Remove onboardingCompleted from the Zustand auth store and stop
persisting or updating it through AuthProvider’s reissue flow. Update
OnboardingCompletedGuardProvider to read data.onboardingCompleted and isLoading
from useGetMyProfile, using the React Query value for redirect decisions. Keep
OauthCallbackContainer’s query cache update as the server-data source.
In `@apps/timo-web/utils/auth/token-manager.ts`:
- Around line 1-2: Update the access-token handling around ACCESS_TOKEN_KEY to
enforce a short accessToken lifetime, apply a restrictive Content Security
Policy through the existing application security configuration, and call
clearAccessToken() immediately when token expiry or a 401 response is detected.
Keep refreshToken storage in its existing httpOnly-cookie flow.
- Around line 11-14: localStorage 접근이 예외를 전파해 앱 초기화와 인증 흐름을 중단하지 않도록 처리하세요.
getAccessToken과 함께 토큰 저장·삭제를 담당하는 관련 함수들의
localStorage.getItem/setItem/removeItem 호출을 각각 try/catch로 감싸고, 읽기 실패는 null을 반환하며
쓰기·삭제 실패는 현재 인증 흐름을 계속할 수 있도록 예외를 삼키세요.
In `@apps/timo-web/utils/date/date.ts`:
- Around line 63-76: Update formatDate to derive the month and day without
local-timezone shifts by using UTC date accessors on the parsed Date,
specifically getUTCMonth() and getUTCDate(), while preserving the existing M/D
output format.
In `@apps/timo-web/utils/date/get-day-of-week-key.ts`:
- Line 11: Rename the union type alias DayOfWeekKey to DayOfWeekKeyTypes in
get-day-of-week-key.ts, and update every related reference to use the new name
while preserving the existing type definition and behavior.
- Around line 13-15: Update getDayOfWeekKey to validate the Date before indexing
DAY_OF_WEEK_KEYS, using Number.isNaN(date.getTime()) to reject invalid dates
instead of returning "SUN"; preserve the existing weekday mapping for valid
dates and define the appropriate invalid-input behavior consistent with
surrounding callers.
🪄 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: 0a2fe5eb-1c70-4c2a-8794-078baf5f8cfb
📒 Files selected for processing (39)
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeLanguageSyncContainer.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/todo-card/HomeDayHeaderContainer.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-view-mode.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_mocks/home-view-mock.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_mocks/todo-mock.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_queries/use-my-profile.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_utils/date.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_utils/day-of-week.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/page.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayDateHeaderContainer.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsxapps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsxapps/timo-web/app/[locale]/(main)/statistics/_components/StatisticsCalendar.tsxapps/timo-web/app/[locale]/(main)/statistics/_containers/StatisticsContainer.tsxapps/timo-web/app/[locale]/layout.tsxapps/timo-web/app/[locale]/login/page.tsxapps/timo-web/app/[locale]/oauth/callback/_containers/OauthCallbackContainer.tsxapps/timo-web/app/[locale]/onboarding/_containers/OnboardingFunnelContainer.tsxapps/timo-web/app/[locale]/onboarding/page.tsxapps/timo-web/components/layout/sidebar/time/TimeSidebarHeader.tsxapps/timo-web/components/layout/sidebar/time/TimeboxPanel.tsxapps/timo-web/components/timer/TimerCompleteModalPanel.tsxapps/timo-web/components/timer/TimerStopModalPanel.tsxapps/timo-web/components/todo-modal/CreateTodoModalContent.tsxapps/timo-web/providers/auth/AuthGuardProvider.tsxapps/timo-web/providers/auth/AuthProvider.tsxapps/timo-web/providers/auth/GuestGuardProvider.tsxapps/timo-web/providers/auth/OnboardingCompletedGuardProvider.tsxapps/timo-web/stores/auth/useAuthStore.tsapps/timo-web/types/.gitkeepapps/timo-web/utils/.gitkeepapps/timo-web/utils/auth/token-manager.tsapps/timo-web/utils/date/date.tsapps/timo-web/utils/date/get-day-of-week-key.tsapps/timo-web/utils/date/get-hour-label.tsapps/timo-web/utils/duration/convert-duration-to-minutes.tsapps/timo-web/utils/duration/convert-duration-to-time-text.tsapps/timo-web/utils/duration/format-duration-label.ts
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 10
🤖 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]/(main)/(with-time-sidebar)/home/_containers/HomeLanguageSyncContainer.tsx:
- Around line 9-10: Update isSupportedLocale to derive its accepted locale
values from the shared i18n/routing configuration or its inferred locale type
instead of hardcoding "ko" and "en"; preserve the existing type-guard behavior
and reuse the routing source of truth.
In `@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/page.tsx:
- Around line 9-11: Update the AsyncBoundary wrapping HomeLanguageSyncContainer
to provide errorFallback={null}, ensuring profile query failures are contained
and the non-critical language synchronization does not interrupt homepage
rendering.
In `@apps/timo-web/providers/auth/AuthProvider.tsx`:
- Around line 19-24: AuthProvider의 useEffect에서 hasChecked.current = true를
accessToken 검사보다 먼저 설정해 reissue 시도를 한 번만 수행하도록 수정하세요. 이후 accessToken이 없어져
effect가 재실행되어도 hasChecked 가드가 먼저 동작해 useReissue가 다시 호출되지 않도록 기존 초기화 흐름은 유지하세요.
In `@apps/timo-web/stores/auth/useAuthStore.ts`:
- Around line 30-34: Rename clearAccessToken to clearAuth or clearSession
throughout the auth store and its call sites, preserving its existing behavior
of clearing both the access token and onboarding completion state.
- Line 14: Remove onboardingCompleted from the Zustand auth store and stop
persisting or updating it through AuthProvider’s reissue flow. Update
OnboardingCompletedGuardProvider to read data.onboardingCompleted and isLoading
from useGetMyProfile, using the React Query value for redirect decisions. Keep
OauthCallbackContainer’s query cache update as the server-data source.
In `@apps/timo-web/utils/auth/token-manager.ts`:
- Around line 1-2: Update the access-token handling around ACCESS_TOKEN_KEY to
enforce a short accessToken lifetime, apply a restrictive Content Security
Policy through the existing application security configuration, and call
clearAccessToken() immediately when token expiry or a 401 response is detected.
Keep refreshToken storage in its existing httpOnly-cookie flow.
- Around line 11-14: localStorage 접근이 예외를 전파해 앱 초기화와 인증 흐름을 중단하지 않도록 처리하세요.
getAccessToken과 함께 토큰 저장·삭제를 담당하는 관련 함수들의
localStorage.getItem/setItem/removeItem 호출을 각각 try/catch로 감싸고, 읽기 실패는 null을 반환하며
쓰기·삭제 실패는 현재 인증 흐름을 계속할 수 있도록 예외를 삼키세요.
In `@apps/timo-web/utils/date/date.ts`:
- Around line 63-76: Update formatDate to derive the month and day without
local-timezone shifts by using UTC date accessors on the parsed Date,
specifically getUTCMonth() and getUTCDate(), while preserving the existing M/D
output format.
In `@apps/timo-web/utils/date/get-day-of-week-key.ts`:
- Line 11: Rename the union type alias DayOfWeekKey to DayOfWeekKeyTypes in
get-day-of-week-key.ts, and update every related reference to use the new name
while preserving the existing type definition and behavior.
- Around line 13-15: Update getDayOfWeekKey to validate the Date before indexing
DAY_OF_WEEK_KEYS, using Number.isNaN(date.getTime()) to reject invalid dates
instead of returning "SUN"; preserve the existing weekday mapping for valid
dates and define the appropriate invalid-input behavior consistent with
surrounding callers.
🪄 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: 0a2fe5eb-1c70-4c2a-8794-078baf5f8cfb
📒 Files selected for processing (39)
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeLanguageSyncContainer.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/todo-card/HomeDayHeaderContainer.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-view-mode.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_mocks/home-view-mock.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_mocks/todo-mock.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_queries/use-my-profile.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_utils/date.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_utils/day-of-week.tsapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/page.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayDateHeaderContainer.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsxapps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsxapps/timo-web/app/[locale]/(main)/statistics/_components/StatisticsCalendar.tsxapps/timo-web/app/[locale]/(main)/statistics/_containers/StatisticsContainer.tsxapps/timo-web/app/[locale]/layout.tsxapps/timo-web/app/[locale]/login/page.tsxapps/timo-web/app/[locale]/oauth/callback/_containers/OauthCallbackContainer.tsxapps/timo-web/app/[locale]/onboarding/_containers/OnboardingFunnelContainer.tsxapps/timo-web/app/[locale]/onboarding/page.tsxapps/timo-web/components/layout/sidebar/time/TimeSidebarHeader.tsxapps/timo-web/components/layout/sidebar/time/TimeboxPanel.tsxapps/timo-web/components/timer/TimerCompleteModalPanel.tsxapps/timo-web/components/timer/TimerStopModalPanel.tsxapps/timo-web/components/todo-modal/CreateTodoModalContent.tsxapps/timo-web/providers/auth/AuthGuardProvider.tsxapps/timo-web/providers/auth/AuthProvider.tsxapps/timo-web/providers/auth/GuestGuardProvider.tsxapps/timo-web/providers/auth/OnboardingCompletedGuardProvider.tsxapps/timo-web/stores/auth/useAuthStore.tsapps/timo-web/types/.gitkeepapps/timo-web/utils/.gitkeepapps/timo-web/utils/auth/token-manager.tsapps/timo-web/utils/date/date.tsapps/timo-web/utils/date/get-day-of-week-key.tsapps/timo-web/utils/date/get-hour-label.tsapps/timo-web/utils/duration/convert-duration-to-minutes.tsapps/timo-web/utils/duration/convert-duration-to-time-text.tsapps/timo-web/utils/duration/format-duration-label.ts
🛑 Comments failed to post (10)
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeLanguageSyncContainer.tsx (1)
9-10: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
isSupportedLocale가i18n/routing.ts의 locale 목록을 하드코딩으로 중복합니다.
routing.ts에locales: ["en", "ko"]가 이미 정의되어 있는데, 이 함수가 동일한 값을 별도로 하드코딩하고 있습니다. 라우팅 설정이 변경되면 두 곳을 동기화해야 하는 유지보수 리스크가 발생합니다.next-intl의 routing 설정에서 타입을 유도하거나 공유 상수를 import하는 방식을 고려해 보세요.🤖 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]/(main)/(with-time-sidebar)/home/_containers/HomeLanguageSyncContainer.tsx around lines 9 - 10, Update isSupportedLocale to derive its accepted locale values from the shared i18n/routing configuration or its inferred locale type instead of hardcoding "ko" and "en"; preserve the existing type-guard behavior and reuse the routing source of truth.apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/page.tsx (1)
9-11: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
HomeLanguageSyncContainer에errorFallback누락 — 프로필 API 실패 시 홈페이지 전체 중단 위험
useSuspenseQuery는 쿼리 실패 시 에러를 throw하여 가장 가까운 에러 바운더리로 전파합니다. 현재AsyncBoundary에errorFallback이 없어<ErrorBoundary>가 래핑되지 않으므로, 프로필 조회 실패 시 에러가 상위로 전파되어 홈페이지 전체가 깨질 수 있습니다.
HomeLanguageSyncContainer는null을 반환하는 비핵심 사이드이펙트 컴포넌트이므로, 언어 동기화 실패가 홈페이지 렌더링을 방해해서는 안 됩니다.errorFallback={null}을 추가하여 에러를 조용히 무시하도록 처리하는 것을 권장합니다.<AsyncBoundary> <HomeLanguageSyncContainer /> </AsyncBoundary><AsyncBoundary errorFallback={null}> <HomeLanguageSyncContainer /> </AsyncBoundary>참고: React Error Boundaries 문서, TanStack Query useSuspenseQuery 에러 처리
📝 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.<AsyncBoundary errorFallback={null}> <HomeLanguageSyncContainer /> </AsyncBoundary>🤖 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]/(main)/(with-time-sidebar)/home/page.tsx around lines 9 - 11, Update the AsyncBoundary wrapping HomeLanguageSyncContainer to provide errorFallback={null}, ensuring profile query failures are contained and the non-critical language synchronization does not interrupt homepage rendering.apps/timo-web/providers/auth/AuthProvider.tsx (1)
19-24: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash set -euo pipefail # Locate the provider and inspect the relevant section with line numbers. git ls-files 'apps/timo-web/providers/auth/AuthProvider.tsx' wc -l apps/timo-web/providers/auth/AuthProvider.tsx cat -n apps/timo-web/providers/auth/AuthProvider.tsx | sed -n '1,220p' # Also inspect any related token-clear / reissue logic to understand the effect flow. rg -n "clearAccessToken|setAccessToken|hasChecked|reissue|mutate\\(" apps/timo-web/providers/auth -SRepository: Team-Timo/Timo-client
Length of output: 2262
🏁 Script executed:
#!/bin/bash set -euo pipefail # Inspect auth store and any code paths that clear the token. rg -n "clearAccessToken|setAccessToken\\(|accessToken\\s*=\\s*null|setInitialized\\(|useAuthStore" apps/timo-web -S # Read the auth store file if present. fd -a "useAuthStore" apps/timo-webRepository: Team-Timo/Timo-client
Length of output: 3762
🏁 Script executed:
#!/bin/bash set -euo pipefail # Read the auth store and axios client that clears/reissues tokens. cat -n apps/timo-web/stores/auth/useAuthStore.ts printf '\n---\n' cat -n apps/timo-web/api/client/axios.tsRepository: Team-Timo/Timo-client
Length of output: 4479
🏁 Script executed:
#!/bin/bash set -euo pipefail # Inspect the generated reissue hook used by AuthProvider. rg -n "export const useReissue|useReissue\\(" apps/timo-web/api/generated -S fd -a "auth.ts" apps/timo-web/api/generatedRepository: Team-Timo/Timo-client
Length of output: 362
🏁 Script executed:
#!/bin/bash set -euo pipefail # Inspect the generated reissue hook implementation around the exported hook. sed -n '176,250p' apps/timo-web/api/generated/endpoints/auth/auth.tsRepository: Team-Timo/Timo-client
Length of output: 2012
hasChecked를 먼저 세팅해서 reissue 재진입을 막아주세요.
좋은 흐름입니다. 지금은accessToken이 초기엔 있어도, 이후axios인터셉터의clearAccessToken()으로 토큰이null이 되면useEffect가 다시 돌면서useReissue가 한 번 더 호출될 수 있습니다. “reissue 1회”가 목적이라면hasChecked.current = true를accessToken분기보다 앞에 두는 편이 맞습니다.
ReactuseEffect의 재실행 조건은 공식 문서도 같이 보면 좋습니다: https://react.dev/reference/react/useEffect🤖 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/AuthProvider.tsx` around lines 19 - 24, AuthProvider의 useEffect에서 hasChecked.current = true를 accessToken 검사보다 먼저 설정해 reissue 시도를 한 번만 수행하도록 수정하세요. 이후 accessToken이 없어져 effect가 재실행되어도 hasChecked 가드가 먼저 동작해 useReissue가 다시 호출되지 않도록 기존 초기화 흐름은 유지하세요.apps/timo-web/stores/auth/useAuthStore.ts (2)
14-14: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
onboardingCompleted는 서버 데이터이므로 Zustand 저장이 path instruction에 위배됩니다.
onboardingCompleted는 OAuth callback에서data.user.onboardingCompleted로 수신하는 서버 데이터입니다.apps/timo-web/stores/**path instruction은 "API 서버 데이터를 store에 저장 금지 (React Query가 담당)"을 명시합니다. 현재OauthCallbackContainer에서queryClient.setQueryData(getGetMyProfileQueryKey(), data.user)로 React Query에도 저장하므로 두 출처가 발생합니다.또한
AuthProvider의 reissue flow는onboardingCompleted를 갱신하지 않으므로, 다른 기기에서 온보딩을 완료하거나 관리자가 상태를 리셋한 경우 localStorage 값이 stale해져 가드가 잘못된 리다이렉트를 수행할 수 있습니다.대안으로
OnboardingCompletedGuardProvider에서useGetMyProfilehook의data.onboardingCompleted와isLoading을 직접 사용하는 것을 고려해 보세요.As per path instructions: "API 서버 데이터를 store에 저장 금지 (React Query가 담당)" —
apps/timo-web/stores/**규칙.Also applies to: 24-24, 35-38
🤖 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/stores/auth/useAuthStore.ts` at line 14, Remove onboardingCompleted from the Zustand auth store and stop persisting or updating it through AuthProvider’s reissue flow. Update OnboardingCompletedGuardProvider to read data.onboardingCompleted and isLoading from useGetMyProfile, using the React Query value for redirect decisions. Keep OauthCallbackContainer’s query cache update as the server-data source.Source: Path instructions
30-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
clearAccessToken가onboardingCompleted까지 함께 clear하는 것은 네이밍과 의미가 불일치합니다.함수명은 access token만 제거할 것처럼 보이지만, 실제로는 onboarding 완료 상태도 함께 초기화합니다. 로그아웃 시 모든 인증 상태를 clear하는 것은 올바르지만,
clearAuth또는clearSession으로 명명하면 의도가 더 명확해집니다.🤖 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/stores/auth/useAuthStore.ts` around lines 30 - 34, Rename clearAccessToken to clearAuth or clearSession throughout the auth store and its call sites, preserving its existing behavior of clearing both the access token and onboarding completion state.apps/timo-web/utils/auth/token-manager.ts (2)
1-2: 🔒 Security & Privacy | 🔵 Trivial
localStorage 기반 accessToken 저장의 보안 트레이드오프를 인지하고 계시겠습니다.
accessToken을 localStorage에 저장하면 XSS 공격 시 토큰이 노출됩니다. refreshToken은 httpOnly 쿠키로 보호되므로 하이브리드 방식은 합리적이지만, 다음 완화 조건을 권장합니다:
- accessToken 수명을 짧게 유지 (예: 15분 이내)
- Content Security Policy(CSP) 헤더 적용으로 XSS 공격 표면 최소화
- 토큰 만료/401 응답 시
clearAccessToken()호출로 즉시 제거참고: OWASP Token Storage Cheat Sheet
🤖 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/utils/auth/token-manager.ts` around lines 1 - 2, Update the access-token handling around ACCESS_TOKEN_KEY to enforce a short accessToken lifetime, apply a restrictive Content Security Policy through the existing application security configuration, and call clearAccessToken() immediately when token expiry or a 401 response is detected. Keep refreshToken storage in its existing httpOnly-cookie flow.
11-14: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
localStorage 접근 시 예외 처리가 필요합니다.
localStorage.getItem/setItem/removeItem은QuotaExceededError나SecurityError를 throw할 수 있습니다. getter는 store 초기화 시점(앱 시작)에 호출되므로 예외 발생 시 전체 앱이 크래시되고, setter는 로그인/온보딩 완료 시점에 호출되므로 인증 플로우가 중단됩니다.🛡️ 제안하는 예외 처리 래퍼
const ACCESS_TOKEN_KEY = "accessToken"; const ONBOARDING_COMPLETED_KEY = "onboardingCompleted"; -const isBrowser = typeof window !== "undefined"; +const isBrowser = typeof window !== "undefined"; + +const safeGet = (key: string): string | null => { + try { + return window.localStorage.getItem(key); + } catch { + return null; + } +}; + +const safeSet = (key: string, value: string): void => { + try { + window.localStorage.setItem(key, value); + } catch { + // quota 또는 security 제한 — 로그만 남기거나 조용히 무시 + } +}; + +const safeRemove = (key: string): void => { + try { + window.localStorage.removeItem(key); + } catch { + // 조용히 무시 + } +}; export const getAccessToken = (): string | null => { if (!isBrowser) return null; - return window.localStorage.getItem(ACCESS_TOKEN_KEY); + return safeGet(ACCESS_TOKEN_KEY); }; export const setAccessToken = (token: string): void => { if (!isBrowser) return; - window.localStorage.setItem(ACCESS_TOKEN_KEY, token); + safeSet(ACCESS_TOKEN_KEY, token); }; export const clearAccessToken = (): void => { if (!isBrowser) return; - window.localStorage.removeItem(ACCESS_TOKEN_KEY); + safeRemove(ACCESS_TOKEN_KEY); }; export const getOnboardingCompleted = (): boolean => { if (!isBrowser) return false; - return window.localStorage.getItem(ONBOARDING_COMPLETED_KEY) === "true"; + return safeGet(ONBOARDING_COMPLETED_KEY) === "true"; }; export const setOnboardingCompleted = (completed: boolean): void => { if (!isBrowser) return; - window.localStorage.setItem(ONBOARDING_COMPLETED_KEY, String(completed)); + safeSet(ONBOARDING_COMPLETED_KEY, String(completed)); }; export const clearOnboardingCompleted = (): void => { if (!isBrowser) return; - window.localStorage.removeItem(ONBOARDING_COMPLETED_KEY); + safeRemove(ONBOARDING_COMPLETED_KEY); };Also applies to: 21-24, 29-32, 39-42, 49-52, 57-60
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/timo-web/utils/auth/token-manager.ts` around lines 11 - 14, localStorage 접근이 예외를 전파해 앱 초기화와 인증 흐름을 중단하지 않도록 처리하세요. getAccessToken과 함께 토큰 저장·삭제를 담당하는 관련 함수들의 localStorage.getItem/setItem/removeItem 호출을 각각 try/catch로 감싸고, 읽기 실패는 null을 반환하며 쓰기·삭제 실패는 현재 인증 흐름을 계속할 수 있도록 예외를 삼키세요.apps/timo-web/utils/date/date.ts (1)
63-76: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash set -euo pipefail # 1) 관련 파일의 현재 구현 확인 sed -n '1,140p' apps/timo-web/utils/date/date.ts # 2) formatDate 사용처 확인 rg -n "formatDate\(" apps/timo-web -S # 3) Node 런타임에서 ISO 문자열의 로컬/UTC 날짜 차이를 재현 node - <<'JS' const iso = "2025-07-10T00:00:00Z"; for (const tz of ["UTC", "Asia/Seoul", "America/Los_Angeles", "America/New_York"]) { process.env.TZ = tz; const d = new Date(iso); console.log( tz, "=>", `${d.getMonth() + 1}/${d.getDate()}`, "| UTC:", `${d.getUTCMonth() + 1}/${d.getUTCDate()}` ); } JSRepository: Team-Timo/Timo-client
Length of output: 2726
🏁 Script executed:
#!/bin/bash set -euo pipefail # 1) formatDate 사용 파일의 주변 맥락 확인 sed -n '1,120p' apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsx # 2) todo.date의 타입/출처 확인 rg -n "todo\.date|date:" apps/timo-web -S # 3) date 관련 다른 유틸/포맷 관례 확인 rg -n "getUTCMonth|getUTCDate|formatDateKey|parseDateKey|formatDate" apps/timo-web/utils apps/timo-web/app -SRepository: Team-Timo/Timo-client
Length of output: 212
🏁 Script executed:
#!/bin/bash set -euo pipefail FILE='apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsx' sed -n '1,160p' "$FILE" printf '\n---\n' rg -n "todo\.date|date:" apps/timo-web -S printf '\n---\n' rg -n "getUTCMonth|getUTCDate|formatDateKey|parseDateKey|formatDate" apps/timo-web/utils apps/timo-web/app -SRepository: Team-Timo/Timo-client
Length of output: 16300
타임존 영향 없이
formatDate를 계산하세요
좋은 분리입니다. 다만todo.date는z.iso.date()/yyyy-MM-dd라new Date(todo.date)뒤getMonth()/getDate()를 쓰면 음수 UTC 오프셋 환경에서 날짜가 하루 밀릴 수 있습니다.getUTCMonth()/getUTCDate()로 바꾸거나 문자열을 직접 파싱하세요. MDN: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Date/getUTCMonth🤖 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/utils/date/date.ts` around lines 63 - 76, Update formatDate to derive the month and day without local-timezone shifts by using UTC date accessors on the parsed Date, specifically getUTCMonth() and getUTCDate(), while preserving the existing M/D output format.apps/timo-web/utils/date/get-day-of-week-key.ts (2)
11-11: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
유니언 타입 별칭 이름을 컨벤션에 맞춰주세요.
DayOfWeekKey는 유니언 타입 별칭이므로 프로젝트 규칙에 따라DayOfWeekKeyTypes처럼Types접미사를 사용해야 합니다. 관련 사용처도 함께 갱신해 주세요. TypeScript는 유니언 타입에type별칭을 사용할 수 있습니다. (typescriptlang.org)🤖 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/utils/date/get-day-of-week-key.ts` at line 11, Rename the union type alias DayOfWeekKey to DayOfWeekKeyTypes in get-day-of-week-key.ts, and update every related reference to use the new name while preserving the existing type definition and behavior.Source: Coding guidelines
13-15: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash set -euo pipefail printf '\n## file\n' sed -n '1,120p' apps/timo-web/utils/date/get-day-of-week-key.ts | cat -n printf '\n## related search\n' rg -n "getDayOfWeekKey|DAY_OF_WEEK_KEYS|DayOfWeekKey" apps/timo-web -SRepository: Team-Timo/Timo-client
Length of output: 2373
🏁 Script executed:
#!/bin/bash set -euo pipefail printf '\n## day-of-week helper\n' sed -n '1,120p' apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_utils/day-of-week.ts | cat -n printf '\n## today header\n' sed -n '1,120p' apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayDateHeaderContainer.tsx | cat -n printf '\n## sidebar header\n' sed -n '1,120p' apps/timo-web/components/layout/sidebar/time/TimeSidebarHeader.tsx | cat -n printf '\n## focus date util\n' sed -n '1,120p' apps/timo-web/app/[locale]/(main)/focus/_utils/date.ts | cat -nRepository: Team-Timo/Timo-client
Length of output: 235
🏁 Script executed:
#!/bin/bash set -euo pipefail printf '\n## day-of-week helper\n' sed -n '1,120p' 'apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_utils/day-of-week.ts' | cat -n printf '\n## today header\n' sed -n '1,120p' 'apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayDateHeaderContainer.tsx' | cat -n printf '\n## sidebar header\n' sed -n '1,120p' 'apps/timo-web/components/layout/sidebar/time/TimeSidebarHeader.tsx' | cat -n printf '\n## focus date util\n' sed -n '1,120p' 'apps/timo-web/app/[locale]/(main)/focus/_utils/date.ts' | cat -nRepository: Team-Timo/Timo-client
Length of output: 4871
요일 매핑은 깔끔하지만, invalid
Date를"SUN"으로 숨기지 않게 해 주세요.
getDay()는 잘못된 날짜에서NaN을 반환하므로 현재 fallback이 오동작을 조용히 감춥니다. 입력이 항상 유효한Date가 아니라면Number.isNaN(date.getTime())로 먼저 거부하거나, 호출부에서 검증 책임을 분리해 주세요.
MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getDay🤖 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/utils/date/get-day-of-week-key.ts` around lines 13 - 15, Update getDayOfWeekKey to validate the Date before indexing DAY_OF_WEEK_KEYS, using Number.isNaN(date.getTime()) to reject invalid dates instead of returning "SUN"; preserve the existing weekday mapping for valid dates and define the appropriate invalid-input behavior consistent with surrounding callers.
- 홈 페이지 전용이던 언어 동기화를 루트 LanguageSyncProvider로 옮겨 로그인 상태면 어느 페이지에서든 프로필 언어를 따르도록 했습니다 - useMyProfile을 공용 쿼리로 승격하고 인증 초기화 전/비로그인 상태에서는 요청하지 않도록 했습니다 - 로케일 쿠키가 없는 첫 방문에 한해 Vercel 지오 헤더로 접속 국가가 한국이면 ko, 아니면 en을 기본 로케일로 적용하도록 proxy.ts를 수정했습니다
…route-guards # Conflicts: # apps/timo-web/app/[locale]/oauth/callback/_containers/OauthCallbackContainer.tsx
ehye1
left a comment
There was a problem hiding this comment.
기존 방식에서 미들웨어는 쿠키, 클라이언트는 accessToken을 기준으로 인증을 판단해서 두 상태가 어긋날 경우에 리다이렉트 루프가 발생할 수 있었습니다. 오늘 겪었던 로그인 관련 이슈들도 인증 상태를 두 군데에서 판단하면서 생긴 문제였는데요, accessToken과 isInitialized를 기준으로 인증상태를 하나로 통일하니 문제가 발생하지 않네요. 완전 굿굿 수고하셨습니다,,,😍
- LanguageSyncProvider용 useMyProfile과 settings 전용 useSettingsProfileQuery가 같은 GET /api/v1/users를 각자 다른 방식으로 중복 조회하고 있어 하나로 합쳤습니다 - 응답 스키마를 types/user-profile-type.ts로 공용화하고, useMyProfile을 suspense 쿼리로 통일했습니다 - LanguageSyncProvider는 isInitialized && accessToken이 확정된 뒤에만 Suspense 서브트리를 마운트해 비로그인 상태에서 suspend되지 않도록 했습니다
- profileImageUrl/calendarEmail이 실제로는 null로 내려오는데 optional()만 허용해 런타임에서 ZodError가 발생했습니다 - nullish()로 바꿔 string | null | undefined를 모두 허용하도록 했습니다
- '/' 진입 시 빈 화면 대신 서버에서 /home으로 리다이렉트하도록 했습니다 - (main)/home은 이미 AuthGuardProvider로 보호되고 있어 비로그인 사용자는 자동으로 /login으로 재리다이렉트됩니다
- updateLanguage 성공 후 useMyProfile 캐시를 갱신하지 않아, commitLanguage로 로케일을 바꿔도 LanguageSyncProvider가 예전 캐시값을 보고 다시 되돌리고 있었습니다 - updateLanguage 응답의 language 값으로 useMyProfile 캐시를 즉시 patch해 commitLanguage 이전에 최신 상태가 되도록 했습니다
…꾼다 (#179) - 기존엔 useMyProfile이 실패하면 근처에 ErrorBoundary가 없어 전역 error.tsx까지 전파되어 앱 전체가 깨질 수 있었습니다 - AsyncBoundary(+errorFallback=null)로 감싸 이 기능만 조용히 무시되도록 하고, 다른 suspense 쿼리들과 동일한 패턴으로 맞췄습니다
ISSUE 🔗
close #179
What is this PR? 🔍
accessToken 저장 방식을 localStorage로 전환하고 인증 상태에 따른 라우트 접근 제어(로그인/온보딩 가드)를 추가했습니다. 세션 초기화를 루트 한 곳에서 처리하도록 리팩터링해 하이드레이션 깜빡임을 제거했습니다. 또한 언어 동기화를 앱 전역으로 확장해, 로그인 사용자는 어느 페이지에서든 프로필에 저장된 언어를 따르고 비로그인 사용자는 접속 국가(Geo-IP) 기준으로 기본 로케일이 정해지도록 했습니다. 부수적으로 utils 폴더를 도메인별로 재구성했습니다.
배경
/login·/onboarding에는 로그인/온보딩 상태에 따른 접근 제어가 없었습니다. 언어 동기화도 홈 페이지에서만 동작했고, 비로그인 사용자의 기본 로케일은 브라우저 Accept-Language에만 의존했습니다. 루트(/)는 빈 화면만 보여줬습니다.AuthGuardProvider가 마운트되는 라우트마다 중복 실행됐고,/login에 로그인 상태로 진입하면 로그인 폼이 잠깐 그려졌다가 홈으로 리다이렉트되는 깜빡임이 있었습니다. 홈이 아닌 다른 페이지에서는 프로필 언어와 로케일이 어긋나도 동기화되지 않았고,/로 접근하면 아무 안내 없이 빈 화면만 보였습니다.AuthProvider에서 앱당 1회만 수행하도록 통합했습니다. 언어 동기화도 같은 패턴으로 루트LanguageSyncProvider로 올리고, 비로그인 사용자는 접속 국가 기반으로 초기 로케일을 정하도록 미들웨어를 확장했습니다./는/home으로 서버 리다이렉트해 기존AuthGuardProvider가 로그인 여부에 따른 분기를 그대로 처리하게 했습니다.경로 접근 흐름 정리
로그인 여부·접속 국가에 따라 실제로 어디로 도달하는지 정리했습니다.
1) 로케일 결정 (
proxy.ts, 첫 방문만)2) 루트 경로
/{locale}/{locale}/home으로 리다이렉트 (app/[locale]/page.tsx). 로그인 여부 판단은 하지 않고 3)에 위임합니다.3)
/{locale}/home,/{locale}/today,/{locale}/focus,/{locale}/statistics,/{locale}/settings—AuthGuardProvider로 보호4)
/{locale}/login—GuestGuardProvider로 보호5)
/{locale}/onboarding—AuthGuardProvider+OnboardingCompletedGuardProvider중첩6)
/{locale}/oauth/callback— 의도적으로 가드 없음accessToken/onboardingCompleted저장 →isNewUser가 true면/onboarding, 아니면/home으로 이동합니다. 로그인 자체를 처리하는 경로라 다른 가드가 적용되면 안 됩니다.요약 매트릭스
//home등 보호된 페이지/login/onboarding/home→/login/login/login/home(그대로 진입 가능)/home/home/home/home인증 세션 (AuthProvider / 가드)
/login접근과 온보딩 완료 사용자의/onboarding접근을 차단하는 가드를 추가했습니다. 루트(/)는/home으로 리다이렉트해 별도 로직 없이 기존 가드에 위임합니다.utils/auth/token-manager.ts가window.localStorage의accessToken/onboardingCompleted키를 get/set/clear합니다.useAuthStore가 초기 상태를 이 값으로 hydrate하고,isInitialized: boolean을 추가했습니다. 루트 레이아웃(app/[locale]/layout.tsx)에 한 번만 마운트되는AuthProvider가 mount 시 accessToken이 없으면useReissue를 1회 호출하고, 성공/실패 관계없이isInitialized(true)를 세팅합니다(children은 항상 즉시 렌더링해 블로킹하지 않습니다).AuthGuardProvider/GuestGuardProvider/OnboardingCompletedGuardProvider세 가드는 각자 API를 호출하지 않고isInitialized가true가 되기 전까지null을 반환해 서버 렌더와 클라이언트 첫 렌더를 동일하게 유지합니다.useReissue응답에는 온보딩 정보가 없어onboardingCompleted는 로그인·온보딩 완료 응답 시점에만 갱신됩니다.언어 동기화 (전역 + Geo-IP)
settings/_queries/use-settings-profile.ts가 같은GET /api/v1/users를 별도 스키마로 다시 조회하고 있어 중복이었습니다.useMyProfile(queries/use-my-profile.ts)을 두 도메인이 함께 쓰는 공용 훅으로 승격하고, 응답 스키마를types/user-profile-type.ts(zod)로 표준화해useSuspenseQuery로 통일했습니다.LanguageSyncProvider는isInitialized && accessToken이 확정된 뒤에만<Suspense>로 감싼 내부 컴포넌트를 마운트해useMyProfile을 호출하므로, 비로그인 상태에서는 아예 요청하지 않고 suspend되지도 않습니다. 프로필의language(KO/EN)를 소문자로 정규화해 현재 로케일과 다르면router.replace(pathname, { locale })로 전환합니다.proxy.ts는NEXT_LOCALE쿠키가 없고 URL에 로케일 prefix도 없는 첫 요청에 한해 Vercel의x-vercel-ip-country헤더를 읽어KR이면ko, 아니면en으로 리다이렉트하고 쿠키를 심습니다. 쿠키가 있거나 이미 로케일이 명시된 URL이면 next-intl 기본 미들웨어로 위임해 기존 동작(명시적 선택 우선)을 그대로 보존합니다.x-vercel-ip-country헤더는 Vercel 배포 환경에서만 존재해 로컬 개발 환경에서는 항상en으로 떨어집니다. 로그인 사용자의 프로필 언어가 항상 최우선이며, Geo-IP 분기는 비로그인 사용자에게만 적용됩니다.profileImageUrl/calendarEmail은 API가null을 내려줄 수 있어 zod 스키마를nullish()로 방어적으로 검증합니다(런타임 ZodError로 실제 발견해 수정).utils 폴더 구조화
utils/하위에 섞여 있던 duration 변환 유틸 3개와 date/시간 유틸 3개를 각각utils/duration/,utils/date/로 이동했습니다.utils/todo/처럼 도메인별 하위 폴더 컨벤션이 있었는데,token-manager.ts가utils/auth/에 추가되며 flat 구조와 도메인 구조가 혼재해 정리했습니다.git mv로 파일을 이동하고, 참조하던 15개 파일의 import 경로를 일괄 치환했습니다. 함수 로직은 변경하지 않았습니다.develop이 앞서 나가 있어([FEAT] 오늘 페이지 뷰 구현 #141, [FEAT] 약관 페이지 및 설정 약관 탭 구현 #163, [FEAT] 설정 프로필 조회·언어 수정 API 연동 #172 머지) 두 차례 브랜치를 다시 만들거나 병합하는 과정에서TodayTodoListContainer.tsx,OauthCallbackContainer.tsximport/로직 충돌을 수동으로 해결했습니다.To Reviewers
AuthProvider가/oauth/callback페이지에서도 무조건 마운트되기 때문에, 로그인 처리 중에 reissue가 한 번 더(실패로) 호출됩니다. 기능상 문제는 없지만 불필요한 네트워크 요청이라 개선 여지가 있다고 생각해 알려드립니다.Geo-IP 로케일 분기는 Vercel 헤더에 의존하고 있어 실제 국가별 리다이렉트는 로컬에서 재현할 수 없었습니다 — 배포 후 실제 확인이 필요합니다.
LanguageSyncProvider가 로케일을 바꾸는 시점이useEffect이후라 최초 렌더 순간에는 잠깐 이전 로케일로 보일 수 있는데, 이 부분 허용 범위인지 확인 부탁드립니다.비로그인 사용자가
/로 들어오면/home을 거쳐/login으로 리다이렉트되는 2단 리다이렉트 구조입니다 —/에서 바로/login으로 보내는 것보다 기존 가드를 재사용하는 쪽을 택했는데, 이 트레이드오프가 괜찮은지 의견 부탁드립니다.Screenshot 📷
Test Checklist ✔
pnpm check-types통과pnpm lint통과/→/home→ (비로그인 시)/login리다이렉트 체인 확인 — 미실행: 개발 서버로 직접 확인 필요