임동욱 임동욱 08-11
feat: 로그아웃 실패 시 사용자 알림 추가
로그아웃 Server Action이 백엔드 호출 실패를 /login?logoutError=1로 알리면, 새 클라이언트
컴포넌트 LogoutNotice가 마운트 시 기존 FeedbackProvider/Toast로 경고 토스트를 띄우고
쿼리 파라미터를 제거한다. redirect()는 Server Action에서 즉시 던져져 클라이언트가 결과를
돌려받을 수 없으므로, 토스트 표시를 목적지 페이지 마운트 이후로 미뤄 redirect와 경쟁하지
않게 한다. 내부 백엔드 메시지는 노출하지 않고 고정 문구만 보여준다.

Co-Authored-By: Claude Opus 5 
@4c90fb10e391791627b9de02830fc89b44be2f2a
 
app/(public)/login/_components/logout-notice.tsx (added)
+++ app/(public)/login/_components/logout-notice.tsx
@@ -0,0 +1,42 @@
+'use client';
+
+import { useEffect } from 'react';
+import { useRouter } from 'next/navigation';
+import { useFeedback } from '@/app/_hooks/use-feedback';
+
+interface LogoutNoticeProps {
+  /** 로그아웃 Server Action이 백엔드 호출 실패를 알리려고 실어 보낸 쿼리 파라미터가 있었는가. */
+  showError: boolean;
+}
+
+const LOGOUT_ERROR_MESSAGE =
+  '로그아웃 처리 중 일부 오류가 있었지만 로그아웃되었습니다.';
+
+/**
+ * 화면에 그릴 UI가 없는 알림 트리거 컴포넌트 — `app/(protected)/_actions.ts`의 `logout()`이
+ * 백엔드 로그아웃 호출 실패를 알리려고 `/login`에 실어 보낸 쿼리 파라미터를 Page(Server
+ * Component)가 읽어 `showError`로 넘겨주면, 마운트 시 1회 토스트로 보여준다.
+ *
+ * Server Action은 redirect() 이후 클라이언트에 값을 돌려줄 방법이 없다(redirect가 즉시
+ * 던져짐) — 그래서 이 화면에 도착해 마운트된 뒤(=redirect가 끝난 뒤)에만 알릴 수 있다. 이
+ * 컴포넌트는 오직 이 시점에만 반응하므로 redirect와 토스트 표시가 시점상 겹치지 않는다.
+ *
+ * 표시 후에는 쿼리 파라미터를 제거한다(router.replace) — 새로고침·북마크 재방문 시 지난 실패가
+ * 다시 뜨는 것을 막는다.
+ */
+export function LogoutNotice({ showError }: LogoutNoticeProps) {
+  const router = useRouter();
+  const { showToast } = useFeedback();
+
+  useEffect(() => {
+    if (!showError) {
+      return;
+    }
+
+    // 내부 정보(백엔드 message 원문 등)는 노출하지 않는다 — 항상 이 고정 문구만 보여준다.
+    showToast({ variant: 'warning', message: LOGOUT_ERROR_MESSAGE });
+    router.replace('/login');
+  }, [showError, showToast, router]);
+
+  return null;
+}
app/(public)/login/page.tsx
--- app/(public)/login/page.tsx
+++ app/(public)/login/page.tsx
@@ -1,16 +1,31 @@
 import { redirect } from 'next/navigation';
 import { getSessionAdmin } from '@/lib/auth/dal';
+import { LOGOUT_ERROR_QUERY_PARAM } from '@/lib/constants/logout';
 import { LoginForm } from './_components/login-form';
+import { LogoutNotice } from './_components/logout-notice';
 
 // cookies()로 이미 동적이지만, 정적 프리렌더로 데이터가 빌드 산출물에 박히는 경로를
 // 원천 차단하기 위해 명시적으로 강제한다.
 export const dynamic = 'force-dynamic';
 
-export default async function Page() {
+interface PageProps {
+  searchParams: Promise<Record<string, string | string[] | undefined>>;
+}
+
+export default async function Page({ searchParams }: PageProps) {
   const admin = await getSessionAdmin();
   if (admin) {
     redirect('/');
   }
 
-  return <LoginForm />;
+  const resolvedSearchParams = await searchParams;
+  const logoutError = resolvedSearchParams[LOGOUT_ERROR_QUERY_PARAM];
+  const showLogoutError = typeof logoutError === 'string' && logoutError.length > 0;
+
+  return (
+    <>
+      <LogoutNotice showError={showLogoutError} />
+      <LoginForm />
+    </>
+  );
 }
Add a comment
List