게임 실행 흐름
플레이어가 오퍼레이터 사이트에서 게임을 누른 순간부터 게임 화면이 뜨고, 나가고, 세션이 끝나기까지의 흐름입니다. 요청 · 응답 필드 상세는 POST /launch 를 보세요.
전체 순서
플레이어 브라우저 오퍼레이터 서버 플랫폼
│ ① 게임 클릭 │ │
│ (빈 새 창을 먼저 연다) │ │
│ ───── 실행 요청 ────────▶ │ │
│ │ ② POST /operator/v1/launch │
│ │ ───────────────────────────▶ │ 플레이어 생성 · 갱신
│ │ ◀─────────────────────────── │ launch_url (60초 · 1회용)
│ ◀──── launch_url ─────── │ │
│ ③ 새 창(또는 iframe)을 launch_url 로 이동 │
│ ─────────────────────────────────────────────────────▶ │ 토큰 교환 → 플랫폼 세션
│ ◀─── postMessage game.loaded ────────────────────────── │
│ │ ④ 로비 · 게임 진행
│ │ ◀──── 지갑 콜백 (balance · debit · credit · rollback)
│ ◀─── postMessage balance.changed ────────────────────── │
│ ⑤ 나가기 → postMessage game.closed, 창 닫힘 │- 플레이어가 사이트에서 게임이나 테이블을 누르면, 브라우저는 오퍼레이터 백엔드에 실행을 요청합니다.
- 오퍼레이터 서버가 로그인 · 차단 여부 등을 확인한 뒤
POST /operator/v1/launch를 서명해 부르고launch_url을 받습니다. - 브라우저가 새 창(또는 iframe)을
launch_url로 보냅니다. 게임 화면은 URL 의 토큰을 즉시 교환해 플랫폼 세션을 만들고 로비나 지정한 게임 · 테이블로 들어갑니다. - 게임이 진행되는 동안 플랫폼은 오퍼레이터 지갑에 콜백을 보내고, 잔액이 바뀌면 사이트 창에 postMessage 로 알립니다.
- 플레이어가 나가면 게임 화면이
game.closed를 보내고 창을 닫습니다.
자격증명은 서버에만
POST /launch 는 반드시 오퍼레이터 서버에서 부르세요. 브라우저에서 Operator API 를 직접 부르면 서명 비밀키가 노출됩니다.
실행 요청
오퍼레이터 서버가 보내는 요청의 주요 필드입니다. 전체 필드 · 오류 코드는 POST /launch 에 있습니다.
json
{
"player": {
"external_id": "user_1001",
"nickname": "Lucky7",
"currency": "PHP",
"country": "PH"
},
"game_code": "live_holdem",
"table_id": "tbl_0192f3a1c4e87b2c9d0e1f2a3b4c5d70",
"lang": "en",
"device": "mobile",
"return_url": "https://www.acme.example/",
"operator_session_id": "c2f1e8a09b7d4e36"
}| 필드 | 동작 |
|---|---|
player.external_id | 오퍼레이터의 플레이어 ID. 처음 보는 값이면 플랫폼 플레이어가 새로 만들어집니다. |
player.nickname | 게임 화면에 보이는 이름(2~16자). 실행할 때마다 이 값으로 갱신됩니다. |
player.currency | 플레이어 통화. 처음 실행한 통화로 고정되며, 이후 다른 통화로 실행하면 409 CURRENCY_MISMATCH 입니다. |
game_code · table_id | 둘 다 없으면 전체 로비, game_code 만 있으면 그 게임의 로비, table_id 까지 있으면 그 테이블로 바로 들어갑니다. |
lang | 게임 화면 언어. 아래 언어 참고 |
device | desktop(기본) 또는 mobile. 라이브 영상 화질 선택에 쓰입니다. |
return_url | 게임 화면에서 "돌아가기"를 누를 때 이동할 주소이자, postMessage 대상 오리진. 비우면 등록된 기본 복귀 URL 을 씁니다. |
operator_session_id | 오퍼레이터가 정하는 세션 식별자. 이 실행으로 생긴 세션의 지갑 콜백 본문에 그대로 실려 옵니다. |
operator_session_id 에는 불투명한 ID 를
이 값은 플랫폼의 세션 기록과 지갑 콜백 본문에 남습니다. 사이트 로그인 쿠키나 인증 토큰 원문 대신, 세션을 가리키는 별도의 임의 ID 를 쓰세요.
응답:
json
{
"launch_url": "{GAME_ORIGIN}/launch#token=lt_9c1e0b7d2f4a4c6e8a0b1c2d3e4f5a6b",
"expires_at": "2026-10-19T03:21:07.512Z",
"player_id": "pl_0192f3a1c4e87b2c9d0e1f2a3b4c5d6e"
}launch_url 과 토큰
| 항목 | 내용 |
|---|---|
| 형식 | {GAME_ORIGIN}/launch#token=lt_… |
| 유효 시간 | 발급 후 60초 (expires_at) |
| 사용 횟수 | 1회. 한 번 열면 토큰은 소멸합니다. |
| 위치 | URL fragment(# 뒤). 브라우저가 서버로 보내지 않으므로 웹 서버 로그 · Referer 에 남지 않고, 게임 화면은 읽자마자 주소창에서 지웁니다. |
launch_url은 받은 그대로 쓰세요. 쿼리를 덧붙이거나 fragment 를 바꾸지 마세요.- 플레이어가 누를 때마다 새로 발급하세요. 미리 만들어 두거나 캐시해 두면 60초가 지나 쓸 수 없습니다.
- 이미 쓴 토큰이나 만료된 토큰으로 열면 게임 화면에 만료 안내와 돌아가기 버튼이 나옵니다.
- 토큰 교환은 플랫폼과 플레이어 브라우저 사이에서 이뤄집니다. 오퍼레이터는 플랫폼 세션 토큰을 다룰 필요가 없습니다.
창 열기
새 창 (권장)
브라우저 팝업 차단을 피하려면 클릭 처리기 안에서 동기적으로 빈 창을 먼저 열고, 서버에서 launch_url 을 받은 뒤 그 창을 이동시킵니다.
js
async function openGame(gameCode, tableId) {
// 1) 클릭 이벤트 안에서 바로 연다 (await 뒤에 열면 팝업 차단에 걸린다)
const mobile = matchMedia('(max-width: 768px)').matches;
const features = mobile
? undefined // 모바일은 새 탭
: `popup,left=${screen.availLeft ?? 0},top=${screen.availTop ?? 0},width=${screen.availWidth},height=${screen.availHeight}`;
const win = window.open('about:blank', `game_${gameCode}`, features); // noopener 를 주지 않는다
// 2) 오퍼레이터 백엔드가 POST /operator/v1/launch 를 부르고 launch_url 을 돌려준다
const res = await fetch('/api/launch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ game_code: gameCode, table_id: tableId, device: mobile ? 'mobile' : 'desktop' }),
});
if (!res.ok) {
win?.close();
return; // 오류 안내
}
const { launch_url } = await res.json();
// 3) 빈 창을 게임으로 보낸다
if (win) win.location.href = launch_url;
else location.href = launch_url; // 팝업이 막혔으면 현재 창에서
}noopener·noreferrer를 쓰지 마세요. 게임 화면은window.opener로 사이트 창에 postMessage 를 보내고, 모바일에서는 opener 가 있을 때 첫 터치에 전체화면으로 바꿉니다. opener 가 없으면 이벤트가 오지 않습니다.- 창 이름을 게임별로 정하면(
game_<game_code>) 같은 게임을 다시 눌렀을 때 새 창이 늘지 않고 기존 창이 재사용됩니다. - 창이 닫혔는지 주기적으로(
win.closed) 확인해 사이트의 잔액 표시를 새로 고치면 좋습니다.
iframe
사이트 안에 iframe 으로 넣을 수도 있습니다.
html
<iframe src="LAUNCH_URL" allow="autoplay; fullscreen" style="border:0;width:100%;height:100%"></iframe>- iframe 을 넣을 사이트 도메인을 온보딩 때 임베드 도메인(
embed_origins) 으로 등록해야 합니다. 게임 화면은 등록된 도메인 안에서만 iframe 으로 열리고(CSPframe-ancestors), 등록되지 않은 도메인에서는 브라우저가 표시를 막습니다. - iframe 에서는 게임 화면이
window.parent로 postMessage 를 보냅니다. - iframe 에서는 게임 화면이 스스로 창을 닫거나 이동하지 않습니다.
game.closed를 받으면 오퍼레이터가 iframe 을 닫거나 숨겨야 합니다. - iframe 안에서는 모바일 자동 전체화면을 하지 않습니다.
return_url은 iframe 을 넣은 페이지의 오리진으로 주세요. postMessage 대상 오리진이 이 값으로 정해집니다.
나가기 · return_url
게임 화면의 나가기("사이트로 돌아가기")는 아래처럼 동작합니다. 어느 경우든 먼저 game.closed 를 보냅니다.
| 게임 화면이 열린 방식 | 동작 |
|---|---|
| iframe | game.closed 만 보냅니다. iframe 정리는 오퍼레이터가 합니다. |
| 새 창 (opener 있음) | 창을 닫습니다. |
| opener 없는 창 · 탭 | return_url 로 이동합니다. |
세션 만료 화면, 이미 쓴 토큰 화면의 "돌아가기" 버튼도 return_url 로 갑니다. return_url 도 기본 복귀 URL 도 없으면 postMessage 이벤트가 전혀 오지 않으므로, 둘 중 하나는 꼭 정하세요.
postMessage 이벤트
게임 화면은 사이트 창(새 창이면 window.opener, iframe 이면 window.parent)에 아래 이벤트를 보냅니다.
- 메시지 모양:
{ source: "games-platform", type, payload } - 대상 오리진(
targetOrigin):return_url의 오리진(실행 요청 값, 없으면 등록된 기본 복귀 URL). 사이트 창의 오리진이 이와 다르면 브라우저가 메시지를 전달하지 않습니다. - 사이트 → 게임 화면 방향의 메시지는 받지 않습니다.
type | 보내는 때 | payload |
|---|---|---|
game.loaded | 토큰 교환이 끝나 게임 화면이 시작될 때 | { "game_code": "live_holdem" } — 게임을 지정하지 않았으면 null |
balance.changed | 지갑 거래로 플레이어 잔액이 바뀌었을 때 | { "balance": 1250000, "currency": "PHP" } — 금액은 최소 단위 정수 |
game.closed | 플레이어가 나가기를 눌렀을 때 | { "return_url": "https://www.acme.example/" } |
session.expired | 세션이 만료 · 종료되어 만료 화면으로 바뀔 때 | {} |
받는 쪽 예시:
js
const GAME_ORIGIN = '{GAME_ORIGIN}'; // 환경별로 발급한 게임 화면 오리진으로 바꾼다
window.addEventListener('message', (e) => {
if (e.origin !== GAME_ORIGIN) return; // 오리진 확인
const m = e.data;
if (!m || m.source !== 'games-platform') return; // 출처 표시 확인
switch (m.type) {
case 'balance.changed':
showBalance(m.payload.balance, m.payload.currency);
break;
case 'game.closed':
case 'session.expired':
refreshBalanceFromServer(); // 지갑 원장 기준으로 다시 조회
closeGameFrameIfAny();
break;
}
});event.origin을 게임 화면 오리진(환경별 주소)과 반드시 비교하세요.balance.changed는 화면 표시용입니다. 잔액의 원본은 오퍼레이터 지갑이므로 정산 · 출금 판단에는 쓰지 마세요.
언어
| 항목 | 내용 |
|---|---|
| 지원 언어 | ko en zh ja th vi |
| 형식 | zh-CN, ja_JP, EN 처럼 지역 · 대소문자가 섞여도 앞부분으로 맞춥니다. |
| 기본값 | 없거나 지원하지 않는 언어면 ko |
| 게임 안 변경 | 플레이어가 게임 화면의 언어 버튼으로 바꿀 수 있습니다. |
통화
- 플레이어 통화는
player.currency로 정하고, 오퍼레이터에 등록된 사용 통화 안이어야 합니다(아니면400 CURRENCY_NOT_SUPPORTED). - 한 플레이어(
external_id)의 통화는 바꿀 수 없습니다. 여러 통화를 쓰는 사이트라면 통화별로 다른external_id를 쓰세요. - 플레이어는 자기 통화로 운영되는 테이블만 봅니다.
세션
| 항목 | 내용 |
|---|---|
| 만들어지는 때 | launch_url 을 열어 토큰을 교환할 때마다 세션 하나 |
| 최대 수명 | 12시간. 그 뒤에는 만료 화면이 나오고 session.expired 를 보냅니다. 계속하려면 새로 실행해야 합니다. |
| 유지 | 게임 화면이 알아서 갱신합니다. 오퍼레이터가 따로 연장할 일은 없습니다. |
| 강제 종료 | POST /players/{external_id}/logout — 그 플레이어의 모든 세션을 끊고, 앉아 있던 테이블에서 일으켜 세웁니다(진행 중인 판이 끝난 뒤 캐시아웃). block: true 면 이후 실행도 막습니다. |
여러 창 · 여러 기기
- 실행할 때마다 독립된 세션이 생기고, 이전 세션은 그대로 유지됩니다. 사이트에서 로그아웃해도 게임 세션은 자동으로 끝나지 않으므로, 사이트 로그아웃 · 자기 배제 · 계정 정지 때는
POST /players/{external_id}/logout을 함께 부르세요. - 중복 게임 설정(
multi_table, 기본 꺼짐)이 꺼져 있으면, 한 플레이어는 창 · 기기를 합쳐 한 번에 테이블 하나에만 들어갈 수 있습니다. 다른 테이블에 이미 들어가 있으면 새 테이블 입장이 거절됩니다(MULTI_TABLE). 설정은PUT /settings로 바꿉니다. - 라이브 테이블은 앉거나 베팅하지 않고 보기만 하는 시간이 설정값(
watch_limit_sec)을 넘으면 로비로 돌아갑니다. 설정은PUT /settings에 있습니다.
차단된 플레이어
block: true로 차단된 플레이어는POST /launch가403 PLAYER_BLOCKED로 거절됩니다.- 차단 전에 발급된
launch_url을 열어도 교환 단계에서 거절되고 만료 화면이 나옵니다. - 차단을 풀려면
block: false로 다시 호출합니다(이때도 열려 있던 세션은 끊깁니다).