- 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.
452 lines
13 KiB
TypeScript
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;
|