diff --git a/backend/app/core/security.py b/backend/app/core/security.py index fd8ef7a..d66e466 100644 --- a/backend/app/core/security.py +++ b/backend/app/core/security.py @@ -82,10 +82,13 @@ async def require_api_key(api_key: str | None = Security(_api_key_header)) -> st ) if not is_valid_credential(api_key): + # 401: 자격증명 자체가 무효(잘못된 키·만료된 세션) — 프론트가 저장된 토큰을 + # 지우고 로그인 화면으로 보내는 트리거. 403은 권한 부족(관리자 전용)에만 사용. + # (세션 24h 만료 후 403이 돌아와 화면 전체가 오류로 멈추던 문제, 2026-09-22) logger.warning("[Security] API 키/세션 토큰 불일치 — 요청 거부") raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="유효하지 않은 API 키입니다.", + status_code=status.HTTP_401_UNAUTHORIZED, + detail="유효하지 않은 API 키이거나 만료된 세션입니다. 다시 로그인하세요.", ) return api_key @@ -101,8 +104,8 @@ async def optional_api_key(api_key: str | None = Security(_api_key_header)) -> s if not is_valid_credential(api_key): raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="유효하지 않은 API 키입니다.", + status_code=status.HTTP_401_UNAUTHORIZED, + detail="유효하지 않은 API 키이거나 만료된 세션입니다. 다시 로그인하세요.", ) return api_key diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 39e644f..45e24b9 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -128,11 +128,19 @@ api.interceptors.response.use( window.location.reload() break - case 403: - // 권한 부족 + case 403: { + // 권한 부족. 단, 구버전 백엔드는 무효/만료 자격증명에도 403을 돌려주므로 + // 그 메시지면 401과 동일하게 토큰 제거 → 로그인 화면으로. + const detail = String((error.response.data as any)?.detail ?? '') + if (detail.includes('유효하지 않은 API 키')) { + console.warn('Credential rejected (403):', config.url) + localStorage.removeItem('api_key') + window.location.reload() + break + } console.error('🚫 Permission denied:', message) - // 필요시 권한 오류 토스트 표시 break + } case 429: // 요청 제한