지시 1 — 사이드 「종단 설계 기준」 컨테이너를 기본 접힘으로. section()에
collapsed 인자를 더해 초기 is-collapsed만 붙이고 토글은 전역 collapsible이
그대로 맡는다(B06 buildGroup과 같은 패턴).
지시 4 — 배수관 유입·유출 바닥 보호를 "보호공" 한 축으로 개편:
- 구 돌붙임 있음/없음 + 표면처리(찰/메) 두 축 → 돌붙임(찰)/돌붙임(메)/도수로.
도수로·산비탈수로(B4)는 유출부에서 돌붙임 대신 쓰는 공종이라 별도 구조물이
아니라 이 선택지로 받는다(별표2 유출구~원지반 도달 의무)
- "없음" 삭제 — 물이 흐르는 자리라 보호공은 필수(사용자 확정). 기본값은
유입 돌붙임(찰)·유출 돌붙임(메)
- 치수는 배타 — 돌붙임이면 면적(㎡, 기본 10), 도수로면 폭(m, 기본 1.0)
- 저장 키 개명 *_pitching → *_protection, *_pitching_finish 삭제,
*_protection_width_m 신설. 독립 기슭막이(D4)도 같은 축
- 구 저장분은 parse_pipe_points가 읽는 순간 새 키로 옮긴다("없음"은 부위
기본값으로 승격). 폼 write()도 구 키를 폴백으로 읽어 structures.json 몫을 덮는다
- revetment.pitching_area_m2가 기본값과 required를 동시에 갖던 모순도 정리
지시 5 — 700줄 초과 파일 분리(순수 이동 + import 정리, 공개 인터페이스 불변):
- Drainage_Facility 724 → 401 (+_Facility_Fields 354)
- Structures_Panel 753 → 695 (+_Structures_List 113)
- Drainage_Panel 822 → 693 (+_Drainage_Chrome 150, +_Drainage_Interact 159)
- Page 1031 → 911 (+_Page_Helpers 144) — 아직 초과
- Profile_Panel 1152 — 미착수. 두 파일은 가변 클로저 상태를 곳곳에서 직접
읽고 써 상태 승격이 선행돼야 하며, 프론트 테스트가 없어 순수 이동으로는
떼어낼 수 없다. 분리안은 PLAN.md 3-3에 기록
검증: pytest tmp/tests 107 passed·7 skipped(신규 보호공 마이그레이션 6건 포함),
npm run typecheck 무오류, npm run build 성공(374 modules).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
113 lines
4.9 KiB
TypeScript
113 lines
4.9 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;
|
|
/** 설치측 표시 이름 — 패널과 같은 목록을 쓴다. */
|
|
sideLabels: ReadonlyArray<[string, string]>;
|
|
/** 지금 폼에 올라와 있는 구조물(없으면 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);
|
|
const sideLabel = params.sideLabels.find(([value]) => value === structure.side)?.[1] ?? "";
|
|
info.textContent = [sideLabel, structure.memo].filter(Boolean).join(" · ");
|
|
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);
|
|
});
|
|
}
|