Files
Aislo/B05_Profile/B05_Profile_UI_Structures_List.ts
T
eomsangdon b92ba4592e refactor(B05): 구조물 설치측·이격 입력 폐지 — 데이터 필드까지 제거
사용자 지시로 두 값을 걷어낸다. 구조물별 옵션·정의는 앞으로 타입마다
따로 정한다. 어차피 소비처가 없어(B06~B08은 structures를 읽지 않는다)
화면에만 남아 있던 값이었다.

- 폼: 설치측 select·이격 칸 행 삭제, 목록 부제·종단 벌룬에서 측 표기 제거
- 계약: StructureSide 타입, StructureInstance.side/offset_m 삭제(프론트·스키마)
- 설계지문에서 두 값 제외 — 후속 단계 무효화 판정에서 빠진다
- 구 저장분은 읽을 때 두 키만 떨어낸다. extra="forbid"라 그냥 두면 정본이
  통째로 버려진다. 다른 낯선 키는 계속 거절한다
2026-08-17 20:04:32 +09:00

114 lines
4.7 KiB
TypeScript

/* =============================================================================
* B05_Profile_UI_Structures_List.ts
* 「구조물 배치」 폼 아래 목록 — 구조물 정본(structures)과 계곡 통과 시설 정본
* (pipe_points)을 기준점 오름차순으로 하나의 목록에 병합해 그린다.
*
* 패널 본체(B05_Profile_UI_Structures_Panel)가 700줄 한계에 닿아 분리했다.
* 목록은 상태를 갖지 않는다 — 그릴 때마다 현재 목록·선택 상태를 인자로 받고,
* 항목을 누르면 넘겨받은 콜백으로 폼 로드를 되돌려 준다.
* ========================================================================== */
import {
structureAnchorM,
type StructureInstance,
type StructureType,
} from "./B05_Profile_Api_Structures";
import type { PipeFacilityItem } from "./B05_Profile_UI_Structures_Panel";
import { formatStation } from "./B05_Profile_Util_Station";
export interface StructureListParams {
/** 목록을 담을 <ul>. 그릴 때마다 통째로 갈아 끼운다. */
list: HTMLElement;
structures: ReadonlyArray<StructureInstance>;
pipeFacilities: ReadonlyArray<PipeFacilityItem>;
/** 타입 이름을 찾을 레지스트리 사전. */
typeMap: Map<string, StructureType>;
/** 측점 표기용 측점 간격(m). */
intervalM: number;
/** 지금 폼에 올라와 있는 구조물(없으면 null). */
editingId: string | null;
/** 지금 폼에 올라와 있는 계곡 통과 시설의 누가거리(없으면 null). */
selectedPipeChainage: number | null;
onSelectStructure: (structure: StructureInstance) => void;
onSelectPipe: (pipe: PipeFacilityItem) => void;
}
/** 구조물 한 건의 표시 이름 — 점형은 기준 측점, 구간형은 시작~종료 측점. */
export function labelOfStructure(
structure: StructureInstance,
typeMap: Map<string, StructureType>,
intervalM: number,
): string {
const name = typeMap.get(structure.type_id)?.name ?? structure.type_id;
const position =
structure.placement === "interval"
? `${formatStation(structure.start_m ?? 0, intervalM)}~${formatStation(structure.end_m ?? 0, intervalM)}`
: formatStation(structure.chainage_m ?? 0, intervalM);
return `${position} · ${name}`;
}
/** 계곡 통과 시설 한 건의 표시 이름 — 점형이라 기준 측점 하나로 적는다
* (2026-08-17 사용자 지시 2). */
export function labelOfPipe(
pipe: PipeFacilityItem,
typeMap: Map<string, StructureType>,
intervalM: number,
): string {
const name = typeMap.get(pipe.facility)?.name ?? pipe.facility;
return `${formatStation(pipe.chainage_m, intervalM)} · ${name}`;
}
export function renderStructureList(params: StructureListParams): void {
const { list, typeMap, intervalM, editingId, selectedPipeChainage } = params;
list.replaceChildren();
// 두 정본을 하나의 목록으로 — 기준점 오름차순 병합 (2026-08-17 통합 표시).
const rows: Array<
| { kind: "structure"; anchor: number; structure: StructureInstance }
| { kind: "pipe"; anchor: number; pipe: PipeFacilityItem }
> = [
...params.structures.map((structure) => ({
kind: "structure" as const,
anchor: structureAnchorM(structure),
structure,
})),
...params.pipeFacilities.map((pipe) => ({
kind: "pipe" as const,
anchor: pipe.chainage_m,
pipe,
})),
].sort((left, right) => left.anchor - right.anchor);
if (!rows.length) {
const empty = document.createElement("li");
empty.className = "b05-route__irregular-empty";
empty.textContent = "배치된 구조물이 없습니다.";
list.append(empty);
return;
}
rows.forEach((row) => {
const item = document.createElement("li");
item.className = "b05-route__irregular-item";
const name = document.createElement("strong");
const info = document.createElement("span");
if (row.kind === "structure") {
const { structure } = row;
item.classList.toggle("is-selected", structure.structure_id === editingId);
name.textContent = labelOfStructure(structure, typeMap, intervalM);
info.textContent = structure.memo ?? "";
item.addEventListener("click", () => params.onSelectStructure(structure));
if (structure.structure_id === editingId) item.scrollIntoView({ block: "nearest" });
} else {
const { pipe } = row;
item.classList.toggle(
"is-selected",
selectedPipeChainage !== null && Math.abs(selectedPipeChainage - pipe.chainage_m) < 0.05,
);
name.textContent = labelOfPipe(pipe, typeMap, intervalM);
info.textContent = "배수유역 연동";
item.addEventListener("click", () => params.onSelectPipe(pipe));
}
item.append(name, info);
list.append(item);
});
}