diff --git a/apps/cm/app/api/auth/logout/route.ts b/apps/cm/app/api/auth/logout/route.ts index ba8e23374..8740d9d5b 100644 --- a/apps/cm/app/api/auth/logout/route.ts +++ b/apps/cm/app/api/auth/logout/route.ts @@ -9,10 +9,31 @@ import { export async function POST() { const cookieStore = await cookies(); + + // 백엔드에 로그아웃 알림 (토큰 블랙리스트 처리) + const refreshToken = cookieStore.get(REFRESH_TOKEN)?.value; + + if (refreshToken) { + try { + // ⭐ Spring Boot 백엔드에 로그아웃 요청 + await fetch(`${process.env.NEXT_PUBLIC_SERVER_API_URL}/api/auth/logout`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Cookie": `${REFRESH_TOKEN}=${refreshToken}`, + }, + // credentials: "include", // fetch에서는 직접 쿠키 전달 + }); + } catch (error) { + console.error("Backend logout failed:", error); + } + } + + // Next.js 쿠키 삭제 cookieStore.delete(ACCESS_TOKEN); cookieStore.delete(REFRESH_TOKEN); cookieStore.delete(AUTH_USER_ID); cookieStore.delete(CLAIM_NAME); - return NextResponse.json({ message: "Logged out" }, { status: 200 }); -} + return NextResponse.json({ message: "Logged out" }, { status: 200 }); +} \ No newline at end of file diff --git a/apps/cm/src/entities/auth/lib/token-manager.ts b/apps/cm/src/entities/auth/lib/token-manager.ts index 21ed00218..8fcf21984 100644 --- a/apps/cm/src/entities/auth/lib/token-manager.ts +++ b/apps/cm/src/entities/auth/lib/token-manager.ts @@ -1,4 +1,4 @@ -import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config"; +import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, REFRESH_TOKEN } from "@/shared/config"; import { getCookie, setCookie, removeCookie } from "@/shared/lib"; export class TokenManager { @@ -76,6 +76,8 @@ export class TokenManager { this.removeToken(CLAIM_NAME); this.removeToken(AUTH_USER_ID); + this.removeToken(REFRESH_TOKEN); + this.removeToken(ACCESS_TOKEN); } } diff --git a/packages/common/components/src/ui/header/index.tsx b/packages/common/components/src/ui/header/index.tsx index 15b431b6a..d1675d6f1 100644 --- a/packages/common/components/src/ui/header/index.tsx +++ b/packages/common/components/src/ui/header/index.tsx @@ -64,7 +64,99 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) { queryFn: () => getSiteList(), enabled: isLoggedIn && !viewOnly, }); - + const logout = async () => { + try { + // 1. 백엔드 API 로그아웃 호출 + const response = await fetch("/api/auth/logout", { + method: "POST", + credentials: "include", // ⭐ 쿠키 포함 중요! + }); + + if (!response.ok) { + console.error("Logout API failed"); + // API 실패해도 계속 진행 + } + } catch (error) { + console.error("Logout error:", error); + // 에러가 나도 계속 진행 + } finally { + // 2. 클라이언트 쿠키 삭제 (백업) + clearClientCookies(); + + // 3. 로컬/세션 스토리지 정리 + if (typeof window !== "undefined") { + localStorage.clear(); + sessionStorage.clear(); + } + + // 4. 로그인 페이지로 리다이렉트 + window.location.href = "/auth/login"; + } + }; + + // 쿠키 삭제 헬퍼 함수 + const clearClientCookies = () => { + const actualHost = window.location.hostname; + + // 루트 도메인 추출 + const getRootDomain = (hostname: string): string => { + if (hostname === "localhost" || /^\d+\.\d+\.\d+\.\d+$/.test(hostname)) { + return hostname; + } + + if (hostname.includes("hasuon.or.kr")) { + return ".hasuon.or.kr"; + } + + const parts = hostname.split("."); + if (parts.length >= 2) { + return `.${parts.slice(-2).join(".")}`; + } + + return hostname; + }; + + const domain = getRootDomain(actualHost); + + // 삭제할 쿠키 목록 + const cookiesToDelete = [ + appConfig.AUTH_USER_ID, + appConfig.REFRESH_TOKEN, + appConfig.ACCESS_TOKEN, + appConfig.CLAIM_NAME, + "site-id-key", + "site-theme-key", + ].filter(Boolean); // undefined 제거 + + // 여러 방법으로 쿠키 삭제 시도 + cookiesToDelete.forEach((cookieName) => { + if (!cookieName) return; + + // 방법 1: removeCookie 함수 사용 + if (domain === "localhost") { + removeCookie(cookieName, {}); + } else { + removeCookie(cookieName, { domain }); + } + + // 방법 2: 직접 document.cookie 설정 (도메인 포함) + if (domain !== "localhost") { + document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${domain}`; + } + + // 방법 3: 도메인 없이 삭제 (현재 도메인만) + document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; + + // 방법 4: Secure 속성 포함 삭제 + if (window.location.protocol === "https:") { + if (domain !== "localhost") { + document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${domain}; secure`; + } + document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; secure`; + } + }); + }; + /* const logout = async () => { try { const response = await fetch("/api/auth/logout", { @@ -123,7 +215,7 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) { console.error(error); } }; - +*/ const openPopup = useCallback(async (popup: Popup) => { try { const cookieName = `cs-popup-hide-for-day-${popup.popupSn}`;