auto: 2026-08-17 17:09 (ESD_LAPTOP)

This commit is contained in:
2026-08-17 17:09:05 +09:00
parent 38ea71e15b
commit 3e9be53e7c
15 changed files with 1028 additions and 524 deletions
+58 -53
View File
@@ -130,31 +130,38 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
memoField.type = "text";
memoField.placeholder = "메모(선택)";
// 측점은 3행 — 한 행이 [라벨][측점번호][잔여거리](2026-08-17 사용자 지시 2).
const positionRow = document.createElement("div");
positionRow.className = "b05-route__irregular-row";
positionRow.className = "b05-structure__position-row";
positionRow.append(startFields.wrap, anchorFields.wrap, endFields.wrap);
// 기본 인터페이스는 2열 격자(2026-08-17 사용자 지시 1).
const typeRow = document.createElement("div");
typeRow.className = "b05-structure__grid";
typeRow.append(field("구조물군", groupSelect), field("종류", typeSelect));
const sideRow = document.createElement("div");
sideRow.className = "b05-route__irregular-row";
sideRow.className = "b05-structure__grid";
sideRow.append(field("설치측", sideSelect), field("이격 (m)", offsetField));
// 옵션 칸은 타입마다 다르므로 선택할 때마다 새로 그린다.
const optionRow = document.createElement("div");
optionRow.className = "b05-route__irregular-row";
optionRow.className = "b05-structure__grid";
// 계곡 통과 시설(배관 등)의 부속 옵션 서브폼 — 일반 구조물의 옵션 칸과 같은
// 자리에서, 같은 흐름(종류 선택 → 옵션 → 추가/수정)으로 편집한다(2026-08-17
// 사용자 지시 — 자동·수동 배관은 같은 구조물, 별도 UI 금지). 기슭막이 길이를
// 넣으면 시작·종료 측점이 기준 − 앞 ~ 기준 + 뒤로 자동 채워진다.
const facilityOptions = createFacilityOptionsForm({
onSpanSuggest: (frontM, backM) => {
const step = interval();
const anchor = anchorFields.read(step, false);
if (anchor === null) return;
startFields.write(Math.max(0, anchor - frontM), step);
endFields.write(anchor + backM, step);
},
});
const facilityOptions = createFacilityOptionsForm();
/** 이 타입이 서브폼(계곡 통과 시설 부속·독립 기슭막이)을 쓰는지 — 쓰면 레지스트리
* 옵션 칸 대신 서브폼이 그린다(2026-08-17 사용자 지시 12: 독립 기슭막이도 같은
* 인터페이스). */
function facilityFormKind(type: StructureType | null): PipeFacility | "revetment" | null {
if (!type) return null;
if (type.managed_by) return type.type_id as PipeFacility;
return type.type_id === "revetment" ? "revetment" : null;
}
const primary = document.createElement("button");
primary.type = "button";
@@ -176,10 +183,14 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
// 사용법 설명 문단은 두지 않는다 — 공간 대비 의미 없음(2026-08-17 사용자 지시,
// 필요 시 별도 설명 페이지로).
// 측점 그룹과 옵션 나열 사이 구분선(2026-08-17 사용자 지시 2).
const positionDivider = document.createElement("hr");
positionDivider.className = "b05-structure__divider";
body.append(
field("구조물군", groupSelect),
field("종류", typeSelect),
typeRow,
positionRow,
positionDivider,
sideRow,
optionRow,
facilityOptions.root,
@@ -215,34 +226,28 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
const interval = (): number => callbacks.getInterval();
/** 배치형태·관리 주체에 맞춰 위치 칸을 바꾼다. 계곡 통과 시설은 기준 측점 필수 +
* 시작·종료(범위, 기슭막이 길이로 자동 채움 가능)를 받고, 설치측·이격은 정본
* (관 지점) 소관이라 감춘다. 종류 미선택(빈값)이면 입력 칸 전체를 숨긴다 —
* 리셋 직후 상태(2026-08-17 사용자 지시). */
/** 배치형태·관리 주체에 맞춰 위치 칸을 바꾼다. 계곡 통과 시설(배수관 등)은 점형이라
* 기준 측점 하나만 받고(2026-08-17 사용자 지시 2 — 시작·종료 불필요), 설치측·이격도
* 정본(관 지점) 소관이라 감춘다. 종류 미선택(빈값)이면 입력 칸 전체를 숨긴다 —
* 리셋 직후 상태. */
function syncPlacementFields(): void {
const type = currentType();
const managed = !!type?.managed_by;
const isInterval = !!type && (managed || type.placement === "interval");
const isInterval = !!type && !managed && type.placement === "interval";
positionRow.hidden = !type;
positionDivider.hidden = !type;
startFields.wrap.hidden = !isInterval;
endFields.wrap.hidden = !isInterval;
sideRow.hidden = !type || managed;
primary.disabled = !type;
anchorFields.wrap.querySelector("span")!.textContent =
isInterval && !managed ? "기준 측점 (비우면 시작)" : "기준 측점";
anchorFields.wrap.querySelector("span")!.textContent = isInterval
? "기준 측점 (비우면 시작)"
: "기준 측점";
}
/** 종류에 맞춰 부속 옵션 서브폼을 켠다(계곡 통과 시설일 때만, 값은 인자로). */
function syncFacilityForm(
options: Record<string, string | number> = {},
source?: PipeSource,
): void {
const type = currentType();
facilityOptions.setFacility(
type?.managed_by ? (type.type_id as PipeFacility) : null,
options,
source,
);
/** 종류에 맞춰 부속 옵션 서브폼을 켠다(계곡 통과 시설·독립 기슭막이, 값은 인자로). */
function syncFacilityForm(options: Record<string, string | number> = {}): void {
facilityOptions.setFacility(facilityFormKind(currentType()), options);
}
/** 타입의 옵션 스키마대로 입력 칸을 다시 그린다 — 상세(detail) 옵션은 B06/B07
@@ -251,7 +256,8 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
const type = currentType();
optionInputs = [];
optionRow.replaceChildren();
const visible = type && !type.managed_by ? type.options.filter(isB05Option) : [];
// 서브폼이 담당하는 타입(계곡 통과 시설·독립 기슭막이)은 여기서 그리지 않는다.
const visible = type && !facilityFormKind(type) ? type.options.filter(isB05Option) : [];
if (!visible.length) {
optionRow.hidden = true;
return;
@@ -261,11 +267,11 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
const preset = values[option.key] ?? option.default ?? "";
let input: HTMLInputElement | HTMLSelectElement;
if (option.input === "select") {
// 필수 선택지는 첫 항목을 슬쩍 고른 것처럼 두지 않는다 — 재료·형식은 설계자가
// 고르는 값이라, 안 고른 상태를 빈칸으로 드러낸다(2026-08-16 크로스체크 지적 2).
// 빈("선택하세요") 항목은 두지 않는다 — 첫 항목이 곧 기본값이고, 기본값은
// 구조물별로 레지스트리에서 지정한다(2026-08-17 사용자 지시 1).
const choices = option.choices.map((choice) => [choice, choice] as [string, string]);
input = select(option.required ? [["", "선택하세요"], ...choices] : choices);
input.value = String(preset || (option.required ? "" : (option.choices[0] ?? "")));
input = select(choices);
input.value = String(preset || (option.choices[0] ?? ""));
} else if (option.input === "number") {
input = numberInput("0.1", "0");
input.value = String(preset ?? "");
@@ -323,12 +329,8 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
function labelOfPipe(pipe: PipeFacilityItem): string {
const type = typeMap().get(pipe.facility);
const name = type?.name ?? pipe.facility;
const step = interval();
const position =
pipe.start_m !== undefined && pipe.end_m !== undefined && pipe.end_m > pipe.start_m
? `${formatStation(pipe.start_m, step)}~${formatStation(pipe.end_m, step)}`
: formatStation(pipe.chainage_m, step);
return `${position} · ${name}`;
// 계곡 통과 시설은 점형 — 기준 측점 하나로 표기한다(2026-08-17 지시 2).
return `${formatStation(pipe.chainage_m, interval())} · ${name}`;
}
function renderList(): void {
@@ -407,6 +409,7 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
offsetField.value = String(target.offset_m ?? 0);
memoField.value = target.memo ?? "";
renderOptionFields(target.options);
syncFacilityForm(target.options);
} else {
anchorFields.write(null, step);
startFields.write(null, step);
@@ -414,8 +417,8 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
offsetField.value = "0";
memoField.value = "";
renderOptionFields();
syncFacilityForm();
}
syncFacilityForm();
[anchorFields, startFields, endFields].forEach((fields) => fields.clearInvalid());
syncPlacementFields();
syncButtons();
@@ -436,10 +439,11 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
syncTypeOptions(pipe.facility);
}
anchorFields.write(pipe.chainage_m, step);
startFields.write(pipe.start_m ?? null, step);
endFields.write(pipe.end_m ?? null, step);
// 계곡 통과 시설은 점형 — 시작·종료 칸은 감춰지므로 비워 둔다.
startFields.write(null, step);
endFields.write(null, step);
memoField.value = "";
syncFacilityForm(pipe.options ?? {}, pipe.source);
syncFacilityForm(pipe.options ?? {});
[anchorFields, startFields, endFields].forEach((fields) => fields.clearInvalid());
syncPlacementFields();
syncButtons();
@@ -448,6 +452,8 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
}
function readOptions(): Record<string, string | number> {
// 서브폼이 담당하는 타입(독립 기슭막이)은 서브폼이 값을 읽는다.
if (facilityFormKind(currentType())) return facilityOptions.readOptions();
const values: Record<string, string | number> = {};
optionInputs.forEach((input) => {
// 빈 칸은 아예 넣지 않는다 — 빈 숫자를 0으로 저장하면 "0으로 확정"과 구분이 안 된다.
@@ -475,13 +481,8 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
anchorFields.station.focus();
return;
}
// 배수관 등 계곡 통과 시설은 점형 — 기준 측점 하나뿐이다(2026-08-17 지시 2).
const attributes: FacilityAttributes = { facility: type.type_id as PipeFacility };
const start = startFields.read(step, false);
const end = endFields.read(step, false);
if (start !== null && end !== null && end > start) {
attributes.start_m = start;
attributes.end_m = end;
}
const options = facilityOptions.readOptions();
if (Object.keys(options).length) attributes.options = options;
if (selectedPipeChainage !== null) {
@@ -565,6 +566,10 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
typeSelect.addEventListener("change", () => {
syncPlacementFields();
renderOptionFields();
// 배수관·BOX암거·세월교·독립 기슭막이는 부속 옵션 서브폼이 따라 열려야 한다 —
// 수동 추가든 자동 배치 지점 편집이든 같은 옵션을 받는다(2026-08-17 사용자 지시:
// 자동은 통수단면으로 자리를 잡아 준 것일 뿐 같은 구조물이다).
syncFacilityForm();
if (editingId) {
editingId = null;
syncButtons();