Skip to content

Comments

fix: 계정 간 본인인증 상태 오염 및 Hydration mismatch 콘솔 경고#386

Merged
Hyeonjun0527 merged 1 commit intodevelopfrom
fix/hydration-n-phone-verification
Feb 8, 2026
Merged

fix: 계정 간 본인인증 상태 오염 및 Hydration mismatch 콘솔 경고#386
Hyeonjun0527 merged 1 commit intodevelopfrom
fix/hydration-n-phone-verification

Conversation

@Hyeonjun0527
Copy link
Member

@Hyeonjun0527 Hyeonjun0527 commented Feb 8, 2026

fix: SSR/CSR Hydration 불일치 및 UX 개선

🎯 Summary

서버 사이드 렌더링(SSR)과 클라이언트 사이드 렌더링(CSR) 간의 hydration 불일치 문제를 해결하고, 토스트 및 공유 기능의 사용자 경험을 개선했습니다.

📋 주요 변경사항

1. Auth Hydration 아키텍처 개선 ⚡️

문제:

  • useAuth()가 클라이언트에서 쿠키를 읽기 전까지 undefined를 반환하여 SSR/CSR 간 불일치 발생

  • Hydration mismatch로 인한 콘솔 경고 및 잠재적 렌더링 이슈

  • 인증 상태 확인 시점이 명확하지 않아 race condition 발생 가능

해결:

  • AuthHydrationProvider 도입: 서버에서 읽은 initialAccessToken을 클라이언트로 전달하여 초기 렌더링 일관성 보장

  • useAuth() 개선:

  • isHydrated 상태 추가로 hydration 완료 여부 추적

  • useStateuseEffect로 클라이언트 쿠키 읽기를 안전하게 처리

  • useAuthReady() 훅 추가:

  • isAuthReady = isHydrated && isAuthenticated로 인증 체크 시점 명확화

  • 컴포넌트에서 안전하게 인증 상태 확인 가능

영향 범위:

  • 30개 이상의 컴포넌트를 useAuthReady()로 업데이트

  • Auth-gated 쿼리 (notification-api, group-study-member-api) 수정

  • 모든 레이아웃 ((admin), (landing), (service)) 업데이트

업데이트된 컴포넌트: (전체 목록)

  • 페이지: group-study-list-page, mentoring-list-page, premium-study-list-page, my-study/page

  • 위젯: todo-list, my-page/sidebar

  • 기능: start-study-modal, reservation-list, study-matching-toggle, phone-verification-modal

  • 그룹 스터디: apply-group-study-modal, group-study-form-modal, group-study-member-item

  • 기타: voting-detail-view, tab-navigation, admin-sidebar

파일:

  • 추가: src/hooks/common/auth-hydration-context.tsx

  • 수정: src/hooks/common/use-auth.ts (+ useAuthReady() 훅)

  • 수정: src/providers/index.tsx (AuthHydrationProvider 적용)

  • 문서: docs/HYDRATION_AUTH_CHECKLIST.md

2. Phone Verification Store 개선 🔐

문제:

  • 계정 간 본인인증 상태 오염 가능 (A 계정 인증 → B 계정 로그인 시 인증된 것처럼 보임)

  • verifiedAt이 Date 객체로 localStorage에 직렬화 불가능

  • Hydration 완료 여부 추적 불가

해결:

  • memberId 필드 추가: 인증 상태가 특정 계정에만 유효하도록 보장

  • verifiedAt을 Date → ISO string으로 변경하여 직렬화 문제 해결

  • hasHydrated 상태 추가 및 onRehydrateStorage 콜백 구현

  • partialize로 필요한 필드만 persist 대상으로 지정

파일:

  • 수정: src/features/phone-verification/model/store.ts

3. Toast 컴포넌트 UX 개선 🎨

문제:

  • z-index 충돌로 다른 요소에 가려질 수 있음

  • 퇴장 애니메이션 없이 갑자기 사라짐

  • 렌더링 타이밍 이슈

해결:

  • React Portal 사용: document.body에 직접 렌더링하여 z-index 문제 완전 해결

  • 부드러운 애니메이션 추가:

  • 진입: toast-enter (300ms fade-in + slide-down)

  • 퇴장: toast-exit (200ms fade-out + slide-up)

  • 상태 관리 개선: isRendered, isExiting 분리로 애니메이션 완료까지 DOM 유지

파일:

  • 수정: src/components/ui/toast.tsx

  • 수정: src/app/global.css (keyframes 애니메이션 정의)

4. Voting 공유 기능 개선 📤

문제:

  • Web Share API 실패 시 fallback 로직 미흡

  • Clipboard API 실패 시 사용자 피드백 부족

  • 에러 처리가 복잡하고 불명확

해결:

  • 3단계 Fallback 전략 구현:
  1. navigator.clipboard.writeText() (최신 브라우저)

  2. document.execCommand('copy') (레거시 지원)

  3. window.prompt() (수동 복사)

  • 각 단계별 성공/실패 처리 명확화

  • Toast 메시지 동적 처리 (openShareToast)

파일:

  • 수정: src/components/voting/voting-detail-view.tsx

5. 유틸리티 및 문서 추가 📚

새 유틸리티:

  • src/utils/safe-server-prefetch.ts: 서버 사이드 prefetch 에러를 안전하게 처리

새 문서:

  • docs/HYDRATION_AUTH_CHECKLIST.md: Auth hydration 업데이트 추적 체크리스트

  • docs/PRODUCTION_DEPLOYMENT_VERIFICATION.md: 프로덕션 배포 검증 가이드 (작성 중)

🧪 테스트 시나리오

Auth Hydration

  • 비로그인 상태에서 페이지 접근 시 hydration 경고 없음

  • 로그인 상태에서 페이지 새로고침 시 인증 상태 즉시 반영

  • 서버 렌더링과 클라이언트 렌더링 결과 일치 확인

  • 토큰 만료 시 자동 갱신 후 상태 정상 동작

Phone Verification

  • A 계정 인증 → 로그아웃 → B 계정 로그인 시 미인증 상태로 표시

  • 본인인증 완료 후 새로고침해도 인증 상태 유지

  • memberId 불일치 시 자동으로 미인증 처리

Toast

  • Toast가 모든 요소보다 위에 표시됨 (z-index 문제 없음)

  • 진입/퇴장 애니메이션이 부드럽게 작동

  • 여러 Toast가 연속으로 표시될 때 정상 동작

Voting 공유

  • 최신 브라우저에서 Clipboard API로 복사 성공

  • Clipboard API 미지원 브라우저에서 fallback 동작

  • 복사 성공 시 Toast 표시

  • 수동 복사 프롬프트에서 취소 시 Toast 미표시

📊 영향 분석

변경된 파일 통계

  • 51개 파일 수정 (+1,768줄, -360줄)

  • 3개 파일 추가 (문서 2개, 유틸리티 1개)

영향받는 도메인

  • 인증 (Auth) ⚡️ 높음

  • 본인인증 (Phone Verification) ⚡️ 높음

  • UI 컴포넌트 (Toast) 🔧 중간

  • 커뮤니티 (Voting 공유) 🔧 중간

🔄 Migration Guide

기존 useAuth() 사용하던 컴포넌트

Before:

const { data, isAuthenticated } = useAuth();

  

if (!isAuthenticated) return null; // ❌ Hydration mismatch

After:

const { memberId, isAuthReady } = useAuthReady();

  

if (!isAuthReady) return null; // ✅ Hydration-safe

Phone Verification 체크

Before:

const { isVerified } = usePhoneVerificationStore();

// 계정 간 오염 가능

After:

const { isVerified } = usePhoneVerificationStatus(memberId);

// memberId 기반 안전한 체크

🚀 배포 전 확인사항

  • yarn typecheck 통과

  • yarn lint 통과

  • yarn build 성공

  • Hydration 경고 없음 확인

  • 개발/스테이징 환경 테스트 완료

📝 Breaking Changes

없음 - 기존 API 호환성 유지됨

🔗 관련 이슈

  • Fixes: Hydration mismatch 콘솔 경고

  • Fixes: 계정 간 본인인증 상태 오염

  • Improves: Toast z-index 및 애니메이션

  • Improves: Voting 공유 신뢰성


🤖 Co-Authored-By: Claude Sonnet 4.5 noreply@anthropic.com

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 글로벌 토스트 시스템 확장 및 복사/공유 토스트 추가
    • 주요 서비스 페이지에 토스트 렌더링 추가
  • 버그 수정

    • 전화 인증 모달 닫기 시 상태 초기화 범위 확대
    • 인증/권한 관련 표시·동작의 준비(readiness) 처리 개선
  • UI/UX 개선

    • 토스트 진입/종료 애니메이션 추가
    • 전화 인증에 로딩·오류 피드백 추가 및 안내 메시지 강화
    • 일부 목록의 표시 항목(모집인원 중복) 정리

@Hyeonjun0527 Hyeonjun0527 self-assigned this Feb 8, 2026
@vercel
Copy link

vercel bot commented Feb 8, 2026

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

Project Deployment Actions Updated (UTC)
study-platform-client-dev Ready Ready Preview, Comment Feb 8, 2026 4:14pm

@coderabbitai
Copy link

coderabbitai bot commented Feb 8, 2026

📝 Walkthrough

Walkthrough

서버 사이드에서 accessToken을 읽어 초기화 토큰을 MainProvider에 주입하고, 인증 수화(hydration)/준비 상태 훅을 도입하여 useAuth → useAuthReady로 마이그레이션했으며, 휴대폰 인증 상태를 서버 중심으로 동기화하고 토스트를 포탈로 렌더링하도록 UI와 레이아웃을 변경했습니다.

Changes

Cohort / File(s) Summary
Layouts — server token injection
src/app/(admin)/layout.tsx, src/app/(landing)/layout.tsx, src/app/(service)/layout.tsx
레이아웃을 async로 변경하고 getServerCookie('accessToken')로 초기 accessToken을 읽어 MainProviderinitialAccessToken으로 전달.
Auth hydration & provider
src/hooks/common/auth-hydration-context.tsx, src/hooks/common/use-auth.ts, src/providers/index.tsx
AuthHydrationProvider 추가, useAuth에 isHydrated 추가, 새 훅 useAuthReady 도입, MainProvider/ProviderProps가 initialAccessToken을 수신하도록 변경.
useAuth → useAuthReady migration
src/components/**, src/features/**, src/hooks/queries/**, src/widgets/**
광범위한 컴포넌트에서 useAuthuseAuthReady로 변경하고 isAuthReady/memberId 기반 게이팅으로 수정(여러 파일에 걸친 일관된 인증-readiness 전파).
Phone verification refactor
src/features/phone-verification/**, src/features/my-page/ui/profile.tsx, src/features/study/**, src/widgets/home/todo-list.tsx
zustand store 구조 변경(memberId, hasHydrated, verifiedAt string 등), usePhoneVerificationStatus를 서버 우선 동기화로 재작성(인수로 memberId, isLoading/isError/setVerified 반환).
GlobalToast & toast refactor
src/components/ui/toast.tsx, src/app/global.css, src/components/ui/global-toast*, src/app/(service)/**
Toast를 createPortal로 렌더링하고 enter/exit 애니메이션 추가. GlobalToast 컴포넌트를 여러 레이아웃/페이지에 삽입. CSS에 toast-enter/toast-exit 키프레임 추가.
New layouts / UI placement
src/app/(service)/(my)/layout.tsx, src/app/(service)/group-study/layout.tsx, src/app/(service)/premium-study/layout.tsx, src/app/(service)/home/page.tsx
GlobalToast 삽입, 새로운 GroupStudyLayout·PremiumStudyLayout 추가 및 기존 레이아웃에 토스트 배치 조정.
Safe server prefetch util
src/utils/safe-server-prefetch.ts, src/features/study/one-to-one/.../community-tab.tsx, .../archive-tab.tsx, .../hall-of-fame-tab.tsx
safeServerPrefetch 유틸 추가로 서버 프리페치 오류를 캡처하고 로깅; 일부 초기 데이터 로드 경로에 적용.
Miscellaneous behavioral tweaks
src/components/voting/voting-detail-view.tsx, src/components/summary/study-info-summary.tsx, src/entities/user/model/use-user-profile-query.ts, src/features/auth/model/use-auth-mutation.ts, src/features/phone-verification/ui/phone-verification-modal.tsx, src/features/study/group/channel/ui/comment-section.tsx
공통: GTM/payload 변경, 중복 정보 제거, staleTime 조정, 로그아웃 시 phone-verification 리셋, 모달 클린업 확장, 댓글 총합 변수 추가 등.

Sequence Diagram(s)

sequenceDiagram
    participant Layout as Layout (server)
    participant Server as ServerRuntime
    participant Cookie as getServerCookie
    participant Provider as MainProvider
    participant AuthHydration as AuthHydrationProvider

    Layout->>Server: 실행(SSR) (async layout)
    Server->>Cookie: getServerCookie('accessToken')
    Cookie-->>Server: accessToken | undefined
    Server->>Provider: render MainProvider(initialAccessToken)
    Provider->>AuthHydration: wrap with initialAccessToken
    AuthHydration-->>Provider: provide token via context
Loading
sequenceDiagram
    participant Component as Client Component
    participant AuthHook as useAuthReady
    participant AuthHydration as AuthHydrationProvider (context)
    participant Store as PhoneVerificationStore / UserProfileAPI

    Component->>AuthHook: call useAuthReady()
    AuthHook->>AuthHydration: read initialAccessToken & isHydrated
    AuthHook->>Store: obtain memberId / decoded token
    AuthHook-->>Component: { isAuthReady, memberId, ... }
Loading
sequenceDiagram
    participant Modal as PhoneVerificationModal
    participant StatusHook as usePhoneVerificationStatus
    participant Server as UserProfile API
    participant Store as Zustand phone-verification store

    Modal->>StatusHook: usePhoneVerificationStatus(memberId)
    StatusHook->>Server: fetch userProfile (if memberId)
    Server-->>StatusHook: verification data
    StatusHook->>Store: sync server data into store
    StatusHook-->>Modal: return { isVerified, isLoading, isError, setVerified }
Loading
sequenceDiagram
    participant ToastComp as Toast Component
    participant DOM as document.body (portal)
    participant CSS as global.css animations

    ToastComp->>ToastComp: isVisible true -> isRendered true
    ToastComp->>DOM: createPortal(toastElement)
    DOM->>CSS: apply .toast-enter animation (300ms)
    ToastComp->>ToastComp: onClose -> add .toast-exit
    DOM->>CSS: apply .toast-exit animation (200ms)
    ToastComp->>DOM: remove portal element
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

refactoring

Poem

🐰 서버에서 토큰 한 줌 주워와,
준비된 마음에 인증을 심고,
전화번호는 서버와 손잡아,
토스트는 포탈로 살포시 떠오르네.
모두 준비됐네, 깡총깡총 축하! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 주요 변경사항인 계정 간 본인인증 상태 오염(phone verification cross-account contamination) 및 Hydration mismatch 문제를 명확히 설명하고 있으며, 광범위한 파일 변경(51개 파일)과 여러 기능 개선(Auth Hydration, Toast UX, Phone Verification store 등)의 핵심을 잘 요약하고 있습니다.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/hydration-n-phone-verification

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

- Fixes: 계정 간 본인인증 상태 오염

- Improves: Toast z-index 및 애니메이션

- Improves: Voting 공유 신뢰성

fix:typecheck fix
@Hyeonjun0527 Hyeonjun0527 force-pushed the fix/hydration-n-phone-verification branch from 7faf75f to 7021d6c Compare February 8, 2026 16:11
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/entities/user/model/use-user-profile-query.ts (1)

10-16: ⚠️ Potential issue | 🟠 Major

staleTime을 프로젝트 가이드라인인 60초로 변경하세요

Line 15의 staleTime: 0은 프로젝트 가이드라인("Use TanStack Query for server state with a default staleTime of 60 seconds")을 따르지 않습니다. 이 설정으로 인해 쿼리는 마운트, 윈도우 포커스, 네트워크 재연결 시 불필요한 재요청이 발생합니다. staleTime: 60000 (60초)으로 변경해 주세요.

src/components/voting/voting-detail-view.tsx (1)

324-332: ⚠️ Potential issue | 🟡 Minor

프로덕션 코드에서 디버깅용 console.log를 제거하세요.

디버깅용 로그가 프로덕션 코드에 남아있습니다. 브라우저 콘솔에 내부 상태가 노출되며, 불필요한 성능 오버헤드가 발생합니다.

🧹 제거 제안
-  // 디버깅용 로그 추가
-  console.log('Voting Data:', {
-    myVote: voting.myVote,
-    hasVoted,
-    endsAt: voting.endsAt,
-    rawIsActive: voting.isActive,
-    isActiveByEndsAt,
-    isActive,
-  });
-
   const showVoteOptions = isAuthReady && !hasVoted && isActive;
🤖 Fix all issues with AI agents
In `@src/app/`(landing)/layout.tsx:
- Around line 65-67: The Clarity initialization inside the async server-only
layout never runs because typeof window !== 'undefined' is always false on the
server; move the Clarity.init(CLARITY_PROJECT_ID) call into a client-side
context: create a client component (or a useEffect within a client component)
that checks CLARITY_PROJECT_ID and calls Clarity.init, then import/render that
client component from your layout; update references to Clarity.init and
CLARITY_PROJECT_ID in the new client component so initialization happens only on
the client.

In `@src/components/ui/toast.tsx`:
- Around line 27-47: Summary: useEffect reads isRendered but doesn't include it
in the dependency array, breaking exhaustive-deps. Fix: update the dependency
array of the useEffect that manages isVisible/isRendered to include isRendered
(so it becomes [isVisible, isRendered, duration, onClose]); you can omit
exitDuration if it is a true constant (200). Keep the same timer setup and
cleanup logic in the effect; no other logic changes are needed. Ensure you
reference the existing symbols useEffect, isVisible, isRendered, duration,
exitDuration, and onClose when making the change.

In `@src/features/phone-verification/model/store.ts`:
- Around line 40-50: The partialize callback in phone-verification-storage
returns an object whose properties are inferred as any; fix this by declaring an
explicit return type for partialize (or casting the returned object) to the
store slice type containing isVerified, phoneNumber, verifiedAt, and memberId so
TypeScript can check types; update the partialize signature in
src/features/phone-verification/model/store.ts (the partialize function) to
return e.g. Partial<PhoneVerificationState> (or the specific typed shape)
ensuring each property matches the declared types of isVerified, phoneNumber,
verifiedAt, and memberId.

In `@src/features/study/group/ui/group-study-form-modal.tsx`:
- Around line 101-103: The refineStudyDetail helper returns undefined while
isGroupStudyLoading is true which causes GroupStudyForm to receive
defaultValues={undefined} and never update after load; fix by guarding render or
ensuring form reset on prop change: either render GroupStudyForm only when
isGroupStudyLoading is false and refineStudyDetail(value) yields the DTO, or in
GroupStudyForm add a useEffect that watches defaultValues and calls
reset(defaultValues) when they change; reference refineStudyDetail,
isGroupStudyLoading, GroupStudyForm, defaultValues and reset in your change.
🧹 Nitpick comments (9)
src/features/study/participation/ui/reservation-list.tsx (2)

99-106: 본인인증 상태 체크 로직이 적절합니다.

로딩 중 조기 반환과 에러 시 알림 표시가 적절합니다. 다만, 사용자 경험 향상을 위해 isVerificationLoading 상태일 때 버튼에 시각적 피드백(예: disabled 상태 또는 스피너)을 추가하는 것을 고려해 보세요.


46-49: memberId 타입 체크 일관성 검토

Line 47에서 memberId !== null 체크를 사용하고 있지만, useAuthReady는 hydration 전에 undefined를 반환합니다. !!memberId로 간소화하면 nullundefined 모두를 처리할 수 있습니다.

♻️ 제안된 수정
 const firstMemberId = useMemo(
-    () => (autoMatching && memberId !== null ? memberId : null),
+    () => (autoMatching && memberId ? memberId : null),
     [autoMatching, memberId],
   );
src/features/study/participation/ui/start-study-modal.tsx (1)

142-162: 중복된 알림 메시지 상수화 권장

handleOpenChange(Line 115)와 useEffect(Line 145)에서 동일한 에러 메시지가 사용됩니다. 메시지를 상수로 추출하면 유지보수성이 향상됩니다.

♻️ 제안된 수정
+const VERIFICATION_ERROR_MESSAGE = '인증 상태를 확인할 수 없습니다. 잠시 후 다시 시도해주세요.';
+
 export default function StartStudyModal({
   // ...
 }) {
   // handleOpenChange 내부
   if (isOpen && isError) {
-    alert('인증 상태를 확인할 수 없습니다. 잠시 후 다시 시도해주세요.');
+    alert(VERIFICATION_ERROR_MESSAGE);
     return;
   }
   
   // useEffect 내부
   if (isError) {
-    alert('인증 상태를 확인할 수 없습니다. 잠시 후 다시 시도해주세요.');
+    alert(VERIFICATION_ERROR_MESSAGE);
     // ...
   }
src/app/(service)/(my)/layout.tsx (1)

2-2: GlobalToast 컴포넌트를 루트 레이아웃으로 이동하는 것을 권장합니다.

Portal 구현은 올바릅니다(Toast 컴포넌트가 createPortaldocument.body에 렌더링). 그러나 검증 결과 GlobalToast가 4개 레이아웃/페이지에서 중복되고 있습니다:

  • src/app/(service)/(my)/layout.tsx
  • src/app/(service)/group-study/layout.tsx
  • src/app/(service)/home/page.tsx
  • src/app/(service)/premium-study/layout.tsx

각 위치에서 동일한 useToastStore 상태를 구독하는 독립적인 컴포넌트 인스턴스가 4개 생성되므로 불필요한 렌더링 오버헤드가 발생합니다. 단일 루트 레이아웃에서 한 번만 렌더링하는 것이 더 효율적입니다.

src/widgets/my-page/sidebar.tsx (1)

13-16: memberId ?? 0 폴백 사용 시 주의 필요

useUserProfileQueryenabled: !!memberId 조건이 있어 memberId가 0이면 쿼리가 실행되지 않습니다. 하지만 isAuthReady를 활용하여 인증 상태를 더 명확하게 체크하면 의도가 더 분명해집니다.

- const { memberId } = useAuthReady();
+ const { memberId, isAuthReady } = useAuthReady();
  const { mutateAsync: logout } = useLogoutMutation();

- const { data: profile } = useUserProfileQuery(memberId ?? 0);
+ const { data: profile } = useUserProfileQuery(memberId ?? 0);
+ // 또는 조건부 렌더링에 isAuthReady 활용 고려

현재 구현도 동작하지만, 다른 컴포넌트들(notification-api.ts 등)에서 isAuthReady로 쿼리를 게이팅하는 패턴과 일관성을 맞추는 것을 권장합니다.

src/components/section/group-study-info-section.tsx (1)

29-31: memberId 이중 소스 사용에 대한 참고

authMemberId(useAuthReady)와 memberId(useUserStore)가 동시에 사용되고 있습니다. 현재 각각의 용도가 다르지만(GTM vs isLeader 체크), 향후 리팩토링 시 useAuthReadymemberId로 통일하는 것을 고려해볼 수 있습니다.

src/components/home/tab-navigation.tsx (1)

64-72: 중복된 memberId 확인 로직을 단순화할 수 있습니다.

useAuthReady()에서 반환하는 memberId는 이미 hydration 상태를 고려하므로, getCookie('memberId') 폴백은 불필요해 보입니다. isAuthReadyfalse일 때 최종적으로 canViewHistoryfalse가 되므로 현재 구현은 안전하지만, 코드 단순화를 고려해볼 수 있습니다.

♻️ 단순화 제안
   useEffect(() => {
     if (!isHydrated) {
       setCanViewHistory(false);
 
       return;
     }
-    const hasMemberId = !!memberId || !!getCookie('memberId');
-    setCanViewHistory(isAuthReady && hasMemberId);
+    setCanViewHistory(isAuthReady && !!memberId);
   }, [isAuthReady, isHydrated, memberId]);
src/features/study/group/ui/group-study-member-item.tsx (1)

34-42: isMe 계산에 isAuthReady 가드 추가를 고려해보세요.

isLeaderisAuthReady로 가드되어 있지만, Line 41의 isMe는 그렇지 않습니다. myIdundefined일 때 member.id === undefinedfalse가 되므로 실제로 안전하지만, 아래 GreetingBoxisMe 계산(Line 222)과 일관성을 위해 동일한 패턴 적용을 고려해볼 수 있습니다.

♻️ 일관성 개선 제안
-  const isMe = member.id === myId;
+  const isMe = isAuthReady && member.id === myId;
   const isLeader = isAuthReady && leaderId === myId;
src/features/study/group/ui/group-study-form-modal.tsx (1)

70-86: 인증 상태 로딩 중 무반응 UX를 보완하는 게 좋습니다.
현재 로딩 중에는 클릭해도 피드백이 없어 혼란 가능성이 있습니다. 트리거 비활성화나 토스트 안내를 고려해주세요.

Comment on lines 65 to 67
if (typeof window !== 'undefined' && CLARITY_PROJECT_ID) {
Clarity.init(CLARITY_PROJECT_ID);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

서버 컴포넌트에서 Clarity 초기화 코드가 실행되지 않습니다.

async function 레이아웃은 서버에서만 실행되므로 typeof window !== 'undefined' 조건이 항상 false가 되어 Clarity.init()이 절대 호출되지 않습니다. Clarity 초기화는 클라이언트 컴포넌트나 별도의 useEffect 훅으로 이동해야 합니다.

🤖 Prompt for AI Agents
In `@src/app/`(landing)/layout.tsx around lines 65 - 67, The Clarity
initialization inside the async server-only layout never runs because typeof
window !== 'undefined' is always false on the server; move the
Clarity.init(CLARITY_PROJECT_ID) call into a client-side context: create a
client component (or a useEffect within a client component) that checks
CLARITY_PROJECT_ID and calls Clarity.init, then import/render that client
component from your layout; update references to Clarity.init and
CLARITY_PROJECT_ID in the new client component so initialization happens only on
the client.

Comment on lines 27 to 47
useEffect(() => {
if (isVisible) {
setIsRendered(true);
setIsExiting(false);
const timer = setTimeout(() => {
onClose();
}, duration);

return () => clearTimeout(timer);
}

if (isRendered) {
setIsExiting(true);
const timer = setTimeout(() => {
setIsRendered(false);
setIsExiting(false);
}, exitDuration);

return () => clearTimeout(timer);
}
}, [isVisible, duration, onClose]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/components/ui/toast.tsx

Repository: code-zero-to-one/study-platform-client

Length of output: 2620


🏁 Script executed:

fd -e .eslintrc -e .eslintrc.js -e .eslintrc.json -e .eslintrc.yml

Repository: code-zero-to-one/study-platform-client

Length of output: 64


🏁 Script executed:

rg "react-hooks/exhaustive-deps" --type json --type javascript --type typescript

Repository: code-zero-to-one/study-platform-client

Length of output: 116


🏁 Script executed:

find . -maxdepth 3 -name ".eslint*" -o -name "eslint*" 2>/dev/null | head -20

Repository: code-zero-to-one/study-platform-client

Length of output: 958


🏁 Script executed:

find . -maxdepth 3 -type f \( -name "*.config.js" -o -name "*.config.ts" \) | grep -i eslint

Repository: code-zero-to-one/study-platform-client

Length of output: 159


🏁 Script executed:

rg "exhaustive-deps" -t ts -t tsx -t js -A 2 -B 2

Repository: code-zero-to-one/study-platform-client

Length of output: 109


🏁 Script executed:

cat -n .eslintrc.cjs

Repository: code-zero-to-one/study-platform-client

Length of output: 2976


🏁 Script executed:

rg "exhaustive-deps" -A 2 -B 2

Repository: code-zero-to-one/study-platform-client

Length of output: 1504


useEffect 의존성 검토 필요

isRendered 상태는 useEffect 내에서 읽히지만(라인 38) 의존성 배열에 포함되지 않아 react-hooks/exhaustive-deps 규칙에 위배됩니다. 다만 exitDuration은 상수(200)로 의존성에 추가할 필요가 없습니다.

🛠️ 권장 수정
-  }, [isVisible, duration, onClose]);
+  }, [isVisible, duration, onClose, isRendered]);
🤖 Prompt for AI Agents
In `@src/components/ui/toast.tsx` around lines 27 - 47, Summary: useEffect reads
isRendered but doesn't include it in the dependency array, breaking
exhaustive-deps. Fix: update the dependency array of the useEffect that manages
isVisible/isRendered to include isRendered (so it becomes [isVisible,
isRendered, duration, onClose]); you can omit exitDuration if it is a true
constant (200). Keep the same timer setup and cleanup logic in the effect; no
other logic changes are needed. Ensure you reference the existing symbols
useEffect, isVisible, isRendered, duration, exitDuration, and onClose when
making the change.

@Hyeonjun0527 Hyeonjun0527 merged commit e1d70b7 into develop Feb 8, 2026
9 checks passed
@Hyeonjun0527 Hyeonjun0527 deleted the fix/hydration-n-phone-verification branch February 8, 2026 16:16
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@src/app/`(service)/(my)/my-study/page.tsx:
- Around line 18-24: memberId from useAuthReady() can be undefined during
hydration but useMemberStudyListQuery expects a number
(MemberStudyListRequest.memberId); update the call to pass a non-undefined
number (same pattern used in my-participating-studies-section.tsx) by coercing
memberId to 0 when undefined and keep the existing enabled guard (e.g., pass
memberId ?? 0 into useMemberStudyListQuery while leaving enabled: memberId > 0)
so TypeScript types align and runtime behavior is unchanged.

In `@src/features/phone-verification/model/store.ts`:
- Around line 4-13: PhoneVerificationState currently defines verifiedAt as
string | null which conflicts with use-phone-verification-state.ts where
verifiedAt is Date | null; pick one canonical type and apply it consistently:
either change PhoneVerificationState.verifiedAt to Date | null (and adjust
setVerified to accept/assign a Date) or change the hook to use string | null and
convert Date↔ISO at setVerified/get points; update all related symbols
(PhoneVerificationState, setVerified, reset, and the hook
usePhoneVerificationState) so the stored type and any conversions are explicit
and consistent to prevent runtime mismatches.

In `@src/features/phone-verification/model/use-phone-verification-status.ts`:
- Around line 186-193: The setVerified callback currently calls
store.setVerified(phone, memberId ?? undefined) which can overwrite another
user's cached verification when memberId is null/undefined; change setVerified
(the useCallback defined around setVerified) to first check that memberId is a
defined value and perform a no-op if it's null/undefined, otherwise call
store.setVerified(phone, memberId), so the store is only updated when a concrete
memberId exists.

In `@src/features/study/one-to-one/archive/model/use-archive-actions.ts`:
- Line 69: The returned object in use-archive-actions.ts maps isAuthReady to the
misleading key isAuthenticated; update the return to expose the actual value
name (e.g., return isAuthReady: isAuthReady instead of isAuthenticated:
isAuthReady) and then update all consumers (notably archive-tab-client.tsx) to
read props.isAuthReady; ensure references to useAuthReady(), isAuthReady and
isAuthenticated are reconciled so the boolean meaning remains clear.

In `@src/features/study/one-to-one/schedule/ui/today-study-card.tsx`:
- Around line 34-42: The query runs before auth/hydration completes; get the
auth-ready flag from useAuthReady (e.g. const { memberId: authMemberId,
isAuthReady } = useAuthReady()) and gate useDailyStudyDetailQuery with it:
compute queryEnabled = tutorialMode ? !!queryStudyDate /*or true per existing
logic*/ : (isAuthReady && !!studyDate) and call
useDailyStudyDetailQuery(queryStudyDate, { enabled: queryEnabled }). Update
references around useAuthReady, tutorialMode, studyDate and
useDailyStudyDetailQuery so the query only fires after isAuthReady when not in
tutorialMode.

In `@src/providers/index.tsx`:
- Around line 16-23: The effect currently triggers fetchAndSetUser only when
memberId is falsy; change the guard to compare identities so it runs whenever
the store's memberId differs from authMemberId. In the useEffect that uses
useAuthReady and useUserStore (symbols: useEffect, useAuthReady, useUserStore,
fetchAndSetUser, authMemberId, memberId), replace the condition `!memberId` with
`memberId !== authMemberId` so you fetch and set the user when the authenticated
member changed (covering edge cases where logout didn’t fully clear the store).
🧹 Nitpick comments (6)
src/entities/user/model/use-user-profile-query.ts (1)

15-15: staleTime: 0 설정 검토 필요

staleTime: 0으로 변경하면 데이터가 즉시 stale로 간주되어, 컴포넌트 마운트나 윈도우 포커스 시마다 리페칭이 발생합니다. 권장 기본값인 60초와 달리 서버 부하가 증가할 수 있습니다.

의도적인 변경이라면 무시해도 되지만, 적절한 값(예: staleTime: 1000 * 60)을 고려해 보세요.

Based on learnings: "Use TanStack Query for server state with a default staleTime of 60 seconds"

src/hooks/common/auth-hydration-context.tsx (1)

25-27: 개발 환경에서의 디버깅을 위한 선택적 개선

Provider 외부에서 useAuthHydration이 호출되면 빈 객체가 반환됩니다. 개발 중 실수 방지를 위해 경고를 추가하는 것을 고려해 보세요.

♻️ 선택적 개선 제안
+const AuthHydrationContext = createContext<AuthHydrationContextValue | null>(null);
+
 export function useAuthHydration() {
-  return useContext(AuthHydrationContext);
+  const context = useContext(AuthHydrationContext);
+  if (context === null) {
+    if (process.env.NODE_ENV === 'development') {
+      console.warn('useAuthHydration must be used within AuthHydrationProvider');
+    }
+    return {};
+  }
+  return context;
 }
src/features/study/group/ui/group-study-member-item.tsx (1)

41-42: isMe 계산에서 isAuthReady 가드 누락

Line 42의 isLeaderisAuthReady && 가드를 사용하지만, Line 41의 isMe는 가드 없이 member.id === myId로 비교합니다. myId가 hydration 전 undefined일 때 비교는 항상 false가 되어 버그는 발생하지 않지만, 동일 파일 내 GreetingBox 컴포넌트(Line 222)와 패턴이 일관되지 않습니다.

♻️ 일관성을 위한 선택적 수정
   const isMe = member.id === myId;
-  const isLeader = isAuthReady && leaderId === myId;
+  const isLeader = isAuthReady && isMe && leaderId === myId;

또는 isMe에도 가드를 추가:

-  const isMe = member.id === myId;
+  const isMe = isAuthReady && member.id === myId;
   const isLeader = isAuthReady && leaderId === myId;
src/features/study/one-to-one/archive/model/use-archive-actions.ts (1)

8-9: import 문 뒤 빈 줄 누락

Line 8과 Line 9 사이에 빈 줄이 없어 import와 interface 선언이 붙어 있습니다. 가독성을 위해 빈 줄 추가를 권장합니다.

♻️ 포맷 수정
 import { useAuthReady } from '@/hooks/common/use-auth';
+
 interface ArchiveViewTarget {
src/features/phone-verification/model/store.ts (1)

19-22: 불필요한 타입 캐스트를 제거하세요.

null as string | null, null as number | null 같은 타입 캐스트는 불필요합니다. TypeScript에서 null은 이미 nullable 타입에 할당 가능합니다.

♻️ 타입 캐스트 제거 제안
     (set) => ({
       isVerified: false,
-      phoneNumber: null as string | null,
-      verifiedAt: null as string | null,
-      memberId: null as number | null,
+      phoneNumber: null,
+      verifiedAt: null,
+      memberId: null,
       hasHydrated: false,
       reset: () =>
         set({
           isVerified: false,
-          phoneNumber: null as string | null,
-          verifiedAt: null as string | null,
-          memberId: null as number | null,
+          phoneNumber: null,
+          verifiedAt: null,
+          memberId: null,
         }),

Also applies to: 33-35

src/features/phone-verification/model/use-phone-auth-mutation.ts (1)

45-46: || 대신 nullish coalescing 사용을 고려하세요.

currentMemberId || undefinedcurrentMemberId0인 경우에도 undefined로 변환됩니다. member ID가 0이 될 가능성은 낮지만, 더 안전한 처리를 위해 nullish coalescing(??)을 사용하는 것이 좋습니다.

♻️ Nullish coalescing 사용 제안
         // 인증 상태 저장 (memberId 포함)
-        setVerified(variables.phoneNumber, currentMemberId || undefined);
+        setVerified(variables.phoneNumber, currentMemberId ?? undefined);

Comment on lines +18 to 24
const { memberId } = useAuthReady();

const { data, isLoading } = useMemberStudyListQuery({
memberId: authData?.memberId,
memberId,
studyType: 'GROUP_STUDY',
studyStatus: 'BOTH',
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the MemberStudyListRequest type definition
fd --type f -e ts -e tsx | xargs rg "interface MemberStudyListRequest|type MemberStudyListRequest" -A 5

Repository: code-zero-to-one/study-platform-client

Length of output: 634


🏁 Script executed:

# Check the useAuthReady hook return type
fd --type f -e ts -e tsx -path "*hooks*use-auth*" | head -5

Repository: code-zero-to-one/study-platform-client

Length of output: 250


🏁 Script executed:

# Find useMemberStudyListQuery implementation
fd --type f -e ts -e tsx | xargs rg "useMemberStudyListQuery|MemberStudyListQuery" -A 10 -B 2 | head -80

Repository: code-zero-to-one/study-platform-client

Length of output: 7220


🏁 Script executed:

# Check the actual code in the target file
cat -n src/app/\(service\)/\(my\)/my-study/page.tsx

Repository: code-zero-to-one/study-platform-client

Length of output: 3505


🏁 Script executed:

# Check my-participating-studies-section.tsx to see how it handles memberId
sed -n '40,50p' src/components/section/my-participating-studies-section.tsx

Repository: code-zero-to-one/study-platform-client

Length of output: 523


🏁 Script executed:

# Check useAuthReady hook return type
cat -n src/hooks/common/use-auth.ts

Repository: code-zero-to-one/study-platform-client

Length of output: 4646


🏁 Script executed:

# Check useMemberStudyListQuery implementation to see how it handles the parameters
cat -n src/features/study/group/model/use-member-study-list-query.ts

Repository: code-zero-to-one/study-platform-client

Length of output: 1256


memberId의 타입 불일치: undefined | numbernumber로 전달

useAuthReady()에서 반환되는 memberId는 hydration 이전에 undefined일 수 있습니다. MemberStudyListRequestmemberId: number로 정의되어 있어 타입 호환성 문제가 발생합니다. useMemberStudyListQueryenabled: memberId > 0 조건이 런타임 안전성을 보장하지만, 타입 검사 단계에서 오류가 발생할 수 있습니다.

my-participating-studies-section.tsx에서 사용되는 패턴(memberId ?? 0)을 적용하여 타입 안전성을 확보하세요.

🤖 Prompt for AI Agents
In `@src/app/`(service)/(my)/my-study/page.tsx around lines 18 - 24, memberId from
useAuthReady() can be undefined during hydration but useMemberStudyListQuery
expects a number (MemberStudyListRequest.memberId); update the call to pass a
non-undefined number (same pattern used in my-participating-studies-section.tsx)
by coercing memberId to 0 when undefined and keep the existing enabled guard
(e.g., pass memberId ?? 0 into useMemberStudyListQuery while leaving enabled:
memberId > 0) so TypeScript types align and runtime behavior is unchanged.

Comment on lines 4 to 13
interface PhoneVerificationState {
isVerified: boolean;
phoneNumber: string | null;
verifiedAt: Date | null;
setVerified: (phoneNumber: string) => void;
verifiedAt: string | null;
memberId: number | null;
hasHydrated: boolean;
setVerified: (phoneNumber: string, memberId?: number) => void;
reset: () => void;
setHasHydrated: (hasHydrated: boolean) => void;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check all verifiedAt usages and type definitions in phone-verification feature

echo "=== Searching for verifiedAt in phone-verification directory ==="
rg -n "verifiedAt" src/features/phone-verification/

echo -e "\n=== Checking store.ts file ==="
cat -n src/features/phone-verification/model/store.ts | head -50

echo -e "\n=== Checking use-phone-verification-state.ts file ==="
fd "use-phone-verification-state" src/features/phone-verification/ -x cat -n {}

Repository: code-zero-to-one/study-platform-client

Length of output: 4130


store.tsuse-phone-verification-state.tsverifiedAt 타입 불일치를 수정하세요.

store.ts에서 verifiedAtstring | null로 정의되어 ISO 문자열로 저장되지만, use-phone-verification-state.ts에서는 verifiedAt: Date | null로 정의되어 Date 객체로 저장됩니다. 이 타입 불일치는 두 상태 관리자가 상호작용할 때 런타임 오류를 유발할 수 있습니다.

두 파일의 verifiedAt 타입을 통일하거나, 각각의 용도에 맞게 명확히 분리하고 타입 변환 로직을 추가해야 합니다.

🤖 Prompt for AI Agents
In `@src/features/phone-verification/model/store.ts` around lines 4 - 13,
PhoneVerificationState currently defines verifiedAt as string | null which
conflicts with use-phone-verification-state.ts where verifiedAt is Date | null;
pick one canonical type and apply it consistently: either change
PhoneVerificationState.verifiedAt to Date | null (and adjust setVerified to
accept/assign a Date) or change the hook to use string | null and convert
Date↔ISO at setVerified/get points; update all related symbols
(PhoneVerificationState, setVerified, reset, and the hook
usePhoneVerificationState) so the stored type and any conversions are explicit
and consistent to prevent runtime mismatches.

Comment on lines +186 to +193
// 인증 완료 시 호출 — memberId를 자동 주입
const setVerified = useCallback(
(phone: string) => {
store.setVerified(phone, memberId ?? undefined);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[memberId],
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

memberId 미확정 상태의 setVerified는 오염 가능성
memberId가 null/undefined일 때 store.setVerified가 기존 memberId를 유지한 채 인증 정보를 덮을 수 있어 계정 간 캐시 오염 여지가 있습니다. memberId가 없으면 no-op 처리하는 방어 로직을 추가해 주세요.

🛡️ 수정 제안
 const setVerified = useCallback(
   (phone: string) => {
-    store.setVerified(phone, memberId ?? undefined);
+    if (memberId == null) return;
+    store.setVerified(phone, memberId);
   },
   // eslint-disable-next-line react-hooks/exhaustive-deps
   [memberId],
 );
🤖 Prompt for AI Agents
In `@src/features/phone-verification/model/use-phone-verification-status.ts`
around lines 186 - 193, The setVerified callback currently calls
store.setVerified(phone, memberId ?? undefined) which can overwrite another
user's cached verification when memberId is null/undefined; change setVerified
(the useCallback defined around setVerified) to first check that memberId is a
defined value and perform a no-op if it's null/undefined, otherwise call
store.setVerified(phone, memberId), so the store is only updated when a concrete
memberId exists.

updateArchive: handleUpdateArchive,
openAndRecordView,
isAuthenticated,
isAuthenticated: isAuthReady,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/features/study/one-to-one/archive/model/use-archive-actions.ts

Repository: code-zero-to-one/study-platform-client

Length of output: 3049


🏁 Script executed:

rg 'useArchiveActions' --type=ts --type=tsx -B 2 -A 8

Repository: code-zero-to-one/study-platform-client

Length of output: 109


🏁 Script executed:

rg 'useArchiveActions' -t ts -A 10

Repository: code-zero-to-one/study-platform-client

Length of output: 4343


🏁 Script executed:

rg 'from.*use-archive-actions' -t ts

Repository: code-zero-to-one/study-platform-client

Length of output: 356


🏁 Script executed:

cat src/features/study/one-to-one/archive/ui/archive-tab-client.tsx | head -100

Repository: code-zero-to-one/study-platform-client

Length of output: 3326


🏁 Script executed:

rg 'useArchiveActions' src/features/study/one-to-one/archive/ui/archive-tab-client.tsx -A 20 -B 2

Repository: code-zero-to-one/study-platform-client

Length of output: 1771


🏁 Script executed:

rg 'useAuthReady' --type ts -B 2 -A 10 | head -60

Repository: code-zero-to-one/study-platform-client

Length of output: 5293


🏁 Script executed:

fd 'use-auth' --type ts

Repository: code-zero-to-one/study-platform-client

Length of output: 251


🏁 Script executed:

find src -name '*use-auth*' -type f

Repository: code-zero-to-one/study-platform-client

Length of output: 191


🏁 Script executed:

cat src/hooks/common/use-auth.ts

Repository: code-zero-to-one/study-platform-client

Length of output: 3715


isAuthenticated 필드 네이밍 수정 필요

반환 객체에서 isAuthenticated: isAuthReady로 매핑하고 있습니다. 이는 의미론적 불일치입니다.

useAuthReady()isAuthReadyisHydrated && isAuthenticated이므로, "인증 여부만 확인"하는 isAuthenticated와 다릅니다. 필드명 isAuthenticated는 단순한 인증 상태를 의미하지만, 실제 값은 hydration 완료 여부까지 포함합니다.

소비자(archive-tab-client.tsx)가 이 필드를 사용할 때 명확한 의미를 알 수 없습니다. 필드명을 isAuthReady로 변경하거나, 값을 실제 isAuthenticated로 수정하세요.

🤖 Prompt for AI Agents
In `@src/features/study/one-to-one/archive/model/use-archive-actions.ts` at line
69, The returned object in use-archive-actions.ts maps isAuthReady to the
misleading key isAuthenticated; update the return to expose the actual value
name (e.g., return isAuthReady: isAuthReady instead of isAuthenticated:
isAuthReady) and then update all consumers (notably archive-tab-client.tsx) to
read props.isAuthReady; ensure references to useAuthReady(), isAuthReady and
isAuthenticated are reconciled so the boolean meaning remains clear.

Comment on lines +34 to 42
const { memberId: authMemberId } = useAuthReady();
const memberId = tutorialMode
? forcedRole === 'INTERVIEWER'
? TUTORIAL_DAILY_STUDY_MOCK.interviewerId
: TUTORIAL_DAILY_STUDY_MOCK.intervieweeId
: (authData?.memberId ?? null);
: (authMemberId ?? null);

const queryStudyDate = tutorialMode ? '' : studyDate;
const { data: todayStudyData } = useDailyStudyDetailQuery(queryStudyDate);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find useDailyStudyDetailQuery definition and usage
fd -t f '\.(ts|tsx)$' src | xargs rg -l 'useDailyStudyDetailQuery' | head -20

Repository: code-zero-to-one/study-platform-client

Length of output: 202


🏁 Script executed:

# Find useAuthReady definition and usage
fd -t f '\.(ts|tsx)$' src | xargs rg -l 'useAuthReady' | head -20

Repository: code-zero-to-one/study-platform-client

Length of output: 1081


🏁 Script executed:

# Get the full content of the target file to understand context
cat -n "src/features/study/one-to-one/schedule/ui/today-study-card.tsx" | head -60

Repository: code-zero-to-one/study-platform-client

Length of output: 2729


🏁 Script executed:

# Find and examine useAuthReady implementation
rg -nA 15 'export.*useAuthReady|function useAuthReady' src/hooks/common/use-auth.ts

Repository: code-zero-to-one/study-platform-client

Length of output: 466


🏁 Script executed:

# Find and examine useDailyStudyDetailQuery implementation
rg -nA 20 'export.*useDailyStudyDetailQuery|function useDailyStudyDetailQuery' src/features/study/interview/model/use-interview-query.ts

Repository: code-zero-to-one/study-platform-client

Length of output: 732


Hydration 완료 후 쿼리를 실행해야 합니다.

라인 41-42에서 useDailyStudyDetailQueryisAuthReady와 무관하게 실행됩니다. 쿼리의 enabled: !!params 옵션은 tutorialModestudyDate 값에만 의존하며, hydration 전(isHydrated=false)에도 params가 유효하면 쿼리가 실행될 수 있습니다. 이 경우 인증 토큰 없이 API 호출이 발생하여 401 오류와 재시도가 발생할 수 있으므로, isAuthReady로 쿼리 실행을 게이트해야 합니다.

🔧 제안 변경
-  const { memberId: authMemberId } = useAuthReady();
+  const { memberId: authMemberId, isAuthReady } = useAuthReady();

-  const queryStudyDate = tutorialMode ? '' : studyDate;
+  const queryStudyDate = tutorialMode || !isAuthReady ? '' : studyDate;
🤖 Prompt for AI Agents
In `@src/features/study/one-to-one/schedule/ui/today-study-card.tsx` around lines
34 - 42, The query runs before auth/hydration completes; get the auth-ready flag
from useAuthReady (e.g. const { memberId: authMemberId, isAuthReady } =
useAuthReady()) and gate useDailyStudyDetailQuery with it: compute queryEnabled
= tutorialMode ? !!queryStudyDate /*or true per existing logic*/ : (isAuthReady
&& !!studyDate) and call useDailyStudyDetailQuery(queryStudyDate, { enabled:
queryEnabled }). Update references around useAuthReady, tutorialMode, studyDate
and useDailyStudyDetailQuery so the query only fires after isAuthReady when not
in tutorialMode.

Comment on lines +16 to +23
const { memberId: authMemberId, isAuthReady } = useAuthReady();
const { memberId, fetchAndSetUser } = useUserStore();

useEffect(() => {
if (isAuthenticated && authData?.memberId && !memberId) {
fetchAndSetUser(authData.memberId).catch(console.error);
if (isAuthReady && authMemberId && !memberId) {
fetchAndSetUser(authMemberId).catch(console.error);
}
}, [isAuthenticated, authData?.memberId, memberId, fetchAndSetUser]);
}, [isAuthReady, authMemberId, memberId, fetchAndSetUser]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's explore the repository structure
find . -type f -name "*.tsx" -o -name "*.ts" | grep -E "(provider|store)" | head -20

Repository: code-zero-to-one/study-platform-client

Length of output: 1461


🏁 Script executed:

# Look at the full context of src/providers/index.tsx
cat -n src/providers/index.tsx

Repository: code-zero-to-one/study-platform-client

Length of output: 1705


🏁 Script executed:

# Search for useUserStore implementation
fd useUserStore | head -10

Repository: code-zero-to-one/study-platform-client

Length of output: 108


🏁 Script executed:

# Search for UserStore or useUserStore definition
rg -n "useUserStore\s*=" --type=ts --type=tsx -A 5

Repository: code-zero-to-one/study-platform-client

Length of output: 109


🏁 Script executed:

# Look for logout or reset handlers
rg -n "logout\|\.reset\|onLogout" --type=ts --type=tsx -B 2 -A 3

Repository: code-zero-to-one/study-platform-client

Length of output: 109


🏁 Script executed:

# Read useUserStore implementation
cat -n src/stores/useUserStore.ts

Repository: code-zero-to-one/study-platform-client

Length of output: 1718


🏁 Script executed:

# Search for logout or reset with correct file types
rg -n "logout" --type ts -B 2 -A 3

Repository: code-zero-to-one/study-platform-client

Length of output: 6841


🏁 Script executed:

# Search for reset method
rg -n "\.reset\(" --type ts -B 2 -A 3

Repository: code-zero-to-one/study-platform-client

Length of output: 1329


🏁 Script executed:

# Search for where memberId is cleared
rg -n "memberId\s*=" --type ts -B 2 -A 2

Repository: code-zero-to-one/study-platform-client

Length of output: 50394


🏁 Script executed:

# Look for useAuthReady implementation to understand auth flow
fd "auth" src | grep -E "\.(ts|tsx)$"

Repository: code-zero-to-one/study-platform-client

Length of output: 706


🏁 Script executed:

# Read the full use-auth-mutation.ts file
cat -n src/features/auth/model/use-auth-mutation.ts

Repository: code-zero-to-one/study-platform-client

Length of output: 2469


🏁 Script executed:

# Also check if useUserStore.reset() is called anywhere
rg -n "useUserStore.*reset\|\.reset\(\)" --type ts

Repository: code-zero-to-one/study-platform-client

Length of output: 64


🏁 Script executed:

# Check useAuthReady to understand authMemberId behavior
cat -n src/hooks/common/use-auth.ts

Repository: code-zero-to-one/study-platform-client

Length of output: 4646


로그아웃 시 스토어 리셋이 제대로 구현되어 있지만, 계정 전환 시 더욱 견고한 비교로 개선하세요.

useLogoutMutation에서 resetUserStore()가 올바르게 호출되므로 정상적인 로그아웃 흐름에서는 문제가 없습니다. 다만 로그아웃이 완전히 실행되지 않는 엣지 케이스를 방어하기 위해, 현재 !memberId 조건을 memberId !== authMemberId 비교로 개선하면 더욱 견고합니다. 이는 코드베이스의 다른 스토어(예: usePhoneVerificationStore)에서 이미 사용 중인 패턴입니다.

🔧 개선 제안
-  if (isAuthReady && authMemberId && !memberId) {
+  if (isAuthReady && authMemberId && memberId !== authMemberId) {
     fetchAndSetUser(authMemberId).catch(console.error);
   }
🤖 Prompt for AI Agents
In `@src/providers/index.tsx` around lines 16 - 23, The effect currently triggers
fetchAndSetUser only when memberId is falsy; change the guard to compare
identities so it runs whenever the store's memberId differs from authMemberId.
In the useEffect that uses useAuthReady and useUserStore (symbols: useEffect,
useAuthReady, useUserStore, fetchAndSetUser, authMemberId, memberId), replace
the condition `!memberId` with `memberId !== authMemberId` so you fetch and set
the user when the authenticated member changed (covering edge cases where logout
didn’t fully clear the store).

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.

1 participant