/* ============================================================================= * common_util_summary_item.ts * 구조물 집계표 항목 참조(PLAN 21-1) — 구조물 · 관 지점이 「어느 집계표 항목」인지 가리키는 값. * * summary_item: { layer: "system"|"company"|"personal", owner: null|"{회사}"|"{회사}/{사용자}", * key: "SS000004", name: "돌기슭막이 메쌓기 1.2m" } * * 없으면 null(= 미지정 · 집계표에 안 들어감). 옵션(options) 밖의 칸이다. * import 없음 — 시험이 이 파일만 옮겨 돌림. * ========================================================================== */ export type SummaryLayer = "system" | "company" | "personal"; export interface SummaryItem { layer: SummaryLayer; owner: string | null; key: string; name: string; } const LAYERS: readonly string[] = ["system", "company", "personal"]; /** 참조 모양이 맞는가 — 서버 응답 · 세션 초안을 읽을 때 걸러 쓴다. */ export function isSummaryItem(value: unknown): value is SummaryItem { if (!value || typeof value !== "object") return false; const item = value as Record; return ( typeof item.layer === "string" && LAYERS.includes(item.layer) && (item.owner === null || typeof item.owner === "string") && typeof item.key === "string" && item.key !== "" && typeof item.name === "string" ); } /** 같은 항목인가 — 층 · 주인 · 키가 같으면 같음(이름은 바뀔 수 있어 안 봄). */ export function sameSummaryItem(left: SummaryItem | null, right: SummaryItem | null): boolean { if (!left || !right) return left === right; return left.layer === right.layer && left.owner === right.owner && left.key === right.key; } /** 서버 · 세션에서 온 값을 참조 또는 null 로 — 모양이 틀리면 null(미지정). */ export function readSummaryItem(value: unknown): SummaryItem | null { return isSummaryItem(value) ? value : null; } /** 옵션 그룹(M02 PLAN 46)마다 고른 항목 `{그룹 id: 참조}` — 본체는 `summary_item`. */ export type GroupItems = Record; /** 그룹 복사본 id 잇개 — `{구조물 id}__{그룹 id}`(서버 `GROUP_JOIN` 과 짝). */ export const GROUP_JOIN = "__"; /** 서버 · 세션 값을 그룹 참조 묶음으로 — 모양이 틀린 항목은 뺌 · 없으면 빈 묶음. */ export function readGroupItems(value: unknown): GroupItems { if (!value || typeof value !== "object" || Array.isArray(value)) return {}; return Object.fromEntries( Object.entries(value as Record).filter( (entry): entry is [string, SummaryItem] => /^g[0-9a-z]{1,16}$/.test(entry[0]) && isSummaryItem(entry[1]), ), ); } /** 저장 · 이음에 실을 연결 칸 — 지정한 것만(본체 `summary_item` · 그룹 `group_items`). */ export function linkFields(source: { summary_item?: unknown; group_items?: unknown }): { summary_item?: SummaryItem; group_items?: GroupItems; } { const summary = readSummaryItem(source.summary_item); const groups = readGroupItems(source.group_items); return { ...(summary ? { summary_item: summary } : {}), ...(Object.keys(groups).length ? { group_items: groups } : {}), }; } /** 연결이 하나라도 있나 — 본체 또는 그룹. */ export function hasLinks(source: { summary_item?: unknown; group_items?: unknown }): boolean { return Object.keys(linkFields(source)).length > 0; } /** 표시 글 — 이름(없으면 「미지정」). 키는 앞에 내세우지 않는다(PLAN 21-0). */ export function summaryLabel(item: SummaryItem | null): string { return item ? item.name || item.key : "미지정"; } /** 툴팁 — 이름 + 어느 그룹 것인지 · 키는 작은 글로만. */ export function summaryTooltip(item: SummaryItem | null): string { if (!item) return "집계 항목 미지정 — 집계표에 안 들어감"; const group = { system: "마스터", company: "회사용", personal: "개인용" }[item.layer]; return `${item.name} · ${group} · ${item.key}`; }