feat(B05·B06): 「구조물 옵션 보이기」에서 끈 옵션을 배수관 · BOX암거 · 세월교 · 기슭막이 하위 폼에서도 가림(21-10 흠 8)

- 시설 폼 칸에 옵션 키 표식(data-option-keys) · 끈 키의 칸은 가림 · 칸이 다 가려진 줄은 줄째
- 키가 여럿인 칸(본체 규격 · 유입구 구조)은 키가 다 꺼졌을 때만 가림 · 독립 기슭막이는 유입 · 유출 채널 키로 넓힘
- 숨긴 칸은 칸 값(제안값 채우기 포함)이 아니라 폼이 받은 옛 값을 저장에 그대로 실음 · 이 폼에는 필수 검사가 없음
- 가리기 코드는 새 파일(_Facility_Hidden)로 분리해 Facility.ts 700줄 아래 유지
- 시험 3개 추가

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 14:20:06 +09:00
co-authored by Claude Sonnet 5.5
parent 8cd4a78efc
commit eb387d5f84
9 changed files with 260 additions and 27 deletions
+37 -10
View File
@@ -60,6 +60,7 @@ import {
import { pipeWallMinHeightM } from "../common_util/common_util_culvert_sets";
import type { SummaryItem } from "../common_util/common_util_summary_item";
import { createFordFields } from "./B05_Profile_UI_Drainage_Facility_Ford";
import { createHiddenOptions } from "./B05_Profile_UI_Drainage_Facility_Hidden";
export type { InletStructureKind }; // 목록은 700줄 한계로 `_Fields` 로 옮김 — 부르던 곳은 그대로
@@ -204,8 +205,11 @@ export interface FacilityOptionsForm {
/** 담당 유역의 설계유량(㎥/s) — 물넘이·세월교 개략 단면의 입력. 없으면 안내만. */
designFlow?: number | null,
) => void;
/** 현재 필드 값을 시설 옵션으로 읽는다(빈 값 생략). */
/** 현재 필드 값을 시설 옵션으로 읽는다(빈 값 생략). 숨긴 칸은 폼이 받은 옛 값 그대로. */
readOptions: () => Record<string, string | number>;
/** 「구조물 옵션 보이기」에서 끈 레지스트리 옵션 키 — 그 칸을 가린다(값은 옛 값 보존).
* `setFacility` 뒤에 부른다. */
setHiddenOptions: (facility: PipeFacility | null, registryKeys: Iterable<string>) => void;
/** 설계유량만 갱신한다 — 필드 값은 건드리지 않는다. 임시 배치(2026-08-18)의
* 유역 재계산 결과를 입력 중인 폼에 살짝 반영하는 용도. */
setDesignFlow: (designFlow: number | null) => void;
@@ -249,8 +253,11 @@ export function createFacilityOptionsForm(
// 관보호공 날개벽 — **개소당 원단위**가 형식마다 붙박이라 고르는 칸이 있어야 물량이 선다
// (2026-09-09, 계획서 4-13 · 소광리 원본 탭 다섯). 비우면 날개벽을 안 센다.
const wingWall = optionalSelect("안 놓음", ["A-TYPE", "C-TYPE", "A-TYPE+집수정"]);
const pipeRow = grid(labeled("관종", pipeMaterial), labeled("관경", pipeDiameter));
const wingRow = grid(labeled("관보호공 날개벽", wingWall));
const pipeRow = grid(
labeled("관종", pipeMaterial, "pipe_kind"),
labeled("관경", pipeDiameter, "pipe_diameter_mm"),
);
const wingRow = grid(labeled("관보호공 날개벽", wingWall, "wing_wall_type"));
// ── 유입구 — 집수정 또는 기슭막이 택일(사용자 지시 5~9) ─────────────────
const inletGroup = group("유입구");
@@ -273,11 +280,14 @@ export function createFacilityOptionsForm(
"돌쌓기(찰)",
"돌쌓기(메)",
]);
const basinMaterialField = labeled("형태", basinMaterial);
const basinMaterialField = labeled("형태", basinMaterial, "inlet_basin_material");
// 집수정 종방향 길이 — 3D 예상형상용, 전후 동일 배분·기본 2m(2026-08-23 사용자).
const basinLength = numberInput("0.1", "0", "제안 2");
const basinRows = [
grid(labeled("형식", basinForm), labeled("길이 (m)", stepper(basinLength, 1))),
grid(
labeled("형식", basinForm, "inlet_basin_form"),
labeled("길이 (m)", stepper(basinLength, 1), "inlet_basin_length_m"),
),
];
const height = (): string =>
String(pipeWallMinHeightM(Number(pipeDiameter.value || "1000") / 1000));
@@ -294,7 +304,11 @@ export function createFacilityOptionsForm(
inletSlot.className = "b05-structure__adjust-slot";
const outletSlot = inletSlot.cloneNode() as HTMLDivElement;
inletGroup.body.append(
grid(labeled("구조", inletType), basinMaterialField, inletRevet.formField),
grid(
labeled("구조", inletType, ["inlet_type", "inlet_structure"]),
basinMaterialField,
inletRevet.formField,
),
...basinRows,
...inletRevet.rows,
inletSlot,
@@ -309,7 +323,7 @@ export function createFacilityOptionsForm(
height,
});
outletGroup.body.append(
grid(labeled("구조", outletType), outletRevet.formField),
grid(labeled("구조", outletType, "outlet_type"), outletRevet.formField),
...outletRevet.rows,
outletSlot,
);
@@ -329,7 +343,7 @@ export function createFacilityOptionsForm(
// 좌·우가 저장 채널(유입/유출) 중 어느 쪽인지는 측점 지형이 정하므로 이름표만
// 바꿔 단다(`setRevetSideLabels`) — 값은 언제나 그 채널 키로 오간다.
const revetSide = optionalSelect("— 안 정함 (제안 양쪽) —", ["양쪽", "좌", "우"]);
const revetTopRow = grid(labeled("설치 측", revetSide));
const revetTopRow = grid(labeled("설치 측", revetSide, "side"));
const revetInlet = createRevetSideGroup(INLET_REVET_KEYS, "돌쌓기(메)");
const revetOutlet = createRevetSideGroup(OUTLET_REVET_KEYS, "돌쌓기(메)");
/** 좌·우 이름표 — B06이 그 측점의 벽 방향을 알려 주면 바뀐다. 기본은 유입=좌. */
@@ -372,12 +386,20 @@ export function createFacilityOptionsForm(
]);
const boxWidth = numberInput("0.1", "0", `제안 ${BOX_SIZE_PRESETS[0][1]}`);
const boxHeight = numberInput("0.1", "0", `제안 ${BOX_SIZE_PRESETS[0][2]}`);
const boxCustomRow = grid(labeled("본체 폭 (m)", boxWidth), labeled("본체 높이 (m)", boxHeight));
const boxCustomRow = grid(
labeled("본체 폭 (m)", boxWidth, "body_width_m"),
labeled("본체 높이 (m)", boxHeight, "body_height_m"),
);
const boxWrap = document.createElement("div");
boxWrap.append(grid(labeled("본체 규격 (m)", boxSize)), boxCustomRow);
boxWrap.append(
grid(labeled("본체 규격 (m)", boxSize, ["body_width_m", "body_height_m"])),
boxCustomRow,
);
const wingInFields = createWingFields("날개벽(유입)", WING_IN_KEYS);
const wingOutFields = createWingFields("날개벽(유출)", WING_OUT_KEYS);
const hiddenOptions = createHiddenOptions(root);
/** 고른 프리셋을 폭·높이 칸에 실어 둔다 — 저장도, "사용자 지정"으로 바꿨을 때의
* 출발값도 이 두 칸이 정본이다. 사용자 지정 상태에서는 손대지 않는다. */
function syncBoxSize(): void {
@@ -551,8 +573,12 @@ export function createFacilityOptionsForm(
onInletStructureChange(handler) {
inletStructureHandler = handler;
},
setHiddenOptions(facility, registryKeys) {
hiddenOptions.set(facility, registryKeys);
},
setFacility(facility, options = {}, designFlow = null) {
current = facility;
hiddenOptions.remember(options);
ford.setDesignFlow(designFlow ?? null);
if (facility === null) {
syncVisibility();
@@ -658,6 +684,7 @@ export function createFacilityOptionsForm(
revetInlet.fields.read(options);
revetOutlet.fields.read(options);
}
hiddenOptions.restore(options); // 숨긴 칸은 폼이 받은 옛 값 그대로
return options;
},
};
@@ -81,9 +81,15 @@ export function fordSection(designFlowM3s: number | null, widthM: number): FordS
/* ── 폼 조각 ────────────────────────────────────────────────────────────── */
export function labeled(text: string, input: HTMLElement): HTMLLabelElement {
export function labeled(
text: string,
input: HTMLElement,
/** 이 칸이 적는 옵션 키 — 「구조물 옵션 보이기」에서 끈 키의 칸을 가리는 표식(21-10 흠 8). */
optionKeys: string | readonly string[] = [],
): HTMLLabelElement {
const wrapper = document.createElement("label");
wrapper.className = "b05-route__field";
if (optionKeys.length) wrapper.dataset.optionKeys = [optionKeys].flat().join(" ");
const caption = document.createElement("span");
caption.textContent = text;
wrapper.append(caption, input);
@@ -355,11 +361,11 @@ export function createRevetmentFields(
};
// 눈금은 조정창과 같은 값이다 — 길이 1m, 전/후 0.5m, 높이 0.1m(2026-08-29 일원화).
const formField = labeled("형태", form);
const lengthField = length ? labeled("길이 (m)", stepper(length, 1)) : null;
const heightField = labeled("높이 (m)", stepper(height, 0.1));
const beforeField = before ? labeled("기준측점 전 (m)", stepper(before, 0.5)) : null;
const afterField = after ? labeled("기준측점 후 (m)", stepper(after, 0.5)) : null;
const formField = labeled("형태", form, keys.form);
const lengthField = length ? labeled("길이 (m)", stepper(length, 1), keys.length) : null;
const heightField = labeled("높이 (m)", stepper(height, 0.1), keys.height);
const beforeField = before ? labeled("기준측점 전 (m)", stepper(before, 0.5), keys.before) : null;
const afterField = after ? labeled("기준측점 후 (m)", stepper(after, 0.5), keys.after) : null;
// [제안값 넣기] — **누른 때만** 빈 칸에 제안값을 넣음(적은 칸은 안 건드림) · 층따기 단추와 같은 모양.
const fill = document.createElement("button");
@@ -494,15 +500,15 @@ export function createWingFields(title: string, keys: WingKeys): WingFields {
const angle = numberInput("1", "0", `제안 ${WING_DEFAULTS.angle}`);
// 숫자 칸은 기슭막이·집수정과 같은 [-][값][+] 묶음으로 맞춘다(2026-08-30 사용자).
const heightField = labeled("짧은쪽 높이 (m)", stepper(height, 0.1));
const heightField = labeled("짧은쪽 높이 (m)", stepper(height, 0.1), keys.height);
const dimsRow = grid(
labeled("길이 (m)", stepper(length, 0.1)),
labeled("각도 (°)", stepper(angle, 5, 0)),
labeled("길이 (m)", stepper(length, 0.1), keys.length),
labeled("각도 (°)", stepper(angle, 5, 0), keys.angle),
);
const box = group(title);
const slot = document.createElement("div");
slot.className = "b05-structure__adjust-slot";
box.body.append(grid(labeled("설치", install), heightField), dimsRow, slot);
box.body.append(grid(labeled("설치", install, keys.install), heightField), dimsRow, slot);
function syncVisibility(): void {
const off = install.value === "없음";
@@ -41,12 +41,12 @@ export interface FordFields {
export function createFordFields(emit: () => void): FordFields {
// 세월교 — 구체 내 배관 수량(련). 련은 정수라 소수 자릿수를 두지 않는다.
const count = numberInput("1", "1", "련");
const countRow = grid(labeled("수량 (련)", stepper(count, 1, 0)));
const countRow = grid(labeled("수량 (련)", stepper(count, 1, 0), "pipe_count"));
const width = numberInput("0.1");
const height = numberInput("0.01");
const widthRow = grid(
labeled("월류 폭 (m)", stepper(width, 0.1)),
labeled("월류 높이 (m)", stepper(height, 0.1)),
labeled("월류 폭 (m)", stepper(width, 0.1), "ford_width_m"),
labeled("월류 높이 (m)", stepper(height, 0.1), "ford_height_m"),
);
// 물넘이 바닥은 유입(상류)이 높고 유출이 낮게 기운다(2026-08-28 사용자 확정).
// 비우면 그 측점의 **노면 횡단경사**를 그대로 쓴다 — 횡단도가 판단한다.
@@ -56,9 +56,9 @@ export function createFordFields(emit: () => void): FordFields {
const thickness = numberInput("1");
const length = numberInput("0.1");
const slopeRow = grid(
labeled("바닥 경사 유입→유출 (%)", stepper(slope, 0.1)),
labeled("포장 두께 (㎝)", stepper(thickness, 1, 0)),
labeled("포장 길이 노폭 방향 (m)", stepper(length, 0.1)),
labeled("바닥 경사 유입→유출 (%)", stepper(slope, 0.1), "ford_slope_pct"),
labeled("포장 두께 (㎝)", stepper(thickness, 1, 0), "thickness_cm"),
labeled("포장 길이 노폭 방향 (m)", stepper(length, 0.1), "length_m"),
);
const summary = document.createElement("p");
summary.className = "b05-drainage__facility-note";
@@ -0,0 +1,59 @@
/* =============================================================================
* B05_Profile_UI_Drainage_Facility_Hidden.ts
* 계곡 통과 시설 폼의 **끈 옵션 가리기** — M02 「구조물 옵션 보이기」에서 끈 옵션
* (`GET /api/projects/structure-types` 의 `visible: false`)의 칸을 시설 폼에서도 안 그린다
* (PLAN 21-10 흠 8).
*
* · 칸은 `labeled(..., 옵션키)` 가 붙인 `data-option-keys` 표식으로 찾는다 — 키가 여럿인 칸(본체 규격 ·
* 유입구 구조)은 **키가 다 꺼졌을 때** 가린다(하나만 꺼지면 남은 키를 적는 칸이라 둠).
* · 숨긴 칸의 값은 칸이 아니라 **폼이 받은 옛 값**(`remember`)을 저장에 그대로 싣는다 —
* 칸에 제안값을 채워도 숨긴 값이 몰래 바뀌지 않는다.
* · 필수 검사는 이 폼에 없다(레지스트리 옵션 칸은 `renderOptionFields` 가 숨긴 칸을 뺌).
* ========================================================================== */
import { hiddenFormKeys } from "./B05_Profile_UI_Drainage_Facility_Merge";
export interface HiddenOptions {
/** 폼이 받은 옵션 — 숨긴 칸의 옛 값 보존용. `setFacility` 가 부른다. */
remember: (options: Record<string, string | number>) => void;
/** 끈 레지스트리 키를 받아 칸을 가린다. */
set: (facility: string | null, registryKeys: Iterable<string>) => void;
/** 읽은 옵션에서 숨긴 칸을 칸 값이 아닌 옛 값으로 바꾼다. */
restore: (options: Record<string, string | number>) => void;
}
export function createHiddenOptions(root: HTMLElement): HiddenOptions {
let hidden: ReadonlySet<string> = new Set();
let stored: Record<string, string | number> = {};
const apply = (): void => {
root.querySelectorAll<HTMLElement>("[data-option-keys]").forEach((node) => {
const keys = (node.dataset.optionKeys ?? "").split(" ");
node.classList.toggle(
"is-option-hidden",
keys.every((key) => hidden.has(key)),
);
});
// 칸이 다 가려진 줄은 줄째(빈 줄 간격이 안 남게)
root.querySelectorAll<HTMLElement>(".b05-structure__grid").forEach((row) => {
const kids = [...row.children];
const all =
kids.length > 0 && kids.every((kid) => kid.classList.contains("is-option-hidden"));
row.classList.toggle("is-option-hidden", all);
});
};
return {
remember(options) {
stored = { ...options };
},
set(facility, registryKeys) {
hidden = hiddenFormKeys(facility, registryKeys);
apply();
},
restore(options) {
for (const key of hidden) {
delete options[key];
if (stored[key] !== undefined) options[key] = stored[key];
}
},
};
}
@@ -53,6 +53,28 @@ export const FORM_OPTION_KEYS: Readonly<Record<string, readonly string[]>> = {
revetment: ["side", ...revetKeys("inlet"), ...revetKeys("outlet")],
};
/** 독립 기슭막이 레지스트리 키 → 폼은 유입·유출 두 채널 키로 적는다. */
const REVETMENT_FIELDS = ["form", "length_m", "height_m", "before_m", "after_m"];
/** 「구조물 옵션 보이기」에서 끈 레지스트리 키 → 시설 폼이 가릴 칸 키(21-10 흠 8).
* · 독립 기슭막이는 채널 키(`inlet_revet_*` · `outlet_revet_*`)로 넓힘
* · 유입구 「구조」한 칸이 `inlet_type` 과 `inlet_structure` 를 함께 적음 */
export function hiddenFormKeys(
facility: string | null,
registryHidden: Iterable<string>,
): Set<string> {
const hidden = new Set<string>(registryHidden);
if (facility === "revetment") {
for (const field of REVETMENT_FIELDS) {
if (!hidden.has(field)) continue;
hidden.add(`inlet_revet_${field}`);
hidden.add(`outlet_revet_${field}`);
}
}
if (hidden.has("inlet_type")) hidden.add("inlet_structure");
return hidden;
}
interface MergeableAttributes {
facility: string;
start_m?: number;
@@ -38,6 +38,7 @@ import {
loadPipeForm as loadPipeFormInto,
} from "./B05_Profile_UI_Structures_Panel_Commit";
import { renderStructureList } from "./B05_Profile_UI_Structures_List";
import { isOptionVisible } from "./B05_Profile_UI_Structures_Summary";
import { stationLabel as formatStation } from "../common_util/common_util_station";
/** 공개 타입·표시 상수는 700줄 제한으로 `_Structures_Panel_Types` 로 옮겼다 —
@@ -228,7 +229,13 @@ export function createStructuresSection(
options: Record<string, string | number> = {},
designFlow: number | null = null,
): void {
facilityOptions.setFacility(facilityFormKind(currentType()), options, designFlow);
const kind = facilityFormKind(currentType());
facilityOptions.setFacility(kind, options, designFlow);
// 「구조물 옵션 보이기」에서 끈 옵션은 시설 폼에서도 가림(값은 옛 값 그대로 저장)
facilityOptions.setHiddenOptions(
kind,
(currentType()?.options ?? []).filter((o) => !isOptionVisible(o)).map((o) => o.key),
);
syncOptionLock();
}
@@ -484,3 +484,8 @@
.b05-route__irregular-item.is-assigned span {
color: color-mix(in srgb, var(--color-success) 65%, var(--color-text));
}
/* 「구조물 옵션 보이기」에서 끈 옵션의 시설 폼 칸 */
.b05-drainage__facility .is-option-hidden {
display: none;
}
@@ -66,6 +66,20 @@ class FakeNode {
(this.listeners.click || []).forEach((h) => h({ target: this }));
}
scrollIntoView() {}
// 시험에 필요한 두 가지 선택자만 — `[data-option-keys]` · `.클래스`
querySelectorAll(selector) {
const match = selector.startsWith("[")
? (n) => n.dataset && n.dataset.optionKeys !== undefined
: (n) => n.classList.contains(selector.slice(1));
const found = [];
const walk = (node) =>
node.children.forEach((kid) => {
if (kid.classList && match(kid)) found.push(kid);
walk(kid);
});
walk(this);
return found;
}
}
globalThis.document = { createElement: (tag) => new FakeNode(tag) };
globalThis.Option = function Option(text, value) {
@@ -101,6 +115,9 @@ function loadTs(file) {
if (STUBS[request]) return STUBS[request];
// 화면 공용 부품(@ui/…) — 이 시험이 안 쓰는 것이라 뭘 불러도 아무 일 안 하는 함수로 막아 둠
if (request.startsWith("@ui/")) return new Proxy({}, { get: () => () => {} });
if (request.startsWith(".") && request.endsWith(".json")) {
return JSON.parse(fs.readFileSync(path.join(path.dirname(full), request), "utf8"));
}
return request.startsWith(".")
? loadTs(path.join(path.dirname(full), `${request}.ts`))
: require(request);
@@ -118,6 +135,8 @@ const { buildSummaryRow } = B05("B05_Profile_UI_Structures_Summary_Row");
const { renderStructureList } = B05("B05_Profile_UI_Structures_List");
const { renderOptionFields } = B05("B05_Profile_UI_Structures_Panel_Options");
const commitModule = B05("B05_Profile_UI_Structures_Panel_Commit");
const { createHiddenOptions } = B05("B05_Profile_UI_Drainage_Facility_Hidden");
const { labeled } = B05("B05_Profile_UI_Drainage_Facility_Fields");
const out = {};
const A = {
@@ -358,6 +377,58 @@ const prev = () => ({
summary_item: A,
});
// 시설 폼 끈 옵션 가리기(PLAN 21-10 흠 8)
{
const tagged = labeled("관종", new FakeNode("select"), "pipe_kind");
const multi = labeled("구조", new FakeNode("select"), ["inlet_type", "inlet_structure"]);
const plain = labeled("메모", new FakeNode("select"));
out.labelTag = [tagged.dataset.optionKeys, multi.dataset.optionKeys, plain.dataset.optionKeys];
const root = new FakeNode("div");
const rowA = new FakeNode("div");
rowA.className = "b05-structure__grid";
const kind = labeled("관종", new FakeNode("select"), "pipe_kind");
const dia = labeled("관경", new FakeNode("select"), "pipe_diameter_mm");
rowA.append(kind, dia);
const rowB = new FakeNode("div");
rowB.className = "b05-structure__grid";
const inlet = labeled("구조", new FakeNode("select"), ["inlet_type", "inlet_structure"]);
const free = labeled("메모", new FakeNode("select")); // 표식 없는 칸은 안 가림
rowB.append(inlet, free);
root.append(rowA, rowB);
const hiddenOf = () => ({
kind: kind.classList.contains("is-option-hidden"),
dia: dia.classList.contains("is-option-hidden"),
rowA: rowA.classList.contains("is-option-hidden"),
inlet: inlet.classList.contains("is-option-hidden"),
free: free.classList.contains("is-option-hidden"),
rowB: rowB.classList.contains("is-option-hidden"),
});
const hidden = createHiddenOptions(root);
hidden.set("pipe", ["pipe_kind"]);
out.hideOne = hiddenOf();
hidden.set("pipe", ["pipe_kind", "pipe_diameter_mm"]);
out.hideRow = hiddenOf(); // 줄의 칸이 다 가려지면 줄째
hidden.set("pipe", ["inlet_structure"]);
out.hideInletPartial = hiddenOf(); // 키가 여럿인 칸은 하나만 꺼지면 그대로
hidden.set("pipe", ["inlet_type"]);
out.hideInlet = hiddenOf(); // 유입구 「구조」는 inlet_type 을 끄면 함께 가려짐(구조 키 포함)
hidden.set("pipe", []);
out.showAll = hiddenOf(); // [모두 보임] 뒤 되돌아옴
// 숨긴 칸은 폼이 받은 옛 값 그대로 — 칸에서 읽은 값(제안값 채우기 포함)이 아니라
hidden.remember({ pipe_kind: "흄관", pipe_diameter_mm: 1000, other: 1 });
hidden.set("pipe", ["pipe_kind", "inlet_type"]);
const read = { pipe_kind: "VR관", pipe_diameter_mm: 800, inlet_type: "제안", other: 2 };
hidden.restore(read);
out.restored = read;
hidden.remember({}); // 옛 값이 없던 숨긴 칸은 값이 안 실림
const read2 = { pipe_kind: "VR관", pipe_diameter_mm: 800 };
hidden.restore(read2);
out.restoredEmpty = read2;
out.revetKeys = [...merge.hiddenFormKeys("revetment", ["form", "height_m", "side"])].sort();
out.pipeKeys = [...merge.hiddenFormKeys("pipe", ["inlet_type"])].sort();
out.otherKeys = [...merge.hiddenFormKeys("box_culvert", ["form"])].sort();
}
async function main() {
const fresh = await saveWith({ previous: null, summary: null, formOptions: { H: 3 } });
out.commitNew = { hasKey: "summary_item" in fresh, options: fresh.options };
@@ -136,3 +136,39 @@ def test_배수유역_화면은_들어올_때_세션_초안_관을_먼저_씀():
body = source.split("const loadSaved", 1)[1].split("/**", 1)[0]
assert "readPendingPipes(" in body and "computeDetailBasins(" in body
assert body.index("readPendingPipes(") < body.index("fetchDetailPipePoints(")
def test_시설_폼_끈_옵션_가리기(out):
# 칸 표식 — 키 하나 · 여럿 · 없음
assert out["labelTag"] == ["pipe_kind", "inlet_type inlet_structure", None]
none = dict(kind=False, dia=False, rowA=False, inlet=False, free=False, rowB=False)
assert out["hideOne"] == {**none, "kind": True}
# 줄의 칸이 다 가려지면 줄째 · 표식 없는 칸은 그대로
assert out["hideRow"] == {**none, "kind": True, "dia": True, "rowA": True}
assert out["hideInletPartial"] == none
assert out["hideInlet"] == {**none, "inlet": True}
assert out["showAll"] == none # [모두 보임] 뒤 되돌아옴
def test_숨긴_칸은_옛_값_그대로_저장에_실림(out):
# 칸에서 읽은 값(VR관 · 제안)이 아니라 폼이 받은 옛 값 — 안 숨긴 칸은 칸 값
assert out["restored"] == {"pipe_kind": "흄관", "pipe_diameter_mm": 800, "other": 2}
# 옛 값이 없던 숨긴 칸은 값이 안 실림(칸의 제안값이 몰래 확정되지 않음)
assert out["restoredEmpty"] == {"pipe_diameter_mm": 800}
def test_끈_키를_폼_칸_키로_넓힘(out):
# 독립 기슭막이는 유입 · 유출 채널 키로 · 유입구 구조는 inlet_structure 까지
assert out["revetKeys"] == sorted(
[
"form",
"height_m",
"side",
"inlet_revet_form",
"outlet_revet_form",
"inlet_revet_height_m",
"outlet_revet_height_m",
]
)
assert out["pipeKeys"] == ["inlet_structure", "inlet_type"]
assert out["otherKeys"] == ["form"]