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:
@@ -2,9 +2,10 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { create } from 'zustand';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import { CHAT_MAX, EMOTE_LABEL, EMOTES, catalogById, formatRoomCode, type PublicUser, type RoomView } from '@bg/shared/lite';
|
||||
import { CHAT_MAX, EMOTE_LABEL, EMOTES, formatRoomCode, type PublicUser, type RoomView } from '@bg/shared/lite';
|
||||
import { Avatar, CodeLabel, ConfirmSheet, CopyButton, ShareButton, Sheet, TopBar } from '../components/ui';
|
||||
import { NicknameForm } from '../components/NicknameForm';
|
||||
import { GameNotice, gameName } from '../components/GameNotice';
|
||||
import { QrModal } from '../components/QrModal';
|
||||
import { api, type RoomPreview } from '../net/api';
|
||||
import { socket, useRoom } from '../net/socket';
|
||||
@@ -44,7 +45,7 @@ export default function RoomPage() {
|
||||
{preview ? (
|
||||
<>
|
||||
<h1>
|
||||
{catalogById(preview.gameId)?.nameKo ?? '게임'} 방에 초대받았어요
|
||||
{gameName(preview.gameId)} 방에 초대받았어요
|
||||
</h1>
|
||||
<p className="muted">
|
||||
방장 {preview.host} · {preview.seated}/{preview.maxPlayers}명 · 코드 {preview.codeLabel}
|
||||
@@ -132,7 +133,7 @@ function Room({ code, me }: { code: string; me: PublicUser }) {
|
||||
);
|
||||
}
|
||||
|
||||
const g = catalogById(room.gameId);
|
||||
const g = { nameKo: gameName(room.gameId) };
|
||||
const inGame = room.status === 'playing' || room.status === 'paused' || (room.status === 'finished' && !!game);
|
||||
const isHost = room.hostId === me.id;
|
||||
const doLeave = () => {
|
||||
@@ -241,9 +242,9 @@ function Lobby({ room, ui, me }: { room: RoomView; ui: GameUI; me: PublicUser })
|
||||
const seated = room.seats.filter(Boolean).length;
|
||||
const need = Math.max(0, room.minPlayers - seated);
|
||||
const url = `${location.origin}/r/${room.code}`;
|
||||
const gameName = catalogById(room.gameId)?.nameKo ?? '게임';
|
||||
const gName = gameName(room.gameId);
|
||||
const host = room.seats.find((s) => s?.user.id === room.hostId)?.user.nickname ?? me.nickname;
|
||||
const shareText = `${host}님이 ${gameName} 방에 초대했어요! 코드 ${formatRoomCode(room.code)}`;
|
||||
const shareText = `${host}님이 ${gName} 방에 초대했어요! 코드 ${formatRoomCode(room.code)}`;
|
||||
const notReady = room.seats.filter((s) => s && !s.ready && s.user.id !== room.hostId).length;
|
||||
const [askStart, setAskStart] = useState(false);
|
||||
|
||||
@@ -257,7 +258,7 @@ function Lobby({ room, ui, me }: { room: RoomView; ui: GameUI; me: PublicUser })
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
|
||||
<CopyButton text={room.code} label="🔢 코드 복사" />
|
||||
<CopyButton text={url} label="🔗 링크 복사" />
|
||||
<ShareButton title={`${gameName} 같이 해요`} text={shareText} url={url} />
|
||||
<ShareButton title={`${gName} 같이 해요`} text={shareText} url={url} />
|
||||
<button className="btn btn-secondary" onClick={() => setQr(true)}>
|
||||
📷 QR 보기
|
||||
</button>
|
||||
@@ -315,6 +316,8 @@ function Lobby({ room, ui, me }: { room: RoomView; ui: GameUI; me: PublicUser })
|
||||
<p className="muted small">
|
||||
{room.visibility === 'public' ? '공개 방 (누구나 들어올 수 있어요)' : '친구만 (코드·링크로만 들어와요)'}
|
||||
</p>
|
||||
{room.lockedOptions.length > 0 && <p className="muted small">🔒 일부 규칙은 관리자가 정해 두어서 바꿀 수 없어요.</p>}
|
||||
<GameNotice gameId={room.gameId} />
|
||||
</section>
|
||||
|
||||
<div className="bottom-bar">
|
||||
@@ -406,6 +409,7 @@ function SettingsSheet({ room, ui, onClose }: { room: RoomView; ui: GameUI; onCl
|
||||
return (
|
||||
<Sheet onClose={onClose} label="게임 설정">
|
||||
<h2>게임 설정</h2>
|
||||
{room.lockedOptions.length > 0 && <p className="muted small">🔒 관리자가 고정한 규칙은 바꿔도 저장되지 않아요.</p>}
|
||||
<ui.Options value={options} onChange={setOptions} />
|
||||
<hr style={{ border: 0, borderTop: '1px solid var(--border)', width: '100%' }} />
|
||||
<strong>방 공개</strong>
|
||||
|
||||
Reference in New Issue
Block a user