import { useState, useEffect, useCallback, useRef } from "react"; import { useRouter } from "next/navigation"; import { apiCall } from "@/lib/api/client"; interface UserInfo { userId: string; userName: string; userNameEng?: string; userNameCn?: string; deptCode?: string; deptName?: string; positionCode?: string; positionName?: string; email?: string; tel?: string; cellPhone?: string; userType?: string; userTypeName?: string; authName?: string; partnerCd?: string; locale?: string; isAdmin: boolean; sabun?: string; photo?: string | null; companyCode?: string; company_code?: string; } interface AuthStatus { isLoggedIn: boolean; isAdmin: boolean; userId?: string; deptCode?: string; } interface LoginResult { success: boolean; message: string; errorCode?: string; } interface ApiResponse { success: boolean; message: string; data?: T; errorCode?: string; } // JWT 토큰 관리 유틸리티 (client.ts와 동일한 localStorage 키 사용) const TokenManager = { getToken: (): string | null => { if (typeof window !== "undefined") { return localStorage.getItem("authToken"); } return null; }, setToken: (token: string): void => { if (typeof window !== "undefined") { localStorage.setItem("authToken", token); // 쿠키에도 저장 (미들웨어에서 사용) document.cookie = `authToken=${token}; path=/; max-age=86400; SameSite=Lax`; } }, removeToken: (): void => { if (typeof window !== "undefined") { localStorage.removeItem("authToken"); document.cookie = "authToken=; path=/; max-age=0; SameSite=Lax"; } }, isTokenExpired: (token: string): boolean => { try { const payload = JSON.parse(atob(token.split(".")[1])); return payload.exp * 1000 < Date.now(); } catch { return true; } }, }; /** * 인증 상태 관리 훅 * - 401 처리는 client.ts의 응답 인터셉터에서 통합 관리 * - 이 훅은 상태 관리와 사용자 정보 조회에만 집중 */ export const useAuth = () => { const router = useRouter(); const [user, setUser] = useState(null); const [authStatus, setAuthStatus] = useState({ isLoggedIn: false, isAdmin: false, }); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const initializedRef = useRef(false); /** * 현재 사용자 정보 조회 */ const fetchCurrentUser = useCallback(async (): Promise => { try { const response = await apiCall("GET", "/auth/me"); if (response.success && response.data) { // 사용자 로케일 정보 조회 try { const localeResponse = await apiCall("GET", "/admin/user-locale"); if (localeResponse.success && localeResponse.data) { const userLocale = localeResponse.data; (window as any).__GLOBAL_USER_LANG = userLocale; (window as any).__GLOBAL_USER_LOCALE_LOADED = true; localStorage.setItem("userLocale", userLocale); localStorage.setItem("userLocaleLoaded", "true"); } } catch { (window as any).__GLOBAL_USER_LANG = "KR"; (window as any).__GLOBAL_USER_LOCALE_LOADED = true; localStorage.setItem("userLocale", "KR"); localStorage.setItem("userLocaleLoaded", "true"); } return response.data; } return null; } catch { return null; } }, []); /** * 인증 상태 확인 */ const checkAuthStatus = useCallback(async (): Promise => { try { const response = await apiCall("GET", "/auth/status"); if (response.success && response.data) { return { isLoggedIn: (response.data as any).isAuthenticated || response.data.isLoggedIn || false, isAdmin: response.data.isAdmin || false, }; } return { isLoggedIn: false, isAdmin: false }; } catch { return { isLoggedIn: false, isAdmin: false }; } }, []); /** * 사용자 데이터 새로고침 * - API 실패 시에도 토큰이 유효하면 토큰 기반으로 임시 인증 유지 * - 토큰 자체가 없거나 만료된 경우에만 비인증 상태로 전환 */ const refreshUserData = useCallback(async () => { try { setLoading(true); const token = TokenManager.getToken(); if (!token || TokenManager.isTokenExpired(token)) { setUser(null); setAuthStatus({ isLoggedIn: false, isAdmin: false }); setLoading(false); return; } // 토큰이 유효하면 우선 인증된 상태로 설정 setAuthStatus({ isLoggedIn: true, isAdmin: false, }); try { const [userInfo, authStatusData] = await Promise.all([fetchCurrentUser(), checkAuthStatus()]); if (userInfo) { setUser(userInfo); const isAdminFromUser = userInfo.userId === "plm_admin" || userInfo.userType === "ADMIN"; const finalAuthStatus = { isLoggedIn: authStatusData.isLoggedIn, isAdmin: authStatusData.isAdmin || isAdminFromUser, }; setAuthStatus(finalAuthStatus); // API 결과가 비인증이면 상태만 업데이트 (리다이렉트는 client.ts가 처리) if (!finalAuthStatus.isLoggedIn) { TokenManager.removeToken(); setUser(null); setAuthStatus({ isLoggedIn: false, isAdmin: false }); } } else { // userInfo 조회 실패 → 토큰에서 최소 정보 추출하여 유지 try { const payload = JSON.parse(atob(token.split(".")[1])); const tempUser: UserInfo = { userId: payload.userId || payload.id || "unknown", userName: payload.userName || payload.name || "사용자", companyCode: payload.companyCode || payload.company_code || "", isAdmin: payload.userId === "plm_admin" || payload.userType === "ADMIN", }; setUser(tempUser); setAuthStatus({ isLoggedIn: true, isAdmin: tempUser.isAdmin, }); } catch { // 토큰 파싱도 실패하면 비인증 상태로 전환 TokenManager.removeToken(); setUser(null); setAuthStatus({ isLoggedIn: false, isAdmin: false }); } } } catch { // API 호출 전체 실패 → 토큰 기반 임시 인증 유지 시도 try { const payload = JSON.parse(atob(token.split(".")[1])); const tempUser: UserInfo = { userId: payload.userId || payload.id || "unknown", userName: payload.userName || payload.name || "사용자", companyCode: payload.companyCode || payload.company_code || "", isAdmin: payload.userId === "plm_admin" || payload.userType === "ADMIN", }; setUser(tempUser); setAuthStatus({ isLoggedIn: true, isAdmin: tempUser.isAdmin, }); } catch { TokenManager.removeToken(); setUser(null); setAuthStatus({ isLoggedIn: false, isAdmin: false }); } } } catch { setError("사용자 정보를 불러오는데 실패했습니다."); setUser(null); setAuthStatus({ isLoggedIn: false, isAdmin: false }); } finally { setLoading(false); } }, [fetchCurrentUser, checkAuthStatus]); /** * 로그인 처리 */ const login = useCallback( async (userId: string, password: string): Promise => { try { setLoading(true); setError(null); const response = await apiCall("POST", "/auth/login", { userId, password, }); if (response.success && response.data?.token) { TokenManager.setToken(response.data.token); await refreshUserData(); return { success: true, message: response.message || "로그인에 성공했습니다.", }; } else { return { success: false, message: response.message || "로그인에 실패했습니다.", errorCode: response.errorCode, }; } } catch (error: any) { const errorMessage = error.message || "로그인 중 오류가 발생했습니다."; setError(errorMessage); return { success: false, message: errorMessage, }; } finally { setLoading(false); } }, [refreshUserData], ); /** * 회사 전환 처리 (WACE 관리자 전용) */ const switchCompany = useCallback( async (companyCode: string): Promise<{ success: boolean; message: string }> => { try { setLoading(true); setError(null); const response = await apiCall("POST", "/auth/switch-company", { companyCode, }); if (response.success && response.data?.token) { TokenManager.setToken(response.data.token); return { success: true, message: response.message || "회사 전환에 성공했습니다.", }; } else { return { success: false, message: response.message || "회사 전환에 실패했습니다.", }; } } catch (error: any) { const errorMessage = error.message || "회사 전환 중 오류가 발생했습니다."; setError(errorMessage); return { success: false, message: errorMessage, }; } finally { setLoading(false); } }, [], ); /** * 로그아웃 처리 */ const logout = useCallback(async (): Promise => { try { setLoading(true); const response = await apiCall("POST", "/auth/logout"); TokenManager.removeToken(); localStorage.removeItem("userLocale"); localStorage.removeItem("userLocaleLoaded"); (window as any).__GLOBAL_USER_LANG = undefined; (window as any).__GLOBAL_USER_LOCALE_LOADED = undefined; setUser(null); setAuthStatus({ isLoggedIn: false, isAdmin: false }); setError(null); router.push("/login"); return response.success; } catch { TokenManager.removeToken(); localStorage.removeItem("userLocale"); localStorage.removeItem("userLocaleLoaded"); (window as any).__GLOBAL_USER_LANG = undefined; (window as any).__GLOBAL_USER_LOCALE_LOADED = undefined; setUser(null); setAuthStatus({ isLoggedIn: false, isAdmin: false }); router.push("/login"); return false; } finally { setLoading(false); } }, [router]); /** * 메뉴 접근 권한 확인 */ const checkMenuAuth = useCallback(async (menuUrl: string): Promise => { try { const response = await apiCall<{ menuUrl: string; hasAuth: boolean }>("GET", "/auth/menu-auth"); if (response.success && response.data) { return response.data.hasAuth; } return false; } catch { return false; } }, []); /** * 초기 인증 상태 확인 */ useEffect(() => { if (initializedRef.current) return; initializedRef.current = true; if (typeof window === "undefined") return; // 로그인 페이지에서는 인증 상태 확인하지 않음 if (window.location.pathname === "/login") { setLoading(false); return; } const token = TokenManager.getToken(); if (token && !TokenManager.isTokenExpired(token)) { // 유효한 토큰 → 우선 인증 상태로 설정 후 API 확인 setAuthStatus({ isLoggedIn: true, isAdmin: false, }); refreshUserData(); } else if (token && TokenManager.isTokenExpired(token)) { // 만료된 토큰 → 정리 (리다이렉트는 AuthGuard에서 처리) TokenManager.removeToken(); setAuthStatus({ isLoggedIn: false, isAdmin: false }); setLoading(false); } else { // 토큰 없음 → 비인증 상태 (리다이렉트는 AuthGuard에서 처리) setAuthStatus({ isLoggedIn: false, isAdmin: false }); setLoading(false); } }, []); return { user, authStatus, loading, error, isLoggedIn: authStatus.isLoggedIn, isAdmin: authStatus.isAdmin, userId: user?.userId, userName: user?.userName, companyCode: user?.companyCode || user?.company_code, login, logout, switchCompany, checkMenuAuth, refreshUserData, clearError: () => setError(null), }; }; export default useAuth;