File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
'use server';
import { redirect } from 'next/navigation';
import { revalidatePath } from 'next/cache';
import { deleteSession, readSessionToken } from '@/lib/auth/session';
import { getSessionAccessToken } from '@/lib/auth/dal';
import { requestAdminLogout } from '@/lib/data/repositories/auth-repository';
import { LOGOUT_ERROR_QUERY_PARAM } from '@/lib/constants/logout';
/**
* 로그아웃 Server Action — 자기 세션을 파기하는 동작이라 `verifySession()`의 전제(관리자 조회)가
* 성립하지 않는다. 토큰 존재 여부만 확인해 이미 로그아웃된 상태의 중복 호출도 안전하게 처리한다.
*
* 백엔드 로그아웃 호출(GET .../auth/logout)은 best-effort다 — 실패해도(백엔드 다운,
* accessToken 만료 등) 로컬 세션은 반드시 파기한다. 사용자가 로그아웃 자체를 못 하게 되는
* 상황을 절대 만들지 않기 위함이다. accessToken이 애초에 없으면(getSessionAccessToken()이
* 형식·만료·유저일치 검사에서 이미 null을 반환한 경우) 호출 자체를 건너뛴다 — 보낼 유효한
* 토큰이 없는 것은 "실패"가 아니라 "애초에 알릴 대상이 없음"이므로 실패로 취급하지 않는다.
*
* 실패 알림은 이 액션이 직접 띄우지 않는다 — Server Action에서 redirect()는 즉시 던져지므로
* (NEXT_REDIRECT) 클라이언트가 반환값을 돌려받아 토스트를 그릴 기회 자체가 없다(redirect와
* 클라이언트 상태 갱신이 같은 턴에서 경쟁하게 됨). 대신 `/login`에 쿼리 파라미터를 실어 보내고,
* 그 화면의 클라이언트 컴포넌트(LogoutNotice)가 마운트된 뒤(=redirect가 완전히 끝난 뒤) 토스트를
* 띄운다 — 시점이 분리되어 있어 경쟁이 원천적으로 없다.
*/
export async function logout(): Promise<void> {
const token = await readSessionToken();
if (!token) {
redirect('/login');
}
const accessToken = await getSessionAccessToken();
const isBackendLogoutSuccessful = accessToken
? await requestAdminLogout(accessToken)
: true;
await deleteSession();
revalidatePath('/', 'layout');
redirect(
isBackendLogoutSuccessful ? '/login' : `/login?${LOGOUT_ERROR_QUERY_PARAM}=1`
);
}