diff --git a/src/hooks/useEnsureGuestTable.ts b/src/hooks/useEnsureGuestTable.ts
new file mode 100644
index 00000000..e18f84b8
--- /dev/null
+++ b/src/hooks/useEnsureGuestTable.ts
@@ -0,0 +1,23 @@
+import { SAMPLE_TABLE_DATA } from '../constants/sample_table';
+import {
+ isGuestFlow,
+ setSessionCustomizeTableData,
+} from '../util/sessionStorage';
+
+export const GUEST_TABLE_ID = 'guest';
+
+/**
+ * 게스트 경로(`/:id === 'guest'`)인데 세션에 테이블 데이터가 없으면
+ * 샘플 테이블을 세션에 채워 게스트 플로우를 복구한다.
+ *
+ * 홈 버튼으로 세션이 비워진 뒤 뒤로가기로 돌아오면 게스트 판별이 풀려
+ * API 저장소로 `Number('guest')`(NaN) 조회가 나가므로, 쿼리가 저장소를
+ * 고르기 전에 동기적으로 실행되도록 렌더 중에 처리한다.
+ * 같은 페이지 인스턴스에서 id만 `guest`로 바뀌는 경우도 막기 위해 매 렌더마다
+ * 확인하며, 세션 데이터가 있으면 아무것도 하지 않으므로 반복 실행해도 안전하다.
+ */
+export function useEnsureGuestTable(id: string | undefined) {
+ if (id === GUEST_TABLE_ID && !isGuestFlow()) {
+ setSessionCustomizeTableData(SAMPLE_TABLE_DATA);
+ }
+}
diff --git a/src/page/TableOverviewPage/TableOverviewPage.tsx b/src/page/TableOverviewPage/TableOverviewPage.tsx
index 4da88f9e..fd921181 100644
--- a/src/page/TableOverviewPage/TableOverviewPage.tsx
+++ b/src/page/TableOverviewPage/TableOverviewPage.tsx
@@ -26,11 +26,13 @@ import {
isSupportedLang,
} from '../../util/languageRouting';
import useAnalytics from '../../hooks/useAnalytics';
+import { useEnsureGuestTable } from '../../hooks/useEnsureGuestTable';
// 토론 개요를 보여주고 공유, 수정, 시작 액션을 제공하는 페이지다.
export default function TableOverviewPage() {
const { t, i18n } = useTranslation();
const { id } = useParams();
+ useEnsureGuestTable(id);
const tableId = Number(id);
const navigate = useNavigate();
const { openModal, closeModal, ModalWrapper } = useModal();
diff --git a/src/page/TableOverviewPage/guestBackNavigation.test.tsx b/src/page/TableOverviewPage/guestBackNavigation.test.tsx
new file mode 100644
index 00000000..dd99ac3e
--- /dev/null
+++ b/src/page/TableOverviewPage/guestBackNavigation.test.tsx
@@ -0,0 +1,127 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { createMemoryRouter, RouterProvider } from 'react-router-dom';
+import { act, render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { afterEach, beforeEach, describe, expect, it } from 'vitest';
+import { GlobalPortal } from '../../util/GlobalPortal';
+import TableOverviewPage from './TableOverviewPage';
+import TableSharingPage from '../TableSharingPage/TableSharingPage';
+import { encodeDebateTableData } from '../../util/arrayEncoding';
+import { server } from '../../mocks/server';
+import { DebateTableData } from '../../types/type';
+import { SAMPLE_TABLE_DATA } from '../../constants/sample_table';
+
+const sharedTable: DebateTableData = {
+ info: {
+ name: '공유받은 테이블',
+ agenda: '공유 주제',
+ prosTeamName: '찬성',
+ consTeamName: '반대',
+ },
+ table: [
+ {
+ stance: 'PROS',
+ speechType: '입론',
+ boxType: 'NORMAL',
+ time: 60,
+ timePerTeam: null,
+ timePerSpeaking: null,
+ speaker: '1',
+ bell: [{ type: 'BEFORE_END', time: 0, count: 2 }],
+ },
+ ],
+};
+
+function renderWithRouter(initialEntry: string) {
+ const router = createMemoryRouter(
+ [
+ { path: '/share', element: },
+ { path: '/overview/:type/:id', element: },
+ { path: '/home', element:
HOME
},
+ ],
+ { initialEntries: [initialEntry] },
+ );
+
+ render(
+
+
+
+
+ ,
+ );
+
+ return router;
+}
+
+describe('비회원 테이블 개요 페이지 - 세션 데이터 유실', () => {
+ const requestedUrls: string[] = [];
+ const onRequestStart = ({ request }: { request: Request }) => {
+ requestedUrls.push(request.url);
+ };
+
+ beforeEach(() => {
+ sessionStorage.clear();
+ requestedUrls.length = 0;
+ server.events.on('request:start', onRequestStart);
+ });
+
+ afterEach(() => {
+ server.events.removeListener('request:start', onRequestStart);
+ });
+
+ it('공유 링크 -> 개요 -> 홈 -> 뒤로가기 시 NaN 조회 없이 샘플 테이블로 복구한다', async () => {
+ const router = renderWithRouter(
+ `/share?data=${encodeURIComponent(encodeDebateTableData(sharedTable))}`,
+ );
+
+ await waitFor(() =>
+ expect(router.state.location.pathname).toBe('/overview/customize/guest'),
+ );
+ await screen.findByText('공유받은 테이블');
+
+ // 헤더 홈 버튼은 게스트 세션 데이터를 삭제한다.
+ await userEvent.click(screen.getAllByLabelText('홈으로 이동')[0]);
+ await waitFor(() => expect(router.state.location.pathname).toBe('/home'));
+ expect(sessionStorage.getItem('DebateTableData')).toBeNull();
+
+ await act(async () => {
+ await router.navigate(-1);
+ });
+ expect(router.state.location.pathname).toBe('/overview/customize/guest');
+
+ await screen.findByText(SAMPLE_TABLE_DATA.info.name);
+ expect(sessionStorage.getItem('DebateTableData')).not.toBeNull();
+ expect(requestedUrls.filter((url) => url.includes('NaN'))).toEqual([]);
+ });
+
+ it('세션 데이터 없이 게스트 경로로 직접 진입해도 샘플 테이블을 보여준다', async () => {
+ renderWithRouter('/overview/customize/guest');
+
+ await screen.findByText(SAMPLE_TABLE_DATA.info.name);
+ expect(requestedUrls.filter((url) => url.includes('NaN'))).toEqual([]);
+ });
+
+ it('같은 페이지에서 회원 테이블 id가 게스트로 바뀌어도 샘플 테이블로 복구한다', async () => {
+ const router = renderWithRouter('/overview/customize/1');
+ await screen.findByText('나의 자유토론 테이블');
+
+ await act(async () => {
+ await router.navigate('/overview/customize/guest');
+ });
+
+ await screen.findByText(SAMPLE_TABLE_DATA.info.name);
+ expect(sessionStorage.getItem('DebateTableData')).not.toBeNull();
+ expect(requestedUrls.filter((url) => url.includes('NaN'))).toEqual([]);
+ });
+
+ it('세션에 게스트 데이터가 있으면 덮어쓰지 않는다', async () => {
+ sessionStorage.setItem(
+ 'DebateTableData',
+ JSON.stringify({ id: -1, ...sharedTable }),
+ );
+ renderWithRouter('/overview/customize/guest');
+
+ await screen.findByText('공유받은 테이블');
+ expect(screen.queryByText(SAMPLE_TABLE_DATA.info.name)).toBeNull();
+ });
+});
diff --git a/src/page/TimerPage/TimerPage.tsx b/src/page/TimerPage/TimerPage.tsx
index 02c7a0c5..2bad5a36 100644
--- a/src/page/TimerPage/TimerPage.tsx
+++ b/src/page/TimerPage/TimerPage.tsx
@@ -20,6 +20,7 @@ import clsx from 'clsx';
import ErrorIndicator from '../../components/ErrorIndicator/ErrorIndicator';
import LoadingIndicator from '../../components/LoadingIndicator/LoadingIndicator';
import { isGuestFlow } from '../../util/sessionStorage';
+import { useEnsureGuestTable } from '../../hooks/useEnsureGuestTable';
import useAnalytics from '../../hooks/useAnalytics';
import { consumeTemplateOrigin } from '../../util/analytics/templateOrigin';
import {
@@ -51,6 +52,7 @@ export default function TimerPage() {
const { t } = useTranslation();
const [answerTime, setAnswerTime] = useState(30);
const pathParams = useParams();
+ useEnsureGuestTable(pathParams.id);
const tableId = Number(pathParams.id);
const {
openUseTooltipModal,