Merge remote-tracking branch 'origin/dev' into main_laptop_1
This commit is contained in:
@@ -332,6 +332,26 @@
|
||||
"unit": "%",
|
||||
"default": null,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"key": "thickness_cm",
|
||||
"label": "포장 두께",
|
||||
"input": "number",
|
||||
"unit": "㎝",
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 표가 안 서고 「두께를 적어야 섬」 사유가 뜸 — 두께로 관측 원단위(울진 20㎝)를 고름(2026-09-14 A3)"
|
||||
},
|
||||
{
|
||||
"key": "length_m",
|
||||
"label": "포장 길이(노폭 방향)",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 면적(월류 폭 × 길이)이 0 이라 표가 안 서고 사유가 뜸 — 노견까지 전폭을 적음(2026-09-14 A3)"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -479,6 +499,15 @@
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 실무 붙박이(콘크리트 개거)로 돎"
|
||||
},
|
||||
{
|
||||
"key": "length_m",
|
||||
"label": "연장",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": false,
|
||||
"empty_means": "비우면 표가 안 서고 「연장을 적어야 섬」 사유가 뜸 — 개거는 m당 원단위라 연장이 곧 밑수(2026-09-14 A2)"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -45,7 +45,10 @@ import {
|
||||
fordSection,
|
||||
grid,
|
||||
group,
|
||||
INLET_KINDS,
|
||||
INLET_REVET_KEYS,
|
||||
inletKindOf,
|
||||
type InletStructureKind,
|
||||
labeled,
|
||||
numberInput,
|
||||
optionalSelect,
|
||||
@@ -57,29 +60,8 @@ import {
|
||||
WING_OUT_KEYS,
|
||||
} from "./B05_Profile_UI_Drainage_Facility_Fields";
|
||||
|
||||
/** B06 유입측 구조물 형식 — 조정창 드롭다운과 같은 값 집합(2026-08-29 병합).
|
||||
* 문자열 리터럴로 두어 B05가 B06 모듈에 기대지 않게 한다. */
|
||||
export type InletStructureKind = "auto" | "revet" | "I" | "L" | "U";
|
||||
|
||||
/** 유입구 "구조" 목록 — 조정창의 구조물 형식(자동/기슭막이/집수정 I·ㄴ·ㄷ)을 이 한
|
||||
* 드롭다운으로 합쳤다(2026-08-29 사용자 지시 5: 일단 리스트를 합치고 중복은 뒤에 뺀다).
|
||||
* 고른 값 하나가 정본 둘을 정한다 — 관 옵션 `inlet_type`(기슭막이/집수정)과 B06
|
||||
* 유입측 형식(`inlet_structure`). */
|
||||
const INLET_KINDS: ReadonlyArray<{
|
||||
label: string;
|
||||
type: "기슭막이" | "집수정";
|
||||
structure: InletStructureKind;
|
||||
}> = [
|
||||
{ label: "기슭막이", type: "기슭막이", structure: "revet" },
|
||||
{ label: "집수정", type: "집수정", structure: "auto" },
|
||||
{ label: "자동(규칙)", type: "기슭막이", structure: "auto" },
|
||||
{ label: "집수정 I형", type: "집수정", structure: "I" },
|
||||
{ label: "집수정 ㄴ형", type: "집수정", structure: "L" },
|
||||
{ label: "집수정 ㄷ형", type: "집수정", structure: "U" },
|
||||
];
|
||||
|
||||
const inletKindOf = (label: string): (typeof INLET_KINDS)[number] =>
|
||||
INLET_KINDS.find((kind) => kind.label === label) ?? INLET_KINDS[0];
|
||||
// 유입구 형식 목록은 700줄 한계로 `_Fields` 로 옮김(2026-09-14) — 부르던 곳은 그대로 여기서.
|
||||
export type { InletStructureKind };
|
||||
|
||||
/** 시설 확장 정보 한 건 — 관 지점(chainage)에 얹힌다. 배관(pipe)도 부속 옵션을
|
||||
* 가지면 저장한다. */
|
||||
@@ -382,7 +364,15 @@ export function createFacilityOptionsForm(
|
||||
// 비우면 그 측점의 **노면 횡단경사**를 그대로 쓴다 — 횡단도가 판단한다.
|
||||
const fordSlope = numberInput("0.1");
|
||||
fordSlope.placeholder = "노면 기울기";
|
||||
const fordSlopeRow = grid(labeled("바닥 경사 유입→유출 (%)", stepper(fordSlope, 0.1)));
|
||||
// 포장 두께·노폭 방향 길이 — 수량(㎡ = 월류 폭 × 길이 · 두께로 원단위 고름)이 읽는 칸(2026-09-14 A3).
|
||||
// 비우면 표가 안 서고 그 까닭이 뜸 — 지어내지 않는다.
|
||||
const fordThickness = numberInput("1");
|
||||
const fordLength = numberInput("0.1");
|
||||
const fordSlopeRow = grid(
|
||||
labeled("바닥 경사 유입→유출 (%)", stepper(fordSlope, 0.1)),
|
||||
labeled("포장 두께 (㎝)", stepper(fordThickness, 1, 0)),
|
||||
labeled("포장 길이 노폭 방향 (m)", stepper(fordLength, 0.1)),
|
||||
);
|
||||
const fordSummary = document.createElement("p");
|
||||
fordSummary.className = "b05-drainage__facility-note";
|
||||
/** 담당 유역 설계유량(㎥/s) — 개략 단면의 입력. 유역이 없으면 null. */
|
||||
@@ -634,6 +624,8 @@ export function createFacilityOptionsForm(
|
||||
// 새 시설을 올리는 참이다 — 저장된 높이는 사용자 값으로 보고 자동 추적을 끊는다.
|
||||
fordAutoDepthM = null;
|
||||
fordSlope.value = text("ford_slope_pct");
|
||||
fordThickness.value = text("thickness_cm");
|
||||
fordLength.value = text("length_m");
|
||||
revetSide.value = text("side") || "양쪽";
|
||||
const spread = legacyRevetOptions(options);
|
||||
revetInlet.fields.write(spread);
|
||||
@@ -679,6 +671,8 @@ export function createFacilityOptionsForm(
|
||||
putNumber(options, "ford_width_m", fordWidth.value);
|
||||
putFordHeight(options);
|
||||
putNumber(options, "ford_slope_pct", fordSlope.value);
|
||||
putNumber(options, "thickness_cm", fordThickness.value);
|
||||
putNumber(options, "length_m", fordLength.value);
|
||||
} else if (current === "ford_bridge") {
|
||||
options.pipe_kind = pipeMaterial.value;
|
||||
options.pipe_diameter_mm = Number(pipeDiameter.value);
|
||||
|
||||
@@ -10,6 +10,32 @@
|
||||
|
||||
import { FORD_MANNING_N, FORD_SLOPE } from "@config/config_frontend";
|
||||
|
||||
/* ── 유입구 구조 ──────────────────────────────────────────────────────────── */
|
||||
|
||||
/** B06 유입측 구조물 형식 — 조정창 드롭다운과 같은 값 집합(2026-08-29 병합).
|
||||
* 문자열 리터럴로 두어 B05가 B06 모듈에 기대지 않게 한다. */
|
||||
export type InletStructureKind = "auto" | "revet" | "I" | "L" | "U";
|
||||
|
||||
/** 유입구 "구조" 목록 — 조정창의 구조물 형식(자동/기슭막이/집수정 I·ㄴ·ㄷ)을 이 한
|
||||
* 드롭다운으로 합쳤다(2026-08-29 사용자 지시 5: 일단 리스트를 합치고 중복은 뒤에 뺀다).
|
||||
* 고른 값 하나가 정본 둘을 정한다 — 관 옵션 `inlet_type`(기슭막이/집수정)과 B06
|
||||
* 유입측 형식(`inlet_structure`). */
|
||||
export const INLET_KINDS: ReadonlyArray<{
|
||||
label: string;
|
||||
type: "기슭막이" | "집수정";
|
||||
structure: InletStructureKind;
|
||||
}> = [
|
||||
{ label: "기슭막이", type: "기슭막이", structure: "revet" },
|
||||
{ label: "집수정", type: "집수정", structure: "auto" },
|
||||
{ label: "자동(규칙)", type: "기슭막이", structure: "auto" },
|
||||
{ label: "집수정 I형", type: "집수정", structure: "I" },
|
||||
{ label: "집수정 ㄴ형", type: "집수정", structure: "L" },
|
||||
{ label: "집수정 ㄷ형", type: "집수정", structure: "U" },
|
||||
];
|
||||
|
||||
export const inletKindOf = (label: string): (typeof INLET_KINDS)[number] =>
|
||||
INLET_KINDS.find((kind) => kind.label === label) ?? INLET_KINDS[0];
|
||||
|
||||
/* ── 물넘이·세월교 개략 단면 ────────────────────────────────────────────── */
|
||||
|
||||
/** 월류 단면 한 벌 — 설계유량을 흘리는 데 **필요한** 수심·단면적과 그때의 유속. */
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_Drainage_Facility_Merge.ts
|
||||
* 계곡 통과 시설 [저장] — **폼이 아는 칸만 갈아 끼우고, 폼이 모르는 칸은 건드리지 않음**
|
||||
* (2026-09-14 브레인 판정).
|
||||
*
|
||||
* ⚠ 앞서 저장이 옵션을 **통째로** 갈아 끼웠음 — 구조물 집계표·구조물도로 적은 칸(관 기슭막이
|
||||
* 기초 `revet_foundation` · 독립 기슭막이 `foundation`·뒷길이·돌 종류 …)이 B05 에서 그 시설을
|
||||
* 한 번 더 저장하면 **조용히 지워졌음**. 「작업본 쓰기는 [저장]에서만」인데 그 [저장]이 옆 칸까지
|
||||
* 지우던 자리.
|
||||
* ⚠ 통째로 갈아 끼우는 것은 **시설 종류 자체를 바꿀 때만** — 그때는 옛 칸이 뜻을 잃음.
|
||||
* ⚠ 폼이 아는 칸을 폼이 비워 보내면(값을 지움·유입구를 집수정 → 기슭막이로 바꿔 집수정 칸이 빠짐)
|
||||
* 그 칸은 지워짐 — 사용자가 폼에서 한 일이므로 맞음.
|
||||
* ⚠ 폼에 칸을 늘리면 아래 목록에도 넣을 것. 빠뜨리면 그 칸을 **폼에서 지워도 옛 값이 남는**
|
||||
* 쪽으로 틀림(지워지는 쪽보다 덜 위험). 거울 시험 `test_b05_facility_options_merge`.
|
||||
* ⚠ import 없음 — 시험이 이 파일만 옮겨 돌림.
|
||||
* ========================================================================== */
|
||||
|
||||
const revetKeys = (side: "inlet" | "outlet"): string[] =>
|
||||
["form", "length_m", "height_m", "before_m", "after_m"].map((key) => `${side}_revet_${key}`);
|
||||
const wingKeys = (side: "in" | "out"): string[] => [
|
||||
`wing_${side}`,
|
||||
`wing_${side}_height_m`,
|
||||
`wing_${side}_length_m`,
|
||||
`wing_${side}_angle_deg`,
|
||||
];
|
||||
|
||||
/** 시설 종류별로 **폼이 적는 칸** — `createFacilityOptionsForm().readOptions()` 가 쓰는 키 전부. */
|
||||
export const FORM_OPTION_KEYS: Readonly<Record<string, readonly string[]>> = {
|
||||
pipe: [
|
||||
"pipe_diameter_mm",
|
||||
"pipe_kind",
|
||||
"wing_wall_type",
|
||||
"inlet_type",
|
||||
"inlet_structure",
|
||||
"inlet_basin_form",
|
||||
"inlet_basin_material",
|
||||
"inlet_basin_length_m",
|
||||
...revetKeys("inlet"),
|
||||
"outlet_type",
|
||||
...revetKeys("outlet"),
|
||||
],
|
||||
box_culvert: ["body_width_m", "body_height_m", ...wingKeys("in"), ...wingKeys("out")],
|
||||
ford_pavement: ["ford_width_m", "ford_height_m", "ford_slope_pct", "thickness_cm", "length_m"],
|
||||
ford_bridge: [
|
||||
"pipe_kind",
|
||||
"pipe_diameter_mm",
|
||||
"pipe_count",
|
||||
"ford_width_m",
|
||||
"ford_height_m",
|
||||
...wingKeys("in"),
|
||||
...wingKeys("out"),
|
||||
],
|
||||
revetment: ["side", ...revetKeys("inlet"), ...revetKeys("outlet")],
|
||||
};
|
||||
|
||||
interface MergeableAttributes {
|
||||
facility: string;
|
||||
start_m?: number;
|
||||
end_m?: number;
|
||||
options?: Record<string, string | number>;
|
||||
}
|
||||
|
||||
/** 저장할 시설 정보 — 같은 종류면 폼이 모르는 옛 칸을 이어 붙임. */
|
||||
export function mergeFacilityOptions<T extends MergeableAttributes>(
|
||||
previous: T | null,
|
||||
next: T,
|
||||
): T {
|
||||
if (!previous || previous.facility !== next.facility) return next;
|
||||
const managed = new Set(FORM_OPTION_KEYS[next.facility] ?? []);
|
||||
const kept = Object.entries(previous.options ?? {}).filter(([key]) => !managed.has(key));
|
||||
const options = { ...Object.fromEntries(kept), ...(next.options ?? {}) };
|
||||
return { ...next, options: Object.keys(options).length ? options : undefined };
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import { buildStrengthArray } from "../B04_PreProcess/B04_PreProcess_UI_FlowRamp
|
||||
import { resampleRoute } from "../B04_PreProcess/B04_PreProcess_UI_RouteSamples";
|
||||
import { createPipeEditor } from "./B05_Profile_UI_Drainage_Pipes";
|
||||
import { createFacilityStore } from "./B05_Profile_UI_Drainage_Facility";
|
||||
import { mergeFacilityOptions } from "./B05_Profile_UI_Drainage_Facility_Merge";
|
||||
import { writePendingPipes } from "./B05_Profile_Api_Pipes_Draft";
|
||||
import { createDrainageChrome } from "./B05_Profile_UI_Drainage_Chrome";
|
||||
import { bindDrainageInteractions } from "./B05_Profile_UI_Drainage_Interact";
|
||||
@@ -607,8 +608,10 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
pipeEditor.addAtChainage(chainageM);
|
||||
},
|
||||
updatePipeFacility(fromChainageM, toChainageM, attributes) {
|
||||
// ⚠ 통째로 갈아 끼우지 않음 — 폼이 모르는 칸(집계표로 적은 기초 등)을 지키려 이어 붙임.
|
||||
const previous = facilityStore.get(fromChainageM);
|
||||
facilityStore.set(fromChainageM, null);
|
||||
facilityStore.set(toChainageM, attributes);
|
||||
facilityStore.set(toChainageM, mergeFacilityOptions(previous, attributes));
|
||||
if (Math.abs(fromChainageM - toChainageM) > 0.005) {
|
||||
// 기준점이 옮겨졌다 — 관을 이동시키면 onCommit이 재계산을 돌리고,
|
||||
// attach가 새 위치로 시설 정보를 승계한다.
|
||||
|
||||
@@ -273,7 +273,12 @@ def expand_observed(
|
||||
if scale <= 0:
|
||||
from B08_Quantity.B08_Quantity_Wording import type_label
|
||||
|
||||
return [], [f"{type_label(type_id)}의 연장·면적이 0 이라 물량을 내지 않았습니다"]
|
||||
where = (
|
||||
"면적(월류 폭 × 포장 길이)이 0 — 두 칸을 적으면 섬"
|
||||
if found.get("unit") == "㎡"
|
||||
else "연장이 0 — 연장을 적으면 섬"
|
||||
)
|
||||
return [], [f"{type_label(type_id)}의 {where}"]
|
||||
|
||||
source_key = str(found.get("source") or "")
|
||||
options = structure.get("options") or {}
|
||||
|
||||
@@ -519,7 +519,9 @@ def open_ditch(length_m: float, options: dict[str, Any]) -> tuple[list[Component
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import DESTINATION
|
||||
|
||||
if length_m <= 0:
|
||||
return [], ["연장이 없어 전개하지 않음"]
|
||||
from B08_Quantity.B08_Quantity_Wording import option_missing
|
||||
|
||||
return [], [option_missing("length_m", "open_ditch")]
|
||||
spec = str(options.get("ditch_spec") or "콘크리트 개거 150×200").strip()
|
||||
table = OPEN_DITCH_FORMS.get(spec)
|
||||
if table is None:
|
||||
|
||||
@@ -28,6 +28,7 @@ TYPE_LABELS = {
|
||||
"pipe": "배수관",
|
||||
"pipe_inlet_basin": "배수관 유입부 집수정",
|
||||
"ford_pavement": "물넘이포장",
|
||||
"open_ditch": "개거(겉도랑)",
|
||||
"ford_bridge": "세월교",
|
||||
"box_culvert": "BOX암거",
|
||||
# 사방 시설 — 레지스트리 이름 그대로 옮긴 것(2026-09-08 확인). 지어낸 이름 아님.
|
||||
@@ -44,6 +45,8 @@ OPTION_LABELS = {
|
||||
"form": ("옹벽 형식", "구조물 상세 입력"),
|
||||
"height_m": ("높이(m)", "구조물 배치"),
|
||||
"length_m": ("연장(m)", "구조물 배치"),
|
||||
# 물넘이포장(관 지점 시설) — B05 시설 칸 · 구조물 집계표에서 적음(2026-09-14 A3).
|
||||
"thickness_cm": ("포장 두께(㎝)", "구조물 배치 물넘이포장 칸 · 구조물 집계표"),
|
||||
# 2026-09-09 칸이 생겼음(`ebdf2988`) — 「아직 칸이 없음」이 거짓이 되어 고침.
|
||||
# ⚠ 비워 두면 품셈 표준경사표로 자동 판정된다(확정 ⑨).
|
||||
"face_slope_ratio": ("전면 기울기", "구조물 상세 입력 — 비우면 품셈 표준경사로 자동"),
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"""B05 시설 [저장]이 폼이 모르는 칸을 안 지우는가 (2026-09-14 브레인 판정).
|
||||
|
||||
⚠ 앞서 저장이 옵션을 통째로 갈아 끼워, 구조물 집계표·구조물도로 적은 칸(관 기슭막이 기초 등)이
|
||||
B05 에서 그 시설을 한 번 더 저장하면 조용히 지워졌음 — 되돌릴 길이 없는 사고.
|
||||
짝: `B05_Profile/B05_Profile_UI_Drainage_Facility_Merge.ts` — 실제 TS 를 옮겨 돌림.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
|
||||
MERGE_TS = ROOT / "B05_Profile" / "B05_Profile_UI_Drainage_Facility_Merge.ts"
|
||||
FORM_TS = ROOT / "B05_Profile" / "B05_Profile_UI_Drainage_Facility.ts"
|
||||
|
||||
RUNNER = """
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { FORM_OPTION_KEYS, mergeFacilityOptions } from "./B05_Profile_UI_Drainage_Facility_Merge.js";
|
||||
const cases = JSON.parse(readFileSync(process.argv[2], "utf8"));
|
||||
const out = cases.map(([previous, next]) => mergeFacilityOptions(previous, next));
|
||||
writeFileSync(process.argv[3], JSON.stringify({ out, keys: FORM_OPTION_KEYS }));
|
||||
"""
|
||||
|
||||
PIPE_BEFORE = {
|
||||
"facility": "pipe",
|
||||
"options": {
|
||||
"pipe_diameter_mm": 1000,
|
||||
"inlet_type": "집수정",
|
||||
"inlet_basin_form": "□형(기본형)",
|
||||
"revet_foundation": "기초유", # 집계표로 적은 칸 — 폼에 없음
|
||||
"inlet_basin_before_m": 1.5, # 횡단(B06)이 적는 칸 — 폼에 없음
|
||||
},
|
||||
}
|
||||
# 폼이 다시 저장 — 유입구를 기슭막이로 바꿔 집수정 칸은 안 보냄
|
||||
PIPE_FORM = {
|
||||
"facility": "pipe",
|
||||
"options": {
|
||||
"pipe_diameter_mm": 800,
|
||||
"inlet_type": "기슭막이",
|
||||
"inlet_revet_form": "돌쌓기(찰)",
|
||||
},
|
||||
}
|
||||
REVET_BEFORE = {"facility": "revetment", "options": {"side": "양쪽", "back_len_cm": "35"}}
|
||||
TYPE_CHANGE = {"facility": "ford_pavement", "options": {"ford_width_m": 5}}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def merged(tmp_path_factory) -> dict:
|
||||
if not TSC.is_file():
|
||||
pytest.skip("프론트엔드 의존성 미설치")
|
||||
out = tmp_path_factory.mktemp("merge_js")
|
||||
subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
[
|
||||
"node",
|
||||
str(TSC),
|
||||
str(MERGE_TS),
|
||||
"--outDir",
|
||||
str(out),
|
||||
"--module",
|
||||
"esnext",
|
||||
"--target",
|
||||
"es2022",
|
||||
"--moduleResolution",
|
||||
"bundler",
|
||||
"--ignoreConfig",
|
||||
],
|
||||
cwd=str(ROOT),
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
(out / "runner.mjs").write_text(RUNNER, encoding="utf-8")
|
||||
cases = [
|
||||
[PIPE_BEFORE, PIPE_FORM],
|
||||
[REVET_BEFORE, {"facility": "revetment", "options": {"side": "좌"}}],
|
||||
[PIPE_BEFORE, TYPE_CHANGE],
|
||||
[None, PIPE_FORM],
|
||||
]
|
||||
(out / "cases.json").write_text(json.dumps(cases, ensure_ascii=False), encoding="utf-8")
|
||||
subprocess.run( # noqa: S603
|
||||
["node", str(out / "runner.mjs"), str(out / "cases.json"), str(out / "result.json")],
|
||||
cwd=str(ROOT),
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
return json.loads((out / "result.json").read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test_폼이_모르는_칸은_남고_폼이_아는_칸은_폼대로(merged) -> None:
|
||||
options = merged["out"][0]["options"]
|
||||
assert options["revet_foundation"] == "기초유" # 집계표 값이 살아남음
|
||||
assert options["inlet_basin_before_m"] == 1.5
|
||||
assert options["pipe_diameter_mm"] == 800 # 폼 값이 이김
|
||||
assert "inlet_basin_form" not in options # 폼이 아는 칸을 폼이 비움 → 지움
|
||||
assert merged["out"][1]["options"] == {"side": "좌", "back_len_cm": "35"}
|
||||
|
||||
|
||||
def test_시설_종류를_바꾸면_통째로_갈아_끼움(merged) -> None:
|
||||
assert merged["out"][2] == TYPE_CHANGE
|
||||
assert merged["out"][3] == PIPE_FORM
|
||||
|
||||
|
||||
def test_폼이_적는_칸이_모두_목록에_있다(merged) -> None:
|
||||
"""폼에 칸을 늘리고 목록에 안 넣으면 여기서 잡힘."""
|
||||
source = FORM_TS.read_text(encoding="utf-8")
|
||||
body = source[source.index("readOptions() {") :]
|
||||
branches = re.split(r'current === "(\w+)"', body)[1:]
|
||||
assert branches[::2] == ["pipe", "box_culvert", "ford_pavement", "ford_bridge", "revetment"]
|
||||
for facility, block in zip(branches[::2], branches[1::2]):
|
||||
written = set(re.findall(r'putNumber\(options, "(\w+)"', block))
|
||||
written |= set(re.findall(r"options\.(\w+) =", block))
|
||||
missing = written - set(merged["keys"][facility])
|
||||
assert not missing, f"{facility}: 폼이 적는데 목록에 없음 {missing}"
|
||||
@@ -0,0 +1,60 @@
|
||||
"""등록부 칸 채우기 A2·A3 (2026-09-14) — 표가 읽는 칸이 등록부에 없어 영영 안 서던 자리.
|
||||
|
||||
A2 개거(점 배치) — 연장 칸 · A3 물넘이포장(관 지점 시설) — 두께·노폭 방향 길이 칸.
|
||||
⚠ 옛 저장본(칸 없음)은 안 깨지고 **사유가 보임** — 「값이 없으면 표가 안 선다」가 아니라 「사유를 보인다」.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B05_Profile.B05_Profile_Structures_Repository import _validate_types # noqa: E402
|
||||
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance, structure_type_map # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Engine_Pipe import facility_structures # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table # noqa: E402
|
||||
|
||||
|
||||
def _ditch(**options) -> dict:
|
||||
return build_table(
|
||||
[{"structure_id": "d", "type_id": "open_ditch", "chainage_m": 10.0, "options": options}]
|
||||
)["structures"][0]
|
||||
|
||||
|
||||
def test_개거_연장_칸이_등록부에_서고_구조물_목록에_저장된다() -> None:
|
||||
keys = {option.key for option in structure_type_map()["open_ditch"].options}
|
||||
assert "length_m" in keys
|
||||
item = StructureInstance(
|
||||
type_id="open_ditch", placement="point", chainage_m=10.0, options={"length_m": 12}
|
||||
)
|
||||
_validate_types([item]) # 등록부에 없는 칸이면 여기서 거절됨
|
||||
|
||||
|
||||
def test_개거_연장이_있으면_표가_서고_없으면_어디서_적는지_사유() -> None:
|
||||
assert _ditch(length_m=12)["components"]
|
||||
old = _ditch() # 옛 저장본 — 칸이 없음
|
||||
assert not old["components"]
|
||||
assert old["notes"][0].startswith("개거(겉도랑) 연장(m)이(가) 아직 입력되지 않았습니다")
|
||||
|
||||
|
||||
def _ford(**options) -> dict:
|
||||
point = {"facility": "ford_pavement", "chainage_m": 30.0, "options": options}
|
||||
return build_table(facility_structures([point]))["structures"][0]
|
||||
|
||||
|
||||
def test_물넘이포장_두께_길이_칸이_있으면_면적으로_선다() -> None:
|
||||
keys = {option.key: option for option in structure_type_map()["ford_pavement"].options}
|
||||
assert keys["thickness_cm"].phase == "detail" and keys["length_m"].phase == "detail"
|
||||
row = _ford(ford_width_m=5, thickness_cm=20, length_m=8)
|
||||
assert row["components"] and (row["billing_unit"], row["billing_quantity"]) == ("㎡", 40.0)
|
||||
|
||||
|
||||
def test_물넘이포장_칸이_비면_무엇을_적을지_사유() -> None:
|
||||
old = _ford(ford_width_m=5)
|
||||
assert not old["components"] and "포장 두께(㎝)" in old["notes"][0]
|
||||
no_length = _ford(ford_width_m=5, thickness_cm=20)
|
||||
assert not no_length["components"] and "포장 길이" in no_length["notes"][0]
|
||||
Reference in New Issue
Block a user