From b2f998fa957b9b85875698943d09b176716cc599 Mon Sep 17 00:00:00 2001 From: EJClaw Date: Sun, 4 Oct 2026 04:40:23 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20=EC=82=AC?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EA=B3=84=ED=9A=8D(14-admin),=20=EC=83=81?= =?UTF-8?q?=ED=91=9C=EB=AA=85=20=EC=82=AC=EC=9A=A9=C2=B7=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20game.tkrmagid.kr=20=ED=99=95=EC=A0=95=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- deploy/.env.example | 6 ++- docs/05-accounts-auth.md | 5 ++ docs/08-data-model.md | 3 ++ docs/09-game-engine.md | 1 + docs/11-deployment-ops.md | 4 +- docs/12-roadmap.md | 5 ++ docs/13-decisions.md | 15 ++++-- docs/14-admin.md | 105 ++++++++++++++++++++++++++++++++++++++ docs/README.md | 3 +- docs/games/README.md | 4 +- 10 files changed, 140 insertions(+), 11 deletions(-) create mode 100644 docs/14-admin.md diff --git a/deploy/.env.example b/deploy/.env.example index aab6750..5fff799 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -1,9 +1,11 @@ # Copy to deploy/.env and fill in. Never commit the real file. -SITE_DOMAIN=game.example.com -PUBLIC_ORIGIN=https://game.example.com +SITE_DOMAIN=game.tkrmagid.kr +PUBLIC_ORIGIN=https://game.tkrmagid.kr # 32+ random characters: openssl rand -base64 48 SESSION_SECRET= # Discord developer portal → OAuth2 (redirect: https:///api/auth/discord/callback). Leave empty to hide Discord login. DISCORD_CLIENT_ID= DISCORD_CLIENT_SECRET= LOG_LEVEL=info +# Super admin Discord user IDs (comma separated). Default is the site owner. +SUPERADMIN_DISCORD_IDS=293719842274541579 diff --git a/docs/05-accounts-auth.md b/docs/05-accounts-auth.md index 3490ba0..d05f1f9 100644 --- a/docs/05-accounts-auth.md +++ b/docs/05-accounts-auth.md @@ -61,3 +61,8 @@ - 화면 설정: 글자 크기(보통/크게/아주 크게), 다크 모드(자동/밝게/어둡게), 소리 켜기/끄기, 진동 켜기/끄기, 색약 모드. - 내 전적: 게임별 판 수/승 수. - 로그아웃, 모든 기기 로그아웃, 계정 삭제(맨 아래, 작게). + +## 8. 권한(역할) +- 일반 / 어드민 / 슈퍼어드민. 자세한 내용은 `14-admin.md`. +- `/api/me`는 `role`(`user`·`admin`·`superadmin`)을 함께 돌려주고, 웹은 어드민 이상에게만 계정 페이지에 [관리자 페이지] 버튼을 보여 준다. +- 이용 제한된 계정은 세션이 모두 지워지고 디스코드로 다시 로그인할 수 없다(`/?login=banned`). diff --git a/docs/08-data-model.md b/docs/08-data-model.md index 257e94e..3593a7d 100644 --- a/docs/08-data-model.md +++ b/docs/08-data-model.md @@ -130,3 +130,6 @@ CREATE TABLE user_stats ( - 저장하는 개인정보: 닉네임, 디스코드 사용자 ID/이름/아바타 해시, 접속 시각, User-Agent(세션 관리용). - IP는 DB에 저장하지 않고 속도 제한용 메모리에만 둔다. - 계정 삭제 시 `users.deleted_at` 설정 + 닉네임을 "탈퇴한 사용자"로, OAuth 연결·세션 삭제. 다른 사람의 전적 표시가 깨지지 않게 행은 유지. + +## 6. 관리자 관련 테이블 (마이그레이션 002) +`admins`, `game_settings`, `site_settings`, `admin_audit`, `users.banned_at`/`banned_reason`. 정의는 `14-admin.md` 7절. diff --git a/docs/09-game-engine.md b/docs/09-game-engine.md index 1a91f44..66ce973 100644 --- a/docs/09-game-engine.md +++ b/docs/09-game-engine.md @@ -48,6 +48,7 @@ export interface GameDefinition { ``` ### 규칙 +- **옵션 메타 필수**: `optionsSchema`의 모든 항목에 `.meta({ title: '한국어 제목', labels: { 값: '표시 이름' } })`를 붙인다. 관리자 사이트가 이 정보로 입력 화면을 자동으로 만든다(`14-admin.md` 4절). 규칙 코드 안의 고정 숫자도 가능한 한 옵션으로 꺼낸다. - `apply`는 `validate`가 통과한 행동만 받는다. `apply` 안에서도 불변식이 깨지면 예외를 던진다(GameRunner가 잡음). - 상태는 **불변 객체처럼** 다룬다(새 객체 반환). 구현은 `structuredClone` 후 수정해도 된다(상태가 작음). - 상태는 JSON으로 직렬화 가능해야 한다(Map/Set/클래스 금지). 저장·복구가 그대로 된다. diff --git a/docs/11-deployment-ops.md b/docs/11-deployment-ops.md index a574c83..4db9051 100644 --- a/docs/11-deployment-ops.md +++ b/docs/11-deployment-ops.md @@ -16,6 +16,8 @@ deploy/ | `DISCORD_CLIENT_ID` / `DISCORD_CLIENT_SECRET` | 디스코드 앱 | | `SESSION_SECRET` | 짧은 수명 서명 쿠키(OAuth state)용 32바이트 | | `DB_PATH` | 기본 `/data/app.db` | +| `SUPERADMIN_DISCORD_IDS` | 슈퍼어드민 디스코드 ID(쉼표 구분). 기본 `293719842274541579` | +| `TRUST_PROXY` | `1`이면 Caddy가 붙인 X-Forwarded-For로 IP 판단 | | `PORT` | 기본 3000 | | `LOG_LEVEL` | info | @@ -51,5 +53,5 @@ deploy/ | 항목 | 선택지 | 기본 제안 | |---|---|---| | 서버 위치 | 이 호스트 / .5 Docker 호스트 / 외부 VPS | 개발 중에는 이 호스트에서 테스트, 공개용은 사용자 결정 | -| 도메인 | 예: `game.tkrmagid.kr` | 사용자 결정(DNS 설정 필요) | +| 도메인 | `game.tkrmagid.kr` (확정) | 모든 기능 완성 후 DNS 연결 | | 디스코드 앱 | 개발자 포털에서 생성 → client ID/secret, Redirect URI 등록 | 도메인 정해진 뒤 | diff --git a/docs/12-roadmap.md b/docs/12-roadmap.md index 1d28a10..a07f839 100644 --- a/docs/12-roadmap.md +++ b/docs/12-roadmap.md @@ -14,6 +14,11 @@ - 웹: 홈, 코드 입력, 대기실(코드/링크 복사, 공유, QR), 오목 화면, 결과, 계정, 글자 크기/다크 모드. - 완료 기준: `10-testing.md` 3절 12개 시나리오 자동 테스트 통과, 오목 규칙 테스트 통과, E2E 1개 통과, 성능 예산 통과. +## M1.5 관리자 사이트 +- 슈퍼어드민(디스코드 ID 고정)·어드민(디스코드 ID로 추가) 권한, 관리자 API, 관리자 화면(대시보드·게임·방·사이트·사용자·관리자·기록). +- 게임 옵션 스키마로 설정 화면 자동 생성, 옵션 잠금, 사이트 설정(공지·점검·금지어·제한값). +- 완료 기준: `14-admin.md` 9절 테스트 통과, 관리자 화면 E2E 통과. + ## M2. 2인 전략 게임 + 안정성 검증 - 체스, 바둑, 장기, 오셀로. 개인 시계(초읽기/증가), 무르기 요청, 무승부 제안, 기권. - 디스코드 로그인 실제 연결(앱 정보가 준비된 경우). diff --git a/docs/13-decisions.md b/docs/13-decisions.md index c376093..639dfa1 100644 --- a/docs/13-decisions.md +++ b/docs/13-decisions.md @@ -2,18 +2,23 @@ 아래 항목은 사용자 확인이 필요하다. **답이 없으면 "기본안"으로 진행**하고, 나중에 바꿀 수 있게 옵션으로 만들어 둔다. +## 0. 확정된 결정 (2026-10-04) +- 도메인: **game.tkrmagid.kr** 고정. 모든 기능을 완성한 뒤 연결한다. +- 상표명: 친구끼리 쓰는 사이트라 **원래 이름을 그대로 사용**한다. +- 관리자 사이트 추가. 슈퍼어드민은 디스코드 ID `293719842274541579`, 어드민은 디스코드 ID로 추가(`14-admin.md`). +- 게임 규칙 기본값은 관리자 사이트에서 모두 바꿀 수 있으므로, 3절의 기본안은 "처음 값"이다. + ## 1. 운영 (진행에 꼭 필요) | # | 항목 | 기본안 | 필요한 것 | |---|---|---|---| -| 1 | 사이트 주소(도메인)와 서버 위치 | 개발 중에는 봇 호스트에서 테스트 | 도메인(예: `game.tkrmagid.kr`)과 DNS 연결, 공개 서버 위치 | -| 2 | 디스코드 로그인 | 게스트만으로 먼저 개발 | 디스코드 개발자 포털에서 앱 생성 → Client ID, Client Secret, Redirect URI(`https://<도메인>/api/auth/discord/callback`) 등록 | +| 1 | 서버 위치 | 도메인은 game.tkrmagid.kr 확정. 완성 후 연결 | 공개 서버 위치(이 호스트 / .5 Docker 호스트 등)와 DNS 연결 시점 | +| 2 | 디스코드 로그인 | 게스트만으로 먼저 개발. **관리자 사이트는 디스코드 로그인이 있어야 들어갈 수 있다** | 디스코드 개발자 포털에서 앱 생성 → Client ID, Client Secret, Redirect URI(`https://game.tkrmagid.kr/api/auth/discord/callback`) 등록 | ## 2. 정책 | # | 항목 | 기본안 | |---|---|---| -| 3 | 상표가 있는 게임 이름 | 화면에는 일반 명칭 + 괄호로 익숙한 이름(예: "숫자 타일(루미큐브 방식)"). 그림·카드는 모두 직접 제작 | -| 4 | 포커·섯다·고스톱 같은 웹보드 게임 | 국내에서 공개 서비스하면 보통 청소년이용불가 등급 대상이다. 기본안: 이 게임들을 "어른용" 묶음으로 따로 보여 주고, 처음 들어갈 때 "만 19세 이상" 확인을 받는다. 칩·점수는 방 안에서만 쓰고 돈·충전·환전은 없다. 누구나 들어오는 공개 서비스로 열기 전에는 등급분류 필요 여부를 따로 확인해야 한다 | -| 5 | 보석 상인(스플렌더 방식) 카드 수치 | 원작과 같은 수치를 별도 데이터 파일에 둔다(교체 쉬움). 공개 서비스라면 자체 수치로 바꾸기를 권장 | +| 4 | 포커·섯다·고스톱 같은 웹보드 게임 | 친구끼리 쓰는 비공개 사이트이므로 별도 연령 확인 없이 일반 게임과 같이 둔다. 칩·점수는 방 안에서만 쓰고 돈·충전·환전은 없다. 관리자 사이트에서 게임별로 끌 수 있다 | +| 5 | 스플렌더 카드 수치 | 원작과 같은 수치를 별도 데이터 파일에 둔다(친구끼리 쓰는 사이트라 그대로 사용) | ## 3. 게임 규칙 기본값 (자세한 내용은 각 게임 문서) | 게임 | 기본안 | 바꿀 수 있는 옵션 | diff --git a/docs/14-admin.md b/docs/14-admin.md new file mode 100644 index 0000000..6c8805d --- /dev/null +++ b/docs/14-admin.md @@ -0,0 +1,105 @@ +# 14. 관리자 사이트 + +## 1. 목표 +- 게임 규칙과 사이트 설정을 코드 수정 없이 관리자 화면에서 **하나도 빠짐없이** 바꿀 수 있다. +- 권한은 디스코드 ID로 관리한다. 슈퍼어드민은 고정, 일반 어드민은 슈퍼어드민이 디스코드 ID로 추가한다. +- 관리자가 무엇을 바꿨는지 모두 기록한다. + +## 2. 권한 +| 등급 | 누가 | 할 수 있는 것 | +|---|---|---| +| 슈퍼어드민 | 디스코드 ID `293719842274541579` (설정 `SUPERADMIN_DISCORD_IDS`, 기본값이 이 ID) | 모든 것: 아래 어드민 권한 + 사용자 관리 + 어드민 추가/삭제 | +| 어드민 | 슈퍼어드민이 디스코드 ID로 등록한 사람 | 관리자 사이트 보기, 게임 설정, 사이트 설정, 방 관리, 기록 보기 | +| 일반 | 그 외(게스트 포함) | 관리자 사이트 접근 불가(관리자 주소로 들어오면 "권한이 없어요") | + +- 권한은 **요청마다** 그 사용자에게 연결된 디스코드 ID로 계산한다. 그래서 어드민 등록·해제가 즉시 반영된다. +- 게스트는 디스코드 ID가 없으므로 어드민이 될 수 없다. 관리자는 반드시 디스코드로 로그인해야 한다. +- 아직 사이트에 한 번도 들어온 적 없는 사람도 디스코드 ID로 미리 어드민 등록할 수 있다(처음 로그인하는 순간부터 어드민). +- 설정 파일에 있는 슈퍼어드민은 관리자 화면에서 삭제할 수 없다. + +## 3. 화면 구성 (`/admin`, 관리자 화면 코드는 관리자만 내려받는 별도 묶음) +| 탭 | 어드민 | 슈퍼어드민 | 내용 | +|---|---|---|---| +| 대시보드 | ○ | ○ | 접속 수, 진행 중 방, 오늘/전체 판 수, 사용자 수(게스트/디스코드), 서버 가동 시간·메모리 | +| 게임 | ○ | ○ | 게임별 설정(4절) | +| 방 | ○ | ○ | 열린 방 목록(코드, 게임, 상태, 자리, 관전 수, 만든 시각), 방 닫기 | +| 사이트 | ○ | ○ | 사이트 설정(5절) | +| 사용자 | × | ○ | 검색(닉네임/ID/디스코드 ID), 상세(전적·로그인 방식·접속 시각), 닉네임 변경, 이용 제한/해제, 모든 기기 로그아웃, 삭제 | +| 관리자 | × | ○ | 어드민 목록, 디스코드 ID로 추가(메모 포함), 삭제 | +| 기록 | ○ | ○ | 관리자 작업 기록(누가, 언제, 무엇을, 바꾸기 전/후) | + +계정 페이지(`/me`)에 관리자에게만 [관리자 페이지] 버튼을 보여 준다. + +## 4. 게임 설정 (게임마다) +| 항목 | 설명 | +|---|---| +| 사용 | 끄면 새 방을 만들 수 없고 목록에서 숨김. 진행 중인 방은 그대로 끝까지 | +| 표시 이름 / 한 줄 설명 / 안내문 | 홈·게임 화면에 보이는 글. 안내문은 규칙 화면 위에 표시 | +| 인원 범위 | 원작 최소~최대 안에서만 좁힐 수 있음 | +| 기본 규칙 | 그 게임의 **모든 옵션**(게임 모듈의 `optionsSchema` 전체)을 기본값으로 지정 | +| 방장 변경 금지(잠금) | 옵션마다 잠그면 방장이 대기실에서 바꿀 수 없고 관리자 값으로 고정 | +| 초기화 | 코드 기본값으로 되돌리기 | + +- 화면의 입력 칸은 게임 모듈의 옵션 스키마(zod → JSON Schema)에서 **자동으로 만든다**. 새 옵션이 생기면 관리자 화면에 자동으로 나타나므로 빠지는 항목이 없다. +- 그래서 모든 게임 모듈은 옵션마다 한국어 제목과 선택지 이름을 `meta`로 붙인다(`09-game-engine.md` 2절). 게임 규칙 안에 숫자로 박혀 있던 값(예: 오목 자동 착수 허용 횟수, 무승부 재제안 간격)도 옵션으로 꺼내서 관리자가 바꿀 수 있게 한다. +- 저장할 때 스키마로 검사하고, 통과하지 못하면 저장하지 않는다. +- 바꾼 설정은 **새로 만드는 방과 새로 시작하는 판**부터 적용된다. 진행 중인 판의 규칙은 중간에 바뀌지 않는다(공정성). + +## 5. 사이트 설정 +| 항목 | 기본값 | 효과 | +|---|---|---| +| 사이트 이름 | 같이 놀자 보드게임 | 홈 제목, 브라우저 탭 제목 | +| 공지 | 없음 | 모든 화면 위에 띠로 표시 | +| 점검 모드 | 꺼짐 | 켜면 새 방 만들기·빠른 시작을 막고 공지 표시. 진행 중인 게임은 계속 | +| 금지어 | 없음(내장 목록 외 추가) | 채팅 가림, 닉네임 거부 | +| 사용자당 동시 방 수 | 3 | 방 만들기 제한 | +| 서버 전체 방 수 | 2000 | 방 만들기 제한 | +| 연결 끊김 유예(초) | 60 | 새 방의 기본값 | +| 채팅 기본값 | 켜짐 / 필터 켜짐 | 새 방의 기본값 | + +## 6. 사용자 관리 (슈퍼어드민) +- **이용 제한**: 사유와 함께 설정. 즉시 그 사용자의 모든 세션 삭제와 접속 종료. 제한된 디스코드 계정은 다시 로그인할 수 없다("이용이 제한된 계정이에요"). 해제하면 다시 로그인 가능. +- **닉네임 변경**: 관리자가 바꾸면 하루 변경 횟수 제한을 무시. +- **삭제**: `05-accounts-auth.md`의 계정 삭제와 같음(전적 익명화). +- 슈퍼어드민 자신은 제한·삭제할 수 없다. + +## 7. 저장 (`08-data-model.md`에 추가) +```sql +CREATE TABLE admins (discord_id TEXT PRIMARY KEY, note TEXT, added_by TEXT, added_at INTEGER NOT NULL); +CREATE TABLE game_settings (game_id TEXT PRIMARY KEY, json TEXT NOT NULL, updated_by TEXT, updated_at INTEGER NOT NULL); +CREATE TABLE site_settings (key TEXT PRIMARY KEY, json TEXT NOT NULL, updated_by TEXT, updated_at INTEGER NOT NULL); +CREATE TABLE admin_audit (id INTEGER PRIMARY KEY AUTOINCREMENT, actor_id TEXT NOT NULL, action TEXT NOT NULL, + target TEXT, before_json TEXT, after_json TEXT, at INTEGER NOT NULL); +ALTER TABLE users ADD COLUMN banned_at INTEGER; +ALTER TABLE users ADD COLUMN banned_reason TEXT; +``` +- 설정은 서버 메모리에 캐시하고, 저장할 때 DB와 캐시를 같이 바꾼다. + +## 8. API (`/api/admin/*`, 모두 로그인 + 권한 검사, 바꾸는 요청은 Origin 검사 + 기록) +| 메서드 | 경로 | 권한 | +|---|---|---| +| GET | `/api/admin/me` | 어드민+ | +| GET | `/api/admin/overview` | 어드민+ | +| GET | `/api/admin/games` | 어드민+ (게임별 현재 설정 + 옵션 JSON Schema + 코드 기본값) | +| PUT | `/api/admin/games/:id` | 어드민+ | +| POST | `/api/admin/games/:id/reset` | 어드민+ | +| GET / PUT | `/api/admin/site` | 어드민+ | +| GET | `/api/admin/rooms` | 어드민+ | +| POST | `/api/admin/rooms/:code/close` | 어드민+ | +| GET | `/api/admin/audit` | 어드민+ | +| GET | `/api/admin/users?q=&page=` | 슈퍼어드민 | +| GET / PATCH / DELETE | `/api/admin/users/:id` | 슈퍼어드민 | +| POST | `/api/admin/users/:id/logout` | 슈퍼어드민 | +| GET / POST | `/api/admin/admins` | 슈퍼어드민 | +| DELETE | `/api/admin/admins/:discordId` | 슈퍼어드민 | + +## 9. 테스트 +- 게스트·일반 디스코드 사용자는 모든 관리자 API가 403. +- 슈퍼어드민 디스코드 ID로 로그인하면 슈퍼어드민. 어드민 추가 후 그 디스코드 사용자가 바로 어드민이 되고, 삭제하면 바로 403. +- 어드민은 사용자/관리자 API 403, 게임·사이트 설정은 가능. +- 게임 기본 규칙 저장 → 새 방의 옵션에 반영. 잠근 옵션은 방장이 바꿔도 관리자 값 유지. 잘못된 값은 400. +- 게임 끄기 → 새 방 만들기 거부, 목록에서 숨김. +- 점검 모드 → 방 만들기 거부. 금지어 → 채팅 가림. +- 이용 제한 → 세션 삭제, 접속 종료, 디스코드 재로그인 거부. +- 모든 변경이 기록에 남음. +- 관리자 화면 E2E: 게임 설정 저장 → 새 방 대기실 요약에 반영. diff --git a/docs/README.md b/docs/README.md index aed5652..f87c25d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,7 +19,8 @@ | [10-testing.md](10-testing.md) | 테스트 종류, 핵심 시나리오, 부하 기준, 수동 점검표 | | [11-deployment-ops.md](11-deployment-ops.md) | Docker/Caddy 배포, 환경 변수, 백업, 감시, 결정 필요 항목 | | [12-roadmap.md](12-roadmap.md) | 마일스톤 M0~M9와 완료 기준 | -| [13-decisions.md](13-decisions.md) | 사용자 결정이 필요한 항목과 기본안 | +| [13-decisions.md](13-decisions.md) | 사용자 결정 항목(확정/대기) | +| [14-admin.md](14-admin.md) | 관리자 사이트: 권한(슈퍼어드민/어드민), 게임·사이트 설정, 사용자 관리, 기록 | ## 게임 문서 [games/README.md](games/README.md) — 전체 게임 목록, 우선순위, 제외 이유, 상표 표시 방침. diff --git a/docs/games/README.md b/docs/games/README.md index a19ee29..5a52565 100644 --- a/docs/games/README.md +++ b/docs/games/README.md @@ -52,5 +52,5 @@ | 글룸헤이븐, 테라포밍 마스 등 대형 전략 게임 | 플레이 시간이 길고 구성 요소가 많아 "가볍고 쉬운" 목표와 맞지 않음 | | 5초 준다, 갈팡질팡 | 말로 하는 파티 게임(음성 필요) | -## 5. 상표·이름 표시 (사용자 결정 필요) -루미큐브, 우노, 뱅!, 스플렌더, 할리갈리, 다빈치 코드, 모두의 마블/부루마블, 도블, 쿼리도, 달무티 등은 제품 상표다. 친구끼리 쓰는 비공개 사이트라면 익숙한 이름이 가장 쉽지만, 누구나 들어오는 공개 서비스라면 일반 명칭(예: "숫자 타일 게임")을 쓰고 설명에 "OO과 비슷한 규칙"이라고만 적는 편이 안전하다. 기본안: **화면 이름은 일반 명칭 + 괄호로 익숙한 이름**(예: "숫자 타일(루미큐브 방식)"), 그림·카드 디자인은 모두 직접 제작. +## 5. 상표·이름 표시 (확정) +친구끼리만 쓰는 사이트이므로 **원래 상표명을 그대로 쓴다**(루미큐브, 우노, 뱅!, 스플렌더, 할리갈리, 다빈치 코드, 부루마블 등). 표시 이름은 관리자 사이트에서 언제든 바꿀 수 있다(`14-admin.md` 4절). 카드·화투·타일 그림은 계속 직접 만든 SVG를 쓴다.