import { useState, useEffect, useCallback, useRef } from "react"; import { useRouter } from "next/navigation"; import { apiCall, API_BASE_URL } 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; } // API 응답 타입 정의 interface ApiResponse { success: boolean; message: string; data?: T; errorCode?: string; } /** * JWT 토큰 관리 유틸리티 */ const TokenManager = { getToken: (): string | null => { if (typeof window !== "undefined") { return localStorage.getItem("authToken"); } return null; }, setToken: (token: string): void => { if (typeof window !== "undefined") { // localStorage에 저장 localStorage.setItem("authToken", token); // 쿠키에도 저장 (미들웨어에서 사용) document.cookie = `authToken=${token}; path=/; max-age=86400; SameSite=Lax`; } }, removeToken: (): void => { if (typeof window !== "undefined") { // localStorage에서 제거 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; } }, }; /** * 인증 상태 관리 훅 * 로그인, 로그아웃, 사용자 정보 조회, 권한 확인 등을 담당 */ 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); // API 기본 URL 설정 (동적으로 결정) /** * 현재 사용자 정보 조회 */ 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에도 저장 (새 창에서 공유) localStorage.setItem("userLocale", userLocale); localStorage.setItem("userLocaleLoaded", "true"); } } catch (localeError) { console.warn("⚠️ 사용자 로케일 조회 실패, 기본값 사용:", localeError); (window as any).__GLOBAL_USER_LANG = "KR"; (window as any).__GLOBAL_USER_LOCALE_LOADED = true; // localStorage에도 저장 localStorage.setItem("userLocale", "KR"); localStorage.setItem("userLocaleLoaded", "true"); } return response.data; } return null; } catch (error) { console.error("사용자 정보 조회 실패:", error); return null; } }, []); /** * 인증 상태 확인 */ const checkAuthStatus = useCallback(async (): Promise => { try { const response = await apiCall("GET", "/auth/status"); if (response.success && response.data) { // 백엔드에서 isAuthenticated를 반환하므로 isLoggedIn으로 매핑 const mappedData = { isLoggedIn: (response.data as any).isAuthenticated || response.data.isLoggedIn || false, isAdmin: response.data.isAdmin || false, }; return mappedData; } return { isLoggedIn: false, isAdmin: false, }; } catch (error) { console.error("인증 상태 확인 실패:", error); return { isLoggedIn: false, isAdmin: false, }; } }, []); /** * 사용자 데이터 새로고침 */ const refreshUserData = useCallback(async () => { try { setLoading(true); // JWT 토큰 확인 const token = TokenManager.getToken(); if (!token) { setUser(null); setAuthStatus({ isLoggedIn: false, isAdmin: false }); setTimeout(() => { router.push("/login"); }, 3000); return; } // 토큰이 있으면 임시로 인증된 상태로 설정 setAuthStatus({ isLoggedIn: true, isAdmin: false, // API 호출 후 업데이트될 예정 }); try { // 병렬로 사용자 정보와 인증 상태 조회 const [userInfo, authStatusData] = await Promise.all([fetchCurrentUser(), checkAuthStatus()]); setUser(userInfo); // 관리자 권한 확인 로직 개선 let finalAuthStatus = authStatusData; if (userInfo) { // 사용자 정보를 기반으로 관리자 권한 추가 확인 const isAdminFromUser = userInfo.userId === "plm_admin" || userInfo.userType === "ADMIN"; finalAuthStatus = { isLoggedIn: authStatusData.isLoggedIn, isAdmin: authStatusData.isAdmin || isAdminFromUser, }; } setAuthStatus(finalAuthStatus); // console.log("✅ 최종 사용자 상태:", { // userId: userInfo?.userId, // userName: userInfo?.userName, // companyCode: userInfo?.companyCode || userInfo?.company_code, // }); // 디버깅용 로그 // 로그인되지 않은 상태인 경우 토큰 제거 (리다이렉트는 useEffect에서 처리) if (!finalAuthStatus.isLoggedIn) { TokenManager.removeToken(); setUser(null); setAuthStatus({ isLoggedIn: false, isAdmin: false }); } else { } } catch (apiError) { console.error("API 호출 실패:", apiError); // API 호출 실패 시에도 토큰이 있으면 임시로 인증된 상태로 처리 // 토큰에서 사용자 정보 추출 시도 try { const payload = JSON.parse(atob(token.split(".")[1])); const tempUser = { 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 (tokenError) { console.error("토큰 파싱 실패:", tokenError); // 토큰 파싱도 실패하면 로그인 페이지로 리다이렉트 TokenManager.removeToken(); setUser(null); setAuthStatus({ isLoggedIn: false, isAdmin: false }); setTimeout(() => { router.push("/login"); }, 3000); } } } catch (error) { console.error("사용자 데이터 새로고침 실패:", error); setError("사용자 정보를 불러오는데 실패했습니다."); // 오류 발생 시 토큰 제거 및 로그인 페이지로 리다이렉트 TokenManager.removeToken(); setUser(null); setAuthStatus({ isLoggedIn: false, isAdmin: false }); setTimeout(() => { router.push("/login"); }, 3000); } finally { setLoading(false); } }, [fetchCurrentUser, checkAuthStatus, router]); /** * 로그인 처리 */ 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) { // JWT 토큰 저장 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); } }, [apiCall, refreshUserData], ); /** * 회사 전환 처리 (WACE 관리자 전용) */ const switchCompany = useCallback( async (companyCode: string): Promise<{ success: boolean; message: string }> => { try { // console.log("🔵 useAuth.switchCompany 시작:", companyCode); setLoading(true); setError(null); // console.log("🔵 API 호출: POST /auth/switch-company"); const response = await apiCall("POST", "/auth/switch-company", { companyCode, }); // console.log("🔵 API 응답:", response); if (response.success && response.data?.token) { // console.log("🔵 새 토큰 받음:", response.data.token.substring(0, 20) + "..."); // 새로운 JWT 토큰 저장 TokenManager.setToken(response.data.token); // console.log("🔵 토큰 저장 완료"); // refreshUserData 호출하지 않고 바로 성공 반환 // (페이지 새로고침 시 자동으로 갱신됨) // console.log("🔵 회사 전환 완료 (페이지 새로고침 필요)"); return { success: true, message: response.message || "회사 전환에 성공했습니다.", }; } else { // console.error("🔵 API 응답 실패:", response); return { success: false, message: response.message || "회사 전환에 실패했습니다.", }; } } catch (error: any) { // console.error("🔵 switchCompany 에러:", error); const errorMessage = error.message || "회사 전환 중 오류가 발생했습니다."; setError(errorMessage); return { success: false, message: errorMessage, }; } finally { setLoading(false); // console.log("🔵 switchCompany 완료"); } }, [apiCall] ); /** * 로그아웃 처리 */ const logout = useCallback(async (): Promise => { try { setLoading(true); const response = await apiCall("POST", "/auth/logout"); // JWT 토큰 제거 TokenManager.removeToken(); // 로케일 정보도 제거 localStorage.removeItem("userLocale"); localStorage.removeItem("userLocaleLoaded"); (window as any).__GLOBAL_USER_LANG = undefined; (window as any).__GLOBAL_USER_LOCALE_LOADED = undefined; // 로그아웃 API 호출 성공 여부와 관계없이 클라이언트 상태 초기화 setUser(null); setAuthStatus({ isLoggedIn: false, isAdmin: false, }); setError(null); // 로그인 페이지로 리다이렉트 router.push("/login"); return response.success; } catch (error) { console.error("로그아웃 처리 실패:", error); // 오류가 발생해도 JWT 토큰 제거 및 클라이언트 상태 초기화 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); } }, [apiCall, 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 (error) { console.error("메뉴 권한 확인 실패:", error); return false; } }, []); /** * 초기 인증 상태 확인 */ useEffect(() => { // 이미 초기화되었으면 실행하지 않음 if (initializedRef.current) { return; } initializedRef.current = true; // 로그인 페이지에서는 인증 상태 확인하지 않음 if (window.location.pathname === "/login") { return; } // 토큰이 있는 경우에만 인증 상태 확인 const token = TokenManager.getToken(); if (token && !TokenManager.isTokenExpired(token)) { // 토큰이 있으면 임시로 인증된 상태로 설정 (API 호출 전에) setAuthStatus({ isLoggedIn: true, isAdmin: false, // API 호출 후 업데이트될 예정 }); refreshUserData(); } else if (!token) { // 토큰이 없으면 3초 후 로그인 페이지로 리다이렉트 setTimeout(() => { router.push("/login"); }, 3000); } else { TokenManager.removeToken(); setTimeout(() => { router.push("/login"); }, 3000); } }, []); // 초기 마운트 시에만 실행 /** * 세션 만료 감지 및 처리 */ useEffect(() => { const handleSessionExpiry = () => { TokenManager.removeToken(); setUser(null); setAuthStatus({ isLoggedIn: false, isAdmin: false, }); setError("세션이 만료되었습니다. 다시 로그인해주세요."); router.push("/login"); }; // 전역 에러 핸들러 등록 (401 Unauthorized 응답 처리) const originalFetch = window.fetch; window.fetch = async (...args) => { const response = await originalFetch(...args); if (response.status === 401 && window.location.pathname !== "/login") { handleSessionExpiry(); } return response; }; return () => { window.fetch = originalFetch; }; }, [router]); 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;