feat(B05·B06): 구조물 좌측 패널 「집계 항목」 줄 · 옵션 보이기 · [수정] 옵션 보존 · 목록 지정 색(21-4)

- 폼 옵션 아래 「집계 항목」 줄(이름 · 미지정 · [고르기] · [해제]) — 구조물 · 관 계열 하위 폼 · B05 · B06 한 코드
- 옵션 `visible:false` 는 폼에서 숨김(정의 · 옛 값 그대로) · [수정]은 폼이 그린 칸만 갈아 끼우고 안 그린 칸(상세 · 숨긴 칸) 보존 — 옵션 덮어쓰기 흠 고침
- 목록 줄 `.is-assigned` 색 · 툴팁 항목 이름 · 캐시는 지금 초안 기록 칸(structures · pipes) 안 summary_item
- 미지정이면 칸을 안 실음 · 관은 서버 응답에 칸이 없으면 같은 측점에서 화면이 이어받음
- 항목 고르기 모달은 21-5 가 오기 전까지 같은 이름의 임시 함수 · Structures_Panel 700줄 넘어 옵션 그리기를 Panel_Options 로 나눔

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TULoa94ZFL26KU6ZqVpjkF
This commit is contained in:
2026-09-29 12:19:31 +09:00
co-authored by Claude Sonnet 5.5
parent 9d85ce8b4a
commit 71fac35dab
23 changed files with 1018 additions and 104 deletions
@@ -17,6 +17,8 @@ import type { PipeFacility } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
import type { FacilityAttributes, FacilityOptionsForm } from "./B05_Profile_UI_Drainage_Facility";
import type { StationFields } from "./B05_Profile_UI_Structures_Fields";
import { splitPavementRange } from "./B05_Profile_UI_Structures_Pavement";
import { mergeStructureOptions } from "./B05_Profile_UI_Structures_Summary";
import { readSummaryItem, type SummaryItem } from "../common_util/common_util_summary_item";
import type {
PipeFacilityItem,
StructuresCallbacks,
@@ -64,6 +66,9 @@ export interface StructuresCommitContext {
setPositionConfirmed: (value: boolean) => void;
tempPipeChainage: () => number | null;
setTempPipeChainage: (value: number | null) => void;
/** 폼의 「집계 항목」 값(PLAN 21-4) — 저장은 옵션 밖 `summary_item` 칸으로 나간다. */
summary: () => SummaryItem | null;
setSummary: (value: SummaryItem | null) => void;
}
export function loadForm(ctx: StructuresCommitContext, target: StructureInstance | null): void {
@@ -85,6 +90,7 @@ export function loadForm(ctx: StructuresCommitContext, target: StructureInstance
ctx.endFields.write(target.end_m ?? null, step);
ctx.memoField.value = target.memo ?? "";
ctx.renderOptionFields(target.options);
ctx.setSummary(readSummaryItem(target.summary_item));
// 구간 직접 입력 시절에 저장된 항목 호환 — 길이·전길이 옵션이 없으면 저장된
// 시작~종료·기준점에서 역산해 채워 범위 표시·재저장이 이어지게 한다(2026-08-19).
if (target.start_m != null && target.end_m != null && target.end_m > target.start_m) {
@@ -112,6 +118,7 @@ export function loadForm(ctx: StructuresCommitContext, target: StructureInstance
ctx.endFields.write(null, step);
ctx.memoField.value = "";
ctx.renderOptionFields();
ctx.setSummary(null);
ctx.syncFacilityForm();
}
[ctx.anchorFields, ctx.startFields, ctx.endFields].forEach((fields) => fields.clearInvalid());
@@ -158,6 +165,7 @@ export function loadPipeForm(ctx: StructuresCommitContext, pipe: PipeFacilityIte
ctx.endFields.write(null, step);
ctx.memoField.value = "";
ctx.syncFacilityForm(pipe.options ?? {}, pipe.design_flow_m3s ?? null);
ctx.setSummary(readSummaryItem(pipe.summary_item));
[ctx.anchorFields, ctx.startFields, ctx.endFields].forEach((fields) => fields.clearInvalid());
ctx.syncPlacementFields();
ctx.syncButtons();
@@ -199,6 +207,8 @@ export async function commit(ctx: StructuresCommitContext, live = false): Promis
const attributes: FacilityAttributes = { facility: type.type_id as PipeFacility };
const options = ctx.facilityOptions.readOptions();
if (Object.keys(options).length) attributes.options = options;
// 집계 항목 — 미지정(null)도 그대로 실어 옛 지정을 풀 수 있게 한다.
attributes.summary_item = ctx.summary();
if (ctx.tempPipeChainage() !== null) {
// 임시 배치 확정([추가]) — 관은 이미 미리보기로 들어가 있으니 옵션·위치만 반영.
const from = ctx.tempPipeChainage() as number;
@@ -271,6 +281,7 @@ export async function commit(ctx: StructuresCommitContext, live = false): Promis
return;
}
const summary = ctx.summary();
const base = {
type_id: type.type_id,
placement,
@@ -278,6 +289,7 @@ export async function commit(ctx: StructuresCommitContext, live = false): Promis
start_m: start,
end_m: end,
options: readOptions(ctx),
...(summary ? { summary_item: summary } : {}),
memo: ctx.memoField.value.trim(),
placement_source: "manual" as const,
status: "draft" as const,
@@ -304,7 +316,23 @@ export async function commit(ctx: StructuresCommitContext, live = false): Promis
if (ctx.editingId()) {
const index = ctx.structures.findIndex((entry) => entry.structure_id === ctx.editingId());
if (index >= 0) ctx.structures[index] = { ...ctx.structures[index], ...records[0] };
if (index >= 0) {
// [수정] = 폼이 그린 칸만 갈아 끼우고 나머지(안 보이는 칸 · 상세 칸)는 옛 값 보존.
const previous = ctx.structures[index];
const next: StructureInstance = {
...previous,
...records[0],
options: mergeStructureOptions(
previous,
type.type_id,
ctx.optionInputs.map((entry) => entry.key),
records[0].options,
),
};
// 해제 = 칸을 지움(옛 지정이 `...previous` 로 살아 오지 않게)
if (!summary) delete next.summary_item;
ctx.structures[index] = next;
}
records.slice(1).forEach((record) => ctx.structures.push({ ...record, structure_id: null }));
} else {
records.forEach((record) => ctx.structures.push({ ...record, structure_id: null }));