개발 문서
Immercy 운영과 보존
이 문서는 구현된 서버 정책과 설정 후 운영자가 수행할 절차입니다. 실제 배포, 외부 서비스 연결, 게임 플레이, 결제 승인 검증은 수행하지 않았습니다. 초기 연결 순서는 설정 안내, 결제 대조는 PayApp 안내를 따릅니다.
요금과 집계
| 요금제 | 프로젝트 수 | 계정당 주간 Play |
|---|---|---|
| Free | 3개 | 100회 |
| Indie | 제한 없음 | 1,000회 |
| Enterprise | 관리자 계약값 | 관리자 계약값 |
주간 구간은 Asia/Seoul 월요일 00:00부터 다음 월요일 00:00 직전입니다. 주간 초기화를 위해 기록을 삭제하지 않고 시작 시각을 기준으로 다른 구간을 집계합니다. PC에서 Play를 누르면 QR이 표시되고, 스마트폰 연결이 완료되면 게임이 자동으로 시작됩니다. QR을 만드는 시점에는 Play를 차감하지 않으며 서버가 자동 시작 요청을 승인한 세션마다 한 번 집계합니다. 개발자 본인 실행과 Preview도 같은 계정의 주간 사용량에 포함됩니다. 기기나 프로젝트를 나눠도 계정 제한을 늘릴 수 없습니다.
시간은 서버에서 확인한 시작·heartbeat·종료를 기준으로 제한하여 계산합니다. 클라이언트가 제출한 임의의 장시간 플레이 값을 그대로 합산하지 않습니다. Indie 만료는 계정 조회/권한 판정에서도 적용하므로 유지보수 작업이 지연되었다고 무기한 유료 권한이 유지되지 않습니다. Free로 내려가도 기존 프로젝트를 자동 삭제하지 않으며 새 프로젝트 생성은 현재 제한을 따릅니다.
Enterprise의 enterpriseProjectLimit, enterprisePlayLimit은 각각 0~1,000,000,000의 정수 또는 null입니다. null은 제한 없음을 뜻하며 누락한 계약값도 기본적으로 null로 처리합니다. 관리자는 set-plan 작업에서 두 값을 함께 확인합니다. 사용량보다 작은 값으로 낮추더라도 기존 프로젝트/결제를 자동 삭제하거나 환불하지 않고 이후 새 생성·새 실행을 제한합니다. 같은 세션의 시작 요청을 재전송하면 Play를 중복 차감하지 않습니다.
목록 페이지와 운영 조회
프로젝트 목록은 GET /api/projects에서 24개씩 반환합니다. nextCursor가 있으면 GET /api/projects?cursor=<nextCursor>로 다음 페이지를 요청합니다. 커서는 서버가 반환한 값을 그대로 URL 인코딩하며, 정렬은 수정 시각 내림차순과 ID 내림차순입니다. q 검색어는 전체 소유 프로젝트의 이름·설명을 대상으로 하며 다음 페이지에서도 같은 q를 유지합니다. 첫 페이지에 없다는 이유로 프로젝트가 삭제되었다고 판단하지 않습니다.
관리자 목록은 GET /api/admin?page=1부터 각 목록 100개씩 표시합니다. 응답의 page, pageSize, hasMore, pagination을 기준으로 다음 페이지를 탐색합니다. pagination은 프로젝트·신고·사용자·문의 각각에 다음 자료가 있는지 나타냅니다. 프로젝트는 검토 대기/차단 건을, 신고와 문의는 미처리 건을 우선 확인합니다.
업로드와 실행 격리
| 항목 | 현재 서버 제한 |
|---|---|
| 빌드 파일 수 | 500개 |
| 빌드 총합 | 100 MiB |
| 개별 빌드 파일 | 25 MiB |
각 HTML 파일 (.html, .htm) | 2 MiB |
| 표지 이미지 | 5 MiB |
| 업로드 staging 유효 시간 | 1시간 |
| 교체된 파일 정리 대기 | 24시간 |
| QR 연결 대기 | 15분 |
| 플레이 세션 | 최대 2시간 |
정적 웹 빌드만 지원합니다. Unity WebGL, 서버 사이드 코드, 파일 경로 이탈, 심볼릭 링크, 기본적으로 비밀 파일·소스맵은 업로드 대상으로 허용하지 않습니다. 업로드 목록의 경로·크기·SHA-256을 기준으로 파일을 검증하고 모두 완료된 뒤 최신 빌드를 교체합니다. 업데이트가 중단되면 미완료 빌드는 실행 대상으로 전환되지 않습니다.
업로드된 HTML/JavaScript는 sandbox="allow-scripts allow-pointer-lock" iframe에서 실행합니다. allow-same-origin을 붙이지 않습니다. 게임 코드와 플랫폼 로그인 origin을 분리하고, 런타임 메시지의 상대 창과 연결 정보를 확인합니다. 자산 제공 API는 유효한 세션과 현재 프로젝트 접근 권한을 검사합니다. Private Blob을 공개 저장소로 바꾸거나 원본 파일의 공개 URL을 우회 배포하지 않습니다.
독립된 새로운 실행 세션의 시작이 승인될 때마다 1 Play를 차감합니다. 같은 활성 세션 안의 HTML 재요청, 정적 페이지 이동, 게임 내부 재시작은 기존 Play에 포함됩니다. 모든 자산 요청에는 활성 세션과 접근 권한 검사가 계속 적용됩니다. SVG/XML 문서의 실행 스크립트는 CSP로 차단합니다.
이 격리에는 제품 제약이 있습니다. 게임 안의 쿠키·localStorage, Service Worker, 외부 API의 CORS, 전체화면·팝업·외부 탐색 등 브라우저 기능을 일반 최상위 사이트와 동일하게 가정할 수 없습니다. 브라우저 보안 옵션을 완화해서 기능을 맞추지 말고 해당 게임을 지원 범위에 맞게 만듭니다. 웹 빌드 소스가 클라이언트에 전달된 뒤 복사되는 것까지 차단하는 DRM은 제공하지 않습니다.
Vercel 함수에는 요청 본문 제한이 있으므로 대용량 파일을 플랫폼 JSON API 한 번에 넣지 않습니다. 제공된 업로드 경로와 CLI를 사용합니다. 기능 제한을 올리려면 함수 시간·메모리, Blob 전송량, 파일 검증 비용을 함께 다시 검토합니다. Vercel 함수 제한
연결 설정
화면 1대와 스마트폰 1대를 연결합니다. QR 초대 토큰을 최초 claim한 스마트폰만 해당 연결의 peer 토큰을 발급받고, 이후 제어는 그 토큰으로 수행합니다. QR과 세션 URL은 초대 권한이므로 공개 스크린샷·로그에 남기지 않습니다.
SDP와 ICE 시그널은 MongoDB로 전달하지만 지속적인 게임 입력은 WebRTC 데이터 채널로 전달합니다. 데이터베이스를 TURN 서버처럼 사용하지 않습니다. 모바일망·회사망에서 직접 연결이 막히는 경우 TURN을 준비합니다.
| 환경 변수 | 형식 |
|---|---|
TURN_URL | 제공자의 turn: 또는 turns: 주소. 여러 주소는 쉼표로 구분 |
TURN_USERNAME | TURN 사용자명 |
TURN_CREDENTIAL | TURN 인증 값 |
WEBRTC_ICE_SERVERS | 필요 시 RTCIceServer[] 형식의 JSON 전체 설정으로 대체 |
TURN 자격 증명은 WebRTC 연결 과정에서 참여 기기에 전달될 수 있습니다. 제공자가 지원하는 짧은 수명·이용량 제한을 적용하고 정기적으로 교체합니다. WEBRTC_ICE_SERVERS를 설정하면 기본 TURN 설정보다 우선하므로 여러 출처의 설정을 혼합하지 않습니다. 이 구현은 외부 TURN 계정을 자동 개설하거나 자격 증명을 자동 발급하지 않습니다.
유지보수와 데이터
vercel.json의 Cron이 GET /api/cron/maintenance를 호출합니다. Vercel의 CRON_SECRET 환경 변수와 서버의 Bearer 인증이 일치해야 합니다. 이 경로는 브라우저 관리 화면이 아닙니다. 운영자의 비밀을 URL 쿼리에 넣지 않습니다. Vercel Cron의 인증·실행 상태는 Cron 관리 문서를 확인합니다.
유지보수는 DB lease로 겹친 실행을 막고 한 번에 최대 50개 아티팩트를 정리합니다. 만료된 미완료 업로드, 교체된 파일과 사용 기간이 끝난 계정 상태를 처리합니다. 실패가 누적되어 미완료/폐기 파일이 쌓이면 Blob 용량이 증가하므로 Vercel 로그와 저장소 크기를 함께 확인합니다. 실제로 참조 중인 최신 빌드를 수동으로 삭제해서 용량을 줄이지 않습니다.
| 자료 | 보존 방식 |
|---|---|
| OAuth 진행 정보 | 만료 10분, TTL 인덱스 |
| 로그인 세션 | 만료 30일, TTL 인덱스 |
| WebRTC 시그널 | 만료 20분, TTL 인덱스 |
| 연결 세션 | 서버 만료 시각 검사와 만료 인덱스 |
| staging / 교체 파일 | 정리 시한 후 Cron으로 Blob와 DB 정리 |
| 계정·프로젝트·Play 집계·신고·운영 기록 | 현재 자동 TTL 없음 |
| 결제 승인·환불·통보 원장 | 현재 자동 TTL 없음 |
| 읽은 알림 | 생성 후 90일이 지나면 유지보수 작업에서 삭제 |
TTL 삭제는 즉시 실행을 보장하는 접근 제어가 아닙니다. 서버는 만료 시각을 직접 확인합니다. 운영 전에 실제 보존 기간, 삭제 요청 담당자, 백업의 보존/복원 절차를 결정하고 개인정보 문서와 일치시킵니다. 보존 목적이 다른 금융 원장과 게임 실행 원본을 한꺼번에 지우지 않습니다.
데이터 백업은 Atlas의 예약 백업을 설정하고 별도 환경에 복원하는 절차를 마련합니다. MongoDB 백업에는 Blob 원본이 포함되지 않습니다. Blob 원본을 백업한다면 서버 권한을 가진 별도 보관 경로에 파일과 manifest를 함께 보존합니다. Google Drive를 운영자 백업 보관함으로 사용하더라도 공개 공유 링크를 플랫폼의 실행 저장소로 사용하지 않습니다.
관리자 계정과 검토
ADMIN_EMAILS는 신규 계정 생성 시점에만 적용됩니다. 이미 가입한 계정을 관리자에 추가할 때는 Atlas에서 대상 환경과 이메일을 확인한 후 다음 형태로 한 건을 변경합니다. 아래 이메일은 예시이므로 실제 검증된 관리자 Google 이메일로 바꾸고, 조회 결과가 한 계정인지 먼저 확인합니다.
db = db.getSiblingDB('immercy');
const adminEmail = 'verified-admin@example.com';
const account = db.users.findOne({ email: adminEmail }, { _id: 1, email: 1, role: 1 });
if (!account) throw new Error('확인된 계정이 없습니다.');
printjson(account);
조회한 _id와 이메일을 확인한 뒤 같은 관리 세션에서 실행합니다.
db.users.updateOne(
{ _id: account._id, email: adminEmail },
{ $set: { role: 'admin', updatedAt: new Date() } },
);
공개 요청은 프로젝트명·설명·개발자명·표지를 자동 검토합니다. 게임의 모든 파일과 실제 플레이 장면을 AI가 검증하는 기능은 아닙니다. 자동 검토 비설정/실패 시 대기 상태로 남습니다. 운영자는 관리자 화면에서 공개 프로젝트와 신고를 확인하고 승인, 차단, 공개 해제, 사용자 제한·복구, 신고 처리 등을 수행합니다. 관리자 조치는 이유와 함께 남깁니다. 검토 화면을 연 뒤 콘텐츠가 수정되면 승인을 409 REVIEW_CHANGED로 거절합니다. 최신 설명과 표지를 다시 불러와 검토한 뒤 승인하세요. 정지된 계정도 본인 알림 확인과 읽음 처리를 할 수 있습니다.
Enterprise 변경은 결제사의 기존 정기결제를 해지하지 않습니다. 이미 Indie 자동 결제가 있다면 계약 전환 담당자가 본인 결제 관리 또는 PayApp 관리 화면에서 해지를 확인해야 중복 청구를 피할 수 있습니다. 계정 정지 역시 자동 환불·정기결제 해지와 별개입니다.
장애 점검
| 증상 | 먼저 볼 근거 |
|---|---|
| 로그인이 반복되거나 콜백 실패 | APP_URL, Google 콜백 URI, HTTPS와 실제 접속 origin 일치 |
| DB 연결 실패 | Atlas DB 사용자 권한, SRV 문자열, 네트워크 접근 목록, 함수 지역 |
| 업로드 요청 실패 | Private Blob 종류/토큰, 파일 제한, staging 만료, 개발자 API 키 범위 |
| 새 업로드가 공개되지 않음 | 검증/commit 결과, 공개 검토 상태, 최신 빌드와 프로젝트 revision |
| QR은 열리지만 연결 실패 | 초대 만료/다른 폰 claim, TURN 구성, 기기 네트워크, 브라우저 권한 |
| 센서가 반응하지 않음 | 스마트폰 HTTPS, 사용자 버튼 입력으로 요청한 센서 권한, 장치 지원 |
| 결제했으나 Free | PayApp 승인/통보 기록, 등록 번호/금액/주문 검증, DB 트랜잭션 오류 |
| 과금/용량이 빠르게 증가 | Blob 전송량, 함수 호출량, Atlas 사용량, Cron 지연, 업로드 반복 |
로그에는 API 키, QR/peer/host 토큰, OAuth 코드, Blob 토큰, DB URI, PayApp 원문을 남기지 않습니다. 장애 기록은 요청 ID·프로젝트 ID·승인 번호·오류 코드 등 필요한 범위만 수집합니다. 비밀이 노출되면 원본 설정을 교체하고 해당 세션 또는 프로젝트 API 키를 폐기한 뒤 영향을 확인합니다.