이 계획은 기존 localStorage 저장을 “로그인 사용자에 한해 Supabase 동기화”로 확장하는 1차 릴리스 사양이다.
핵심은 다음 4가지다.
pull-tracker와 tactic_maker_autosave는 로컬 유지.pull-tracker:*, tactic_maker_autosave, tactic_maker_autosave_time는 로컬 전용.| Namespace | 로컬 키 | 페이지 | Supabase 동기화 | 현재 Export |
|—|—|—|—|—|
| calc_defense_options | defenseCalc_penetrateOptions, defenseCalc_reduceOptions | /defense-calc/ | 예 | 없음 |
| calc_critical_options | criticalCalc_selfOptions, criticalCalc_buffOptions | /critical-calc/ | 예 | 없음 |
| character_qevel | qevel-value | /character.html (QEVEL 모달) | 예 | 없음 |
| pull_calc | pullCalc_* | /pull-calc/ | 예 | 없음 |
| pay_calc | payCalcBaseResources | /pay-calc/ | 예 | 없음 |
| material_planner | materialPlannerStateV1 | /material-calc/ | 예 | 있음 (backup/load) |
| maps_progress | clickedObjects | /maps/ | 예 | 있음 (backup/restore) |
| wonder_weapon_shard | wonder_weapon_shard_* | /wonder-weapon/ | 예 | 없음 |
| synergy_spoiler | unlockQuestSpoiler_* | /synergy/ | 예 | 없음 |
| pull_tracker | pull-tracker:* | /pull-tracker/ | 아니오(로컬 유지) | 있음 (export/import) |
| tactic_maker_autosave | tactic_maker_autosave* | /tactic-maker/ | 아니오(로컬 유지) | 있음(수동 export/import, 단 autosave 자체는 로컬) |
public.user_sync_state
create table if not exists public.user_sync_state (
user_id uuid not null references auth.users(id) on delete cascade,
namespace text not null,
payload jsonb not null default '{}'::jsonb,
payload_hash text not null default '',
payload_size int generated always as (pg_column_size(payload)) stored,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
primary key (user_id, namespace),
constraint user_sync_state_namespace_chk check (
namespace in (
'calc_defense_options',
'calc_critical_options',
'character_qevel',
'pull_calc',
'pay_calc',
'material_planner',
'maps_progress',
'wonder_weapon_shard',
'synergy_spoiler'
)
),
constraint user_sync_state_payload_size_chk check (pg_column_size(payload) <= 262144)
);
create index if not exists user_sync_state_user_updated_idx
on public.user_sync_state (user_id, updated_at desc);
create or replace function public.tg_user_sync_state_updated_at()
returns trigger
language plpgsql
as $$
begin
new.updated_at = now();
return new;
end;
$$;
drop trigger if exists tr_user_sync_state_updated_at on public.user_sync_state;
create trigger tr_user_sync_state_updated_at
before update on public.user_sync_state
for each row execute function public.tg_user_sync_state_updated_at();
alter table public.user_sync_state enable row level security;
alter table public.user_sync_state force row level security;
create policy user_sync_state_select_own
on public.user_sync_state
for select to authenticated
using ((select auth.uid()) = user_id);
create policy user_sync_state_insert_own
on public.user_sync_state
for insert to authenticated
with check ((select auth.uid()) = user_id);
create policy user_sync_state_update_own
on public.user_sync_state
for update to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
create policy user_sync_state_delete_own
on public.user_sync_state
for delete to authenticated
using ((select auth.uid()) = user_id);
grant select, insert, update, delete on public.user_sync_state to authenticated;
assets/js/supabase/user-sync-config.jsassets/js/supabase/user-sync-namespaces.jsassets/js/supabase/user-sync-manager.jsapps/home/js/sync-migration-notice.jsscripts/manual/create-user-sync-state.sql (위 SQL 저장)type SyncNamespace =
| 'calc_defense_options'
| 'calc_critical_options'
| 'character_qevel'
| 'pull_calc'
| 'pay_calc'
| 'material_planner'
| 'maps_progress'
| 'wonder_weapon_shard'
| 'synergy_spoiler';
interface SyncBootstrapResult {
page: string;
namespaces: SyncNamespace[];
status: 'skipped' | 'synced' | 'conflict-resolved' | 'error';
}
window.UserSyncManager.bootstrapForPage(pageKey: string): Promise<SyncBootstrapResult>;
window.UserSyncManager.flush(): Promise<void>;
window.UserSyncManager.getStatus(): { loggedIn: boolean; enabled: boolean; page: string };
bootstrapForPage(pageKey) 실행 시 세션 확인.localStorage.setItem/removeItem 패치로 추적 키 변경 감지.upsert 실행.localStorage에 namespace별 캐시해서 같은 충돌 재질문 최소화.pull-tracker 제외 유지로 대용량 egress 차단.`
`를 아래에 추가.
_includes/defense-calc-body.html_includes/critical-calc-body.html_includes/pull-calc-body.html_includes/pay-calc-body.html_includes/material-calc-body.html_includes/synergy-body.html_includes/wonder-weapon-body.html_includes/character-detail-body.html_includes/maps-body.html각 페이지 초기화 직전에 await window.UserSyncManager.bootstrapForPage('<page-key>') 삽입.
apps/defense-calc/defense-calc.js DOMContentLoaded start 직전._includes/critical-calc-body.html DOMContentLoaded 내부 new CriticalCalc() 직전.apps/pull-calc/pull-calc.js DOMContentLoaded 내부 new PullSimulator() 전.apps/pay-calc/pay-calc.js DOMContentLoaded 내부 new PayCalculator() 전._includes/material-calc-body.html DOMContentLoaded 내부 MaterialPlanner.init() 전.apps/synergy/synergy.js init() 함수 시작부.apps/wonder-weapon/wonder-weapon.js DOMContentLoaded 시작부.assets/js/character/QEVEL.js showQEVELModal() 시작부._includes/maps-body.html 맵 초기화 IIFE 시작부.로그인 기반 저장 전환 테스트 공지 + 로컬 데이터 손실 가능성 안내 + 기존 export 사용 안내.
SYNC_NOTICE_START_AT 이후 활성화.SYNC_NOTICE_END_AT 이전: “2주 유예 중” 메시지.SYNC_NOTICE_END_AT 이후: “유예 종료, 로컬 데이터 보장 불가” 메시지.assets/js/supabase/user-sync-config.js에 ISO 문자열로 명시.
SYNC_NOTICE_START_AT = 'YYYY-MM-DDT00:00:00+09:00'SYNC_NOTICE_END_AT = 'YYYY-MM-DDT23:59:59+09:00'1일간 보지 않기.닫기, 로그인./login/?redirect=<current-url>.아래 3개 파일에 키 추가.
i18n/pages/home/kr.jsi18n/pages/home/en.jsi18n/pages/home/jp.js신규 통합 백업 도구는 이번 범위에서 만들지 않는다. 공지 모달/안내문에 “기존 export 기능 활용”을 명시한다.
/maps/) Backup/Restore/material-calc/) Backup/Load/tactic/) Export/Import/tactic-maker/) Export/Import/pull-tracker/) Export/Import“일부 페이지(방어/크리티컬/가챠 플래너/과금/시너지/원더무기/QEVEL)는 별도 Export 기능이 없습니다. 해당 데이터는 로그인 동기화를 권장합니다.”
scripts/manual/create-user-sync-state.sql 작성.user-sync-config.js (feature flag + 날짜 + namespace map).user-sync-namespaces.js (read/write/hasData/hash).user-sync-manager.js (bootstrap, conflict, flush, patch)._includes/home-body.html, apps/home/js/sync-migration-notice.js.pull-tracker:* 데이터는 어떠한 경우에도 Supabase로 전송되지 않음.tactic_maker_autosave*는 Supabase 전송되지 않음.clickedObjects 동기화 후 맵 체크 상태가 재접속/타기기에서 동일.wonder_weapon_shard_* 동기화 후 무기 체크 상태 유지.unlockQuestSpoiler_* 동기화 후 스포일러 해제 상태 유지.SYNC_ENABLED로 즉시 비활성 가능.bootstrap, conflict, upsert, skip(size/hash) 콘솔 태그.SYNC_ENABLED=false 배포, 기존 로컬 저장 경로 유지.kr/en/jp 필수 반영, cn은 키 구조만 확장 가능하게 유지한다.