/* * 공간 계층의 **단계** — 성과를 어느 단위로 보는가. 순수(import 없음). * * ── 왜 여기 있나 ─────────────────────────────────────────────────────────── * 이 목록을 세 곳이 쓴다: 씬 표현이 "나는 어느 단계에 붙는다" 고 선언할 때(클라이언트), 성과 카드가 * "이 단계로 나눠 달라" 고 물을 때(클라이언트), AI 가 "구역별로 보여 줘" 를 제안할 때(서버). * 세 곳이 각자 목록을 적으면 하나가 늘 때 나머지가 알리지 않고 뒤처진다 — 집약 태그에서 이미 겪었다. * * 그래서 **한 곳**에 두고 셋이 가져다 쓴다. 이 파일은 import 가 없어 클라이언트 번들에 서버 코드를 * 끌고 들어가지 않는다. */ /** * 성과를 접을 수 있는 단계 — 좁은 것부터. * * `equipment` 는 커널 축 이름으로 `resource` 다(설비·인원·자산을 아우르는 이름). 화면과 AI 는 * "설비" 로 말하고, 커널에 물을 때만 축 이름으로 바꾼다 — 그 변환이 아래 한 곳에 있다. */ export const TWIN_LEVELS = ['space', 'twin', 'area', 'location', 'equipment'] as const export type TwinLevel = (typeof TWIN_LEVELS)[number] export function isTwinLevel(v: unknown): v is TwinLevel { return typeof v === 'string' && (TWIN_LEVELS as readonly string[]).includes(v) } /** * 단계 → 커널의 나누는 축(`KpiGroupBy`). **전체를 보는 단계는 축이 없다** — 나누지 않고 통째로 접는다. * * `space` 와 `twin` 이 축 없이 같은 이유: 둘의 차이는 "무엇을 대상으로 삼는가"(공간 전체 대 트윈 하나)이지 * "어떻게 나누는가" 가 아니다. 대상은 질의의 `spaceId`/`instanceId` 가 정한다. */ export function kpiGroupByOfLevel(level: TwinLevel): 'area' | 'location' | 'resource' | undefined { if (level === 'area') return 'area' if (level === 'location') return 'location' if (level === 'equipment') return 'resource' return undefined } /** * 그 단계의 카드가 **대상 하나마다 한 장**인가. * * 구역·자리·설비는 여럿이라 카드가 그 수만큼 생긴다. 공간·트윈은 하나뿐이다. 보드를 자동으로 조립할 때 * 몇 장이 생길지 미리 알아야 한다 — 자리 200개 × 지표 4개면 800장이다. */ export function isPerTargetLevel(level: TwinLevel): boolean { return kpiGroupByOfLevel(level) !== undefined }