feat(design): 구조물 명세 공용 틀 + 횡단배수 배관(표 01) 컨테이너 폼
- 공용 틀(A00_Common/design/design_structure_*) — 명세(컨테이너 필수/선택 · 여러 개 · 항목 고르기/숫자/자동 · 상위 값에 따른 선택지 · 저장 키) · 그리기([추가]/[빼기]) · 길이 = 전 + 후 맞물림(마지막에 고친 둘을 지킴) - 배관 명세 = 관 · 관 보호공(유입) · 관 보호공(유출) · 흙막이 추가 · 도수로/돌붙임 · 저장 키는 pipe_points 옛 키 그대로 · 옛 관종 · 날개벽 · 집수정 형식은 화면에서 빼고 지난 정의 값으로 남김 - 컨테이너 인스턴스마다 「구조물 조합」 잇기(group_items · 그룹 [고르기] 길 그대로) · 명세 종류는 그룹 상자 · 종류 단위 줄 가림 · 종류 단위 잇기 줄은 기준 측점 바로 아래 - 관 길이 자동 = 횡단 기하 computeStoredLayouts 값(서버 Node 와 같은 함수) · 높이 빈 칸 안내 = 관경 기준 최소 높이 - 정의 판 6 — 종류.pipe.이름 표(이름 바꿈 키) · 검사기가 이름 표 받음 · 판 5 이력 보관 - 시험 test_52_structure_spec_pipe · 병합 시험 기대값(빠진 옛 칸은 남음) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"판": 5,
|
||||
"판": 6,
|
||||
"schema_version": 1,
|
||||
"comment": "B05 구조물 타입 레지스트리 정본. 근거·옵션 상세는 PLAN.md 구조물 리스트와 resources/knowledge/technical_info/01_임도 참조. 2026-08-17 선택지 재편: B군(종단배수)·F군(생태/녹화)·G군(노면공)은 enabled:false 보존 — B05 선택지에서 빠지고 B06 개별 횡단도 옵션으로 재사용한다. 바닥막이(bed_sill)·생태연못(eco_pond)은 임도 미사용 확정으로 삭제. 배관·BOX암거·물넘이·세월교는 pipe_points.json 정본 관리(managed_by) — structures.json에 저장하지 않으며, 배관 부속(유형/집수정/기슭막이/돌붙임)·날개벽 옵션 정의는 B06/B07 필수 승격의 선언 근거다. ★기본값 원칙(2026-08-16 크로스체크 2차): default를 두는 것은 ①법정이 등급과 무관하게 못박은 단일 수치 ②사용자가 개발 단계에서 확정한 값 ③도메인 수치가 아닌 표시용 문자열, 이 셋뿐이다. 재료·형식 같은 설계자 판단 선택지와 미확정 제원은 default 없이 required로 두어 사용자가 직접 고르게 한다 (pipe_diameter_mm 1000 = 별표2 원칙값+사용자 확정 — 화이트리스트 등재). BOX암거 본체 2.0×2.0·날개벽 유입/유출 있음·짧은쪽 높이 1m·길이 2m·각도 45°는 2026-08-17 사용자 확정값이다 — 본체 규격은 실무 관측 규격(2.0×2.0·3.0×3.0)을 폼에서 프리셋 select로 받고 필요 시 사용자 지정으로 자유 입력하되, 저장 키는 body_width_m·body_height_m 숫자 그대로다(프리셋 전용 키를 만들지 않는다). 이 값들은 B05 서브폼이 실제로 받으므로 phase를 detail로 두지 않는다. ★보호공 개편(2026-08-17 사용자 지시): 구 `*_pitching`(있음/없음) + `*_pitching_finish`(찰/메) 두 축을 `*_protection` 한 축(돌붙임(찰)·돌붙임(메)·도수로)으로 합쳤다 — 도수로·산비탈수로(B4)는 유출부에서 돌붙임 대신 쓰는 공종이라 별도 구조물이 아니라 보호공 선택지로 받는다. \"없음\"은 삭제(물이 흐르는 자리라 보호공 필수). 치수는 배타 — 돌붙임이면 `*_protection_area_m2`(10㎡), 도수로면 `*_protection_width_m`(1m, 지식DB 수치 근거 없음·사용자 확정, 실무 확인 후 개선). 구 저장분은 `common_util_drainage_pipes.parse_pipe_points`가 새 키로 옮긴다.",
|
||||
"구조물군": {
|
||||
@@ -3232,7 +3232,66 @@
|
||||
"계곡통과시설",
|
||||
"유입유출_묶음",
|
||||
"기슭막이_한벌"
|
||||
]
|
||||
],
|
||||
"이름": {
|
||||
"pipe": "관",
|
||||
"pipe.diameter": "관경",
|
||||
"pipe.diameter=800": "800",
|
||||
"pipe.diameter=1000": "1000",
|
||||
"pipe.diameter=1200": "1200",
|
||||
"pipe.diameter=1500": "1500",
|
||||
"pipe.length": "관 길이 (m)",
|
||||
"inlet": "관 보호공 (유입)",
|
||||
"inlet.structure": "구조",
|
||||
"inlet.structure=집수정": "집수정",
|
||||
"inlet.structure=기슭막이": "흙막이",
|
||||
"inlet.form": "형태",
|
||||
"inlet.form=돌쌓기(찰)": "돌 쌓기(찰)",
|
||||
"inlet.form=돌쌓기(메)": "돌 쌓기(메)",
|
||||
"inlet.form=콘크리트": "콘크리트",
|
||||
"inlet.form=돌망태": "돌망태",
|
||||
"inlet.form=통나무·목재틀": "통나무/목재틀",
|
||||
"inlet.form=바자": "바자",
|
||||
"inlet.form=I": "I형",
|
||||
"inlet.form=L": "L형",
|
||||
"inlet.form=U": "ㄷ형",
|
||||
"inlet.length": "길이 (m)",
|
||||
"inlet.height": "높이 (m)",
|
||||
"inlet.before": "기준측점 전 (m)",
|
||||
"inlet.after": "기준측점 후 (m)",
|
||||
"outlet": "관 보호공 (유출)",
|
||||
"outlet.structure": "구조",
|
||||
"outlet.structure=기슭막이": "흙막이",
|
||||
"outlet.form": "형태",
|
||||
"outlet.form=돌쌓기(찰)": "돌 쌓기(찰)",
|
||||
"outlet.form=돌쌓기(메)": "돌 쌓기(메)",
|
||||
"outlet.form=콘크리트": "콘크리트",
|
||||
"outlet.form=돌망태": "돌망태",
|
||||
"outlet.form=통나무·목재틀": "통나무/목재틀",
|
||||
"outlet.form=바자": "바자",
|
||||
"outlet.length": "길이 (m)",
|
||||
"outlet.height": "높이 (m)",
|
||||
"outlet.before": "기준측점 전 (m)",
|
||||
"outlet.after": "기준측점 후 (m)",
|
||||
"soil_add": "흙막이 추가",
|
||||
"soil_add.structure": "구조",
|
||||
"soil_add.structure=기슭막이": "흙막이",
|
||||
"soil_add.form": "형태",
|
||||
"soil_add.form=돌쌓기(찰)": "돌 쌓기(찰)",
|
||||
"soil_add.form=돌쌓기(메)": "돌 쌓기(메)",
|
||||
"soil_add.form=콘크리트": "콘크리트",
|
||||
"soil_add.form=돌망태": "돌망태",
|
||||
"soil_add.form=통나무·목재틀": "통나무/목재틀",
|
||||
"soil_add.form=바자": "바자",
|
||||
"soil_add.length": "길이 (m)",
|
||||
"soil_add.height": "높이 (m)",
|
||||
"soil_add.before": "기준측점 전 (m)",
|
||||
"soil_add.after": "기준측점 후 (m)",
|
||||
"chute_add": "도수로/돌붙임",
|
||||
"chute_add.structure": "구조",
|
||||
"chute_add.structure=도수로": "도수로",
|
||||
"chute_add.structure=돌붙임": "돌붙임"
|
||||
}
|
||||
},
|
||||
"box_culvert": {
|
||||
"동작": [
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,87 @@
|
||||
/* 구조물 명세 공용 틀 · 배관 명세 단위검증 헬퍼(PLAN 52-1) — `design_structure_spec.ts` ·
|
||||
* `design_structure_pipe.ts` 를 그 자리에서 트랜스파일해 Node 로 돌림(꼴만 import). 결과 = JSON 한 벌.
|
||||
* 인자 `names` 면 배관 이름 표(정의 `종류.pipe.이름`)만 냄.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const load = (file, deps = {}) => {
|
||||
const src = fs.readFileSync(path.join(ROOT, "A00_Common", "design", file), "utf8");
|
||||
const js = ts.transpileModule(src, {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, (m) => deps[m] ?? require(m));
|
||||
return box.exports;
|
||||
};
|
||||
const s = load("design_structure_spec.ts");
|
||||
const { PIPE_SPEC } = load("design_structure_pipe.ts");
|
||||
|
||||
if (process.argv[2] === "names") {
|
||||
process.stdout.write(JSON.stringify(s.nameTable(PIPE_SPEC)));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const old = {
|
||||
pipe_kind: "흄관",
|
||||
wing_wall_type: "A-TYPE",
|
||||
pipe_diameter_mm: 1000,
|
||||
inlet_type: "집수정",
|
||||
inlet_structure: "L",
|
||||
inlet_basin_length_m: 2,
|
||||
inlet_basin_before_m: 1,
|
||||
inlet_basin_after_m: 1,
|
||||
outlet_type: "기슭막이",
|
||||
outlet_revet_form: "돌쌓기(메)",
|
||||
outlet_revet_length_m: 10,
|
||||
outlet_revet_height_m: 2.5,
|
||||
soil_add_2_structure: "기슭막이",
|
||||
soil_add_2_form: "바자",
|
||||
soil_add_5_length_m: 4,
|
||||
chute_add_1_structure: "도수로",
|
||||
};
|
||||
const state = s.readState(PIPE_SPEC, old);
|
||||
const back = s.writeOptions(PIPE_SPEC, state);
|
||||
// 흙막이로 바꿈 → 갈래 키가 기슭막이 쪽 · also(inlet_structure revet)
|
||||
const soil = JSON.parse(JSON.stringify(state));
|
||||
soil.inlet[0].structure = "기슭막이";
|
||||
soil.inlet[0].form = "콘크리트";
|
||||
soil.inlet[0].length = "6";
|
||||
const soilOut = s.writeOptions(PIPE_SPEC, soil);
|
||||
const inlet = PIPE_SPEC.containers.find((c) => c.id === "inlet");
|
||||
const form = inlet.items.find((i) => i.id === "form");
|
||||
const keys = s.specKeys(PIPE_SPEC);
|
||||
|
||||
const out = {
|
||||
containers: PIPE_SPEC.containers.map((c) => [c.id, c.title, c.required]),
|
||||
state,
|
||||
back,
|
||||
soilOut,
|
||||
formBasin: s.choicesOf(form, { structure: "집수정" }).map((c) => c.label),
|
||||
formSoil: s.choicesOf(form, { structure: "기슭막이" }).map((c) => c.label),
|
||||
formNone: s.branchOf(form, {}),
|
||||
outletForms: s
|
||||
.choicesOf(
|
||||
PIPE_SPEC.containers.find((c) => c.id === "outlet").items.find((i) => i.id === "form"),
|
||||
{ structure: "기슭막이" },
|
||||
)
|
||||
.map((c) => c.label),
|
||||
keys,
|
||||
links: PIPE_SPEC.containers.map((c) => s.linkOf(c, 2)),
|
||||
split: {
|
||||
lengthFromSides: s.solveSplit({ length: "10", before: "3", after: "4" }, ["after", "before"]),
|
||||
beforeFromLength: s.solveSplit({ length: "10", before: "3", after: "4" }, ["after", "length"]),
|
||||
defaultKeepsLength: s.solveSplit({ length: "10", before: "3", after: "" }, ["before"]),
|
||||
blankKept: s.solveSplit({ length: "", before: "3", after: "4" }, ["before", "length"]),
|
||||
clamp: s.solveSplit({ length: "2", before: "", after: "5" }, ["after", "length"]),
|
||||
},
|
||||
names: {
|
||||
renamed: s.itemLabel(inlet, form, { "inlet.form": "모양" }),
|
||||
title: s.containerTitle(inlet, {}, "그룹 이름"),
|
||||
titleOwn: s.containerTitle(inlet, { inlet: "유입 보호" }, "그룹 이름"),
|
||||
choice: s.choiceLabel(inlet, form, { value: "U", label: "ㄷ형" }, {}),
|
||||
},
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -0,0 +1,109 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""구조물 명세 공용 틀 + 횡단배수 › 배관(표 01) — PLAN 52-1.
|
||||
|
||||
순수 규칙은 Node 헬퍼(`helper_52_structure_spec.cjs`)가 돌림 · 화면은 ORCA `ui_checks/52-1_배관_컨테이너.js`.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(HERE))
|
||||
|
||||
|
||||
def _node(*args):
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_52_structure_spec.cjs"), *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out():
|
||||
return _node()
|
||||
|
||||
|
||||
def test_컨테이너_다섯_필수_셋_선택_둘(out):
|
||||
assert out["containers"] == [
|
||||
["pipe", "관", True],
|
||||
["inlet", "관 보호공 (유입)", True],
|
||||
["outlet", "관 보호공 (유출)", True],
|
||||
["soil_add", "흙막이 추가", False],
|
||||
["chute_add", "도수로/돌붙임", False],
|
||||
]
|
||||
|
||||
|
||||
def test_형태_선택지는_구조를_따름(out):
|
||||
assert out["formBasin"] == ["I형", "L형", "ㄷ형"]
|
||||
assert out["formSoil"] == [
|
||||
"돌 쌓기(찰)",
|
||||
"돌 쌓기(메)",
|
||||
"콘크리트",
|
||||
"돌망태",
|
||||
"통나무/목재틀",
|
||||
"바자",
|
||||
]
|
||||
assert out["formNone"] is None # 구조 안 고름 = 형태 칸 가림
|
||||
assert out["outletForms"] == out["formSoil"]
|
||||
|
||||
|
||||
def test_옛_키로_읽고_같은_키로_씀_옛_칸은_안_씀(out):
|
||||
back = out["back"]
|
||||
assert back["pipe_diameter_mm"] == 1000
|
||||
assert back["inlet_structure"] == "L" and back["inlet_basin_length_m"] == 2
|
||||
assert back["outlet_revet_height_m"] == 2.5
|
||||
# 표 01 에 없는 옛 칸은 폼이 안 적음(병합이 이어 붙임 = 지난 정의 값)
|
||||
assert "pipe_kind" not in back and "wing_wall_type" not in back
|
||||
|
||||
|
||||
def test_선택_컨테이너는_인스턴스마다_차례대로_다시_매김(out):
|
||||
assert [i["structure"] for i in out["state"]["soil_add"]] == ["기슭막이", ""]
|
||||
assert out["back"]["soil_add_1_form"] == "바자"
|
||||
assert "soil_add_2_form" not in out["back"]
|
||||
assert out["back"]["chute_add_1_structure"] == "도수로"
|
||||
|
||||
|
||||
def test_흙막이로_바꾸면_기슭막이_키와_revet(out):
|
||||
s = out["soilOut"]
|
||||
assert s["inlet_type"] == "기슭막이" and s["inlet_structure"] == "revet"
|
||||
assert s["inlet_revet_form"] == "콘크리트" and s["inlet_revet_length_m"] == 6
|
||||
assert "inlet_basin_length_m" not in s
|
||||
|
||||
|
||||
def test_길이_전_후_맞물림_마지막에_고친_둘을_지킴(out):
|
||||
sp = out["split"]
|
||||
assert sp["lengthFromSides"]["length"] == "7"
|
||||
assert sp["beforeFromLength"]["before"] == "6"
|
||||
assert sp["defaultKeepsLength"]["after"] == "7"
|
||||
assert sp["blankKept"]["length"] == "" # 지킬 칸이 비면 안 셈
|
||||
assert sp["clamp"]["before"] == "0"
|
||||
|
||||
|
||||
def test_잇기_키와_이름_바꿈(out):
|
||||
assert out["links"] == ["grv6lny", "gnbf9m9", "g9lq9uk", "gsoiladd2", "gchuteadd2"]
|
||||
assert out["names"] == {
|
||||
"renamed": "모양",
|
||||
"title": "그룹 이름",
|
||||
"titleOwn": "유입 보호",
|
||||
"choice": "ㄷ형",
|
||||
}
|
||||
assert {"soil_add_", "chute_add_"} == set(out["keys"]["prefixes"])
|
||||
|
||||
|
||||
def test_정의_파일에_이름_표가_올라_있음():
|
||||
names = _node("names")
|
||||
with open(
|
||||
os.path.join(ROOT, "resources", "master_template", "구조물정의.json"), encoding="utf-8"
|
||||
) as f:
|
||||
doc = json.load(f)
|
||||
on_file = doc["종류"]["pipe"]["이름"]
|
||||
assert set(names) == set(on_file)
|
||||
assert on_file["inlet.structure=기슭막이"] == "흙막이"
|
||||
@@ -96,7 +96,8 @@ def test_폼이_모르는_칸은_남고_폼이_아는_칸은_폼대로(merged) -
|
||||
assert options["revet_foundation"] == "기초유" # 집계표 값이 살아남음
|
||||
assert "inlet_basin_before_m" not in options # 폼 칸(24-4a) — 집수정을 걷으면 함께 지움
|
||||
assert options["pipe_diameter_mm"] == 800 # 폼 값이 이김
|
||||
assert "inlet_basin_form" not in options # 폼이 아는 칸을 폼이 비움 → 지움
|
||||
# 표 01 명세(PLAN 52-1)가 뺀 옛 칸 = 폼이 모르는 칸 → 남음(지난 정의 값)
|
||||
assert options["inlet_basin_form"] == "□형(기본형)"
|
||||
assert merged["out"][1]["options"] == {"side": "좌", "back_len_cm": "35"}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user