- Replaced the middleware `checkAuthStatus` with the `AuthController.checkAuthStatus` method in the authentication routes for improved clarity and structure. - Simplified token validation logic in the `useAuth` hook by removing unnecessary checks for expired tokens, allowing the API client to handle token refresh automatically. - Enhanced logging for authentication checks to provide clearer insights into the authentication flow and potential issues. - Adjusted the handling of user authentication status to ensure consistency and reliability in user state management. This refactor streamlines the authentication process and improves the overall maintainability of the authentication logic.
456 lines
13 KiB
TypeScript
456 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);
|
|
|
|
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);
|
|
}
|
|
},
|
|
[],
|
|
);
|
|
|
|
/**
|
|
* 로그아웃 처리
|
|
*/
|
|
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;
|