Skip to content

[FEAT] 인증 세션·라우트 가드 개선 및 언어 동기화 추가 - #181

Merged
kimminna merged 10 commits into
developfrom
feat/web/179-auth-route-guards
Jul 13, 2026
Merged

[FEAT] 인증 세션·라우트 가드 개선 및 언어 동기화 추가#181
kimminna merged 10 commits into
developfrom
feat/web/179-auth-route-guards

Conversation

@kimminna

@kimminna kimminna commented Jul 13, 2026

Copy link
Copy Markdown
Member

ISSUE 🔗

close #179



What is this PR? 🔍

accessToken 저장 방식을 localStorage로 전환하고 인증 상태에 따른 라우트 접근 제어(로그인/온보딩 가드)를 추가했습니다. 세션 초기화를 루트 한 곳에서 처리하도록 리팩터링해 하이드레이션 깜빡임을 제거했습니다. 또한 언어 동기화를 앱 전역으로 확장해, 로그인 사용자는 어느 페이지에서든 프로필에 저장된 언어를 따르고 비로그인 사용자는 접속 국가(Geo-IP) 기준으로 기본 로케일이 정해지도록 했습니다. 부수적으로 utils 폴더를 도메인별로 재구성했습니다.

배경

  • 기존 구조: accessToken이 zustand 인메모리에만 저장되어 새로고침 시 세션이 유지되지 않았고, /login·/onboarding에는 로그인/온보딩 상태에 따른 접근 제어가 없었습니다. 언어 동기화도 홈 페이지에서만 동작했고, 비로그인 사용자의 기본 로케일은 브라우저 Accept-Language에만 의존했습니다. 루트(/)는 빈 화면만 보여줬습니다.
  • 발생 문제: 세션 확인(reissue)이 AuthGuardProvider가 마운트되는 라우트마다 중복 실행됐고, /login에 로그인 상태로 진입하면 로그인 폼이 잠깐 그려졌다가 홈으로 리다이렉트되는 깜빡임이 있었습니다. 홈이 아닌 다른 페이지에서는 프로필 언어와 로케일이 어긋나도 동기화되지 않았고, /로 접근하면 아무 안내 없이 빈 화면만 보였습니다.
  • 해결 방향: accessToken을 localStorage에 영속화하고, 세션 초기화를 루트 AuthProvider에서 앱당 1회만 수행하도록 통합했습니다. 언어 동기화도 같은 패턴으로 루트 LanguageSyncProvider로 올리고, 비로그인 사용자는 접속 국가 기반으로 초기 로케일을 정하도록 미들웨어를 확장했습니다. //home으로 서버 리다이렉트해 기존 AuthGuardProvider가 로그인 여부에 따른 분기를 그대로 처리하게 했습니다.

경로 접근 흐름 정리

로그인 여부·접속 국가에 따라 실제로 어디로 도달하는지 정리했습니다.

1) 로케일 결정 (proxy.ts, 첫 방문만)

요청 도착
 ├─ NEXT_LOCALE 쿠키 있음? ────────────────┐
 │                                          │
 ├─ URL에 이미 /ko 또는 /en prefix 있음? ───┤→ next-intl 기본 미들웨어로 위임
 │                                          │   (쿠키 → Accept-Language → defaultLocale 순)
 └─ 위 둘 다 없음 (진짜 첫 방문)
      └─ x-vercel-ip-country 헤더 확인
           ├─ KR → /ko/... 로 리다이렉트 + NEXT_LOCALE=ko 쿠키 저장
           └─ KR 아님(또는 헤더 없음, 예: 로컬) → /en/... 로 리다이렉트 + NEXT_LOCALE=en 쿠키 저장

2) 루트 경로 /{locale}

  • 서버에서 무조건 /{locale}/home으로 리다이렉트 (app/[locale]/page.tsx). 로그인 여부 판단은 하지 않고 3)에 위임합니다.

3) /{locale}/home, /{locale}/today, /{locale}/focus, /{locale}/statistics, /{locale}/settingsAuthGuardProvider로 보호

페이지 진입
 └─ AuthProvider가 isInitialized=true로 만들 때까지 대기 (빈 화면, 서버 렌더와 동일)
      ├─ accessToken 없음 → /login 으로 리다이렉트
      └─ accessToken 있음 → 페이지 렌더
           └─ LanguageSyncProvider가 프로필 language 조회 → 현재 로케일과 다르면 해당 로케일로 전환

4) /{locale}/loginGuestGuardProvider로 보호

페이지 진입
 └─ isInitialized=true 될 때까지 대기 (빈 화면)
      ├─ accessToken 있음(이미 로그인) → /home 으로 리다이렉트
      └─ accessToken 없음 → 로그인 폼 렌더

5) /{locale}/onboardingAuthGuardProvider + OnboardingCompletedGuardProvider 중첩

페이지 진입
 └─ AuthGuardProvider: accessToken 없음 → /login
      └─ accessToken 있음
           └─ OnboardingCompletedGuardProvider: onboardingCompleted=true → /home
                └─ onboardingCompleted=false → 온보딩 퍼널 렌더

6) /{locale}/oauth/callback — 의도적으로 가드 없음

  • OAuth 인가 code로 토큰 교환 → accessToken/onboardingCompleted 저장 → isNewUser가 true면 /onboarding, 아니면 /home으로 이동합니다. 로그인 자체를 처리하는 경로라 다른 가드가 적용되면 안 됩니다.

요약 매트릭스

상태 / /home 등 보호된 페이지 /login /onboarding
비로그인 /home/login /login 폼 표시 /login
로그인, 온보딩 미완료 /home (그대로 진입 가능) 정상 진입 /home 퍼널 표시
로그인, 온보딩 완료 /home 정상 진입 /home /home

국가(Geo-IP) 분기는 로케일 prefix 결정에만 관여하고, 위 접근 제어 흐름 자체에는 영향을 주지 않습니다. 로그인 사용자는 프로필에 저장된 언어가 Geo-IP보다 항상 우선합니다.

인증 세션 (AuthProvider / 가드)

  • 변경 요약: accessToken을 localStorage 기반으로 저장하고, 로그인 사용자의 /login 접근과 온보딩 완료 사용자의 /onboarding 접근을 차단하는 가드를 추가했습니다. 루트(/)는 /home으로 리다이렉트해 별도 로직 없이 기존 가드에 위임합니다.
  • 이유: 기존에는 accessToken이 인메모리에만 있어 새로고침마다 세션이 끊겼고, 이미 로그인/온보딩을 마친 사용자도 해당 페이지에 재진입할 수 있었기 때문입니다.
  • 구현 방식: utils/auth/token-manager.tswindow.localStorageaccessToken/onboardingCompleted 키를 get/set/clear합니다. useAuthStore가 초기 상태를 이 값으로 hydrate하고, isInitialized: boolean을 추가했습니다. 루트 레이아웃(app/[locale]/layout.tsx)에 한 번만 마운트되는 AuthProvider가 mount 시 accessToken이 없으면 useReissue를 1회 호출하고, 성공/실패 관계없이 isInitialized(true)를 세팅합니다(children은 항상 즉시 렌더링해 블로킹하지 않습니다). AuthGuardProvider/GuestGuardProvider/OnboardingCompletedGuardProvider 세 가드는 각자 API를 호출하지 않고 isInitializedtrue가 되기 전까지 null을 반환해 서버 렌더와 클라이언트 첫 렌더를 동일하게 유지합니다.
  • 경계 · 제약: refreshToken은 기존과 동일하게 httpOnly 쿠키로 서버가 관리합니다. useReissue 응답에는 온보딩 정보가 없어 onboardingCompleted는 로그인·온보딩 완료 응답 시점에만 갱신됩니다.

언어 동기화 (전역 + Geo-IP)

  • 변경 요약: 홈 페이지 전용이던 언어 동기화를 앱 전역으로 확장하고, 비로그인 사용자에게는 접속 국가 기반 기본 로케일을 적용했습니다. 이후 settings 도메인에 별도로 있던 프로필 조회 쿼리와 중복되던 것을 발견해 공용 훅으로 통합했습니다.
  • 이유: 로그인 사용자가 홈이 아닌 다른 페이지로 바로 진입하면 프로필 언어와 로케일이 어긋난 채로 남아 있었고, 비로그인 사용자는 브라우저 언어 설정에만 의존해 실제 접속 위치와 맞지 않는 경우가 있었습니다. settings/_queries/use-settings-profile.ts가 같은 GET /api/v1/users를 별도 스키마로 다시 조회하고 있어 중복이었습니다.
  • 구현 방식: useMyProfile(queries/use-my-profile.ts)을 두 도메인이 함께 쓰는 공용 훅으로 승격하고, 응답 스키마를 types/user-profile-type.ts(zod)로 표준화해 useSuspenseQuery로 통일했습니다. LanguageSyncProviderisInitialized && accessToken이 확정된 뒤에만 <Suspense>로 감싼 내부 컴포넌트를 마운트해 useMyProfile을 호출하므로, 비로그인 상태에서는 아예 요청하지 않고 suspend되지도 않습니다. 프로필의 language(KO/EN)를 소문자로 정규화해 현재 로케일과 다르면 router.replace(pathname, { locale })로 전환합니다. proxy.tsNEXT_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.tsutils/auth/에 추가되며 flat 구조와 도메인 구조가 혼재해 정리했습니다.
  • 구현 방식: git mv로 파일을 이동하고, 참조하던 15개 파일의 import 경로를 일괄 치환했습니다. 함수 로직은 변경하지 않았습니다.
  • 경계 · 제약: 작업 중 develop이 앞서 나가 있어([FEAT] 오늘 페이지 뷰 구현 #141, [FEAT] 약관 페이지 및 설정 약관 탭 구현 #163, [FEAT] 설정 프로필 조회·언어 수정 API 연동 #172 머지) 두 차례 브랜치를 다시 만들거나 병합하는 과정에서 TodayTodoListContainer.tsx, OauthCallbackContainer.tsx import/로직 충돌을 수동으로 해결했습니다.



To Reviewers

AuthProvider/oauth/callback 페이지에서도 무조건 마운트되기 때문에, 로그인 처리 중에 reissue가 한 번 더(실패로) 호출됩니다. 기능상 문제는 없지만 불필요한 네트워크 요청이라 개선 여지가 있다고 생각해 알려드립니다.
Geo-IP 로케일 분기는 Vercel 헤더에 의존하고 있어 실제 국가별 리다이렉트는 로컬에서 재현할 수 없었습니다 — 배포 후 실제 확인이 필요합니다.
LanguageSyncProvider가 로케일을 바꾸는 시점이 useEffect 이후라 최초 렌더 순간에는 잠깐 이전 로케일로 보일 수 있는데, 이 부분 허용 범위인지 확인 부탁드립니다.
비로그인 사용자가 /로 들어오면 /home을 거쳐 /login으로 리다이렉트되는 2단 리다이렉트 구조입니다 — /에서 바로 /login으로 보내는 것보다 기존 가드를 재사용하는 쪽을 택했는데, 이 트레이드오프가 괜찮은지 의견 부탁드립니다.



Screenshot 📷



Test Checklist ✔

  • pnpm check-types 통과
  • pnpm lint 통과
  • 로그인/로그아웃/온보딩 실제 브라우저 플로우 확인 — 미실행: 개발 서버로 직접 확인 필요
  • Geo-IP 기반 로케일 분기 확인 — 미실행: Vercel 배포 환경에서만 확인 가능
  • //home → (비로그인 시) /login 리다이렉트 체인 확인 — 미실행: 개발 서버로 직접 확인 필요

kimminna added 3 commits July 14, 2026 00:45
…179)

- accessToken을 zustand 인메모리에서 localStorage 기반으로 저장하도록 변경했습니다
- 로그인 사용자의 /login 접근과 온보딩 완료 사용자의 /onboarding 접근을 차단하는 가드를 추가했습니다
- 세션 초기화(reissue)를 루트 AuthProvider에서 1회만 수행하도록 통합해 하이드레이션 깜빡임과 중복 호출을 제거했습니다
- 홈 접근 시 유저 프로필을 조회해 서버에 저장된 언어와 현재 로케일이 다르면 자동으로 전환하도록 했습니다
- duration 변환 유틸 3개를 utils/duration/으로, date/시간 유틸 3개를 utils/date/로 이동했습니다
- 이동한 파일을 참조하던 import 경로를 전체 업데이트했습니다
- 더 이상 필요 없는 .gitkeep 플레이스홀더를 정리했습니다
@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 5:08pm

Request Review

@github-actions
github-actions Bot requested review from ehye1 and jjangminii July 13, 2026 15:52
@github-actions github-actions Bot added the ⏰ Timo-web Timo 웹 서비스 label Jul 13, 2026
@github-actions github-actions Bot added ✨ Feature 새로운 기능(기능성) 구현 ♦️ 민아 민아상 labels Jul 13, 2026
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 48 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: a4372573-22fc-489c-bcd4-4b6d48e570b4

📥 Commits

Reviewing files that changed from the base of the PR and between cbeb6a5 and fd70993.

📒 Files selected for processing (9)
  • apps/timo-web/app/[locale]/(main)/settings/_hooks/useSettingsProfile.ts
  • apps/timo-web/app/[locale]/(main)/settings/_types/profile-type.ts
  • apps/timo-web/app/[locale]/layout.tsx
  • apps/timo-web/app/[locale]/oauth/callback/_containers/OauthCallbackContainer.tsx
  • apps/timo-web/app/[locale]/page.tsx
  • apps/timo-web/providers/locale/LanguageSyncProvider.tsx
  • apps/timo-web/proxy.ts
  • apps/timo-web/queries/use-my-profile.ts
  • apps/timo-web/types/user-profile-type.ts

Walkthrough

인증 상태를 localStorage와 auth store에 연동하고, 루트 인증 초기화 및 로그인·온보딩 라우트 가드를 추가했습니다. 홈에서는 프로필 언어에 맞춰 로케일을 동기화하며, 날짜·duration 관련 유틸리티를 도메인별 경로로 재구성했습니다.

Changes

인증 및 유틸리티 개편

Layer / File(s) Summary
도메인별 날짜·duration 유틸리티와 소비자 갱신
apps/timo-web/utils/date/*, apps/timo-web/utils/duration/*, apps/timo-web/app/..., apps/timo-web/components/...
날짜·요일·시간·duration 유틸리티를 추가하고 기존 사용처의 import 경로를 새 도메인별 모듈로 변경했습니다.
토큰 및 온보딩 상태 영속화
apps/timo-web/utils/auth/token-manager.ts, apps/timo-web/stores/auth/useAuthStore.ts
access token과 온보딩 완료 상태를 localStorage에서 읽고 저장하며, 인증 초기화 상태와 관련 store 액션을 추가했습니다.
인증 초기화와 라우트 가드
apps/timo-web/providers/auth/*, apps/timo-web/app/[locale]/layout.tsx, apps/timo-web/app/[locale]/login/page.tsx, apps/timo-web/app/[locale]/onboarding/page.tsx
루트에서 토큰 재발급을 초기화하고, 인증 상태에 따라 로그인 및 온보딩 접근을 제한하도록 provider 구성을 변경했습니다.
OAuth 및 온보딩 완료 상태 반영
apps/timo-web/app/[locale]/oauth/callback/..., apps/timo-web/app/[locale]/onboarding/...
OAuth 성공과 온보딩 완료 시 auth store의 온보딩 완료 상태를 갱신한 뒤 기존 홈 이동을 수행합니다.
홈 프로필 언어와 로케일 동기화
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/...
프로필 조회 훅과 언어 동기화 컨테이너를 추가해 지원 언어가 현재 로케일과 다를 때 홈 경로를 해당 로케일로 교체합니다.

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
Loading

Possibly related issues

  • Team-Timo/Timo-client 179 — 토큰 저장 방식, 인증 라우트 가드, 홈 언어 동기화, 도메인별 유틸리티 재구성 목표를 직접 다룹니다.
  • Team-Timo/Timo-client 166 — auth store의 인증·온보딩 상태와 클라이언트 라우트 가드 변경과 관련됩니다.

Possibly related PRs

Suggested reviewers: yumin-kim2, jjangminii, ehye1

🚥 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
Linked Issues check ✅ Passed 연결된 #179의 핵심 요구사항인 localStorage 전환, 가드 추가, isInitialized 기반 초기화, 언어 동기화, utils 재구성이 모두 반영되었습니다.
Out of Scope Changes check ✅ Passed 요구사항 밖의 독립적인 기능 추가는 보이지 않고, 변경은 모두 인증·언어 동기화·유틸 재구성 범위에 맞습니다.
Title check ✅ Passed 인증 세션, 라우트 가드, 언어 동기화 추가라는 핵심 변경을 잘 요약했습니다.
Description check ✅ Passed 설명이 실제 변경 사항인 토큰 영속화, 가드 추가, 전역 언어 동기화와 일치합니다.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/web/179-auth-route-guards

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 203.14 kB 🔴 409.02 kB
/[locale]/today 187.19 kB 🔴 393.07 kB
/[locale]/focus 159.31 kB 🔴 365.19 kB
/[locale]/settings/account 0 B 🟡 205.88 kB
/[locale]/settings 167.91 kB 🔴 373.79 kB
/[locale]/statistics 155.85 kB 🔴 361.73 kB
/[locale]/[...rest] 0 B 🟡 205.88 kB
/[locale]/login 215.48 kB 🔴 421.36 kB
/[locale]/oauth/callback 122.13 kB 🟡 328.01 kB
/[locale]/onboarding 234.91 kB 🔴 440.79 kB
/[locale] 121.51 kB 🟡 327.39 kB
/[locale]/policy 127.61 kB 🟡 333.49 kB

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

Lighthouse — timo-web
URL Perf A11y LCP CLS TBT
/en/home 🟡 71 🟢 95 🔴 15.5s 🟢 0.000 🟡 210ms
/en/today 🟡 72 🟢 95 🔴 15.3s 🟢 0.000 🟢 190ms
/en/focus 🔴 69 🟢 95 🔴 15.0s 🟢 0.000 🟡 258ms
/en/statistics 🔴 64 🟢 95 🔴 15.0s 🟢 0.000 🟡 413ms

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 변환 권장

측정 커밋: 6ab67d2

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

📥 Commits

Reviewing files that changed from the base of the PR and between 2b0bae8 and cbeb6a5.

📒 Files selected for processing (39)
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeLanguageSyncContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/todo-card/HomeDayHeaderContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-view-mode.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_mocks/home-view-mock.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_mocks/todo-mock.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_queries/use-my-profile.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_utils/date.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_utils/day-of-week.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/page.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayDateHeaderContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsx
  • apps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsx
  • apps/timo-web/app/[locale]/(main)/statistics/_components/StatisticsCalendar.tsx
  • apps/timo-web/app/[locale]/(main)/statistics/_containers/StatisticsContainer.tsx
  • apps/timo-web/app/[locale]/layout.tsx
  • apps/timo-web/app/[locale]/login/page.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/components/layout/sidebar/time/TimeSidebarHeader.tsx
  • apps/timo-web/components/layout/sidebar/time/TimeboxPanel.tsx
  • apps/timo-web/components/timer/TimerCompleteModalPanel.tsx
  • apps/timo-web/components/timer/TimerStopModalPanel.tsx
  • apps/timo-web/components/todo-modal/CreateTodoModalContent.tsx
  • apps/timo-web/providers/auth/AuthGuardProvider.tsx
  • apps/timo-web/providers/auth/AuthProvider.tsx
  • apps/timo-web/providers/auth/GuestGuardProvider.tsx
  • apps/timo-web/providers/auth/OnboardingCompletedGuardProvider.tsx
  • apps/timo-web/stores/auth/useAuthStore.ts
  • apps/timo-web/types/.gitkeep
  • apps/timo-web/utils/.gitkeep
  • apps/timo-web/utils/auth/token-manager.ts
  • apps/timo-web/utils/date/date.ts
  • apps/timo-web/utils/date/get-day-of-week-key.ts
  • apps/timo-web/utils/date/get-hour-label.ts
  • apps/timo-web/utils/duration/convert-duration-to-minutes.ts
  • apps/timo-web/utils/duration/convert-duration-to-time-text.ts
  • apps/timo-web/utils/duration/format-duration-label.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.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 2b0bae8 and cbeb6a5.

📒 Files selected for processing (39)
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeLanguageSyncContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/todo-card/HomeDayHeaderContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_hooks/use-home-view-mode.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_mocks/home-view-mock.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_mocks/todo-mock.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_queries/use-my-profile.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_utils/date.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_utils/day-of-week.ts
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/page.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayDateHeaderContainer.tsx
  • apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsx
  • apps/timo-web/app/[locale]/(main)/focus/_containers/FocusSessionContainer.tsx
  • apps/timo-web/app/[locale]/(main)/statistics/_components/StatisticsCalendar.tsx
  • apps/timo-web/app/[locale]/(main)/statistics/_containers/StatisticsContainer.tsx
  • apps/timo-web/app/[locale]/layout.tsx
  • apps/timo-web/app/[locale]/login/page.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/components/layout/sidebar/time/TimeSidebarHeader.tsx
  • apps/timo-web/components/layout/sidebar/time/TimeboxPanel.tsx
  • apps/timo-web/components/timer/TimerCompleteModalPanel.tsx
  • apps/timo-web/components/timer/TimerStopModalPanel.tsx
  • apps/timo-web/components/todo-modal/CreateTodoModalContent.tsx
  • apps/timo-web/providers/auth/AuthGuardProvider.tsx
  • apps/timo-web/providers/auth/AuthProvider.tsx
  • apps/timo-web/providers/auth/GuestGuardProvider.tsx
  • apps/timo-web/providers/auth/OnboardingCompletedGuardProvider.tsx
  • apps/timo-web/stores/auth/useAuthStore.ts
  • apps/timo-web/types/.gitkeep
  • apps/timo-web/utils/.gitkeep
  • apps/timo-web/utils/auth/token-manager.ts
  • apps/timo-web/utils/date/date.ts
  • apps/timo-web/utils/date/get-day-of-week-key.ts
  • apps/timo-web/utils/date/get-hour-label.ts
  • apps/timo-web/utils/duration/convert-duration-to-minutes.ts
  • apps/timo-web/utils/duration/convert-duration-to-time-text.ts
  • apps/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

isSupportedLocalei18n/routing.ts의 locale 목록을 하드코딩으로 중복합니다.

routing.tslocales: ["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

HomeLanguageSyncContainererrorFallback 누락 — 프로필 API 실패 시 홈페이지 전체 중단 위험

useSuspenseQuery는 쿼리 실패 시 에러를 throw하여 가장 가까운 에러 바운더리로 전파합니다. 현재 AsyncBoundaryerrorFallback이 없어 <ErrorBoundary>가 래핑되지 않으므로, 프로필 조회 실패 시 에러가 상위로 전파되어 홈페이지 전체가 깨질 수 있습니다.

HomeLanguageSyncContainernull을 반환하는 비핵심 사이드이펙트 컴포넌트이므로, 언어 동기화 실패가 홈페이지 렌더링을 방해해서는 안 됩니다. 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 -S

Repository: 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-web

Repository: 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.ts

Repository: 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/generated

Repository: 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.ts

Repository: Team-Timo/Timo-client

Length of output: 2012


hasChecked를 먼저 세팅해서 reissue 재진입을 막아주세요.
좋은 흐름입니다. 지금은 accessToken이 초기엔 있어도, 이후 axios 인터셉터의 clearAccessToken()으로 토큰이 null이 되면 useEffect가 다시 돌면서 useReissue가 한 번 더 호출될 수 있습니다. “reissue 1회”가 목적이라면 hasChecked.current = trueaccessToken 분기보다 앞에 두는 편이 맞습니다.
React useEffect의 재실행 조건은 공식 문서도 같이 보면 좋습니다: 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에서 useGetMyProfile hook의 data.onboardingCompletedisLoading을 직접 사용하는 것을 고려해 보세요.

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

clearAccessTokenonboardingCompleted까지 함께 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/removeItemQuotaExceededErrorSecurityError를 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()}`
  );
}
JS

Repository: 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 -S

Repository: 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 -S

Repository: Team-Timo/Timo-client

Length of output: 16300


타임존 영향 없이 formatDate를 계산하세요
좋은 분리입니다. 다만 todo.datez.iso.date()/yyyy-MM-ddnew 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 -S

Repository: 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 -n

Repository: 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 -n

Repository: 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를 수정했습니다
@kimminna kimminna changed the title [FEAT] 인증 토큰 저장 방식 개선 및 라우트 접근 제어 추가 [FEAT] 인증 세션·라우트 가드 개선 및 언어 동기화 추가 Jul 13, 2026
…route-guards

# Conflicts:
#	apps/timo-web/app/[locale]/oauth/callback/_containers/OauthCallbackContainer.tsx

@ehye1 ehye1 left a comment

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.

기존 방식에서 미들웨어는 쿠키, 클라이언트는 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 쿼리들과 동일한 패턴으로 맞췄습니다
@kimminna
kimminna merged commit 297538e into develop Jul 13, 2026
10 checks passed
@kimminna
kimminna deleted the feat/web/179-auth-route-guards branch July 13, 2026 17:12
@kimminna kimminna mentioned this pull request Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 새로운 기능(기능성) 구현 ⏰ Timo-web Timo 웹 서비스 ♦️ 민아 민아상

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 인증 토큰 저장 방식 개선 및 라우트 접근 제어 추가

2 participants