jskim-node #402

Merged
kjs merged 8 commits from jskim-node into main 2026-03-05 13:32:18 +09:00
3 changed files with 22 additions and 19 deletions
Showing only changes of commit d43f0821ed - Show all commits

View File

@ -2,7 +2,6 @@
// Phase 2-1B: 핵심 인증 API 구현 // Phase 2-1B: 핵심 인증 API 구현
import { Router } from "express"; import { Router } from "express";
import { checkAuthStatus } from "../middleware/authMiddleware";
import { AuthController } from "../controllers/authController"; import { AuthController } from "../controllers/authController";
const router = Router(); const router = Router();
@ -12,7 +11,7 @@ const router = Router();
* API * API
* Java ApiLoginController.checkAuthStatus() * Java ApiLoginController.checkAuthStatus()
*/ */
router.get("/status", checkAuthStatus); router.get("/status", AuthController.checkAuthStatus);
/** /**
* POST /api/auth/login * POST /api/auth/login

View File

@ -161,13 +161,14 @@ export const useAuth = () => {
setLoading(true); setLoading(true);
const token = TokenManager.getToken(); const token = TokenManager.getToken();
if (!token || TokenManager.isTokenExpired(token)) { if (!token) {
AuthLogger.log("AUTH_CHECK_FAIL", `refreshUserData: 토큰 ${!token ? "없음" : "만료됨"}`); AuthLogger.log("AUTH_CHECK_FAIL", "refreshUserData: 토큰 없음");
setUser(null); setUser(null);
setAuthStatus({ isLoggedIn: false, isAdmin: false }); setAuthStatus({ isLoggedIn: false, isAdmin: false });
setLoading(false); setLoading(false);
return; return;
} }
// 만료된 토큰이라도 apiClient 요청 인터셉터가 자동 갱신하므로 여기서 차단하지 않음
AuthLogger.log("AUTH_CHECK_START", "refreshUserData: API로 인증 상태 확인 시작"); AuthLogger.log("AUTH_CHECK_START", "refreshUserData: API로 인증 상태 확인 시작");
@ -177,6 +178,10 @@ export const useAuth = () => {
}); });
try { 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()]); const [userInfo, authStatusData] = await Promise.all([fetchCurrentUser(), checkAuthStatus()]);
if (userInfo) { if (userInfo) {
@ -184,19 +189,12 @@ export const useAuth = () => {
const isAdminFromUser = userInfo.userId === "plm_admin" || userInfo.userType === "ADMIN"; const isAdminFromUser = userInfo.userId === "plm_admin" || userInfo.userType === "ADMIN";
const finalAuthStatus = { const finalAuthStatus = {
isLoggedIn: authStatusData.isLoggedIn, isLoggedIn: true,
isAdmin: authStatusData.isAdmin || isAdminFromUser, isAdmin: authStatusData.isAdmin || isAdminFromUser,
}; };
setAuthStatus(finalAuthStatus); setAuthStatus(finalAuthStatus);
AuthLogger.log("AUTH_CHECK_SUCCESS", `사용자: ${userInfo.userId}, 인증: ${finalAuthStatus.isLoggedIn}`); AuthLogger.log("AUTH_CHECK_SUCCESS", `사용자: ${userInfo.userId}, 인증: ${finalAuthStatus.isLoggedIn}`);
if (!finalAuthStatus.isLoggedIn) {
AuthLogger.log("AUTH_CHECK_FAIL", "API 응답에서 비인증 상태 반환 → 토큰 제거");
TokenManager.removeToken();
setUser(null);
setAuthStatus({ isLoggedIn: false, isAdmin: false });
}
} else { } else {
AuthLogger.log("AUTH_CHECK_FAIL", "userInfo 조회 실패 → 토큰 기반 임시 인증 유지 시도"); AuthLogger.log("AUTH_CHECK_FAIL", "userInfo 조회 실패 → 토큰 기반 임시 인증 유지 시도");
try { try {
@ -412,18 +410,19 @@ export const useAuth = () => {
const token = TokenManager.getToken(); const token = TokenManager.getToken();
if (token && !TokenManager.isTokenExpired(token)) { if (token) {
AuthLogger.log("AUTH_CHECK_START", `초기 인증 확인: 유효한 토큰 존재 (경로: ${window.location.pathname})`); // 유효/만료 모두 refreshUserData로 처리
// apiClient 요청 인터셉터가 만료 토큰을 자동 갱신하므로 여기서 삭제하지 않음
const isExpired = TokenManager.isTokenExpired(token);
AuthLogger.log(
"AUTH_CHECK_START",
`초기 인증 확인: 토큰 ${isExpired ? "만료됨 → 갱신 시도" : "유효"} (경로: ${window.location.pathname})`,
);
setAuthStatus({ setAuthStatus({
isLoggedIn: true, isLoggedIn: true,
isAdmin: false, isAdmin: false,
}); });
refreshUserData(); refreshUserData();
} else if (token && TokenManager.isTokenExpired(token)) {
AuthLogger.log("TOKEN_EXPIRED_DETECTED", `초기 확인 시 만료된 토큰 발견 → 정리 (경로: ${window.location.pathname})`);
TokenManager.removeToken();
setAuthStatus({ isLoggedIn: false, isAdmin: false });
setLoading(false);
} else { } else {
AuthLogger.log("AUTH_CHECK_FAIL", `초기 확인: 토큰 없음 (경로: ${window.location.pathname})`); AuthLogger.log("AUTH_CHECK_FAIL", `초기 확인: 토큰 없음 (경로: ${window.location.pathname})`);
setAuthStatus({ isLoggedIn: false, isAdmin: false }); setAuthStatus({ isLoggedIn: false, isAdmin: false });

View File

@ -329,6 +329,11 @@ apiClient.interceptors.request.use(
const newToken = await refreshToken(); const newToken = await refreshToken();
if (newToken) { if (newToken) {
config.headers.Authorization = `Bearer ${newToken}`; config.headers.Authorization = `Bearer ${newToken}`;
} else {
// 갱신 실패 시 인증 없는 요청을 보내면 TOKEN_MISSING 401 → 즉시 redirectToLogin 연쇄 장애
// 요청 자체를 차단하여 호출부의 try/catch에서 처리하도록 함
authLog("TOKEN_REFRESH_FAIL", `요청 인터셉터에서 갱신 실패 → 요청 차단 (${config.url})`);
return Promise.reject(new Error("TOKEN_REFRESH_FAILED"));
} }
} }
} }