Files
invyone/frontend/hooks/useAuth.ts
T
DDD1542 2f398ae0b3 chore: 제어모드 IDE 작업 + v2/legacy 레지스트리 컴포넌트 폐기
- 제어모드 IDE: ControlCardPanel, control/ide/* (Canvas/LeftRail/RightRail/PanZoomStage/V3RuleNode 등), schemas, lib/api/control
- 레지스트리 정리: aggregation-widget, status-count, section-card/paper, table-list(legacy/v2), tabs-widget 폐기 → table/_shared/ 로 통합
- InvLegacyButtonConfigPanel cp 마이그레이션
- canonical data view cleanup 후속 노트
2026-05-19 21:31:03 +09:00

314 lines
8.7 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";
import { TokenManager } from "@/lib/auth/tokenManager";
interface UserInfo {
user_id?: string;
user_name?: string;
userNameEng?: string;
userNameCn?: string;
deptCode?: string;
dept_name?: string;
positionCode?: string;
position_name?: string;
email?: string;
tel?: string;
cellPhone?: string;
user_type?: string;
userTypeName?: string;
authName?: string;
partnerCd?: string;
locale?: string;
isAdmin: boolean;
sabun?: string;
photo?: string | null;
company_code?: string;
force_password_change?: boolean;
}
interface AuthStatus {
isLoggedIn: boolean;
isAdmin: boolean;
userId?: string;
deptCode?: string;
}
interface ApiResponse<T = any> {
success: boolean;
message: string;
data?: T;
errorCode?: string;
}
/**
* 인증 상태 관리 훅
* - 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) {
const userLocale = response.data.locale || "KR";
(window as any).__GLOBAL_USER_LANG = userLocale;
(window as any).__GLOBAL_USER_LOCALE_LOADED = true;
localStorage.setItem("userLocale", userLocale);
localStorage.setItem("userLocaleLoaded", "true");
const data = response.data;
return {
...data,
user_type: data.user_type,
company_code: data.company_code,
user_id: data.user_id,
user_name: data.user_name,
};
}
return null;
} catch {
return null;
}
}, []);
/**
* 사용자 데이터 새로고침
* - 백엔드 /auth/me 가 성공해야만 로그인 상태 유지
* - /auth/me 응답에 is_admin, locale, force_password_change 등이 모두 포함되므로
* 이전에 별도로 호출하던 /auth/status, /admin/user-locale 은 제거됨 (라운드트립 절약)
*/
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,
});
const userInfo = await fetchCurrentUser();
if (userInfo) {
setUser(userInfo);
// 백엔드 AuthService.checkAuthStatus 와 동일한 판정 로직을 user_type 기반으로 적용.
// (별도 /auth/status 호출 없이 동일 결과)
const userType = userInfo.user_type;
const isAdmin = userInfo.user_id === "plm_admin"
|| userType === "ADMIN"
|| userType === "SUPER_ADMIN"
|| userType === "COMPANY_ADMIN";
const finalAuthStatus = {
isLoggedIn: true,
isAdmin,
};
setAuthStatus(finalAuthStatus);
AuthLogger.log("AUTH_CHECK_SUCCESS", `사용자: ${userInfo.user_id}, 인증: ${finalAuthStatus.isLoggedIn}`);
} else {
AuthLogger.log("AUTH_CHECK_FAIL", "userInfo 조회 실패 → 비인증 전환");
TokenManager.removeToken();
setUser(null);
setAuthStatus({ isLoggedIn: false, isAdmin: false });
}
} catch {
AuthLogger.log("AUTH_CHECK_FAIL", "API 호출 예외 → 비인증 전환");
TokenManager.removeToken();
setUser(null);
setAuthStatus({ isLoggedIn: false, isAdmin: false });
setError("사용자 정보를 불러오는데 실패했습니다.");
} finally {
setLoading(false);
}
}, [fetchCurrentUser]);
/**
* 회사 전환 처리 (Invyone 관리자 전용)
*/
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", {
company_code: 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,
forcePasswordChange: user?.force_password_change === true,
userId: user?.user_id,
userName: user?.user_name,
companyCode: user?.company_code,
logout,
switchCompany,
checkMenuAuth,
refreshUserData,
clearError: () => setError(null),
};
};
export default useAuth;