/* * 인증 컨텍스트 — JWT를 secure-store에 영속, 앱 부팅 시 복원. * 활성 워크스페이스(행사) 선택 상태 보관. 비밀번호는 저장하지 않는다. */ import React, { createContext, useCallback, useContext, useEffect, useMemo, useState, } from 'react'; import { setAccessToken } from '../lib/api'; import { landingPathFor } from '../lib/roleTrack'; import { secureDelete, secureGet, secureSet } from '../lib/secureStore'; import type { AuthUser, LoginResponse, WorkspaceDto } from '../lib/types'; const TOKEN_KEY = 'kintex.accessToken'; const USER_KEY = 'kintex.user'; const LANDING_KEY = 'kintex.landing'; interface AuthState { ready: boolean; token: string | null; user: AuthUser | null; workspaces: WorkspaceDto[]; activeEventId: string | null; /** 로그인 시 역할로 계산한 랜딩 라우트(콜드 부팅 시 workspaces 미복원 문제 회피용으로 지속). */ landingPath: string | null; } interface AuthContextValue extends AuthState { activeWorkspace: WorkspaceDto | null; signIn: (res: LoginResponse) => Promise; signOut: () => Promise; selectWorkspace: (eventId: string) => void; } const AuthContext = createContext(undefined); export function AuthProvider({ children }: { children: React.ReactNode }) { const [state, setState] = useState({ ready: false, token: null, user: null, workspaces: [], activeEventId: null, landingPath: null, }); // 부팅 시 토큰 복원 useEffect(() => { let cancelled = false; (async () => { const token = await secureGet(TOKEN_KEY); const userRaw = await secureGet(USER_KEY); const landingPath = await secureGet(LANDING_KEY); if (cancelled) return; const user = userRaw ? (JSON.parse(userRaw) as AuthUser) : null; if (token) setAccessToken(token); setState((s) => ({ ...s, ready: true, token, user, landingPath })); })(); return () => { cancelled = true; }; }, []); const signIn = useCallback(async (res: LoginResponse) => { setAccessToken(res.accessToken); // 역할 우선순위로 랜딩 경로를 계산·지속(콜드 부팅 시 재사용 — workspaces 미복원 misroute 방지). const landingPath = landingPathFor({ user: res.user, workspaces: res.workspaces }); await secureSet(TOKEN_KEY, res.accessToken); await secureSet(USER_KEY, JSON.stringify(res.user)); await secureSet(LANDING_KEY, landingPath); setState((s) => ({ ...s, token: res.accessToken, user: res.user, workspaces: res.workspaces ?? [], activeEventId: res.workspaces?.[0]?.eventId ?? null, landingPath, })); }, []); const signOut = useCallback(async () => { setAccessToken(null); await secureDelete(TOKEN_KEY); await secureDelete(USER_KEY); await secureDelete(LANDING_KEY); setState((s) => ({ ...s, token: null, user: null, workspaces: [], activeEventId: null, landingPath: null, })); }, []); const selectWorkspace = useCallback((eventId: string) => { setState((s) => ({ ...s, activeEventId: eventId })); }, []); const value = useMemo(() => { const activeWorkspace = state.workspaces.find((w) => w.eventId === state.activeEventId) ?? null; return { ...state, activeWorkspace, signIn, signOut, selectWorkspace }; }, [state, signIn, signOut, selectWorkspace]); return {children}; } export function useAuth(): AuthContextValue { const ctx = useContext(AuthContext); if (!ctx) throw new Error('useAuth must be used within AuthProvider'); return ctx; }