- 프로젝트명 조합기를 글자 단위 소유 추적으로 다시 만듦(`B02_ProjRegister_UI_Name.ts`) — 자동 글자 사이에 글을 끼워 넣어도 사업연도·사업지역·임도종류 연동이 끊기지 않음 (실측: 「울진군 금강송면」의 「금강송면」 앞 삽입 후에도 지역·종류·연도 변경이 반영) - 비고 칸을 맨 마지막으로 이동 - 제목·여백·버튼 줄을 공용 템플릿(`createGeneralLayout` · `ui-general-block__actions`)으로 전환, 화면 전용 제목 스타일 제거 - 헤더의 사용자명·로그아웃(로그인·회원가입) 버튼이 붙어 보이던 것 간격 부여 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
97 lines
3.7 KiB
TypeScript
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;
|
|
},
|
|
};
|
|
}
|