Phase D: 닉네임 기반 DB 세이브 동기화

- villages 테이블(pg-jskim): nickname PK + 보호 토큰 + JSONB 세이브
- API: /api/village/check(중복)·register(선점)·save(토큰 검증 업서트)·load
- 닉네임 모달: 촌장 냥이 인사 직후 게임 차단, 입력 멈춘 1초 후 자동
  중복 체크(디바운스), 사용 가능할 때만 결정 버튼 활성화
  한글·영문·숫자 2~12자 검증, 서버 불가 시 로컬 시작 후 재시도
- 동기화: 30초마다 자동 push + 종료 시 push, 부팅 시 서버가 더 최신이면
  교체(replaceState → 마이그레이션·오프라인 정산 재수행)
- 기존 세이브도 닉네임 미설정이면 모달 표시

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
이 커밋은 다음에 포함됨:
kjs
2026-08-14 10:36:22 +09:00
co-authored by Claude Fable 5
부모 401db0cb9e
커밋 5dd74babe8
11개의 변경된 파일과 370개의 추가작업 그리고 0개의 파일을 삭제
+21
파일 보기
@@ -295,6 +295,12 @@ export const engine = {
} catch {
// 저장 데이터가 깨졌으면 새로 시작
}
if (!state) state = createInitialState();
return this.finishLoad();
},
/** 마이그레이션 + 오프라인 정산 (로컬/원격 로드 공통) */
finishLoad(): GameState {
if (!state) state = createInitialState();
// 구버전 세이브 마이그레이션 (필드 보강)
state.resources.stone ??= 0;
@@ -340,6 +346,21 @@ export const engine = {
return state;
},
/** 서버 세이브로 교체 (더 최신일 때) — 마이그레이션·오프라인 정산 재수행 */
replaceState(data: GameState) {
state = data;
this.finishLoad();
this.save();
},
/** 닉네임·토큰 설정 */
setNickname(nickname: string, token: string) {
if (!state) return;
state.nickname = nickname;
state.syncToken = token;
this.save();
},
/** 오프라인 정산 리포트 (1회성) */
consumeOfflineReport(): number | null {
const r = offlineMinutes;