549 lines
16 KiB
TypeScript
549 lines
16 KiB
TypeScript
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<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);
|
|
const initializedRef = useRef(false);
|
|
|
|
// API 기본 URL 설정 (동적으로 결정)
|
|
|
|
/**
|
|
* 현재 사용자 정보 조회
|
|
*/
|
|
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);
|
|
|
|
// 사용자 로케일 정보도 함께 조회하여 전역 저장
|
|
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");
|
|
}
|
|
|
|
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);
|
|
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<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();
|
|
|
|
// 로케일 정보도 제거
|
|
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<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(() => {
|
|
// 이미 초기화되었으면 실행하지 않음
|
|
if (initializedRef.current) {
|
|
return;
|
|
}
|
|
|
|
initializedRef.current = true;
|
|
|
|
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);
|
|
}
|
|
}, []); // 초기 마운트 시에만 실행
|
|
|
|
/**
|
|
* 세션 만료 감지 및 처리
|
|
*/
|
|
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;
|