/* ============================================================================= * B02_ProjRegister_UI_Name.ts * 프로젝트명 조합기 — 사업연도·사업지역·임도종류를 이름 칸에 넣어 두고, 사용자가 그 글을 * 고쳐도 위 항목과의 연결이 끊기지 않게 지킨다 (2026-09-06 사용자 지시). * * 글자마다 「누가 넣은 글자인가」를 같이 들고 다닌다. 문자열을 찾아 바꾸던 종전 방식은 * 사용자가 자동 글자 사이에 한 글자만 끼워 넣어도 찾기가 실패해 연동이 통째로 끊겼다 * (실측: 사업지역 「울진군 금강송면」의 「금강송면」 앞에 글자 삽입). * * 규칙 * - 사용자가 이름을 고치기 전에는 위 세 항목으로 통째로 다시 쓴다. * - 고친 뒤에는 **그 항목이 차지한 구간만** 새 값으로 갈아 끼운다. 사용자가 그 구간 * 밖(사이·끝)에 넣은 글은 그대로 남는다. * - 사용자가 지워 버린 항목은 되살리지 않는다 — 지운 것도 사용자의 뜻이다. * ========================================================================== */ /** 이름을 이루는 자동 조각의 종류. 순서가 곧 이름에 놓이는 차례다. */ export type NameSlot = "year" | "region" | "type"; const SLOTS: readonly NameSlot[] = ["year", "region", "type"]; export type NameParts = Record; export interface NameComposer { /** 지금 이름. */ text(): string; /** 사용자가 이름 칸에 친 글을 반영한다. */ edit(next: string): void; /** 위 항목이 바뀌었을 때 — 갈아 끼운 이름을 돌려준다. */ update(parts: NameParts): string; } export function createNameComposer(): NameComposer { let text = ""; /** 글자마다 그 글자를 넣은 항목(사용자가 친 글자는 null). `text`와 길이가 같다. */ let owners: (NameSlot | null)[] = []; let touched = false; const fill = (slot: NameSlot | null, count: number): (NameSlot | null)[] => Array.from({ length: count }, () => slot); const rebuild = (parts: NameParts): void => { text = ""; owners = []; for (const slot of SLOTS) { const value = parts[slot].trim(); if (!value) continue; if (text.length > 0) { text += " "; owners.push(null); } text += value; owners.push(...fill(slot, value.length)); } }; return { text: () => text, edit(next: string): void { touched = true; // 앞뒤로 그대로인 부분을 뺀 **바뀐 구간**만 계산한다 — 어디를 고쳤는지 알아야 // 나머지 글자의 주인이 밀리지 않는다. let head = 0; while (head < text.length && head < next.length && text[head] === next[head]) head += 1; let tail = 0; while ( tail < text.length - head && tail < next.length - head && text[text.length - 1 - tail] === next[next.length - 1 - tail] ) { tail += 1; } const removed = text.length - tail - head; const inserted = next.slice(head, next.length - tail); owners.splice(head, removed, ...fill(null, inserted.length)); text = next; }, update(parts: NameParts): string { if (!touched) { rebuild(parts); return text; } for (const slot of SLOTS) { const first = owners.indexOf(slot); if (first < 0) continue; // 사용자가 지운 항목은 되살리지 않는다. const last = owners.lastIndexOf(slot); const value = parts[slot].trim(); text = text.slice(0, first) + value + text.slice(last + 1); owners.splice(first, last - first + 1, ...fill(slot, value.length)); } return text; }, }; }