/* * 항목의 **이름**을 붙이는 자리 — 화면이 id 만 보이지 않게. (2026-08-22 공용으로 옮김) * * 이 둘은 항목 질의 안에 사적으로 있었다. 그런데 **전개(BOM 나무) 화면도 같은 이름이 필요하다** — * 사용자가 「각 노드의 식별자가 반복되어서 이해가 어렵다. 식별자와 이름이 보이면」이라고 했다. * 두 벌을 만들면 한쪽만 고쳐지므로 한 곳에 둔다(재발명 금지). */ /* 커널 런타임 로드 — 어느 축이 있는지는 카탈로그가 선언한다(이 파일은 도메인 모양을 알지 않는다). */ const { TWIN_AXES, axisSource } = require('@operato/ops-contract') /** `productionSpec.definition.recipes` 같은 경로를 문서에서 따라간다. */ export const at = (o: any, path: string) => path.split('.').reduce((a: any, k: string) => (a == null ? undefined : a[k]), o) /** * 문서 항목의 **식별자** — 축마다 이름이 다르다(자리는 `id`, 공정은 `key`, 레시피도 `key`). * * 도메인 모양을 코드가 아는 것이 아니라 **식별자 관례**를 아는 것이다. 후보를 순서대로 보고 첫 * 값을 쓴다. 하나도 없으면 그 항목은 가리킬 수 없으므로 `null` — 목록에서 빼지 않고 **id 없음**으로 * 낸다(오류 없이 빠지면 건수가 안 맞는다). */ export const idOf = (o: any, info?: { idField?: string | string[] }): string | null => { if (typeof o === 'string') return o /* * **무엇이 이 항목을 가리키는지는 선언이 말한다**(§`TwinAxisInfo.idField`). * * 예전에는 `id`·`key`·`gtin` 을 짐작했다. 그래서 `id` 가 없는 축(수요 구간 — 정체성이 구간 시작 * 시각이다)의 항목이 전부 「식별자 없음 — 참조할 수 없는 항목」으로 보였다. 값은 실재하는데 * 가리킬 수 없다고 말한 것이다. */ /* * 선언이 **차례**를 말할 수 있다(2026-08-22) — 정체성이 한 칸으로 정해지지 않는 축이 있다. 물품이 * 그렇다: 직렬 물품은 `epc` 가 유일하지만 비직렬 로트가 자리마다 나뉘면 `subLotId` 가 개체를 * 구별한다(같은 로트의 두 부분은 같은 `epc` 를 갖는다). 앞에서 값이 있는 첫 칸이 이긴다. */ for (const f of info?.idField ? (Array.isArray(info.idField) ? info.idField : [info.idField]) : []) { const v = o?.[f] if (v !== undefined && v !== null) return String(v) } return o?.id ?? o?.key ?? o?.gtin ?? null } /** * **id → 이름**, 모든 축을 한 표로. * * ── 왜 이 표가 필요한가 ────────────────────────────────────────────────────── * 행은 다른 것을 **id 로** 가리킨다(사람의 `personnelClassIds`, 설비의 `homeLocationId`, 공정의 * `locationType`). 그 id 의 이름은 **가리켜진 축의 행**에 있으므로, 화면이 그것을 알려면 축을 * 하나씩 더 읽어야 한다 — 그래서 지금까지 표면마다 그냥 id 를 찍었다(`cutter-op`, `cut-station`). * 표면을 하나씩 고치는 것은 끝이 없다: 새 화면은 처음부터 다시 id 를 찍는다. * * 그래서 **서버가 붙인다.** 서버는 이미 모델을 손에 들고 있고, 어느 축이 있는지는 커널 카탈로그가 * 선언한다(`TWIN_AXES`) — 도메인이 늘어도 이 함수는 그대로다. * * 값은 그 항목이 **자기 이름이라고 말한 것**이다(`name`·`label`·`description`). 그중 어떤 것은 * i18n 키이므로(등급의 `description`) 번역은 화면의 일로 남긴다. 이름이 없으면 표에 넣지 않는다 — * 없는 것을 지어내지 않고, 그 경우 화면은 id 를 그대로 보인다. */ export function nameIndex(model: any): Record { const out: Record = {} for (const info of TWIN_AXES as any[]) { /* 관측 축(상태·저널)은 문서에 없다 — 경로가 없는 축을 읽으면 모델 루트를 훑는다. */ if (axisSource(info.axis) !== 'document') continue const raw = at(model ?? {}, info.path) const list = Array.isArray(raw) ? raw : raw ? [raw] : [] for (const o of list) { const id = idOf(o) if (!id || typeof o !== 'object') continue const label = o.name ?? o.label ?? o.description /* 이름이 id 와 같으면 표에 넣지 않는다 — 같은 값을 두 번 보이는 셈이다. */ if (typeof label === 'string' && label && label !== id && !out[id]) out[id] = label } } return out } /** * 이 응답이 **실제로 가리킨 것만** 골라 낸다. * * 모델 전체 표를 그대로 실으면 자리가 10만인 현장에서 목록 한 페이지에 10만 줄짜리 이름표가 * 따라온다. 나온 값에 걸리는 것만 싣는다 — 규모에서 성립하는 유일한 방식이다. */ export function namesFor(rows: any[], index: Record): Record { const out: Record = {} const take = (v: any, depth: number): void => { if (v == null || depth > 4) return if (typeof v === 'string') { if (index[v]) out[v] = index[v]; return } if (Array.isArray(v)) { for (const x of v) take(x, depth + 1); return } if (typeof v === 'object') for (const x of Object.values(v)) take(x, depth + 1) } for (const r of rows) { take(r?.fields ?? r, 0); if (typeof r?.id === 'string' && index[r.id]) out[r.id] = index[r.id] } return out }