- Deleted the following files as they are no longer relevant to the current project structure: - 결재 시스템 구현 현황 - 결재 시스템 v2 사용 가이드 - WACE 시스템 문제점 분석 및 개선 계획 - Agent Pipeline 한계점 분석 - AI 기반 화면 자동 생성 시스템 설계서 - WACE ERP Backend - 분석 문서 인덱스 These deletions help streamline the documentation and remove obsolete information, ensuring that only current and relevant resources are maintained.
457 lines
13 KiB
TypeScript
457 lines
13 KiB
TypeScript
import { useState, useEffect, useCallback, useRef } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { apiCall } from "@/lib/api/client";
|
|
import { AuthLogger } from "@/lib/authLogger";
|
|
|
|
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<T = any> {
|
|
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<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);
|
|
|
|
/**
|
|
* 현재 사용자 정보 조회
|
|
*/
|
|
const fetchCurrentUser = useCallback(async (): Promise<UserInfo | null> => {
|
|
try {
|
|
const response = await apiCall<UserInfo>("GET", "/auth/me");
|
|
|
|
if (response.success && response.data) {
|
|
// 사용자 로케일 정보 조회
|
|
try {
|
|
const localeResponse = await apiCall<string>("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<AuthStatus> => {
|
|
try {
|
|
const response = await apiCall<AuthStatus>("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) {
|
|
AuthLogger.log("AUTH_CHECK_FAIL", "refreshUserData: 토큰 없음");
|
|
setUser(null);
|
|
setAuthStatus({ isLoggedIn: false, isAdmin: false });
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
// 만료된 토큰이라도 apiClient 요청 인터셉터가 자동 갱신하므로 여기서 차단하지 않음
|
|
|
|
AuthLogger.log("AUTH_CHECK_START", "refreshUserData: API로 인증 상태 확인 시작");
|
|
|
|
setAuthStatus({
|
|
isLoggedIn: true,
|
|
isAdmin: false,
|
|
});
|
|
|
|
try {
|
|
// /auth/me 성공 = 인증 확인 완료. /auth/status는 보조 정보(isAdmin)만 참조
|
|
// 두 API를 Promise.all로 호출 시, 토큰 만료 타이밍에 따라
|
|
// /auth/me는 401→갱신→성공, /auth/status는 200 isAuthenticated:false를 반환하는
|
|
// 레이스 컨디션이 발생할 수 있으므로, isLoggedIn 판단은 /auth/me 성공 여부로 결정
|
|
const [userInfo, authStatusData] = await Promise.all([fetchCurrentUser(), checkAuthStatus()]);
|
|
|
|
if (userInfo) {
|
|
setUser(userInfo);
|
|
|
|
const isAdminFromUser = userInfo.userId === "plm_admin" || userInfo.userType === "ADMIN";
|
|
const finalAuthStatus = {
|
|
isLoggedIn: true,
|
|
isAdmin: authStatusData.isAdmin || isAdminFromUser,
|
|
};
|
|
|
|
setAuthStatus(finalAuthStatus);
|
|
AuthLogger.log("AUTH_CHECK_SUCCESS", `사용자: ${userInfo.userId}, 인증: ${finalAuthStatus.isLoggedIn}`);
|
|
} else {
|
|
AuthLogger.log("AUTH_CHECK_FAIL", "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 {
|
|
AuthLogger.log("AUTH_CHECK_FAIL", "토큰 파싱 실패 → 비인증 전환");
|
|
TokenManager.removeToken();
|
|
setUser(null);
|
|
setAuthStatus({ isLoggedIn: false, isAdmin: false });
|
|
}
|
|
}
|
|
} catch {
|
|
AuthLogger.log("AUTH_CHECK_FAIL", "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 {
|
|
AuthLogger.log("AUTH_CHECK_FAIL", "최종 fallback 실패 → 비인증 전환");
|
|
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<LoginResult> => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
const response = await apiCall<any>("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<any>("POST", "/auth/switch-company", {
|
|
companyCode,
|
|
});
|
|
|
|
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 || "회사 전환에 실패했습니다.",
|
|
};
|
|
}
|
|
} catch (error: any) {
|
|
const errorMessage = error.message || "회사 전환 중 오류가 발생했습니다.";
|
|
setError(errorMessage);
|
|
|
|
return {
|
|
success: false,
|
|
message: errorMessage,
|
|
};
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
},
|
|
[refreshUserData],
|
|
);
|
|
|
|
/**
|
|
* 로그아웃 처리
|
|
*/
|
|
const logout = useCallback(async (): Promise<boolean> => {
|
|
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<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 {
|
|
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) {
|
|
// 유효/만료 모두 refreshUserData로 처리
|
|
// apiClient 요청 인터셉터가 만료 토큰을 자동 갱신하므로 여기서 삭제하지 않음
|
|
const isExpired = TokenManager.isTokenExpired(token);
|
|
AuthLogger.log(
|
|
"AUTH_CHECK_START",
|
|
`초기 인증 확인: 토큰 ${isExpired ? "만료됨 → 갱신 시도" : "유효"} (경로: ${window.location.pathname})`,
|
|
);
|
|
setAuthStatus({
|
|
isLoggedIn: true,
|
|
isAdmin: false,
|
|
});
|
|
refreshUserData();
|
|
} else {
|
|
AuthLogger.log("AUTH_CHECK_FAIL", `초기 확인: 토큰 없음 (경로: ${window.location.pathname})`);
|
|
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;
|