Files
Aislo/B02_ProjRegister/B02_ProjRegister_UI_Name.ts
T
eomsangdonandClaude Opus 5 4b5a630f12 fix(B02·A00): 프로젝트명 연동 끊김·항목 순서·공용 템플릿·헤더 버튼 간격
- 프로젝트명 조합기를 글자 단위 소유 추적으로 다시 만듦(`B02_ProjRegister_UI_Name.ts`) —
  자동 글자 사이에 글을 끼워 넣어도 사업연도·사업지역·임도종류 연동이 끊기지 않음
  (실측: 「울진군 금강송면」의 「금강송면」 앞 삽입 후에도 지역·종류·연도 변경이 반영)
- 비고 칸을 맨 마지막으로 이동
- 제목·여백·버튼 줄을 공용 템플릿(`createGeneralLayout` · `ui-general-block__actions`)으로 전환,
  화면 전용 제목 스타일 제거
- 헤더의 사용자명·로그아웃(로그인·회원가입) 버튼이 붙어 보이던 것 간격 부여

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 17:59:22 +09:00

97 lines
3.7 KiB
TypeScript

/* =============================================================================
* 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<NameSlot, string>;
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;
},
};
}