import { useState, useEffect, useCallback } 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; 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.setItem("authToken", token); } }, removeToken: (): void => { if (typeof window !== "undefined") { localStorage.removeItem("authToken"); } }, 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); // API 기본 URL 설정 const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080"; /** * 현재 사용자 정보 조회 */ const fetchCurrentUser = useCallback(async (): Promise => { try { console.log("=== fetchCurrentUser 시작 ==="); const response = await apiCall("GET", "/auth/me"); console.log("fetchCurrentUser 응답:", response); if (response.success && response.data) { console.log("사용자 정보 조회 성공:", response.data); // 사용자 로케일 정보도 함께 조회하여 전역 저장 try { const localeResponse = await apiCall("GET", "/admin/user-locale"); if (localeResponse.success && localeResponse.data) { const userLocale = localeResponse.data; console.log("✅ 사용자 로케일 조회 성공:", userLocale); // 전역 상태에 저장 (다른 컴포넌트에서 사용) (window as any).__GLOBAL_USER_LANG = userLocale; (window as any).__GLOBAL_USER_LOCALE_LOADED = true; // localStorage에도 저장 (새 창에서 공유) localStorage.setItem("userLocale", userLocale); localStorage.setItem("userLocaleLoaded", "true"); console.log("🌐 전역 사용자 로케일 저장됨:", userLocale); } } 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; } console.log("사용자 정보 조회 실패 - 응답이 없음"); return null; } catch (error) { console.error("사용자 정보 조회 실패:", error); return null; } }, []); /** * 인증 상태 확인 */ const checkAuthStatus = useCallback(async (): Promise => { try { console.log("=== checkAuthStatus 시작 ==="); const response = await apiCall("GET", "/auth/status"); console.log("checkAuthStatus 응답:", response); console.log("checkAuthStatus 응답.data:", response.data); console.log("checkAuthStatus 응답.data.isLoggedIn:", response.data?.isLoggedIn); console.log("checkAuthStatus 응답.data.isAuthenticated:", (response.data as any)?.isAuthenticated); if (response.success && response.data) { console.log("인증 상태 확인 성공:", response.data); // 백엔드에서 isAuthenticated를 반환하므로 isLoggedIn으로 매핑 const mappedData = { isLoggedIn: (response.data as any).isAuthenticated || response.data.isLoggedIn || false, isAdmin: response.data.isAdmin || false, }; console.log("매핑된 인증 상태:", mappedData); return mappedData; } console.log("인증 상태 확인 실패 - 응답이 없음"); 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 }); console.log("토큰이 없음 - 3초 후 로그인 페이지로 리다이렉트"); setTimeout(() => { router.push("/login"); }, 3000); return; } console.log("=== refreshUserData 디버깅 ==="); console.log("토큰 존재:", !!token); // 토큰이 있으면 임시로 인증된 상태로 설정 setAuthStatus({ isLoggedIn: true, isAdmin: false, // API 호출 후 업데이트될 예정 }); try { // 병렬로 사용자 정보와 인증 상태 조회 const [userInfo, authStatusData] = await Promise.all([fetchCurrentUser(), checkAuthStatus()]); console.log("userInfo:", userInfo); console.log("authStatusData:", authStatusData); console.log("authStatusData.isLoggedIn:", authStatusData?.isLoggedIn); setUser(userInfo); // 관리자 권한 확인 로직 개선 let finalAuthStatus = authStatusData; if (userInfo) { // 사용자 정보를 기반으로 관리자 권한 추가 확인 const isAdminFromUser = userInfo.userId === "plm_admin" || userInfo.userType === "ADMIN"; finalAuthStatus = { isLoggedIn: authStatusData.isLoggedIn, isAdmin: authStatusData.isAdmin || isAdminFromUser, }; console.log("관리자 권한 확인:", { userId: userInfo.userId, userType: userInfo.userType, isAdminFromAuth: authStatusData.isAdmin, isAdminFromUser: isAdminFromUser, finalIsAdmin: finalAuthStatus.isAdmin, }); } setAuthStatus(finalAuthStatus); // 디버깅용 로그 if (userInfo) { console.log("사용자 정보 업데이트:", { userId: userInfo.userId, userName: userInfo.userName, hasPhoto: !!userInfo.photo, photoStart: userInfo.photo ? userInfo.photo.substring(0, 50) + "..." : "null", }); } // 로그인되지 않은 상태인 경우 토큰 제거 (리다이렉트는 useEffect에서 처리) if (!finalAuthStatus.isLoggedIn) { console.log("로그인되지 않은 상태 - 사용자 정보 제거"); TokenManager.removeToken(); setUser(null); setAuthStatus({ isLoggedIn: false, isAdmin: false }); } else { console.log("로그인된 상태 - 사용자 정보 유지"); } } catch (apiError) { console.error("API 호출 실패:", apiError); // API 호출 실패 시에도 토큰이 있으면 임시로 인증된 상태로 처리 console.log("API 호출 실패했지만 토큰이 존재하므로 임시로 인증된 상태로 처리"); // 토큰에서 사용자 정보 추출 시도 try { const payload = JSON.parse(atob(token.split(".")[1])); console.log("토큰 페이로드:", payload); const tempUser = { userId: payload.userId || "unknown", userName: payload.userName || "사용자", isAdmin: payload.userId === "plm_admin" || payload.userType === "ADMIN", }; setUser(tempUser); setAuthStatus({ isLoggedIn: true, isAdmin: tempUser.isAdmin, }); console.log("임시 사용자 정보 설정:", tempUser); } catch (tokenError) { console.error("토큰 파싱 실패:", tokenError); // 토큰 파싱도 실패하면 로그인 페이지로 리다이렉트 TokenManager.removeToken(); setUser(null); setAuthStatus({ isLoggedIn: false, isAdmin: false }); console.log("토큰 파싱 실패 - 3초 후 로그인 페이지로 리다이렉트"); setTimeout(() => { router.push("/login"); }, 3000); } } } catch (error) { console.error("사용자 데이터 새로고침 실패:", error); setError("사용자 정보를 불러오는데 실패했습니다."); // 오류 발생 시 토큰 제거 및 로그인 페이지로 리다이렉트 TokenManager.removeToken(); setUser(null); setAuthStatus({ isLoggedIn: false, isAdmin: false }); console.log("사용자 데이터 새로고침 실패 - 3초 후 로그인 페이지로 리다이렉트"); 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], ); /** * 로그아웃 처리 */ 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(() => { console.log("=== useAuth 초기 인증 상태 확인 ==="); console.log("현재 경로:", window.location.pathname); // 로그인 페이지에서는 인증 상태 확인하지 않음 if (window.location.pathname === "/login") { console.log("로그인 페이지 - 인증 상태 확인 건너뜀"); return; } // 토큰이 있는 경우에만 인증 상태 확인 const token = TokenManager.getToken(); console.log("localStorage 토큰:", token ? "존재" : "없음"); if (token && !TokenManager.isTokenExpired(token)) { console.log("유효한 토큰 존재 - 사용자 데이터 새로고침"); // 토큰이 있으면 임시로 인증된 상태로 설정 (API 호출 전에) setAuthStatus({ isLoggedIn: true, isAdmin: false, // API 호출 후 업데이트될 예정 }); refreshUserData(); } else if (!token) { console.log("토큰이 없음 - 3초 후 로그인 페이지로 리다이렉트"); // 토큰이 없으면 3초 후 로그인 페이지로 리다이렉트 setTimeout(() => { router.push("/login"); }, 3000); } else { console.log("토큰 만료 - 3초 후 로그인 페이지로 리다이렉트"); TokenManager.removeToken(); setTimeout(() => { router.push("/login"); }, 3000); } }, [refreshUserData, router]); // refreshUserData 의존성 추가 /** * 세션 만료 감지 및 처리 */ 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, // 함수 login, logout, checkMenuAuth, refreshUserData, // 유틸리티 clearError: () => setError(null), }; }; export default useAuth;