Merge remote-tracking branch 'origin/sub_laptop_6' into main_laptop_1
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
/* PLAN 260 단위검증 헬퍼 — [상세] 표 위 CAD 칸 · 옛 글 옮김 · 횡단배수 미리 넣음.
|
||||
* `B06_Section_UI_Cross_Detail_Cad.ts` 를 그 자리에서 트랜스파일해 Node 로 돌린다(CAD · 세션 · 서버 = 흉내).
|
||||
* 결과는 표준출력에 JSON 한 벌. 확장자 `.cjs` 까닭은 `helper_108_drag_box_hit.cjs` 머리와 같다.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const STUBS = {
|
||||
"@ui/cad_host/cad_host": { createCadHost: () => ({}) },
|
||||
"../A00_Common/b_page_state": { readState: () => null, writeState: () => undefined },
|
||||
"../A00_Common/design/design_selection": { PIPE_MATCH_M: 0.51 },
|
||||
"../B04_PreProcess/B04_PreProcess_Api_Fetch": { fetchDetailPipePoints: async () => ({}) },
|
||||
"../B05_Profile/B05_Profile_Api_Pipes_Draft": { readPendingPipes: () => null },
|
||||
};
|
||||
|
||||
function loadTs(file) {
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
const localRequire = (request) => {
|
||||
if (STUBS[request]) return STUBS[request];
|
||||
throw new Error(`흉내 없는 모듈: ${request}`);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const cad = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Detail_Cad.ts"));
|
||||
|
||||
const wall = (role, form, height, length) => ({
|
||||
role,
|
||||
structure: "기슭막이",
|
||||
revet_form: form,
|
||||
revet_height_m: height,
|
||||
revet_length_m: length,
|
||||
});
|
||||
const culvert = (extra = {}) => ({
|
||||
type: "pipe",
|
||||
chainage_m: 1433,
|
||||
pipe_kind: null,
|
||||
diameter_m: 1.2,
|
||||
min_cover_m: 0.5,
|
||||
inlet: wall("inlet", "돌쌓기(찰)", 2, 10),
|
||||
outlet: wall("outlet", "돌쌓기(메)", 2, 10),
|
||||
...extra,
|
||||
});
|
||||
const section = (spec, design = { pipe_length_m: 22 }, chainage = 1433) => ({
|
||||
chainage_m: chainage,
|
||||
station_id: "s",
|
||||
samples: [],
|
||||
culvert: spec,
|
||||
design,
|
||||
});
|
||||
const CHUTE = { chute_add_1_structure: "돌붙임", chute_add_1_bond: "찰붙임", chute_add_1_area: 10 };
|
||||
|
||||
/** 엔티티 → 비교하기 쉬운 꼴(종류 · 글 · 자리 · 색). */
|
||||
const brief = (entities) =>
|
||||
entities.map((entity) => ({
|
||||
type: entity.type,
|
||||
label: entity.shapeData.label ?? null,
|
||||
align: entity.shapeData.options?.textAlign ?? null,
|
||||
x: entity.shapeData.basePoint?.x ?? entity.shapeData.center?.x ?? null,
|
||||
y: entity.shapeData.basePoint?.y ?? entity.shapeData.center?.y ?? null,
|
||||
color: entity.lineColor,
|
||||
layer: entity.layerId,
|
||||
}));
|
||||
const run = (sec, options) => brief(cad.culvertNoteEntities(sec, "NO.71+13", options));
|
||||
|
||||
const out = {
|
||||
full: run(section(culvert()), CHUTE),
|
||||
noLength: run(section(culvert(), {}), {}),
|
||||
basin: run(
|
||||
section(
|
||||
culvert({
|
||||
inlet: {
|
||||
role: "inlet",
|
||||
structure: "집수정",
|
||||
basin_form: "L",
|
||||
basin_height_m: 1.7,
|
||||
basin_length_m: 2,
|
||||
},
|
||||
}),
|
||||
),
|
||||
{},
|
||||
),
|
||||
chuteFallback: run(section(culvert({ chute: { structure: "도수로", thickness_m: 0.45 } })), {}),
|
||||
noSides: run(
|
||||
section(
|
||||
culvert({
|
||||
inlet: { role: "inlet", structure: "" },
|
||||
outlet: { role: "outlet", structure: "" },
|
||||
}),
|
||||
),
|
||||
{},
|
||||
),
|
||||
none: run(section(undefined), CHUTE),
|
||||
ford: run(section(culvert({ body: "ford" })), CHUTE),
|
||||
hidden: run(section(culvert({ hidden_pipe: true })), CHUTE),
|
||||
neighbour: run(section(culvert(), { pipe_length_m: 22 }, 1440), CHUTE),
|
||||
legacy: brief(cad.legacyNoteEntities("\nL형 측구 \n\n집수정 보강\n")),
|
||||
legacyEmpty: cad.legacyNoteEntities(" ").length,
|
||||
station: [
|
||||
cad.stationNoLabel(1433, 20),
|
||||
cad.stationNoLabel(1420, 20),
|
||||
cad.stationNoLabel(1439.97, 20),
|
||||
],
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -0,0 +1,157 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""PLAN 260 — B06 [상세] 표 위 CAD 칸(`structure_note_cad`) → B07 횡단도 표 위.
|
||||
|
||||
- 260-1 칸: 스키마 · 재계산 남김 · 세션 초안 → [저장] patch.
|
||||
- 260-2 미리 넣음: 횡단배수 주인 측점만 · 관 · 유입 · 유출 · 도수로 · 빈 구조물 · 옛 글 옮김
|
||||
(`helper_260_note_cad.cjs` 가 실제 TS 를 Node 로 돌림).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_260_note_cad.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from B06_Section import B06_Section_Router_Design as design_mod # noqa: E402
|
||||
from B06_Section.B06_Section_Schema import CrossSectionPatch # noqa: E402
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
TEXT = {
|
||||
"id": "t1",
|
||||
"type": "Text",
|
||||
"lineColor": "#ff7f7f",
|
||||
"lineWidth": 1,
|
||||
"layerId": "기본",
|
||||
"shapeData": {"label": "NO.71+13", "basePoint": {"x": 15, "y": -1.5}, "options": {}},
|
||||
}
|
||||
CAD = {"entities": [TEXT], "layers": [{"id": "기본", "name": "기본"}]}
|
||||
|
||||
# ── 260-1 칸 ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_CAD_칸은_patch_스키마를_지나_그대로_남는다():
|
||||
dumped = CrossSectionPatch(chainage_m=40.0, structure_note_cad=CAD).model_dump()
|
||||
merged = {k: v for k, v in dumped.items() if k != "chainage_m" and v is not None}
|
||||
assert merged == {"structure_note_cad": CAD}
|
||||
# 빈 엔티티 = 비움 — 걸러지지 않고 실림(옛 글 대신 아무것도 안 놓음)
|
||||
empty = CrossSectionPatch(chainage_m=40.0, structure_note_cad={"entities": []}).model_dump()
|
||||
assert empty["structure_note_cad"] == {"entities": [], "layers": None}
|
||||
# 안 보냄 = 안 바꿈
|
||||
assert CrossSectionPatch(chainage_m=40.0).model_dump()["structure_note_cad"] is None
|
||||
|
||||
|
||||
def test_CAD_칸은_엔티티_수_상한():
|
||||
with pytest.raises(ValueError):
|
||||
CrossSectionPatch(chainage_m=40.0, structure_note_cad={"entities": [TEXT] * 2001})
|
||||
|
||||
|
||||
def test_CAD_칸은_재계산에서도_남는다(monkeypatch):
|
||||
assert "structure_note_cad" in design_mod.USER_TOUCHED_KEYS
|
||||
monkeypatch.setattr(design_mod, "pavement_ranges", lambda root: [(0.0, 100.0)])
|
||||
monkeypatch.setattr(design_mod, "ford_surface_drops", lambda root: [])
|
||||
monkeypatch.setattr(design_mod, "design_elevation_from_longitudinal", lambda lon, ch: 100.0)
|
||||
monkeypatch.setattr(design_mod, "curve_widening_args", lambda section: {})
|
||||
monkeypatch.setattr(
|
||||
design_mod, "compute_cross_design", lambda samples, elev, **kw: {"paved": kw["paved"]}
|
||||
)
|
||||
sections = [{"chainage_m": 40.0, "design": {"paved": False, "structure_note_cad": CAD}}]
|
||||
assert design_mod.enforce_pavement_ranges({}, sections, Path("."), None) == 1
|
||||
assert sections[0]["design"]["structure_note_cad"] == CAD
|
||||
|
||||
|
||||
def test_브라우저가_CAD_칸을_저장_patch_에_싣는다():
|
||||
read = lambda rel: (PROJECT_ROOT / rel).read_text(encoding="utf-8") # noqa: E731
|
||||
persist = read("B06_Section/B06_Section_UI_Page_Persist.ts")
|
||||
assert "readStructureNoteCads(" in persist and ".structure_note_cad = cad" in persist
|
||||
state = read("A00_Common/b_page_state.ts")
|
||||
assert '"structure-note-cads": { bucket: "draft", scope: "route" }' in state
|
||||
# 캐시 설계 → B05 [저장] 길(`crossPatchesFromCache`)도 같은 목록
|
||||
assert '"structure_note_cad",' in read("B06_Section/B06_Section_Cross_Refresh.ts")
|
||||
# 모달 = 빈 CAD(캔버스만) · 글 상자 없음
|
||||
detail = read("B06_Section/B06_Section_UI_Cross_Detail.ts")
|
||||
assert "textarea" not in detail and "mountNoteCad(" in detail and "noteCad?.destroy()" in detail
|
||||
assert "bare: true" in read("B06_Section/B06_Section_UI_Cross_Detail_Cad.ts")
|
||||
|
||||
|
||||
# ── 260-2 미리 넣음 ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out() -> dict:
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_260_note_cad.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def _labels(entities: list[dict]) -> list[str]:
|
||||
return [entity["label"] for entity in entities if entity["type"] == "Text"]
|
||||
|
||||
|
||||
def test_관_유입_유출_도수로_한_벌(out):
|
||||
"""그림(162-21) 꼴 — 원 안 세 줄 · 오른쪽 측점 · 관보호공 둘 · 관유출부보강."""
|
||||
full = out["full"]
|
||||
assert [entity["type"] for entity in full[:2]] == ["Circle", "Line"]
|
||||
assert _labels(full) == [
|
||||
"배수관",
|
||||
"Φ1200m/m",
|
||||
"L=22.0m",
|
||||
"NO.71+13",
|
||||
"관보호공(유입) 기슭막이(찰,H=2.0) L=10.0m",
|
||||
"관보호공(유출) 기슭막이(메,H=2.0) L=10.0m",
|
||||
"관유출부보강(돌붙임,찰) 10m2",
|
||||
]
|
||||
# 원 안 = 가운데 맞춤 · 원 중심 x · 오른쪽 = 왼쪽 맞춤 · 줄 3.0 간격 · 붉은 계열 · 기본 층
|
||||
inside, right = full[2:5], full[5:]
|
||||
assert {(e["align"], e["x"]) for e in inside} == {("center", full[0]["x"])}
|
||||
assert [e["y"] for e in right] == [-1.5, -4.5, -7.5, -10.5]
|
||||
assert {e["align"] for e in right} == {"left"}
|
||||
assert {e["color"] for e in full} == {"#ff7f7f"} and {e["layer"] for e in full} == {"기본"}
|
||||
|
||||
|
||||
def test_관_길이_없으면_L_뒤_빈칸(out):
|
||||
assert _labels(out["noLength"])[2] == "L="
|
||||
# 도수로 상자가 없으면 관유출부보강 줄 없음
|
||||
assert not any(label.startswith("관유출부보강") for label in _labels(out["noLength"]))
|
||||
|
||||
|
||||
def test_유입_집수정_은_형태_높이_길이(out):
|
||||
assert "관보호공(유입) 집수정(L형,H=1.7) L=2.0m" in _labels(out["basin"])
|
||||
|
||||
|
||||
def test_도수로_옵션이_없으면_세트_구조만(out):
|
||||
assert _labels(out["chuteFallback"])[-1] == "관유출부보강(도수로) m2"
|
||||
|
||||
|
||||
def test_보호공을_안_고르면_관과_측점만(out):
|
||||
assert _labels(out["noSides"]) == ["배수관", "Φ1200m/m", "L=22.0m", "NO.71+13"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("case", ["none", "ford", "hidden", "neighbour"])
|
||||
def test_횡단배수_주인_측점이_아니면_빈값(out, case):
|
||||
"""구조물 없음 · 상자형(세월교 · 박스) · 숨은 관(독립 기슭막이) · 이웃 측점 = 빈 도면."""
|
||||
assert out[case] == []
|
||||
|
||||
|
||||
def test_옛_글은_줄마다_Text_로_옮김(out):
|
||||
legacy = out["legacy"]
|
||||
assert _labels(legacy) == ["L형 측구", "집수정 보강"]
|
||||
# 빈 줄은 자리만 — B07 옛 글 줄과 같은 높이(줄 3.0 · 가운데)
|
||||
assert [e["y"] for e in legacy] == [-1.5, -7.5] and {e["x"] for e in legacy} == {0}
|
||||
assert out["legacyEmpty"] == 0
|
||||
|
||||
|
||||
def test_측점_이름은_B07_꼴(out):
|
||||
assert out["station"] == ["NO.71+13", "NO.71", "NO.72"]
|
||||
Reference in New Issue
Block a user