아이템 DB (Supabase)
게임 아이템 카탈로그를 Supabase에 테이블화한 문서입니다.
편집 원본은 TypeScript 정적 데이터 → pnpm db:seed-items로 DB에 올리고,
클라이언트 런타임은 부트 시 Supabase에서 카탈로그를 받아 getItemDef() 레지스트리에 hydrate합니다.
원격 로드 실패·환경변수 부재 시에는 번들 TS defs로 폴백합니다.
결정
| 항목 | 값 |
|---|---|
| 범위 | ItemDef + adjacency combo |
| 편집 원본 | packages/shared/data/artifacts.ts, ooparts.ts, itemCombos.ts |
| 이미지 | Cloudflare R2 (바이너리). DB에는 sprite_key만 저장 |
| 런타임 | 부트 시 Supabase REST → hydrateItemCatalog(); 실패 시 번들 TS |
| 로컬 스키마 | supabase/migrations/ 필수 |
로컬 구조
supabase/
config.toml
migrations/
20260709120000_items_catalog.sql
scripts/
db/
seed-items.mjs
packages/client/src/catalog/
loadItemCatalog.ts # 부트 fetch + hydrate
런타임 로딩
flowchart LR
boot["main.tsx boot"] --> fetch["GET /rest/v1/items + combos"]
fetch --> parse["parseItemCatalog"]
parse --> hydrate["hydrateItemCatalog"]
hydrate --> app["App / AdminBoard"]
fetch -.->|fail / no env| bundled["bundled ITEM_DEFS"]
bundled --> app
packages/client/src/main.tsx가loadItemCatalogAtBoot()완료 후 React 마운트- Publishable key로 RLS SELECT만 사용 (
VITE_SUPABASE_URL,VITE_SUPABASE_PUBLISHABLE_KEY) - Shared:
hydrateItemDefs/hydrateAdjacencyCombos(registry.ts,comboTree.ts)
테이블
public.items
| 컬럼 | 타입 | 설명 |
|---|---|---|
id |
text PK | 예: item.dagger.basic |
origin |
enum artifact | oopart |
|
color |
text | UI 틴트 |
element |
enum nullable | fire/water/earth/wind/void |
max_tier |
int | |
activation_cost |
int nullable | |
max_durability |
int nullable | |
shop_pool |
boolean | |
wear_per_battle |
int nullable | |
shape |
jsonb | [{ "x", "y" }, …] |
atk / def / hp / spd |
jsonb | 티어별 수치 배열 |
tags |
item_tag[] |
|
sprite_key |
text | R2 오브젝트 키 |
created_at / updated_at |
timestamptz |
public.item_adjacency_combos
| 컬럼 | 타입 | 설명 |
|---|---|---|
item_id |
text FK → items | |
combo_key |
text | 예: shield_link |
implemented |
boolean | |
effects |
jsonb | [{ "kind", "value" }] |
related_def_ids |
text[] |
PK: (item_id, combo_key)
RLS: anon / authenticated는 SELECT만. 쓰기는 service role(SUPABASE_SECRET_KEY)만.
R2 스프라이트 키
로컬 public/assets/sprites/ 경로와 동일한 키를 씁니다.
- artifact:
items/artifacts/{id_dots_to_underscores}.png - oopart:
items/ooparts/{id_dots_to_underscores}.png
예: item.dagger.basic → items/artifacts/item_dagger_basic.png
바이너리 업로드·Worker R2 바인딩은 후속 작업입니다. 당분간 클라이언트는 Vite public/ 정적 파일을 사용합니다.
아이콘 생성·투명화: (내부 문서) §10 — DT + rembg (색 키잉 아님).
환경 변수
루트 .env (gitignore) — 시드용:
SUPABASE_URL=https://….supabase.co
SUPABASE_PUBLISHABLE_KEY=…
SUPABASE_SECRET_KEY=… # 시드·관리 쓰기 전용 (클라이언트에 넣지 말 것)
클라이언트 Vite (packages/client/.env / .env.example):
VITE_SUPABASE_URL=https://….supabase.co
VITE_SUPABASE_PUBLISHABLE_KEY=… # publishable only
명령
# 원격 스키마 적용 (supabase/migrations/)
pnpm db:apply-migrations
# TS → Supabase upsert (items + combos)
pnpm db:seed-items
# Admin tuning bundle → game_tuning (id = live)
pnpm db:push-tuning data/admin-tuning.json
마이그레이션 SQL은 레포에 있으며, 원격 적용은 Supabase MCP / Dashboard / DB connection으로 수행합니다.
CLI supabase link는 SUPABASE_ACCESS_TOKEN이 필요합니다.
시드 흐름
ITEM_DEFS로드 →sprite_key생성 →itemsupsert (on_conflict=id)item_adjacency_combos전체 delete 후 insert (TS에서 제거된 콤보 정리)- 현재 규모: items 80 (유적 전용 25 포함), adjacency combos 26
유적 전용 아이템 (25)
| 유적 archetype | 권장 포지션 | 오파츠 (2) | 아티팩트 (3) | 강화 소재 materialId |
|---|---|---|---|---|
ember_foundry |
magnum | oopart.foundry.overdrive_joint, oopart.foundry.heat_sink_spine |
item.foundry.gearblade, item.foundry.articulated_coil, item.foundry.overload_damper |
mat.ruin.ember_foundry |
sunken_reliquary |
ochler | oopart.reliquary.echo_cistern, oopart.reliquary.return_valve |
item.reliquary.condenser_core, item.reliquary.resonance_plate, item.reliquary.portable_still |
mat.ruin.sunken_reliquary |
verdigris_menagerie |
chimera_sage | oopart.menagerie.incubator_gland, oopart.menagerie.symbiote_lattice |
item.menagerie.culture_cradle, item.menagerie.specimen_hook, item.menagerie.sealing_clamp |
mat.ruin.verdigris_menagerie |
hollow_observatory |
mage | oopart.observatory.prismatic_focus, oopart.observatory.mana_astrolabe |
item.observatory.void_lens, item.observatory.channel_rod, item.observatory.tidal_charm |
mat.ruin.hollow_observatory |
ossuary_bastion |
knight | oopart.bastion.aegis_vertebra, oopart.bastion.oath_censer |
item.bastion.gate_shield_fragment, item.bastion.reliquary_plate, item.bastion.warden_sigil |
mat.ruin.ossuary_bastion |
- 데이터:
ruinSatchelOoparts.ts,ruinSatchelArtifacts.ts, 풀ruinRewardPools.ts - 오파츠
maxTier1, 계정oopartEnhancementLevels0–5 (defId 단위), 강화 비용 소재 1/2/3/5/8 - 장착 포지션 하드 제한 없음 —
recommendedPositionId는 소프트 시너지·proc 배율용 - 보스 보상:
rollRuinBossReward+ run 시작 시ownedOopartDefIds스냅샷
이번 패스에서 하지 않음
- R2 버킷 생성·업로드 자동화
- i18n 이름 테이블
- admin board가 Supabase에 직접 쓰기 (shape·아이콘 Apply는 여전히 브라우저
localStorage; 공유 반영은 Export →pnpm db:push-tuning)
어드민 튜닝 DB (game_tuning)
어드민 보드에서 조정한 무기 VFX · 몬스터 배틀 · 스테이지 · 아이템 shape/아이콘 오버라이드를 Supabase에 올려 프로덕션 부트 시 자동 로드합니다.
| 항목 | 값 |
|---|---|
| 테이블 | public.game_tuning |
| 프로덕션 키 | id = 'live' |
| 번들 형식 | AdminTuningBundle v1 (adminTuningBundle.ts) |
| 마이그레이션 | supabase/migrations/20260711190000_game_tuning.sql |
| 시드 | pnpm db:push-tuning [path] — 기본 data/admin-tuning.json |
| 부트 로드 | loadGameTuningAtBoot() → main.tsx |
컬럼
| 컬럼 | localStorage 키 | 설명 |
|---|---|---|
weapon_projectile |
cf-weapon-projectile-overrides |
무기 투사체/VFX 프로필 |
monster_battle |
cf-monster-battle-overrides |
몬스터 스케일·오프셋 |
stage_design |
cf-stage-design-overrides-v1 |
층별 스테이지 오버라이드 |
item_shape |
cf-item-shape-overrides |
shape + 아이콘 회전/오프셋 |
RLS: anon / authenticated SELECT만. 쓰기는 SUPABASE_SECRET_KEY 시드 스크립트만.
워크플로
- 로컬 어드민에서 튜닝 → 튜닝 보내기로 JSON 저장 (
data/admin-tuning.json권장) - 원격에 마이그레이션 적용 (Dashboard / Supabase CLI)
pnpm db:push-tuning data/admin-tuning.jsonpnpm deploy:client— 부트 시 Supabaselive스냅샷이 localStorage에 replace 후 shape hydrate
어드민 DB에서 받기는 개발 중 live 스냅샷을 브라우저로 덮어씁니다 (읽기 전용 REST).
부트 순서
loadItemCatalogAtBoot → loadGameTuningAtBoot → applyShapeOverrides → preloadGameAssets
Supabase 미설정·행 없음·빈 스냅샷이면 기존처럼 브라우저 localStorage만 사용합니다.