드래곤볼
가입 없이 봇 또는 친구와 대전하는 5개 행동 손놀이 게임.
실행
운영 서버의 Git clone·PM2·HTTPS 설정은 배포 안내를 따른다.
Node.js 24 이상, MongoDB Standalone이 필요하다.
npm ci
Copy-Item .config.example.json .config.json
npm run build
npm start
.config.json의 MONGODB_URI, MONGODB_DATABASE, HOST, PORT를 설정한다. 기본 접속은 http://localhost:2100이다. 외부 브라우저에서는 HTTPS가 필요하다. 관리자 인증과 Web Crypto를 포함한 기능을 보호하는 프록시 뒤에 배치한다.
Fastify HTTP API·페이지·Socket.IO /signal은 한 앱 포트와 MongoDB 연결 풀을 공유한다. 앱 다중 인스턴스의 시그널링 공유는 구현하지 않았다. 개발 중 클라이언트는 다시 빌드하고, 서버는 npm run dev로 감시 실행한다. 실행 중인 배포 폴더를 덮어쓰지 않고 새 빌드를 준비한 뒤 교체한다.
대전과 연결
기존 화면·캐릭터·연출·공통 판정을 재사용한다. 봇 대전은 브라우저에서 실행한다. 친구 대전은 reliable ordered WebRTC DataChannel로 턴을 교환한다.
| 행동 | 기 소모 | 효과 |
|---|---|---|
| 으 | 0 | 생존 시 기 +1 |
| 파 | 1 | 으를 맞힘 |
| 막기 | 0 | 파를 막음 |
| 에네르기파 | 3 | 으·파·막기를 맞힘 |
| 순간이동 | 1 | 파·에네르기파 회피 |
준비 3초 → 선택 최대 3초(기본값) → 공개 1.2초. 봇 대전은 내가 선택하면, 친구·랭킹 대전은 양쪽이 선택하면 즉시 진행한다. 기 0으로 시작하며 명중 한 번으로 종료한다. 첫 유효 선택을 잠그고 양쪽 선택이 확정되기 전에는 상대 선택을 공개하지 않는다. 같은 공격은 상쇄되며 공격·회피 비용은 실패해도 소모한다. 봇은 턴 시작 시점의 양쪽 기만 보고 미리 선택한다.
화면에는 내 기만 표시하고 행동 버튼은 내 캐릭터 아래에 배치한다. 상대 기는 화면에서만 숨긴다. P2P 판정·복원을 위해 양쪽 기가 브라우저에 보관되므로 개발자도구에서의 조회나 공개 행동을 통한 기 추적을 차단하지 않는다.
친구 대전은 선택 즉시 선택 해시를 교환하고 양쪽 해시가 모이면 선택과 nonce를 공개한다. 미선택 상태로 마감되면 미입력 해시를 전송한다. 양쪽 브라우저가 독립 계산한 결과 해시와 확인 메시지가 일치해야 공개·기록한다. 한쪽 미입력은 패배, 양쪽 미입력은 무승부다. 명시적 나가기를 상대가 확인하면 기권 처리한다.
글로벌 설정
MongoDB global_settings의 _id: "global" 문서에 revision, turnDurationMs(기본 3000), finishCutsceneSpeed(기본 1), matchingDelayMs(기본 1000), matchSuccessDelayMs(기본 1000), matchPreparationDelayMs(기본 3000), rankedAcceptTimeoutMs(기본 10000)를 저장한다. 랭킹은 매칭 중 → 수락 대기 → 양쪽 수락 시 준비 중, 친구전은 성공 → 준비 중 순서로 표시한다. 랭킹 수락 대기는 매칭 시점의 설정값을 사용한다. 취소·시간 초과 시 승패 기록 없이 매칭을 해제하고, 수락한 상대만 자동 재매칭한다. 준비 화면은 matchPreparationDelayMs만큼 표시하며 안내 지연이 첫 턴 선택 시간을 줄이지 않는다. 컷씬 배속 2는 장면 전환과 애니메이션을 두 배 빠르게 재생한다. 컷씬 종료 후 결과 화면으로 자동 전환하며, 최소 결과 공개 시간(1.2초)을 지킨다.
기존 관리자 Basic 인증으로 GET /api/admin/global-settings를 조회하고, 현재 revision과 설정값 전체을 POST /api/admin/global-settings에 보낸다. 예: {"revision":1,"turnDurationMs":3000,"finishCutsceneSpeed":2,"matchingDelayMs":1000,"matchSuccessDelayMs":1000,"matchPreparationDelayMs":3000,"rankedAcceptTimeoutMs":10000}. 저장 시 리비전이 자동 증가하며, 다른 관리자가 먼저 변경했다면 409를 반환한다. 선택 시간은 50060000ms 정수, 컷씬 배속은 0.254 범위다. 매칭 중·성공·준비 지연은 각각 010000ms 정수다. 랭킹 수락 대기는 100060000ms 정수다. 기존 문서의 누락 필드는 서버 시작 시 기본값으로 추가된다. DB에서 직접 변경할 때도 값의 범위를 지키고 같은 원자적 업데이트에서 revision을 1 증가시킨다.
설정 변경은 서버 재시작 없이 다음 대전에 적용된다. 친구전은 페어링·재대전 시 같은 설정을 방에 저장하고, 봇·랭킹 대전도 생성 시 설정을 확정한다. 진행 중 대전과 새로고침 복구는 기존 대전의 설정을 유지한다. 브라우저는 최초 로딩·창 포커스/가시성 복귀·대전 시작 시 GET /api/global-settings?revision=...로 확인하고, 변경된 경우만 localStorage 캐시를 갱신한다. 조회 실패 시 유효한 캐시 또는 기본값을 유지하며 실제 대전에는 서버가 확정한 설정을 사용한다.
Socket.IO는 방 대기와 SDP/ICE 협상에만 사용하고 DataChannel 준비 확인 후 닫는다. 정상 대전 중 서버는 게임 객체·행동·판정 타이머를 유지하지 않는다. 서버 재시작 중에도 이미 연결된 대전은 진행하며 HTTP 결과 저장은 복구 후 재시도한다.
연결이 끊기면 최대 10초간 새 연결 epoch로 복구한다. 확정되지 않은 선택을 버리고 마지막 상호 확인 상태에서 선택 시간을 다시 시작한다. 복구 불가·합의 불일치는 aborted로 종료하며 승·패·무·연승을 바꾸지 않는다. 처음 연결은 최대 15초다. 모바일 백그라운드 정지나 장시간 탭 중단까지 연속 플레이를 보장하지 않는다.
STUN / TURN
.config.json의 STUN_URLS, TURN_URLS, TURN_SHARED_SECRET을 운영 환경에 맞게 설정한다. URL은 JSON 문자열 배열로 지정한다. 직접 연결이 불가능한 네트워크에서는 TURN이 필요하다. TURN 포트·인증·방화벽과 중계 트래픽 비용은 별도 운영 대상이다.
서버는 방 참가자에게 coturn 호환 단기 HMAC 자격 증명만 제공한다. 공유 비밀값은 서버 환경에만 두며 VITE_* 변수나 번들에 넣지 않는다. ICE 설정이 비어 있어도 일부 로컬 네트워크에서 동작할 수 있지만 외부 접속 가능성을 뜻하지 않는다. 운영 전 실제 TURN 경로와 모바일 네트워크 전환을 별도로 검증한다.
접속 세션별 기록과 임시 방
gameSessionId: 탭sessionStorage의 게임 기록 단위. 같은 탭 새로고침은 복원하고, 저장 정보 없는 새 탭은 새 세션이다. 브라우저의 탭 복제·복원으로 정보가 복제될 수 있으며 한 세션은 미정산 대전 1개만 허용한다.playerId: 기존 익명 방문자 단위. 방문자 토큰은localStorage에 보관한다.- 분석
sessionId: 30분 비활동 후 나누는 방문 세션. 게임 기록 세션과 별개다.
게임 세션은 생성·대전 등록·결과 저장 시점부터 90일 TTL을 적용한다. 저장소 사용이 막히면 메모리 세션을 사용하며 새로고침 복원을 제공하지 못한다.
봇·친구 승/패/무·현재/최고 연승은 game_sessions 단일 문서에 보관한다. 승리는 연승 +1, 패배는 0, 무승부는 유지한다. 등록한 matchId와 순서 seq가 일치하는 결과만 원자적으로 반영하고 처리 영수증도 함께 저장한다. 중복 재전송·뒤늦은 과거 결과는 카운트를 올리지 않는다. 기록 요청에는 upsert를 사용하지 않고 세션 생성에만 사용한다. 다중 문서 트랜잭션은 필요 없다.
제출 결과는 클라이언트 보고로 받아들인다. 공개 경쟁 랭킹 수준의 서버 승리 검증이나 부정행위 방지를 제공하지 않는다.
결과 전송 실패는 2초·5초·10초·이후 30초 간격 및 온라인 복귀 시 재시도한다. 같은 탭에 미전송 결과를 보관하며 저장 확인 전에는 새 대전을 시작하지 않는다.
방은 rooms 임시 문서다. 6자리 코드나 초대 링크로 참가하며 정원·참가자·만료를 API에서도 검사한다. 대기 10분, 참가 완료 24시간 TTL이며 재대전 때 갱신한다. 양쪽 동의 후 새 matchId를 부여한다. 방 만료가 이미 등록한 결과 저장을 막지 않는다.
players는 익명 방문자 식별용으로 유지한다. 전환 전 matches 컬렉션과 서버 판정 전적·상세 조회는 제거했다.
사용하는 컬렉션은 players, game_sessions, rooms, analytics_events, analytics_sessions, analytics_presence_samples, analytics_metadata다.
운영 분석
ADMIN_USER, ADMIN_PASSWORD를 모두 설정하면 /admin/analytics, /api/admin/analytics/*를 HTTP Basic 인증으로 연다. 미설정이면 404다. 자격 증명은 서버에만 저장하며 외부 운영은 HTTPS를 사용한다.
새 분석은 HTTP 배치 최대 20건/5초, 보이는 화면의 활동 보고는 최대 60초당 한 번이다. 숨김 전환의 추가 보고도 분당 한 번으로 제한한다. 턴별 행동 저장·DB touch는 제거했다. 최근 90초 활동으로 활성 방문자를 추정하며 대전 중 사람 수·정확한 현재 대전 수는 미수집으로 표시한다.
관리자 화면은 클라이언트 대전 요약·접속 세션별 전적과 방문·유입·화면·활동 시간·방문 재방문 분석을 표시한다. 대전 수는 클라이언트 시작 보고의 ID로 중복 제거하고, 친구 한 판의 결과 보고는 참가자별 최대 2건이다. 전환 전 서버 판정에 의존하던 행동·턴 상세·완료율·플레이 전환율·재대전율은 제거했다.
기존 DB 정리는 앱을 중지한 상태에서 아래 명령으로 실행한다. 기본 실행은 대상 건수만 조회하며, 적용할 때는 새 백업 파일이 필요하다. 사용자 식별·방문 이력·현재 세션 전적·임시 방은 보존하고, matches, 구형 서버 전용 이벤트·접속 표본, players.firstMatchAt만 제거한다. 서버 시작 시 자동 삭제하지 않는다.
node --import tsx scripts/cleanup-legacy-data.ts --database ssessesse-ki-battle
node --import tsx scripts/cleanup-legacy-data.ts --database ssessesse-ki-battle --apply --backup .work/legacy-cleanup/removed-data.ejson
이벤트·방문 세션은 90일, 활동 표본과 수집 오류 이력은 7일 보관한다. 활동은 10초마다 측정하고 5분마다 최대 활성 방문자 수·측정 횟수·수집 오류 누적값을 저장한다. 정상 종료 시 남은 구간도 저장하며, 비정상 종료 시 아직 저장하지 않은 구간은 유실될 수 있다. 표본 조회 기간은 집계 종료 시각을 기준으로 한다. 서버 시작 시 기존 활동 표본의 만료일도 기록 시각 + 7일로 단축한다. 원문 UA·URL·방 코드·인증 토큰·IP·정밀 위치·오류 본문은 수집하지 않는다. 유입 도메인과 허용 캠페인 분류만 보관한다. 분석 전송 대기는 클라이언트 최대 100건·30분·6회 시도, 서버 최대 100건·30분이다. 분석 장애는 게임 진행과 결과 집계를 막지 않는다.
검증
$env:TEST_MONGODB_URI='mongodb://172.16.0.7:27017/?directConnection=true'
npm test
npm run typecheck
npm run build
테스트는 dragon_ball_test_* DB만 생성·정리한다. 환경변수가 없으면 DB 테스트가 skip되므로 통합 검증으로 간주하지 않는다.
테스트 전용 TEST_MONGODB_URI는 실행 환경변수로 유지하며 앱의 .config.json 연결을 자동으로 재사용하지 않는다.
tests/preview-server.ts는 포트 3001의 로컬 검증 전용 서버다. UI_PREVIEW_CLIENT로 별도 빌드 경로를 지정할 수 있다. /__test/peer는 소켓·RTC 상태를 노출하는 진단 화면, /__test/mobile은 390×844 프레임(?width=320&height=568 지원), /__test/desktop은 1280×720 프레임이다. /__test/admin의 테스트 인증 프록시는 운영 빌드에 포함되지 않는다.
node --import tsx tests/webrtc-load.ts는 3천 세션의 60초 초기 유입과 15분 HTTP/DB 부하를 측정한다. 임의 이름의 전용 테스트 DB와 별도 프로세스를 사용한다. 실제 WebRTC 1,500쌍 또는 TURN 부하 검증을 대신하지 않는다.
검증 결과·디자인 참고 자료·작업 기록은 로컬에 보관하고 Git 추적에서 제외한다. 운영 절차는 배포 안내를 따른다.
후속 범위
친구전은 P2P, 랭킹 매칭·판정은 단일 서버 프로세스에서 실행한다. 서버 재시작 시 진행 중 랭킹 경기는 복원하지 않는다. MMR·관전·채팅은 포함하지 않는다. 다중 앱 인스턴스, 분산 브라우저 1,500쌍, 운영 TURN 비용·실제 모바일 네트워크 검증은 별도 환경에서 확인한다.