Files
vexplor/frontend/hooks/useAuth.ts
DDD1542 27853a9447 feat: Add BOM tree view and BOM item editor components
- Introduced new components for BOM tree view and BOM item editor, enhancing the data management capabilities within the application.
- Updated the ComponentsPanel to include these new components with appropriate descriptions and default sizes.
- Integrated the BOM item editor into the V2PropertiesPanel for seamless editing of BOM items.
- Adjusted the SplitLineComponent to improve the handling of canvas split positions, ensuring better user experience during component interactions.
2026-02-24 10:49:23 +09:00

452 lines
13 KiB
TypeScript

import { useState, useEffect, useCallback, useRef } 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;
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 || TokenManager.isTokenExpired(token)) {
setUser(null);
setAuthStatus({ isLoggedIn: false, isAdmin: false });
setLoading(false);
return;
}
// 토큰이 유효하면 우선 인증된 상태로 설정
setAuthStatus({
isLoggedIn: true,
isAdmin: false,
});
try {
const [userInfo, authStatusData] = await Promise.all([fetchCurrentUser(), checkAuthStatus()]);
if (userInfo) {
setUser(userInfo);
const isAdminFromUser = userInfo.userId === "plm_admin" || userInfo.userType === "ADMIN";
const finalAuthStatus = {
isLoggedIn: authStatusData.isLoggedIn,
isAdmin: authStatusData.isAdmin || isAdminFromUser,
};
setAuthStatus(finalAuthStatus);
// API 결과가 비인증이면 상태만 업데이트 (리다이렉트는 client.ts가 처리)
if (!finalAuthStatus.isLoggedIn) {
TokenManager.removeToken();
setUser(null);
setAuthStatus({ isLoggedIn: false, isAdmin: false });
}
} else {
// 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 {
// 토큰 파싱도 실패하면 비인증 상태로 전환
TokenManager.removeToken();
setUser(null);
setAuthStatus({ isLoggedIn: false, isAdmin: false });
}
}
} catch {
// 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 {
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 && !TokenManager.isTokenExpired(token)) {
// 유효한 토큰 → 우선 인증 상태로 설정 후 API 확인
setAuthStatus({
isLoggedIn: true,
isAdmin: false,
});
refreshUserData();
} else if (token && TokenManager.isTokenExpired(token)) {
// 만료된 토큰 → 정리 (리다이렉트는 AuthGuard에서 처리)
TokenManager.removeToken();
setAuthStatus({ isLoggedIn: false, isAdmin: false });
setLoading(false);
} else {
// 토큰 없음 → 비인증 상태 (리다이렉트는 AuthGuard에서 처리)
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;