/* * 구조 이력의 **차이** — "2번째에 도장 부스가 넷 늘었다" 를 말할 수 있게. * * 리비전은 그 시절 공장을 통째로 담는다(재생하려면 전체 위상이 필요하다). 그런데 사람이 보고 싶은 * 것은 통째가 아니라 **무엇이 달라졌나** 다 — 62KB 짜리 모델 두 개를 나란히 놓고 눈으로 비교할 수는 * 없다. 그 한 줄을 여기서 만든다. * * 순수 함수인 이유: 같은 문장을 화면과 AI 가 함께 써야 한다. 한쪽에서만 계산하면 둘이 다른 말을 한다. */ /** 종류별 증감 — "paint-booth +4" 처럼 읽힌다. */ export interface StructureCountChange { key: string before: number after: number delta: number } export interface StructureDiff { /** 자리 종류별 증감(0 인 것은 빼고). */ locationTypes: StructureCountChange[] /** 설비 종류별 증감(0 인 것은 빼고). */ equipmentKinds: StructureCountChange[] locationsAdded: number locationsRemoved: number equipmentAdded: number equipmentRemoved: number /** 개수는 같은데 내용이 바뀐 경우(용량 조정·자리 이동 등)가 있는가. */ changedInPlace: boolean } const countBy = (rows: readonly T[] | undefined, key: (r: T) => string | undefined): Map => { const out = new Map() for (const r of rows ?? []) { const k = key(r) if (k) out.set(k, (out.get(k) ?? 0) + 1) } return out } const idsOf = (rows: readonly { id?: string }[] | undefined): Set => new Set((rows ?? []).map(r => r.id).filter((x): x is string => !!x)) function changes(before: Map, after: Map): StructureCountChange[] { const keys = [...new Set([...before.keys(), ...after.keys()])].sort() return keys .map(key => ({ key, before: before.get(key) ?? 0, after: after.get(key) ?? 0, delta: (after.get(key) ?? 0) - (before.get(key) ?? 0) })) .filter(c => c.delta !== 0) } /** * 두 구조의 차이. `before` 가 없으면 **최초 리비전**이라 비교 대상이 없다 — 전부 새로 생긴 것으로 센다. * * 아이디는 **집합으로** 비교한다(수만 세면 넷이 사라지고 넷이 생긴 교체를 "변화 없음" 으로 답한다). */ export function diffStructures(before: any | undefined, after: any): StructureDiff { const beforeLocations = before?.locations ?? [] const beforeEquipment = before?.equipment ?? [] const afterLocations = after?.locations ?? [] const afterEquipment = after?.equipment ?? [] const beforeLocationIds = idsOf(beforeLocations) const afterLocationIds = idsOf(afterLocations) const beforeEquipmentIds = idsOf(beforeEquipment) const afterEquipmentIds = idsOf(afterEquipment) const added = (a: Set, b: Set) => [...b].filter(x => !a.has(x)).length const locationsAdded = added(beforeLocationIds, afterLocationIds) const locationsRemoved = added(afterLocationIds, beforeLocationIds) const equipmentAdded = added(beforeEquipmentIds, afterEquipmentIds) const equipmentRemoved = added(afterEquipmentIds, beforeEquipmentIds) /* * 아이디는 그대로인데 속성이 바뀐 경우 — 용량을 늘렸거나 설비를 옮겼다. 지문이 달라 새 리비전이 * 생겼는데 증감이 전부 0 이면, 화면이 "바뀐 게 없다" 고 답하게 된다. 그런 침묵을 막는다. */ const changedInPlace = !locationsAdded && !locationsRemoved && !equipmentAdded && !equipmentRemoved && !!before return { locationTypes: changes(countBy(beforeLocations, (n: any) => n.type), countBy(afterLocations, (n: any) => n.type)), equipmentKinds: changes(countBy(beforeEquipment, (m: any) => m.kind), countBy(afterEquipment, (m: any) => m.kind)), locationsAdded, locationsRemoved, equipmentAdded, equipmentRemoved, changedInPlace } }