ERP-node/frontend/hooks/useAuth.ts

541 lines
16 KiB
TypeScript
Raw Normal View History

2025-08-21 09:41:46 +09:00
import { useState, useEffect, useCallback } from "react";
import { useRouter } from "next/navigation";
2025-09-04 17:31:45 +09:00
import { apiCall, API_BASE_URL } from "@/lib/api/client";
2025-08-21 09:41:46 +09:00
// 사용자 정보 타입 정의
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;
2025-08-28 10:05:06 +09:00
locale?: string;
2025-08-21 09:41:46 +09:00
isAdmin: boolean;
sabun?: string;
photo?: string | null;
2025-09-05 21:52:19 +09:00
companyCode?: string; // 백엔드와 일치하도록 수정
company_code?: string; // 하위 호환성을 위해 유지
2025-08-21 09:41:46 +09:00
}
// 인증 상태 타입 정의
interface AuthStatus {
isLoggedIn: boolean;
isAdmin: boolean;
userId?: string;
deptCode?: string;
}
// 로그인 결과 타입 정의
interface LoginResult {
success: boolean;
message: string;
errorCode?: string;
}
// API 응답 타입 정의
interface ApiResponse<T = any> {
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<UserInfo | null>(null);
const [authStatus, setAuthStatus] = useState<AuthStatus>({
isLoggedIn: false,
isAdmin: false,
});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
2025-09-04 17:31:45 +09:00
// API 기본 URL 설정 (동적으로 결정)
2025-08-21 09:41:46 +09:00
/**
*
*/
const fetchCurrentUser = useCallback(async (): Promise<UserInfo | null> => {
try {
console.log("=== fetchCurrentUser 시작 ===");
const response = await apiCall<UserInfo>("GET", "/auth/me");
console.log("fetchCurrentUser 응답:", response);
if (response.success && response.data) {
console.log("사용자 정보 조회 성공:", response.data);
2025-08-25 17:22:20 +09:00
// 사용자 로케일 정보도 함께 조회하여 전역 저장
try {
const localeResponse = await apiCall<string>("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");
}
2025-08-21 09:41:46 +09:00
return response.data;
}
console.log("사용자 정보 조회 실패 - 응답이 없음");
return null;
} catch (error) {
console.error("사용자 정보 조회 실패:", error);
return null;
}
}, []);
/**
*
*/
const checkAuthStatus = useCallback(async (): Promise<AuthStatus> => {
try {
console.log("=== checkAuthStatus 시작 ===");
const response = await apiCall<AuthStatus>("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);
2025-08-21 09:41:46 +09:00
return;
}
console.log("=== refreshUserData 디버깅 ===");
console.log("토큰 존재:", !!token);
2025-08-21 09:41:46 +09:00
// 토큰이 있으면 임시로 인증된 상태로 설정
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);
}
2025-08-21 09:41:46 +09:00
}
} catch (error) {
console.error("사용자 데이터 새로고침 실패:", error);
setError("사용자 정보를 불러오는데 실패했습니다.");
// 오류 발생 시 토큰 제거 및 로그인 페이지로 리다이렉트
TokenManager.removeToken();
setUser(null);
setAuthStatus({ isLoggedIn: false, isAdmin: false });
console.log("사용자 데이터 새로고침 실패 - 3초 후 로그인 페이지로 리다이렉트");
setTimeout(() => {
router.push("/login");
}, 3000);
2025-08-21 09:41:46 +09:00
} finally {
setLoading(false);
}
}, [fetchCurrentUser, checkAuthStatus, router]);
/**
*
*/
const login = useCallback(
async (userId: string, password: string): Promise<LoginResult> => {
try {
setLoading(true);
setError(null);
const response = await apiCall<any>("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<boolean> => {
try {
setLoading(true);
const response = await apiCall("POST", "/auth/logout");
// JWT 토큰 제거
TokenManager.removeToken();
2025-08-25 17:22:20 +09:00
// 로케일 정보도 제거
localStorage.removeItem("userLocale");
localStorage.removeItem("userLocaleLoaded");
(window as any).__GLOBAL_USER_LANG = undefined;
(window as any).__GLOBAL_USER_LOCALE_LOADED = undefined;
2025-08-21 09:41:46 +09:00
// 로그아웃 API 호출 성공 여부와 관계없이 클라이언트 상태 초기화
setUser(null);
setAuthStatus({
isLoggedIn: false,
isAdmin: false,
});
setError(null);
// 로그인 페이지로 리다이렉트
router.push("/login");
return response.success;
} catch (error) {
console.error("로그아웃 처리 실패:", error);
// 오류가 발생해도 JWT 토큰 제거 및 클라이언트 상태 초기화
TokenManager.removeToken();
2025-08-25 17:22:20 +09:00
// 로케일 정보도 제거
localStorage.removeItem("userLocale");
localStorage.removeItem("userLocaleLoaded");
(window as any).__GLOBAL_USER_LANG = undefined;
(window as any).__GLOBAL_USER_LOCALE_LOADED = undefined;
2025-08-21 09:41:46 +09:00
setUser(null);
setAuthStatus({
isLoggedIn: false,
isAdmin: false,
});
router.push("/login");
return false;
} finally {
setLoading(false);
}
}, [apiCall, router]);
/**
*
*/
const checkMenuAuth = useCallback(async (menuUrl: string): Promise<boolean> => {
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);
2025-08-21 09:41:46 +09:00
// 로그인 페이지에서는 인증 상태 확인하지 않음
if (window.location.pathname === "/login") {
console.log("로그인 페이지 - 인증 상태 확인 건너뜀");
2025-08-21 09:41:46 +09:00
return;
}
// 토큰이 있는 경우에만 인증 상태 확인
const token = TokenManager.getToken();
console.log("localStorage 토큰:", token ? "존재" : "없음");
2025-08-21 09:41:46 +09:00
if (token && !TokenManager.isTokenExpired(token)) {
console.log("유효한 토큰 존재 - 사용자 데이터 새로고침");
// 토큰이 있으면 임시로 인증된 상태로 설정 (API 호출 전에)
setAuthStatus({
isLoggedIn: true,
isAdmin: false, // API 호출 후 업데이트될 예정
});
2025-08-21 09:41:46 +09:00
refreshUserData();
} else if (!token) {
console.log("토큰이 없음 - 3초 후 로그인 페이지로 리다이렉트");
// 토큰이 없으면 3초 후 로그인 페이지로 리다이렉트
setTimeout(() => {
router.push("/login");
}, 3000);
} else {
console.log("토큰 만료 - 3초 후 로그인 페이지로 리다이렉트");
TokenManager.removeToken();
setTimeout(() => {
router.push("/login");
}, 3000);
2025-08-21 09:41:46 +09:00
}
}, [refreshUserData, router]); // refreshUserData 의존성 추가
2025-08-21 09:41:46 +09:00
/**
*
*/
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;