M1.5 관리자 사이트: 슈퍼어드민/어드민 권한, 게임·사이트 설정, 방·사용자 관리, 기록

- 권한: 디스코드 ID로 요청마다 계산. 슈퍼어드민 293719842274541579(SUPERADMIN_DISCORD_IDS),
  어드민은 슈퍼어드민이 디스코드 ID로 추가(로그인 전 미리 등록 가능), 고정 슈퍼어드민은 삭제 불가
- 게임 설정: 사용 여부, 표시 이름·설명·안내문, 원작 범위 안 인원, 모든 규칙 옵션 기본값,
  옵션별 방장 변경 금지(잠금), 초기화. 입력 화면은 옵션 스키마(zod→JSON Schema)로 자동 생성
- 오목 옵션 전체에 한국어 제목·선택지 메타, 엔진 고정값(자동 착수 허용 횟수, 무승부 재제안 간격,
  한 수 제한 초)을 옵션으로 꺼냄
- 사이트 설정: 이름, 공지, 점검 모드, 금지어, 방 수 제한, 연결 유예, 채팅 기본값
- 방 관리(목록·닫기), 사용자 관리(검색·닉네임 변경·이용 제한·강제 로그아웃·삭제, 슈퍼어드민 전용),
  관리자 작업 기록(전/후 값)
- 이용 제한: 세션 삭제 + 접속 종료 + 디스코드 재로그인 거부
- 테스트: 관리자 API 7개(권한·설정 반영·잠금·점검·금지어·방 닫기·이용 제한·기록), 오목 옵션 2개,
  e2e/admin.e2e.ts(슈퍼어드민 로그인→규칙 변경·잠금→어드민 추가→게스트 새 방 반영)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
EJClaw
2026-10-04 04:52:38 +09:00
parent b2f998fa95
commit 242ab3ad71
31 changed files with 2009 additions and 68 deletions

View File

@@ -14,6 +14,19 @@ 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'));
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);
@@ -29,6 +42,7 @@ function Shell() {
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');
@@ -39,6 +53,7 @@ function Shell() {
return (
<>
<ConnectionBanner />
<Announcement />
<Toasts />
<Suspense fallback={<div className="page muted">불러오는 중…</div>}>
<Routes>
@@ -48,6 +63,7 @@ function Shell() {
<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>