- 공통 상단 메뉴(사이트 이름·홈·게임 목록·관리자·어디서나 코드 입력·내 정보) - 홈: 방 만들기 + 코드 입력 카드, 게임 격자, 지금 열린 공개 방 목록(/api/public-rooms) - 대기실: 두 칸(초대·설정 바로 수정·규칙 요약 / 자리·시작 버튼·채팅 항상 보임) - 게임 화면: 판(화면 높이에 맞춤, 좌표 표시) + 오른쪽 패널(상태·선수·시계·버튼·기보·채팅) - 오목: 마우스 올리면 미리 보기 돌, PC는 한 번 클릭으로 착수(두 번 눌러 확정은 터치 기기 기본값, 기기별 설정), GameUI에 Side 패널 추가 - 채팅: PC는 패널로 항상 표시(Enter로 바로 입력), 휴대폰은 시트 - 관리자: 왼쪽 사이드바, 사용자 표, 규칙 입력 두 칸 격자, 저장 바 고정 - 좁은 화면은 같은 화면을 한 칸으로 접어서 지원(방 상단 줄 줄바꿈 수정) - E2E: PC 1440×900 기준(방장 PC, 친구 휴대폰), 관리자 E2E도 PC Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
85 lines
3.4 KiB
TypeScript
85 lines
3.4 KiB
TypeScript
import { StrictMode, Suspense, lazy, useEffect } from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
import { BrowserRouter, Route, Routes, useLocation, useNavigate } from 'react-router';
|
|
import 'pretendard/dist/web/variable/pretendardvariable-dynamic-subset.css';
|
|
import './styles/global.css';
|
|
import { ConnectionBanner, Toasts } from './components/ui';
|
|
import { useSession } from './store/session';
|
|
import { toast } from './store/toast';
|
|
import { Home } from './pages/Home';
|
|
import { SiteHeader } from './components/SiteHeader';
|
|
|
|
const JoinPage = lazy(() => import('./pages/JoinPage'));
|
|
const RoomPage = lazy(() => import('./pages/RoomPage'));
|
|
const GamePage = lazy(() => import('./pages/GamePage'));
|
|
const MePage = lazy(() => import('./pages/MePage'));
|
|
const RulesPage = lazy(() => import('./pages/RulesPage'));
|
|
const NotFound = lazy(() => import('./pages/NotFound'));
|
|
const AdminApp = lazy(() => import('./pages/admin/AdminApp'));
|
|
const GamesPage = lazy(() => import('./pages/GamesPage'));
|
|
|
|
function Announcement() {
|
|
const text = useSession((s) => s.announcement);
|
|
const maintenance = useSession((s) => s.maintenance);
|
|
if (!text && !maintenance) return null;
|
|
return (
|
|
<div role="note" style={{ background: maintenance ? 'var(--warning-soft)' : 'var(--primary-soft)', padding: '8px 16px', textAlign: 'center', fontWeight: 600 }}>
|
|
{maintenance && '🛠 점검 중 · '}
|
|
{text || '새 방을 잠시 만들 수 없어요.'}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Shell() {
|
|
const loaded = useSession((s) => s.loaded);
|
|
const nav = useNavigate();
|
|
const loc = useLocation();
|
|
useEffect(() => {
|
|
void useSession.getState().refresh();
|
|
const onUnauthorized = () => void useSession.getState().refresh();
|
|
window.addEventListener('bg:unauthorized', onUnauthorized);
|
|
return () => window.removeEventListener('bg:unauthorized', onUnauthorized);
|
|
}, []);
|
|
useEffect(() => {
|
|
const q = new URLSearchParams(loc.search);
|
|
if (q.get('linked') === '1') toast('디스코드 계정과 연결했어요.');
|
|
if (q.get('login') === 'failed') toast('디스코드 로그인에 실패했어요. 다시 시도해 주세요.', 'error');
|
|
if (q.get('login') === 'banned') toast('이용이 제한된 계정이에요.', 'error', 6000);
|
|
if (q.has('linked') || q.has('login')) {
|
|
q.delete('linked');
|
|
q.delete('login');
|
|
nav({ pathname: loc.pathname, search: q.toString() }, { replace: true });
|
|
}
|
|
}, [loc.search]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
if (!loaded) return <div className="page muted">불러오는 중…</div>;
|
|
return (
|
|
<>
|
|
<ConnectionBanner />
|
|
<SiteHeader />
|
|
<Announcement />
|
|
<Toasts />
|
|
<Suspense fallback={<div className="page muted">불러오는 중…</div>}>
|
|
<Routes>
|
|
<Route path="/" element={<Home />} />
|
|
<Route path="/j" element={<JoinPage />} />
|
|
<Route path="/games" element={<GamesPage />} />
|
|
<Route path="/r/:code" element={<RoomPage />} />
|
|
<Route path="/g/:gameId" element={<GamePage />} />
|
|
<Route path="/rules/:gameId" element={<RulesPage />} />
|
|
<Route path="/me" element={<MePage />} />
|
|
<Route path="/admin/*" element={<AdminApp />} />
|
|
<Route path="*" element={<NotFound />} />
|
|
</Routes>
|
|
</Suspense>
|
|
</>
|
|
);
|
|
}
|
|
|
|
createRoot(document.getElementById('root')!).render(
|
|
<StrictMode>
|
|
<BrowserRouter>
|
|
<Shell />
|
|
</BrowserRouter>
|
|
</StrictMode>,
|
|
);
|