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:
@@ -4,6 +4,7 @@ import type { AnyGameDefinition } from '@bg/engine';
|
||||
import { isAllowedRoomCode, type PublicUser } from '@bg/shared';
|
||||
import { Room, type RoomConfig, type RoomDeps, type RoomSnapshot } from './room';
|
||||
import type { RoomStore } from './store';
|
||||
import type { SettingsStore } from '../admin/settings';
|
||||
|
||||
export const MAX_ROOMS = 2000;
|
||||
export const MAX_ROOMS_PER_USER = 3;
|
||||
@@ -17,6 +18,7 @@ export interface ManagerDeps {
|
||||
now?: () => number;
|
||||
log?: RoomDeps['log'];
|
||||
random?: () => number;
|
||||
settings?: SettingsStore;
|
||||
}
|
||||
|
||||
export class RoomManager {
|
||||
@@ -37,6 +39,7 @@ export class RoomManager {
|
||||
users: this.deps.users,
|
||||
now: this.now,
|
||||
log: this.deps.log,
|
||||
settings: this.deps.settings,
|
||||
onSeatChange: (room, userId, seated) => {
|
||||
const set = this.seatIndex.get(userId) ?? new Set<string>();
|
||||
if (seated) set.add(room.code);
|
||||
@@ -69,19 +72,26 @@ export class RoomManager {
|
||||
/** Returns the room or a Korean error message. */
|
||||
create(hostId: string, gameId: string, visibility: 'private' | 'public' = 'private'): Room | string {
|
||||
const def = this.deps.games[gameId];
|
||||
if (!def) return '없는 게임이에요.';
|
||||
if (this.byCode.size >= MAX_ROOMS) return '지금은 방이 너무 많아요. 잠시 후 다시 시도해 주세요.';
|
||||
const eff = this.deps.settings?.game(gameId);
|
||||
const site = this.deps.settings?.getSite();
|
||||
if (!def || (eff && !eff.enabled)) return '없는 게임이에요.';
|
||||
if (site?.maintenance) return site.announcement || '지금은 점검 중이라 새 방을 만들 수 없어요.';
|
||||
const maxRooms = site?.maxRooms ?? MAX_ROOMS;
|
||||
const perUser = site?.maxRoomsPerUser ?? MAX_ROOMS_PER_USER;
|
||||
if (this.byCode.size >= maxRooms) return '지금은 방이 너무 많아요. 잠시 후 다시 시도해 주세요.';
|
||||
const mine = [...(this.createdBy.get(hostId) ?? [])].filter((c) => this.byCode.has(c));
|
||||
if (mine.length >= MAX_ROOMS_PER_USER) return `방은 한 번에 ${MAX_ROOMS_PER_USER}개까지 만들 수 있어요.`;
|
||||
if (mine.length >= perUser) return `방은 한 번에 ${perUser}개까지 만들 수 있어요.`;
|
||||
const options = eff?.defaultOptions ?? def.defaultOptions;
|
||||
const base = def.playersFor ? def.playersFor(options).max : def.maxPlayers;
|
||||
const config: RoomConfig = {
|
||||
gameId,
|
||||
options: def.defaultOptions,
|
||||
maxPlayers: def.playersFor ? def.playersFor(def.defaultOptions).max : def.maxPlayers,
|
||||
options,
|
||||
maxPlayers: Math.min(base, eff?.maxPlayers ?? base),
|
||||
visibility,
|
||||
allowSpectators: true,
|
||||
chatEnabled: true,
|
||||
chatFilter: true,
|
||||
graceSec: 60,
|
||||
chatEnabled: site?.chatEnabledDefault ?? true,
|
||||
chatFilter: site?.chatFilterDefault ?? true,
|
||||
graceSec: site?.graceSec ?? 60,
|
||||
};
|
||||
const code = this.newCode();
|
||||
const id = ulid(this.now());
|
||||
@@ -174,6 +184,14 @@ export class RoomManager {
|
||||
}
|
||||
}
|
||||
|
||||
closeByAdmin(code: string): boolean {
|
||||
const room = this.byCode.get(code);
|
||||
if (!room) return false;
|
||||
room.closeByAdmin();
|
||||
this.close(room);
|
||||
return true;
|
||||
}
|
||||
|
||||
close(room: Room): void {
|
||||
room.shutdown();
|
||||
this.deps.store.close(room.id, this.now());
|
||||
|
||||
Reference in New Issue
Block a user