게임 세이브·로드 SDK — 키 기반 저장, 게임 index.html에서 직접 호출
index.html에 SDK를 포함하면 끝입니다. SDK가 gameId(URL 파라미터)와
JWT 토큰(localStorage, 같은 도메인 공유)을 자동으로 읽어 API를 직접 호출합니다.
비로그인 유저는 localStorage로 자동 폴백됩니다.
await MoviSave.set('gold', 100);
?gameId=42 자동 파싱
localStorage('portal_token') 자동 읽기 — 같은 도메인 공유
localStorage('movi_{gameId}_{key}') 폴백
/saves/:gameId/:key → 단일 키 저장 (UPSERT)/saves/:gameId → 여러 키 한번에 저장/saves/:gameId/:key → 단일 키 로드/saves/:gameId → 전체 키-값 로드/saves/:gameId/:key → 단일 키 삭제/saves/:gameId → 전체 삭제index.html에 <script>로 포함.window.MoviSave 객체 노출.principal: 'portal') 전용.game_saves 테이블 생성 + 인덱스.node admin-api/src/migrate-007-game-saves.js
| 컬럼 | 타입 | 키 | 기본값 | 설명 |
|---|---|---|---|---|
| user_id | INTEGER | PKFK | — | portal_users.id (ON DELETE CASCADE) |
| game_id | INTEGER | PKFK | — | games.id (ON DELETE CASCADE) |
| key | VARCHAR(100) | PK | — | 저장 키 이름 (예: 'gold', 'level', 'save1') |
| data | TEXT | — | JSON 직렬화된 값 (DB 무제한, 서버에서 키당 1MB 제한) | |
| updated_at | TIMESTAMPTZ | now() | 마지막 저장 시각 — UPSERT 시 자동 갱신 |
(user_id, game_id, key) — 같은 키 저장 시 자동 덮어쓰기(UPSERT).TEXT는 64KB 제한 → MEDIUMTEXT(16MB)로 변경 필요.node admin-api/src/migrate-007-game-saves.js
Authorization: Bearer <token>, principal: 'portal').
어드민 토큰은 403 반환. userId는 JWT에서 파싱하므로 게임에서 유저를 알 필요 없음.
| 메서드 | 경로 | 설명 | 요청 body | 응답 |
|---|---|---|---|---|
| GET | /saves/:gameId/:key | 단일 키 로드 — 없으면 data: null | — | { data: any|null } |
| GET | /saves/:gameId | 전체 키-값 로드 | — | { data: { key: value, … } } |
| PUT | /saves/:gameId/:key | 단일 키 저장 (UPSERT) — 1MB 초과 시 413 | { data: any } |
{ ok: true, updated_at } |
| PUT | /saves/:gameId | 여러 키 한번에 저장 — 최대 50개 | { data: { k: v, … } } |
{ ok: true, count } |
| DELETE | /saves/:gameId/:key | 단일 키 삭제 | — | { ok: true } |
| DELETE | /saves/:gameId | 해당 게임 전체 세이브 삭제 | — | { ok: true, deleted: N } |
<!-- 포털에서 게임 iframe 로드 시 gameId 포함 -->
<iframe src="/res/MoviSoft/com.movisoft.game/game/index.html?gameId=42"></iframe>
<!-- 게임 index.html -->
<script src="/movigames/sdk/movigame-sdk.js"></script>
<script>
// ── 단일 키 저장 / 로드
await MoviSave.set('gold', 100);
await MoviSave.set('items', ['sword', 'shield']);
const gold = await MoviSave.get('gold'); // → 100 (없으면 null)
const items = await MoviSave.get('items'); // → ['sword', 'shield']
// ── 여러 키 한번에 저장 / 전체 로드
await MoviSave.setMany({ gold: 100, level: 3, items: ['sword'] });
const all = await MoviSave.getAll();
// → { gold: 100, level: 3, items: ['sword'] }
// ── 삭제
await MoviSave.remove('gold'); // 단일 키 삭제
await MoviSave.removeAll(); // 이 게임의 세이브 전체 삭제
// ── 로그인 여부 (동기)
if (MoviSave.isLoggedIn()) { /* 서버 저장 */ } else { /* localStorage 폴백 */ }
</script>
setMany로 묶여 한 번에 전송됩니다.
// 1초마다 호출해도 서버에는 30초마다 1번만 전송
setInterval(async () => {
await MoviSave.set('score', score); // → 30초 동안은 localStorage에만, 이후 서버 플러시
}, 1000);
// 게임 종료 직전 — 버퍼를 즉시 서버에 강제 플러시
window.addEventListener('beforeunload', () => {
MoviSave.flush(); // 쓰로틀 무시하고 즉시 전송
});
movi_{gameId}_{key}MoviSave.getAll()로 로컬 데이터를 읽어 MoviSave.setMany()로 서버에 업로드하는 마이그레이션 로직을 별도로 추가해야 합니다.