CONTRACT HUB / CURRENT

게임 연결 계약을 한곳에.

게임은 자기 화면과 데이터를 소유하고, KOISCORE는 플레이어 셸·로그인·공용 젬을 담당합니다. 출시할 때 필요한 API와 브릿지 계약을 구현 순서대로 정리했습니다.

CONTRACT MAP

어떤 문서를 봐야 하나요?

KOISCORE에 웹게임을 올리려면 “게임 화면만 배포”하는 것으로 끝나지 않습니다. 플레이어가 실제로 경험하는 화면은 포털·앱인토스 공통 셸 안의 iframe이고, 로그인·젬·광고·커뮤니티·친구 초대는 모두 플랫폼과 약속된 메시지·API로 연결해야 합니다. 아래 카드는 그 연결 지점을 계약 단위로 나눈 지도입니다. 각 카드의 protocol 이름은 코드와 postMessage 검증에 그대로 쓰이므로, 구현 전에 어떤 책임이 게임 쪽인지·플랫폼 쪽인지부터 구분해 두는 것이 좋습니다.

신규 게임은 다섯 계약을 모두 적용하는 것을 권장합니다. 기존 라이브 게임은 사용자에게 보이는 동작을 깨지 않도록 Portal Bridge v1을 유지한 채, Community·Identity·Runtime 계약부터 점진적으로 전환하면 됩니다. “일단 iframe만 넣고 나중에 로그인 붙이기”는 검수 단계에서 반려되는 경우가 많습니다. 특히 identity assertion 없이 서버에 playerId를 신뢰하거나, 포털 origin 검증 없이 postMessage를 받는 패턴은 보안·정산 모두에서 문제가 됩니다.

IMPLEMENTATION ORDER

연동 순서

아래 순서는 “검수에서 자주 막히는 지점”을 기준으로 정렬했습니다. 등록만 해두고 브릿지를 붙이지 않으면 샌드박스에서도 타이틀·음소거·로그인 동기화가 되지 않고, 브릿지만 붙이고 identity 검증을 서버에 넣지 않으면 젬·랭킹·세이브가 다른 사용자와 섞일 수 있습니다. 각 단계가 끝날 때마다 포털 샌드박스에서 실제 iframe 크기·내부 스크롤·로그인 전환을 한 번씩 확인해 두면 출시 직전 재작업을 줄일 수 있습니다.

01게임 등록

고정 gameId와 런타임 URL, 장르, 언어, 화면 비율을 draft로 등록합니다.

02프레임 연결

game:ready 이후 portal:init을 받고 origin·source·gameId와 커뮤니티 요청을 검증합니다.

03사용자 연결

identity.playerId로 게임 데이터를 분리하고 assertion을 서버에서 검증합니다.

04검수·출시

프레임 크기, 무스크롤, 결제 멱등성, 플레이타임 조건을 확인합니다.

PORTAL BRIDGE / V1

포털과 게임 iframe

KOISCORE 플랫폼에 웹게임을 정상적으로 연동하려면, 게임 런타임을 HTML5 표준 iframe 안에서 실행하고 포털과 window.postMessage로 상태를 주고받아야 합니다. 포털은 게임 URL을 직접 조작하지 않고, 승인된 HTTPS 진입점을 iframe src로 로드한 뒤 “준비됐는지”를 게임이 먼저 알려주길 기다립니다. 이 순서를 지키지 않으면 locale·음소거·identity가 아직 적용되기 전에 게임이 시작되어, 사용자마다 다른 언어·볼륨·로그인 상태가 보이는 문제가 생깁니다.

게임 쪽에서는 메시지 리스너를 DOM보다 먼저 등록한 다음 game:ready를 부모 포털로 보냅니다. 포털은 origin·iframe window·protocol·gameId를 확인한 뒤 portal:init으로 locale, 셸 설정, 오디오, 게임별 identity를 한 번에 내려줍니다. 이후 실행 중에는 portal:audio로 마스터 음소거가 바뀔 수 있고, 로그인 갱신이 필요하면 game:identity-refresh로 새 assertion을 요청합니다. 아래 표는 자주 쓰이는 메시지 타입만 모은 것이며, 전체 필드 정의는 공개 JSON Schema와 Markdown 원문을 함께 참고하세요.

방향TYPE역할
게임 → 포털game:ready메시지 수신 준비 완료
포털 → 게임portal:initlocale, shell, audio, identity 전체 동기화
포털 → 게임portal:player-update로그인·연결·로그아웃 후 player 컨텍스트만 갱신 (등록 gameId 공통)
포털 → 게임portal:audio실행 중 마스터 음소거 변경
게임 → 포털game:identity-refresh만료 전 새 identity assertion 요청
게임 → 포털game:how-to현지화된 게임 방법과 규칙 전달
게임 → 포털game:community상시 로비 또는 게임 인스턴스 채팅방 열기
게임 → 포털game:community-state-request현재 라운지·풀스크린 상태 조회
포털 → 게임portal:community-state라운지·풀스크린 상태 변경 통지
게임 → 포털game:wallet교환 후 공용 젬 잔액 갱신 요청
게임 → 포털game:match-invite현재 열린 매치의 친구 초대 요청
포털 → 게임portal:match-invite공유·복사·취소 결과
게임 → 포털game:rewarded-ad-request사용자 동작으로 보상형 광고 요청
포털 → 게임portal:rewarded-ad-resultearned·dismissed·failed 결과

표에 있는 타입 이름은 문자 그대로 protocol payload의 type 필드 값입니다. “비슷한 이름으로 보내면 알아듣겠지” 하고 임의 문자열을 쓰면 포털은 조용히 무시합니다. 반대로 포털이 보내는 portal:* 메시지도 게임에서 동일한 protocol(koiscore.portal.v1)과 자신의 gameId가 일치할 때만 처리해야 합니다. 커뮤니티·지갑·친구 초대·보상형 광고는 모두 이 브릿지 위에 얹혀 있으므로, 최소한 ready/init/audio/identity 흐름이 안정적이어야 나머지 기능을 검수에서 통과할 수 있습니다.

아래 예시는 “가장 작은 올바른 구현”입니다. 실제 서비스에서는 applyLocale, setMuted, setIdentity 안에서 UI 문자열 교체, BGM 볼륨, 서버 API 호출용 assertion 저장까지 이어져야 합니다. 특히 identity를 받은 직후 게임 서버에 assertion을 넘겨 JWT 서명을 검증하지 않으면, 클라이언트에서 playerId만 바꿔 치는 공격에 노출됩니다. 예시 코드의 origin 문자열은 운영 환경 기준이며, 로컬 샌드박스에서는 포털이 제공하는 허용 origin 목록을 그대로 사용하세요.

window.addEventListener('message', (event) => {
  if (event.source !== window.parent) return;
  if (event.origin !== 'https://koiscore.com') return;

  const message = event.data;
  if (message?.protocol !== 'koiscore.portal.v1') return;
  if (message?.gameId !== 'your-game-id') return;

  if (message.type === 'portal:init') {
    applyLocale(message.locale);
    setMuted(message.audio?.muted === true);
    setIdentity(message.identity);
  }
});

window.parent.postMessage({
  protocol: 'koiscore.portal.v1',
  type: 'game:ready',
  gameId: 'your-game-id'
}, 'https://koiscore.com');

위 코드에서 특히 주의할 점은 세 가지입니다. 첫째, event.source !== window.parent 검사로 다른 iframe이나 확장 프로그램이 보낸 메시지를 차단합니다. 둘째, event.origin을 와일드카드(*)로 두지 않고 승인된 포털 origin과만 통신합니다. 셋째, game:ready를 보낼 때도 target origin을 명시합니다. postMessage(data, '*')로 identity나 launch 파라미터를 주고받는 예제는 절대 프로덕션에 넣지 마세요. 브라우저 개발자 도구만 열려 있어도 다른 스크립트가 같은 탭에서 메시지를 가로챌 수 있습니다.

postMessage('*')로 사용자 identity를 보내지 마세요. 승인된 포털 origin, 부모 window, protocol과 gameId를 모두 확인해야 합니다.

REWARDED ADS / PORTAL BRIDGE

게임은 한 API만 호출하고 포털이 플랫폼 광고를 선택합니다

보상형 광고를 지원하려면 게임 안에 Google AdMob SDK, Apps in Toss 광고 모듈, 웹 H5 슬롯을 각각 직접 넣을 필요가 없습니다. KOISCORE 포털이 실행 채널(웹·네이티브 앱·앱인토스)을 보고 알맞은 공급자를 고르고, 게임 iframe에는 하나의 JavaScript 클라이언트만 두면 됩니다. portal:init.capabilities.rewardedAdstrue일 때만 요청할 수 있으며, 사용자가 버튼을 눌렀을 때만 호출해야 합니다. 자동 재생·연속 요청·백그라운드 요청은 정책 위반이며 검수에서 거절됩니다.

아래 스니펫은 HTML 페이지에 스크립트를 삽입하는 전형적인 패턴입니다. KoiscorePortalAds.configure로 gameId를 한 번 고정한 뒤, 재시도·추가 목숨·힌트 해금 같은 “게임 세션 기회”에만 requestRewarded를 연결하세요. 젬·현금성 재화·영구 아이템은 광고 완료만으로 지급하지 말고, 서버에서 별도 claim·영수증 검증을 두는 것이 안전합니다.

<script src="https://koiscore.com/portal/koiscore-rewarded-ad-client.js"></script>
<script>
KoiscorePortalAds.configure({ gameId: 'your-game-id' });

async function retryWithAd() {
  const result = await KoiscorePortalAds.requestRewarded({
    placement: 'retry_once'
  });
  if (result.status === 'earned' && result.earned) retryGameOnce();
}
</script>

광고 요청이 끝나면 포털은 portal:rewarded-ad-result로 상태를 돌려줍니다. 아래 표는 게임 로직에서 분기해야 할 대표적인 status 값입니다. earned일 때만 요청한 보상(재시도 1회, 힌트 1개 등)을 지급하고, dismissed·failed에서는 아무 것도 주지 않아야 합니다. “광고가 안 나왔으니 그냥 기회를 주자”는 UX는 단기적으로는 편해 보여도, 광고주·플랫폼 정산과 맞지 않아 계정 제재로 이어질 수 있습니다.

필드의미
statusearned광고 완료 확인. 요청한 플레이 기회 제공 가능
statusdismissed사용자가 광고를 닫음. 보상 제공 금지
statusfailed재고 없음·타임아웃·중복 실행. 보상 제공 금지
providerH5·AdMob·Apps in Toss포털이 선택한 실제 공급자
  • placement는 소문자 영문·숫자·밑줄·하이픈 3~64자로 지정합니다.
  • 사용자가 누른 버튼에서만 요청하고 자동 재생이나 반복 요청을 만들지 않습니다.
  • 포털은 origin, iframe window, protocol, gameId를 모두 확인하고 requestId별 결과를 캐시합니다.
  • 추가 목숨·재시도 같은 게임 세션 기회에는 사용할 수 있지만, 가치가 보존되는 재화는 서버 검증이 필수입니다.

공용 클라이언트 원문 열기

MULTIPLAYER / FRIEND INVITE

친구 링크를 누르면 해당 매치로 바로 연결

멀티플레이 “친구 초대”를 지원하려면 KOISCORE가 매치메이킹을 대신해 주는 것이 아니라, 게임 서버가 만든 매치를 플랫폼 레지스트리에 등록하고 포털이 검증된 초대 URL만 발급하게 해야 합니다. 게임은 자체적으로 https://... 초대 주소를 조립하지 않습니다. 대신 iframe에서 game:match-invite를 보내면 포털이 DB에 열린 매치인지·만료되지 않았는지 확인한 뒤 짧은 추적 URL(/m/{trackingCode})을 만들어 줍니다. 친구가 그 링크로 들어오면 포털이 해당 게임을 열고 portal:init.launch.params.matchId로 참가 정보를 전달합니다.

아래 예시는 서버·클라이언트·종료 처리까지 한 흐름으로 묶은 것입니다. 실제 구현에서는 매치 생성 API, 레지스트리 등록 API, iframe postMessage, 참가 API가 각각 다른 모듈에 있더라도 “등록 → 초대 → launch 수신 → 종료 시 close” 순서는 유지해야 합니다. portal:init은 네트워크 재시도로 여러 번 올 수 있으므로, 같은 matchId로 join을 두 번 호출해도 결과가 같아야 합니다.

// 1. 게임 서버: 매치 생성 직후
await registerMessengerMatch({
  gameId: 'example-game',
  matchId: match.id,
  expiresAt: match.expiresAt
});

// 2. 게임 iframe: 친구 초대 버튼
window.parent.postMessage({
  protocol: 'koiscore.portal.v1',
  type: 'game:match-invite',
  gameId: 'example-game',
  requestId: 'invite_01JABCDEF',
  matchId: match.id
}, 'https://koiscore.com');

// 3. 초대받은 게임: portal:init 수신
if (message.type === 'portal:init' &&
    message.launch?.type === 'match_invite') {
  await gameMatchApi.join(message.launch.params.matchId);
}

// 4. 게임 서버: 종료 시
await closeMessengerMatch('example-game', match.id);
초대 URL은 https://koiscore.com/m/{trackingCode} 형식입니다. URL을 직접 조립하지 말고 게임 iframe의 game:match-invite 요청을 사용하세요.
  • 포털은 열린 매치만 초대하며 링크 진입 때 다시 검증합니다.
  • 초대 launch token은 최대 1시간이며 매치 초대에는 일회용 소비가 적용됩니다.
  • portal:init은 재전송될 수 있으므로 참가 처리는 멱등이어야 합니다.
  • 실제 참가 가능 여부와 네트워크 세션은 게임 서버가 최종 결정합니다.

API 레퍼런스와 전체 예제 보기

COMMUNITY / LOUNGE

게임은 방을 요청하고, 포털이 대화를 관리합니다

게임 내 커뮤니티(로비 채팅, 매치방, DM)를 지원하려면 채팅 UI·신고·차단·로그인 게이트를 게임 안에 새로 만들 필요가 없습니다. KOISCORE 포털 라운지가 그 역할을 맡고, 게임 iframe은 “어떤 방을 열지”만 포털에 알려 줍니다. 모든 게임에는 별도 설정 없이 상시 lobby가 붙어 있어, 싱글·퍼즐 게임도 플레이어가 규칙·QA·피드백을 같은 UX로 나눌 수 있습니다. 매치·파티·길드처럼 참가자가 구분되는 모드는 운영 승인 후 room.id에 서버 매치 ID를 넣어 인스턴스방을 열 수 있습니다.

가장 간단한 연동은 공용 헬퍼 window.KoiscorePortalCommunity를 쓰는 방법입니다. 게임 메뉴의 “로비 열기”, “이 매치 채팅”, “쪽지함” 버튼에서 아래 API를 호출하면 포털이 로그인 여부·신고 정책·슬로우모드를 일관되게 적용합니다. Core 채팅 REST API를 게임에서 직접 두르면 계정 연결·차단·DM 권한이 게임마다 달라져 검수와 운영 모두 어려워집니다.

// 게임의 상시 로비 열기
window.KoiscorePortalCommunity?.openLounge();

// 현재 매치 참가자용 인스턴스방 열기
window.KoiscorePortalCommunity?.openLounge({
  id: 'match_42',
  label: '42번 매치'
});

// DM 메시지함 열기
window.KoiscorePortalCommunity?.openMessages();

라운지 상태 확인

const state = window.KoiscorePortalCommunity?.getLoungeState();
console.log(state?.loungeOpen, state?.fullscreen);

// 포털의 최신 상태를 요청합니다.
window.KoiscorePortalCommunity?.requestLoungeState();
window.addEventListener('koiscore:portal-community-state', event => {
  console.log(event.detail.loungeOpen, event.detail.fullscreen);
});
풀스크린에서는 게임 라운지가 항상 닫히며 loungeOpen은 반드시 false입니다. 풀스크린 중 open-lounge 요청은 거절됩니다.

로컬 샌드박스 멀티플레이 검수

샌드박스를 두 탭에서 열고 각각 계정 A와 B를 선택하면 서로 다른 로그인 사용자처럼 검수할 수 있습니다. 계정별 profileId, 게임별 playerId, 로비·인스턴스 채팅 작성자는 분리되며 모든 요청은 /api/sandbox/*만 사용합니다.

# 로컬 게임 실행 후 KOISCORE 샌드박스를 자동으로 열기
npm exec --yes --package=https://koiscore.com/tools/koiscore-sandbox-cli.tgz -- \
  koiscore-sandbox --port 5173 --game-id your-game
샌드박스에 직접 URL을 넣을 때는 외부 게임 프로젝트가 제공한 HTTPS 서비스 주소를 그대로 사용합니다. 포털 주소나 별도 게임 경로를 추측해서 입력할 필요가 없습니다.
샌드박스 사용자·assertion·채팅은 테스트 전용이며 실제 계정, 젬, 분석, 출시 상태로 복사되거나 승격되지 않습니다. 실제 런칭은 승인된 HTTPS URL과 운영 API 계약을 별도로 검수합니다.

직접 상태를 조회하는 경우

// 게임 → 포털
window.parent.postMessage({
  protocol: 'koiscore.portal.v1',
  type: 'game:community-state-request',
  gameId: 'your-game-id',
  requestId: 'community_state_01'
}, 'https://koiscore.com');

// 포털 → 게임
{
  protocol: 'koiscore.portal.v1',
  type: 'portal:community-state',
  gameId: 'your-game-id',
  requestId: 'community_state_01',
  loungeOpen: false,
  fullscreen: true
}

조회 응답에는 요청의 requestId가 포함됩니다. 사용자 조작으로 상태가 바뀔 때 포털이 먼저 보내는 변경 이벤트에는 requestId가 없을 수 있습니다.

직접 postMessage를 보내는 경우

window.parent.postMessage({
  protocol: 'koiscore.portal.v1',
  type: 'game:community',
  gameId: 'your-game-id',
  requestId: crypto.randomUUID(),
  action: 'open-lounge',
  room: { id: 'match_42', label: '42번 매치' }
}, 'https://koiscore.com');
필드규칙
actionopen-lounge 또는 open-messages
room.id선택값. 영문·숫자·_·- 1~80자. 생략하면 lobby
room.label선택값. 사용자에게 보이는 방 이름, 최대 80자
requestId영문·숫자·_·- 8~80자 고유값
응답같은 requestId와 action을 가진 portal:community, 처리 여부 accepted

운영 권장값

  • 싱글·퍼즐 게임은 상시 로비만 사용하고 인스턴스방은 끕니다.
  • 매치·파티형 게임만 인스턴스방을 켜고 서버의 실제 매치 ID를 불투명 방 키로 사용합니다.
  • 슬로우모드 2초, 메시지 보관 30일로 시작하고 신고량에 따라 조정합니다.
  • 게임 iframe은 메시지 본문, 다른 사용자의 전역 profileId, 인증 토큰을 받거나 저장하지 않습니다.
게임에서 Core 채팅 API를 직접 호출하거나 자체 DM UI를 만들지 마세요. 방 ID만 포털에 전달해야 계정 연결, 차단, 신고 정책이 모든 게임에 동일하게 적용됩니다.

PLATFORM / SESSION · PROGRESS

세션·진행 저장은 플랫폼 API

게임별 /api/app/<game-id>/progress를 새로 만들 필요 없습니다. 등록한 gameId만 바꿔서 공통 엔드포인트를 쓰면, 게스트 진행·Google 연결 승계·로그인 전환 알림까지 플랫폼이 처리합니다. 게임은 progress JSON 스키마와 화면 적용만 담당합니다.

메서드경로역할
GET/api/platform/player/session?gameId=...현재 player 컨텍스트
GET/api/platform/player/progress?gameId=...등록 gameId 진행 JSON
PUT/api/platform/player/progress?gameId=...version + progress 저장 (409 충돌)

로그인·연결 후 포털은 portal:player-update를 보냅니다. 게임은 받은 뒤 progress를 다시 GET해야 합니다. 공용 스크립트가 postMessage와 fetch를 묶어 줍니다.

<script src="https://koiscore.com/portal/koiscore-platform-player-client.js"></script>
<script>
KoiscorePlatformPlayer.configure({ gameId: 'your-registered-game-id' });

KoiscorePlatformPlayer.startSessionSync({
  onChange: async ({ session, progress }) => {
    if (session?.ok) applyPlayerUi(session.player);
    if (progress?.ok) applySave(progress.progress, progress.version);
  },
});

const { session, progress } = await KoiscorePlatformPlayer.loadState();
</script>
  • 브라우저: credentials: 'include' (포털·*.koiscore.com 런타임)
  • 게임 서버: Authorization: Bearer <game-identity JWT>
  • 샌드박스: apiOrigin: 'http://127.0.0.1:9302' + /api/sandbox/platform/player/...

Platform Player API 원문 (Markdown)

LOGIN IDENTITY / V2

로그인은 공용, 데이터는 게임별

로그인 사용자를 게임 데이터에 연결하려면 Google OAuth를 게임 번들에 넣거나 전역 profileId를 세이브 키로 쓰면 안 됩니다. KOISCORE가 로그인 세션·계정 연결·프로필 사진 선택을 관리하고, 게임에는 게임마다 다른 identity.playerId와 짧게 만료되는 Ed25519 서명 assertion(JWT)만 전달합니다. 같은 사람이라도 게임 A와 게임 B의 playerId는 다르며, 이는 GDPR·데이터 삭제·게임 간 데이터 격리를 위해 의도된 설계입니다. 클라이언트가 보내는 playerId 문자열은 UI 표시용일 뿐 서버 권한 판단에 쓰지 마세요.

portal:init 또는 identity API 응답으로 내려오는 JSON은 대략 아래 형태입니다. authenticated: false인 게스트 세션도 정상 케이스이며, 이때는 게임 서버에 영구 진행도를 묶지 않거나 게스트→로그인 연결 정책을 별도로 두어야 합니다. assertion의 expiresAt은 최대 300초 수준이므로, 만료 임박 시 game:identity-refresh로 새 토큰을 받아 API 호출에 사용하세요.

{
  "schemaVersion": "koiscore.game-identity.v2",
  "gameId": "your-game-id",
  "authenticated": true,
  "playerId": "ply_...",
  "displayName": "Koi Player",
  "avatarUrl": "https://koiscore.com/api/profile/avatar/...",
  "locale": "ko",
  "provider": "google",
  "assertion": {
    "format": "jwt",
    "token": "header.payload.signature",
    "expiresAt": "2026-08-04T00:01:00.000Z"
  }
}

프로필 사진 선택 규칙

  1. 사용자가 KOISCORE에 직접 업로드한 프로필 사진을 가장 먼저 사용합니다.
  2. 업로드 사진이 없으면 현재 provider로 선택된 로그인 플랫폼의 프로필 사진을 사용합니다.
  3. 플랫폼 사진도 없으면 KOISCORE 공통 더미 이미지를 사용합니다.

identity.avatarUrl과 호환용 player.avatarUrl은 게임 iframe에서 바로 표시할 수 있는 HTTP(S) 절대 URL입니다. 게임은 이 URL을 사용자 식별 키로 사용하거나 영구 복제하지 말고 표시용으로만 캐시해야 합니다.

게임 서버 필수 검증

  1. GET /api/platform/identity/jwks의 Ed25519 공개키로 JWT 서명을 검증합니다.
  2. iss=https://koiscore.com, aud=gameId, game_id=gameId를 확인합니다.
  3. 검증된 sub만 데이터 키로 사용하고 body의 playerId는 신뢰하지 않습니다.
  4. assertion은 최대 300초만 허용하고 만료되면 game:identity-refresh를 요청합니다.

RUNTIME / V2

게임 프레임과 캔버스 정책

Pixi·Canvas·Unity WebGL 등 어떤 렌더러를 쓰든, KOISCORE 플레이어 iframe 안에서는 “포털이 준 실제 픽셀 크기”가 곧 게임 해상도입니다. 고정 1920×1080 CSS를 강제하거나 내부 document 스크롤을 만들면 모바일 WebView·데스크톱 분할 화면에서 잘리거나 이중 스크롤이 생깁니다. ResizeObserver로 부모 크기 변화(라운지 열림, 풀스크린, 키보드 등)에 맞춰 캔버스를 다시 그리도록 구현해야 검수 뷰포트(390×844, 1366×768 등)를 통과할 수 있습니다. 타이틀 바·공용 젬·광고 슬롯은 호스트 셸 영역이므로 게임 iframe 안에 중복 UI를 그리지 않습니다.

아래 표는 출시 검수에서 반복적으로 확인하는 런타임 항목입니다. “등록만 해두고 나중에 맞추기”보다 샌드박스 단계에서 미리 맞춰 두는 편이 beta→live 전환 비용을 줄입니다. sandbox → shadow → canary → v2-live 단계별 롤백 산출물을 남겨 두면, 문제 발생 시 포털 쪽만 이전 빌드로 되돌릴 수 있습니다.

항목필수 정책
Rendererpixi-canvas, wasm-canvas, unity-webgl 중 등록
Sizeiframe 실제 너비·높이를 사용하고 ResizeObserver로 갱신
Mobilehtml, body, root 100%, 내부 문서 스크롤 금지
Shell타이틀, 광고, 공용 젬 UI는 KOISCORE 호스트가 소유
LaunchURL에 세션 token을 넣지 않고 승인된 호스트 진입만 허용
Migrationsandbox → shadow → canary → v2-live, rollback 산출물 유지

INTEGRATION / V1

등록·광고·젬 경계

KOISCORE 연동의 핵심은 “무엇을 게임이 소유하고, 무엇을 플랫폼이 소유하는가”를 명확히 나누는 것입니다. 게임 화면·세이브·점수·게임 전용 재화·인벤토리·매치 상태는 개발사가 책임집니다. 반면 로그인 세션, 게임별 identity 발급, 공용 젬 원장, 결제 영수증, 웹/앱인토스 광고 슬롯, 포털 커뮤니티 정책은 KOISCORE가 책임집니다. 이 경계를 어기면 — 예를 들어 게임 클라이언트가 젬 잔액을 직접 수정하거나, 포털 광고와 별도로 AdSense를 iframe 안에 또 넣거나 — 검수·정산·스토어 심사에서 모두 걸립니다.

개발자 계정은 회사·팀 단위 퍼블리셔로 묶이며, 자신이 등록한 gameId만 콘솔·MCP·릴리즈 API에서 볼 수 있습니다. KOISCORE 내부 운영 계정은 allowlist로 FIRST PARTY로 표시되지만, 연동 계약 자체는 외부 퍼블리셔와 동일합니다. 젬 구매·교환·구독은 반드시 서버 상품 카탈로그와 영수증 API를 거치고, 게임 지급 엔드포인트는 receiptId·requestKey 기준 멱등 처리를 구현해야 합니다.

  • 게임 화면, 세이브, 점수, 게임 전용 재화와 인벤토리는 개별 게임이 소유합니다.
  • 개발자 계정은 회사·팀 퍼블리셔 단위로 자기 게임만 관리합니다. 승인된 KOISCORE 계정의 게임은 퍼스트파티로 자동 분류됩니다.
  • 로그인 세션, 게임별 identity 발급, 공용 젬 원장과 결제 영수증은 KOISCORE가 소유합니다.
  • 웹 광고는 포털, Apps in Toss 광고는 미니앱 채널이 담당하며 게임에서 중복 요청하지 않습니다.
  • 젬 구매는 서버 상품 카탈로그와 영수증을 사용하고 게임 지급 API는 멱등 처리합니다.

PUBLIC SOURCES

원본 계약 참조

자동화 도구와 CI는 공개 계약 카탈로그에서 현재 Schema URL을 조회할 수 있습니다. 사람이 읽는 브릿지 상세본은 아래 레퍼런스를 사용합니다.