feat(B05·B06·M02): 46-3·46-4 옵션 그룹 보조 컨테이너 — 구조물 하나 · 조합 여럿

- 구조물 기록 · 관 지점 기록에 group_items {그룹 id: summary_item} 칸(검사 parse_group_items)
- 폼: 종류의 그룹마다 상자(제목 = 그룹 이름) · 그 그룹 옵션 칸을 옮겨 담음 · 「집계 항목」 [고르기][해제] · 빈 그룹도 상자
- 정의가 사라진 그룹 연결 = 값 보존 · 경고 한 줄
- 복사본 = 그룹 연결마다 {구조물 id}__{그룹 id} 폴더(관 pipe@측점__그룹)
- 집계표 채움 = 정의된 그룹마다 한 건 더(그 그룹이 고른 열) · 미지정 그룹은 「미지정」 알림에 셈
- B06 관 초안 · B05 시설 보관소 · 병합도 그룹 칸을 이음
- 시험 test_structure_group_items.py · 코드 글자를 보던 시험 셋은 바뀐 자리로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPcuokNm1ZR2kqgGGDfaJA
This commit is contained in:
2026-10-05 13:39:12 +09:00
co-authored by Claude Opus 5.5
parent 37634e2c26
commit 15e08b28cb
27 changed files with 608 additions and 59 deletions
@@ -18,7 +18,12 @@ import type { FacilityAttributes, FacilityOptionsForm } from "./B05_Profile_UI_D
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 {
readGroupItems,
readSummaryItem,
type GroupItems,
type SummaryItem,
} from "../common_util/common_util_summary_item";
import type {
PipeFacilityItem,
StructuresCallbacks,
@@ -69,6 +74,15 @@ export interface StructuresCommitContext {
/** 폼의 「집계 항목」 값(PLAN 21-4) — 저장은 옵션 밖 `summary_item` 칸으로 나간다. */
summary: () => SummaryItem | null;
setSummary: (value: SummaryItem | null) => void;
/** 옵션 그룹 보조 컨테이너마다 고른 항목(PLAN 46-3) — 저장은 `group_items` 칸. */
groupItems: () => GroupItems;
setGroupItems: (value: GroupItems) => void;
}
/** 저장할 그룹 연결 — 고른 것이 있을 때만 칸을 실음(빈 묶음 = 칸 없음). */
function groupField(ctx: StructuresCommitContext): { group_items?: GroupItems } {
const items = ctx.groupItems();
return Object.keys(items).length ? { group_items: items } : {};
}
export function loadForm(ctx: StructuresCommitContext, target: StructureInstance | null): void {
@@ -91,6 +105,7 @@ export function loadForm(ctx: StructuresCommitContext, target: StructureInstance
ctx.memoField.value = target.memo ?? "";
ctx.renderOptionFields(target.options);
ctx.setSummary(readSummaryItem(target.summary_item));
ctx.setGroupItems(readGroupItems(target.group_items));
// 구간 직접 입력 시절에 저장된 항목 호환 — 길이·전길이 옵션이 없으면 저장된
// 시작~종료·기준점에서 역산해 채워 범위 표시·재저장이 이어지게 한다(2026-08-19).
if (target.start_m != null && target.end_m != null && target.end_m > target.start_m) {
@@ -119,6 +134,7 @@ export function loadForm(ctx: StructuresCommitContext, target: StructureInstance
ctx.memoField.value = "";
ctx.renderOptionFields();
ctx.setSummary(null);
ctx.setGroupItems({});
ctx.syncFacilityForm();
}
[ctx.anchorFields, ctx.startFields, ctx.endFields].forEach((fields) => fields.clearInvalid());
@@ -166,6 +182,7 @@ export function loadPipeForm(ctx: StructuresCommitContext, pipe: PipeFacilityIte
ctx.memoField.value = "";
ctx.syncFacilityForm(pipe.options ?? {}, pipe.design_flow_m3s ?? null);
ctx.setSummary(readSummaryItem(pipe.summary_item));
ctx.setGroupItems(readGroupItems(pipe.group_items));
[ctx.anchorFields, ctx.startFields, ctx.endFields].forEach((fields) => fields.clearInvalid());
ctx.syncPlacementFields();
ctx.syncButtons();
@@ -201,6 +218,9 @@ export function commitSummaryOnly(ctx: StructuresCommitContext): boolean {
const summary = ctx.summary();
if (summary) next.summary_item = summary;
else delete next.summary_item;
const groups = groupField(ctx);
if (groups.group_items) next.group_items = groups.group_items;
else delete next.group_items;
ctx.structures[index] = next;
emit(ctx);
return true;
@@ -225,6 +245,7 @@ export async function commit(ctx: StructuresCommitContext, live = false): Promis
if (Object.keys(options).length) attributes.options = options;
// 집계 항목 — 미지정(null)도 그대로 실어 옛 지정을 풀 수 있게 한다.
attributes.summary_item = ctx.summary();
attributes.group_items = groupField(ctx).group_items ?? null; // 빈 것도 실어 옛 지정을 풂
if (ctx.tempPipeChainage() !== null) {
// 임시 배치 확정([추가]) — 관은 이미 미리보기로 들어가 있으니 옵션·위치만 반영.
const from = ctx.tempPipeChainage() as number;
@@ -306,6 +327,7 @@ export async function commit(ctx: StructuresCommitContext, live = false): Promis
end_m: end,
options: readOptions(ctx),
...(summary ? { summary_item: summary } : {}),
...groupField(ctx),
memo: ctx.memoField.value.trim(),
placement_source: "manual" as const,
status: "draft" as const,
@@ -347,6 +369,7 @@ export async function commit(ctx: StructuresCommitContext, live = false): Promis
};
// 해제 = 칸을 지움(옛 지정이 `...previous` 로 살아 오지 않게)
if (!summary) delete next.summary_item;
if (!groupField(ctx).group_items) delete next.group_items;
ctx.structures[index] = next;
}
records.slice(1).forEach((record) => ctx.structures.push({ ...record, structure_id: null }));