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:
@@ -11,14 +11,15 @@ export function normalizeNickname(raw: string): string {
|
||||
return raw.normalize('NFC').trim().replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
export function containsProfanity(text: string): boolean {
|
||||
export function containsProfanity(text: string, extra: readonly string[] = []): boolean {
|
||||
const squashed = text.normalize('NFC').toLowerCase().replace(/[\s_.\-*0-9]/g, '');
|
||||
return BANNED.some((w) => squashed.includes(w));
|
||||
return [...BANNED, ...extra.map((w) => w.toLowerCase())].some((w) => w && squashed.includes(w));
|
||||
}
|
||||
|
||||
export function maskProfanity(text: string): string {
|
||||
export function maskProfanity(text: string, extra: readonly string[] = []): string {
|
||||
let out = text;
|
||||
for (const w of BANNED) {
|
||||
for (const w of [...BANNED, ...extra]) {
|
||||
if (!w) continue;
|
||||
out = out.replace(new RegExp(w.split('').map(escapeRe).join('[\\s_.\\-*]*'), 'gi'), (m) => '*'.repeat(m.length));
|
||||
}
|
||||
return out;
|
||||
@@ -29,12 +30,12 @@ function escapeRe(s: string): string {
|
||||
}
|
||||
|
||||
/** Returns a Korean error message, or null when valid. Input must already be normalized. */
|
||||
export function nicknameError(nick: string): string | null {
|
||||
export function nicknameError(nick: string, extraBanned: readonly string[] = []): string | null {
|
||||
const len = [...nick].length;
|
||||
if (len < NICK_MIN) return `닉네임은 ${NICK_MIN}글자 이상이어야 해요.`;
|
||||
if (len > NICK_MAX) return `닉네임은 ${NICK_MAX}글자까지 쓸 수 있어요.`;
|
||||
if (!ALLOWED.test(nick)) return '한글, 영어, 숫자와 _ - . 만 쓸 수 있어요.';
|
||||
if (containsProfanity(nick)) return '다른 닉네임을 써 주세요.';
|
||||
if (containsProfanity(nick, extraBanned)) return '다른 닉네임을 써 주세요.';
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,8 @@ export interface RoomView {
|
||||
rematchVotes: string[];
|
||||
sessionStats: Record<string, { played: number; wins: number }>;
|
||||
seedHash: string | null;
|
||||
/** Option paths fixed by an admin (hosts cannot change them). */
|
||||
lockedOptions: string[];
|
||||
/** Seed revealed after the game for fairness check. */
|
||||
lastSeed: string | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user