Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_8
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
/* 공용 표(`ui_template/ui_template_data_table.ts`) 견본 — 토적표 모양 · 머리 3줄 · 30열 · 합계.
|
||||
* 그냥 글(스크립트)로 읽어 씀 — Node 헬퍼와 화면 eval 이 같은 견본을 씀.
|
||||
* const s = sampleDataTable(66); // { columns, rows, total, frozen } */
|
||||
function sampleDataTable(n) {
|
||||
const num = (key, label, group, decimals) => ({ key, label, group, kind: "number", decimals });
|
||||
const trio = (pre, group) => [
|
||||
num(pre + "_area", "단면적", group, 2),
|
||||
num(pre + "_vol", "입적", group, 1),
|
||||
num(pre + "_adj", "보정량", group, 1),
|
||||
];
|
||||
const pair = (pre, group) => [
|
||||
num(pre + "_len", "거리", group, 1),
|
||||
num(pre + "_area", "면적", group, 1),
|
||||
];
|
||||
const columns = [
|
||||
{ key: "sta", label: "측점", width: 80 },
|
||||
num("dist", "거리", [], 0),
|
||||
...trio("cs", ["절토", "토사"]),
|
||||
...trio("cr", ["절토", "연암"]),
|
||||
...trio("ds", ["절토", "측구파기 토사"]),
|
||||
...trio("dr", ["절토", "측구파기 암"]),
|
||||
num("adj_sum", "보정량계", ["절토"], 1),
|
||||
num("fill_area", "단면적", ["성토"], 2),
|
||||
num("fill_vol", "입적", ["성토"], 1),
|
||||
num("reuse", "유용토", [], 1),
|
||||
num("diff", "차인토량", [], 1),
|
||||
num("cum", "누가토량", [], 1),
|
||||
...pair("gf", ["면고르기", "성토면"]),
|
||||
...pair("gc", ["면고르기", "절토면"]),
|
||||
...pair("tf", ["지장목제거", "성토"]),
|
||||
...pair("tc", ["지장목제거", "절토"]),
|
||||
...pair("rd", ["노면정리"]),
|
||||
];
|
||||
const rows = [];
|
||||
let cum = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const r = {
|
||||
sta: i % 5 ? `${120 + Math.floor(i / 5)}+${(i % 5) * 4}` : String(120 + i / 5),
|
||||
dist: i ? 4 : 0,
|
||||
};
|
||||
columns.slice(2).forEach((c, j) => {
|
||||
// 빈 값 · 큰 값 · 음수가 섞이게
|
||||
r[c.key] =
|
||||
(i + j) % 7 === 0
|
||||
? null
|
||||
: Math.round(((i * 37 + j * 101) % 997) * (j % 4 === 0 ? 13.7 : 1.13) * 100) / 100;
|
||||
});
|
||||
r.diff = Math.round((r.fill_vol ?? 0) - (r.cs_vol ?? 0)) / 1;
|
||||
cum += r.diff;
|
||||
r.cum = cum;
|
||||
rows.push(r);
|
||||
}
|
||||
const total = { sta: "합계" };
|
||||
columns.slice(1).forEach((c) => {
|
||||
if (c.key !== "cum") total[c.key] = rows.reduce((s, r) => s + (r[c.key] ?? 0), 0);
|
||||
});
|
||||
return { columns, rows, total, frozen: 1 };
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/* 공용 표(`ui_template/ui_template_data_table.ts`) 단위검증 헬퍼 —
|
||||
* 트랜스파일해 Node 로 견본 표 HTML 을 만들고 판정할 값을 JSON 으로 냄. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const file = path.join(__dirname, "..", "..", "ui_template", "ui_template_data_table.ts");
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
const req = (p) => (p.endsWith(".css") ? {} : require(p));
|
||||
new Function("exports", "module", "require", js)(box.exports, box, req);
|
||||
const m = box.exports;
|
||||
|
||||
const src = fs.readFileSync(path.join(__dirname, "fixture_data_table_sample.js"), "utf8");
|
||||
const sampleDataTable = new Function(src + "; return sampleDataTable;")();
|
||||
|
||||
const s = sampleDataTable(66);
|
||||
const html = m.renderDataTableHtml(s);
|
||||
const head = html.split("<thead>")[1].split("</thead>")[0];
|
||||
const headRows = head.split("</tr>").filter(Boolean);
|
||||
const body = html.split("<tbody>")[1].split("</tbody>")[0];
|
||||
const bodyRows = body.split("</tr>").filter(Boolean);
|
||||
const foot = html.split("<tfoot>")[1].split("</tfoot>")[0];
|
||||
|
||||
const t0 = process.hrtime.bigint();
|
||||
const big = m.renderDataTableHtml(sampleDataTable(1000));
|
||||
const ms = Number(process.hrtime.bigint() - t0) / 1e6;
|
||||
|
||||
const one = (c, v) =>
|
||||
m.formatCell(
|
||||
v,
|
||||
c.kind === "number"
|
||||
? new Intl.NumberFormat("ko-KR", {
|
||||
minimumFractionDigits: c.decimals ?? 0,
|
||||
maximumFractionDigits: c.decimals ?? 0,
|
||||
})
|
||||
: null,
|
||||
)[0];
|
||||
const nc = { key: "a", label: "a", kind: "number", decimals: 2 };
|
||||
const tc = { key: "b", label: "b" };
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
cols: s.columns.length,
|
||||
headRows: headRows.length,
|
||||
firstHeadRow: headRows[0],
|
||||
secondHeadRow: headRows[1],
|
||||
bodyRows: bodyRows.length,
|
||||
firstBodyRow: bodyRows[0],
|
||||
foot,
|
||||
frozenCells: (body.match(/class="is-frozen is-edge"/g) || []).length,
|
||||
bigRows: big.split("<tbody>")[1].split("</tbody>")[0].split("</tr>").length - 1,
|
||||
bigMs: ms,
|
||||
fmt: {
|
||||
thousands: one(nc, 1234567.891),
|
||||
neg: one(nc, -97.9),
|
||||
zero: one(nc, 0),
|
||||
nul: one(nc, null),
|
||||
undef: one(nc, undefined),
|
||||
blank: one(nc, ""),
|
||||
nan: one(nc, NaN),
|
||||
text: one(tc, "120+18"),
|
||||
textNum: one(tc, 5),
|
||||
},
|
||||
win: (() => {
|
||||
const b = m
|
||||
.renderDataTableHtml(sampleDataTable(1000), { from: 61, to: 121, rowH: 25 })
|
||||
.split("<tbody>")[1]
|
||||
.split("</tbody>")[0]
|
||||
.split("</tr>")
|
||||
.filter(Boolean);
|
||||
return { n: b.length, top: b[0], second: b[1].slice(0, 20), bottom: b[b.length - 1] };
|
||||
})(),
|
||||
escaped: m.renderDataTableHtml({ columns: [{ key: "x", label: "<b>" }], rows: [{ x: "a&<" }] }),
|
||||
noTotal: m.renderDataTableHtml({ columns: [tc], rows: [] }).includes("<tfoot>"),
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,73 @@
|
||||
"""추천 「종단 반영」 켜기 · 끄기(PLAN 64-1) — 점수 분기 시험.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_64_1_profile_toggle.py -q
|
||||
|
||||
켬(기본) = 후보마다 균형 종단을 세워 토공 점수를 그것으로(63-4) · 끔 = 종단 없이 원지반 가늠
|
||||
(절 + 성). 끈 요청은 켠 요청이 묶음 캐시에 남긴 종단을 걷어 내고 처음부터 끈 것과 같아야 한다.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial import Terrain, generate_initial_route
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Metrics import COST_EARTH_M3
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Pool import GOALS
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Profile import profile_earth
|
||||
from B05_Profile.B05_Profile_Router_Replan import RouteInitialRequest
|
||||
|
||||
GRADE = 0.14
|
||||
RADIUS = 12.0
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def hill() -> Terrain:
|
||||
x = np.arange(0.0, 802.0, 2.0)
|
||||
y = np.arange(0.0, 402.0, 2.0)
|
||||
xx, yy = np.meshgrid(x, y)
|
||||
z = 40.0 * np.exp(-((xx - 400.0) ** 2 + (yy - 200.0) ** 2) / 90.0**2)
|
||||
return Terrain(x, y, z, np.ones(xx.shape, dtype=bool))
|
||||
|
||||
|
||||
def _run(terrain: Terrain, mode: str, with_profile: bool) -> dict:
|
||||
expected = [(float(x), 200.0) for x in np.arange(50.0, 752.0, 4.0)]
|
||||
return generate_initial_route(
|
||||
expected,
|
||||
terrain,
|
||||
[],
|
||||
mode=mode,
|
||||
weights=None,
|
||||
max_grade=GRADE,
|
||||
min_radius_m=RADIUS,
|
||||
with_profile=with_profile,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["earthwork", "cost", "follow"])
|
||||
def test_켬은_종단_끔은_원지반(hill: Terrain, mode: str) -> None:
|
||||
on = _run(hill, mode, True)
|
||||
off = _run(hill, mode, False)
|
||||
assert on["with_profile"] is True and off["with_profile"] is False
|
||||
assert "error" not in on["metrics"]["profile"]
|
||||
assert "profile" not in off["metrics"] and "earth_estimate_m3" not in off["metrics"]
|
||||
# 끔 점수 = 절 + 성 · 켬 점수 = 균형 종단 토공
|
||||
assert GOALS["earthwork"](off["metrics"]) == (
|
||||
off["metrics"]["cut_m3"] + off["metrics"]["fill_m3"],
|
||||
)
|
||||
assert GOALS["earthwork"](on["metrics"]) == (profile_earth(on["metrics"]["profile"]),)
|
||||
# 공사비 어림 = 켬은 토공 몫을 종단 점수로 바꾼 값(고른 노선이 달라도 그 노선의 처음 값 기준)
|
||||
metrics = on["metrics"]
|
||||
shift = COST_EARTH_M3 * (profile_earth(metrics["profile"]) - metrics["earth_estimate_m3"])
|
||||
assert metrics["cost_index"] == pytest.approx(metrics["cost_index_base"] + shift, abs=1.0)
|
||||
|
||||
|
||||
def test_끈_요청은_묶음_캐시의_종단을_걷어_낸다(hill: Terrain) -> None:
|
||||
first = _run(hill, "cost", False)
|
||||
_run(hill, "cost", True) # 같은 묶음 캐시에 종단을 붙임
|
||||
again = _run(hill, "cost", False)
|
||||
assert again["metrics"] == first["metrics"] and again["planned"] == first["planned"]
|
||||
assert "profile" not in again["metrics"]
|
||||
|
||||
|
||||
def test_요청_기본은_켬이고_끔을_받는다() -> None:
|
||||
assert RouteInitialRequest(mode="earthwork").with_profile is True
|
||||
assert RouteInitialRequest(mode="earthwork", with_profile=False).with_profile is False
|
||||
@@ -0,0 +1,119 @@
|
||||
"""B08 4탭 홍수량 계산서 — 처음 값 빈칸 · 공사지 지역 · 재현기간별 R24 · 저장 파일 (PLAN 69-5 · 69-6)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B08_Quantity.B08_Quantity_Engine_Hydro import compute # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Hydro_Defaults import DEFAULTS # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Hydro_Site import ( # noqa: E402
|
||||
SAVED_FILE,
|
||||
read_saved,
|
||||
site_info,
|
||||
site_r24,
|
||||
write_saved,
|
||||
)
|
||||
|
||||
SQUARE = [[[128.0, 36.0], [129.0, 36.0], [129.0, 37.0], [128.0, 37.0], [128.0, 36.0]]]
|
||||
FAR = [[[120.0, 30.0], [121.0, 30.0], [121.0, 31.0], [120.0, 31.0], [120.0, 30.0]]]
|
||||
|
||||
|
||||
def _geojson(*features: tuple[list, dict]) -> str:
|
||||
return json.dumps(
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": p,
|
||||
"geometry": {"type": "Polygon", "coordinates": g},
|
||||
}
|
||||
for g, p in features
|
||||
],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def project(tmp_path: Path) -> Path:
|
||||
drainage = tmp_path / "B04_PreProcess" / "drainage"
|
||||
processed = tmp_path / "B04_PreProcess" / "processed"
|
||||
drainage.mkdir(parents=True)
|
||||
processed.mkdir(parents=True)
|
||||
rain = {
|
||||
"lat": 36.5,
|
||||
"lon": 128.5,
|
||||
"station": {"name": "영양군(도곡리)"},
|
||||
"values": [
|
||||
{"return_period_yr": 50, "duration_min": 1440, "depth_mm": 207.69},
|
||||
{"return_period_yr": 100, "duration_min": 1440, "depth_mm": 231.56},
|
||||
{"return_period_yr": 100, "duration_min": 60, "depth_mm": 70.0},
|
||||
],
|
||||
}
|
||||
(drainage / "rainfall_table.json").write_text(json.dumps(rain), encoding="utf-8")
|
||||
(processed / "행정구역_시군구_bounds.geojson").write_text(
|
||||
_geojson(
|
||||
(FAR, {"sig_kor_nm": "봉화군", "full_nm": "경상북도 봉화군"}),
|
||||
(SQUARE, {"sig_kor_nm": "영양군", "full_nm": "경상북도 영양군"}),
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(processed / "행정구역_읍면동_bounds.geojson").write_text(
|
||||
_geojson((SQUARE, {"full_nm": "경상북도 영양군 일월면 용화리"})), encoding="utf-8"
|
||||
)
|
||||
return tmp_path
|
||||
|
||||
|
||||
def test_처음_값은_표준_값만_측점_고유_값은_빈칸() -> None:
|
||||
for key in ("area_km2", "length_m", "height_m", "diameter_mm", "slope_denominator", "r24_mm"):
|
||||
assert DEFAULTS[key] is None
|
||||
assert DEFAULTS["factor"] == 2.0 and DEFAULTS["runoff"] == 0.8
|
||||
assert DEFAULTS["return_period"] == 100 and DEFAULTS["section"] == "full"
|
||||
|
||||
|
||||
def test_빈칸이면_그_값에_기대는_칸만_비움() -> None:
|
||||
out = compute(DEFAULTS)
|
||||
assert out["design_flow"] is None and out["capacity"] is None
|
||||
assert out["ok"] is None and out["ratio_pct"] is None
|
||||
assert out["n"] == 0.024 # 관 종류 값은 늘 있음
|
||||
# 단면만 채우면 유출량은 나오고 판정은 빈칸
|
||||
half = compute({**DEFAULTS, "diameter_mm": 1000.0, "slope_denominator": 20.0})
|
||||
assert half["capacity"] > 0 and half["design_flow"] is None and half["ok"] is None
|
||||
|
||||
|
||||
def test_지역은_노선_대표점이_드는_행정구역(project: Path) -> None:
|
||||
site = site_info(project, db_region="엉뚱")
|
||||
assert site["region"]["name"] == "영양군"
|
||||
assert site["region"]["full"] == "경상북도 영양군 일월면 용화리"
|
||||
assert site["station"] == "영양군(도곡리)"
|
||||
assert site["periods"] == [50, 100]
|
||||
assert site["r24"] == {"50": 207.69, "100": 231.56}
|
||||
|
||||
|
||||
def test_재현기간을_바꾸면_R24_가_바뀜(project: Path) -> None:
|
||||
assert site_r24(project, 100) == 231.56
|
||||
assert site_r24(project, 50) == 207.69
|
||||
assert site_r24(project, 30) is None
|
||||
|
||||
|
||||
def test_자료가_없으면_등록_지역_재현기간_기본_목록(tmp_path: Path) -> None:
|
||||
site = site_info(tmp_path, db_region="울진군")
|
||||
assert site["region"] == {"name": "울진군", "full": "울진군", "source": "프로젝트 등록"}
|
||||
assert 100 in site["periods"] and site["r24"] == {}
|
||||
assert site_r24(tmp_path, 100) is None
|
||||
|
||||
|
||||
def test_저장_파일은_B08_자기_자리(tmp_path: Path) -> None:
|
||||
stations = {"85.050": {**DEFAULTS, "area_km2": 0.05}}
|
||||
write_saved(tmp_path, stations)
|
||||
assert (tmp_path / SAVED_FILE).is_file()
|
||||
assert read_saved(tmp_path) == stations
|
||||
assert [p.name for p in (tmp_path / SAVED_FILE).parent.iterdir()] == ["hydro_inputs.json"]
|
||||
@@ -4,6 +4,8 @@
|
||||
② 종류 더하기 · 지우기(기본 넷 포함) · 다시 읽어 같음
|
||||
③ 대피소 · 사토장 자리 — 한 자리에 둘 다
|
||||
④ 고른 조각만 도면 층으로 얹힘
|
||||
⑤ 조각 잘게(67장) — 노견선 · 성토 외곽선 · 절토 외곽선 따로 · 기본 넷 처음 체크 = 옛 그림 ·
|
||||
옛 종류 파일의 slope_outline 펼침 · 종류 업데이트(기본 넷 포함) · 확정본 풀림 · 배수시설 기호
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -19,13 +21,16 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanKinds import (
|
||||
layer_id,
|
||||
slope_pieces,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Io import _read_manifest, _write_manifest
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Plan import (
|
||||
_kinds_dir,
|
||||
_refuge_items,
|
||||
add_kind,
|
||||
delete_kind,
|
||||
list_kinds,
|
||||
read_marks,
|
||||
slope_records,
|
||||
update_kind,
|
||||
write_marks,
|
||||
)
|
||||
|
||||
@@ -89,8 +94,11 @@ def test_outline_width_equals_cross_design_at_two_stations(tmp_path: Path) -> No
|
||||
longitudinal_path, longitudinal, designs = _project(tmp_path)
|
||||
records = slope_records(longitudinal_path, longitudinal, designs, 0.0, 40.0)
|
||||
route = [(0.0, 0.0, 0.0), (40.0, 0.0, 40.0)]
|
||||
outline, ticks = slope_pieces(route, records, 0.0, 40.0)
|
||||
left_edge, left_end, right_edge, right_end = outline["lines"]
|
||||
edge, fill, cut, ticks = slope_pieces(route, records, 0.0, 40.0)
|
||||
left_edge, right_edge = edge["lines"]
|
||||
# 좌(+)가 높은 지반 — 좌측 비탈은 절토 · 우측은 성토, 각각 한 줄로 이어짐
|
||||
assert all(r["left_cut"] and not r["right_cut"] for r in records)
|
||||
(left_end,), (right_end,) = cut["lines"], fill["lines"]
|
||||
for chainage in (20, 40): # 견본 측점 둘
|
||||
design = {**designs[chainage], "_c": chainage}
|
||||
index = next(i for i, p in enumerate(left_edge) if abs(p[1] - chainage) < 1e-6)
|
||||
@@ -157,3 +165,68 @@ def test_drawing_has_only_chosen_pieces() -> None:
|
||||
assert layer_id("contour") not in layers and layer_id("station") not in layers
|
||||
labels = [e["shapeData"]["label"] for e in drawing["entities"] if e["type"] == "Text"]
|
||||
assert "R=15" in labels and "계획평면도(시험)" in labels
|
||||
|
||||
|
||||
def test_split_runs_switch_cut_fill_and_stay_joined() -> None:
|
||||
route = [(0.0, 0.0, 0.0), (40.0, 0.0, 40.0)]
|
||||
base = {"left_edge_m": 2.0, "left_end_m": 5.0, "right_edge_m": -2.0, "right_end_m": -5.0}
|
||||
records = [
|
||||
{"chainage_m": 0.0, **base, "left_cut": True, "right_cut": False},
|
||||
{"chainage_m": 40.0, **base, "left_cut": False, "right_cut": False},
|
||||
]
|
||||
_edge, fill, cut, _ticks = slope_pieces(route, records, 0.0, 40.0)
|
||||
(cut_left,) = cut["lines"]
|
||||
fill_left = next(line for line in fill["lines"] if line[0][0] < 0)
|
||||
assert cut_left[-1] == fill_left[0] # 바뀌는 자리에서 두 선이 이어짐
|
||||
assert cut_left[0][1] == 0.0 and fill_left[-1][1] == 40.0
|
||||
|
||||
|
||||
def test_default_kinds_keep_old_picture_and_legacy_file(tmp_path: Path) -> None:
|
||||
root = tmp_path / "project"
|
||||
outline = {"road_edge", "fill_outline", "cut_outline"}
|
||||
for (
|
||||
kind
|
||||
) in DEFAULT_KINDS: # 옛 slope_outline 자리를 나눈 셋이 그대로 채움 · 배수시설은 옛 그림에 없음
|
||||
assert outline <= set(kind["pieces"]) and "drainage" not in kind["pieces"]
|
||||
folder = _kinds_dir(root)
|
||||
folder.mkdir(parents=True)
|
||||
(folder / "old.json").write_text(
|
||||
json.dumps({"name": "옛것", "pieces": ["slope_outline", "station"], "order": 0}), "utf-8"
|
||||
)
|
||||
(kind,) = list_kinds(root)
|
||||
assert set(kind["pieces"]) == outline | {"station"}
|
||||
|
||||
|
||||
def test_update_kind_default_and_release_confirmed(tmp_path: Path) -> None:
|
||||
root = tmp_path / "project"
|
||||
manifest = _read_manifest(root)
|
||||
manifest["drawings"] = {
|
||||
"plan_terrain": {"confirmed": True},
|
||||
"plan_route": {"confirmed": True},
|
||||
}
|
||||
_write_manifest(root, manifest)
|
||||
update_kind(root, "terrain", "지형2", ["contour", "centerline"]) # 기본 종류 고침
|
||||
again = {k["id"]: k for k in list_kinds(root)} # 다시 읽어 같음
|
||||
assert again["terrain"] == {
|
||||
"id": "terrain",
|
||||
"name": "지형2",
|
||||
"pieces": ["contour", "centerline"],
|
||||
}
|
||||
assert [k["id"] for k in list_kinds(root)][0] == "terrain" # 차례 그대로
|
||||
drawings = _read_manifest(root)["drawings"]
|
||||
assert drawings["plan_terrain"]["confirmed"] is False
|
||||
assert drawings["plan_route"]["confirmed"] is True
|
||||
with pytest.raises(ValueError):
|
||||
update_kind(root, "terrain", "노선배치도", ["contour"]) # 다른 종류와 같은 이름
|
||||
with pytest.raises(ValueError):
|
||||
update_kind(root, "terrain", "지형", [])
|
||||
with pytest.raises(FileNotFoundError):
|
||||
update_kind(root, "nokind", "없음", ["contour"])
|
||||
|
||||
|
||||
def test_drainage_symbol_piece() -> None:
|
||||
pieces = {"drainage": {"items": [{"x": 0.0, "y": 20.0, "label": "D800"}]}}
|
||||
drawing = build_plan_kind_drawing("plan_k2", "시험", (-50.0, -10.0, 50.0, 50.0), pieces)
|
||||
mine = [e for e in drawing["entities"] if e.get("layerId") == layer_id("drainage")]
|
||||
assert {e["type"] for e in mine} >= {"Text"} and len(mine) >= 2
|
||||
assert any(e["type"] == "Text" and e["shapeData"]["label"] == "D800" for e in mine)
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""공용 표 양식(PLAN 70-1) 회귀검증 — 묶음 머리 · 고정 열 · 숫자 꼴 · 빈 값 · 합계 · 1,000줄.
|
||||
|
||||
Node 헬퍼(`helper_ui_data_table.cjs`)가 `ui_template/ui_template_data_table.ts` 를 트랜스파일해
|
||||
견본(`fixture_data_table_sample.js` · 머리 3줄 · 30열 · 66줄)으로 HTML 을 만들고, 여기서 판정함.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def r():
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_ui_data_table.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_견본_크기(r):
|
||||
assert r["cols"] == 30
|
||||
assert r["headRows"] == 3
|
||||
assert r["bodyRows"] == 66
|
||||
|
||||
|
||||
def test_묶음_머리_합침(r):
|
||||
first = r["firstHeadRow"]
|
||||
# 측점 · 거리 · 유용토 등 묶음 없는 열은 머리 3줄을 세로로 차지
|
||||
assert 'rowspan="3" class="is-frozen is-edge" style="left:0px" title="측점">측점</th>' in first
|
||||
assert first.count('rowspan="3"') == 5 # 측점 거리 유용토 차인토량 누가토량
|
||||
assert 'colspan="13">절토</th>' in first # 토사 · 연암 · 측구파기 토사 · 암 각 3 + 보정량계
|
||||
assert 'colspan="4">면고르기</th>' in first
|
||||
assert 'colspan="2">노면정리</th>' in first
|
||||
second = r["secondHeadRow"]
|
||||
assert 'colspan="3">토사</th>' in second and 'colspan="3">연암</th>' in second
|
||||
assert 'rowspan="2" data-num="" title="보정량계"' in second # 한 단 묶음은 머리 2줄
|
||||
|
||||
|
||||
def test_고정_열과_숫자_칸(r):
|
||||
row = r["firstBodyRow"]
|
||||
assert row.startswith('<tr><td class="is-frozen is-edge" style="left:0px">120</td>')
|
||||
assert r["frozenCells"] == 66
|
||||
assert '<td data-num="">0</td>' in row # 거리 0 은 빈 값 아님
|
||||
|
||||
|
||||
def test_숫자_꼴(r):
|
||||
f = r["fmt"]
|
||||
assert f["thousands"] == "1,234,567.89"
|
||||
assert f["neg"] == "-97.90"
|
||||
assert f["zero"] == "0.00"
|
||||
assert f["nul"] == f["undef"] == f["blank"] == f["nan"] == "—"
|
||||
assert f["text"] == "120+18" and f["textNum"] == "5"
|
||||
|
||||
|
||||
def test_합계_줄(r):
|
||||
assert r["foot"].startswith('<tr><td class="is-frozen is-edge" style="left:0px">합계</td>')
|
||||
assert "is-empty" in r["foot"] # 누가토량 합계는 비움
|
||||
assert r["noTotal"] is False
|
||||
|
||||
|
||||
def test_글_새지_않음(r):
|
||||
assert "<b>" in r["escaped"] and "a&<" in r["escaped"]
|
||||
|
||||
|
||||
def test_천_줄_빠름(r):
|
||||
assert r["bigRows"] == 1000
|
||||
assert r["bigMs"] < 300
|
||||
|
||||
|
||||
def test_보이는_줄만_그림(r):
|
||||
w = r["win"]
|
||||
assert w["n"] == 62 # 위 빈 줄 + 60줄 + 아래 빈 줄
|
||||
assert 'style="height:1525px"><td colspan="30">' in w["top"] # 61줄 × 25
|
||||
assert w["second"].startswith(
|
||||
'<tr class="is-alt">'
|
||||
) # 61 번째(홀수) 줄 — 번갈아 색은 전체 줄 번호로
|
||||
assert 'style="height:21975px"' in w["bottom"] # (1000 - 121) × 25
|
||||
@@ -0,0 +1,18 @@
|
||||
/* 64-1 — B05 노선 추천 「종단 반영」 켬 · 끔 점수 비교
|
||||
* 시작 화면: B05 종단설계 → [계획노선 편집] → [노선 추천] 열림 · 검증 프로젝트(검증_sub1)
|
||||
* 부르는 법: 실행 뒤 `window.__r` 를 읽음(끝나면 채워짐)
|
||||
* 기대 수치: 끔 = 균형 종단 줄 · 종단 그림 · 덮개 줄 없음 · 토공 최소 절 + 성 = 원지반 가늠 ·
|
||||
* 켬 = 균형 종단 줄 있음 · 토공 최소 노선 달라짐(연장 1,156.7 → 1,153.2 m) · 공사비 어림 6,206 → 18,606
|
||||
*/
|
||||
(()=>{window.__r=undefined;(async()=>{
|
||||
const w=ms=>new Promise(r=>setTimeout(r,ms));
|
||||
const q=s=>document.querySelector(s);
|
||||
const waitDone=async()=>{const t0=Date.now();await w(400);for(;;){await w(300);const s=q('[data-state]').textContent;if(s.includes('미리 보기:')||s.includes('셈하지 못')) return (Date.now()-t0)/1000;if(Date.now()-t0>280000) return -1}};
|
||||
const snap=()=>({m:q('[data-line=metrics]').textContent,w:q('[data-line=metrics-work]').textContent,bal:q('[data-line=balance]').textContent,prof:!q('[data-line=profile]').hidden,cands:q('[data-line=candidates]').textContent.length});
|
||||
const setBox=v=>{if(q('[data-with-profile]').checked!==v) q('[data-with-profile]').click()};
|
||||
const out={};
|
||||
for(const l of ['earthwork','cost','gentle']){
|
||||
setBox(false); q('[data-logic='+l+']').click(); const t1=await waitDone(); out[l+'_off']={t:t1,...snap()};
|
||||
setBox(true); q('[data-act=rerun]').click(); const t2=await waitDone(); out[l+'_on']={t:t2,...snap()};
|
||||
}
|
||||
window.__r=out;})();return 'started'})()
|
||||
@@ -0,0 +1,20 @@
|
||||
/* 64-2 — B05 노선 추천 창 오른쪽 설명 = 한 갈래씩 ◀ ▶ 쪽 넘김
|
||||
* 시작 화면: B05 종단설계 → [계획노선 편집] → [노선 추천] (첫 미리 보기가 뜬 뒤) · 검증 프로젝트(검증_sub1)
|
||||
* 기대 수치: 쪽 수 = 갈래 수 = 8 · 한 번에 한 쪽만 보임 · 처음 쪽 = 고른 갈래 · 1 / 8 에서 ◀ 꺼짐 ·
|
||||
* 8 / 8 에서 ▶ 꺼짐 · 다른 칸을 만져 다시 그려도 넘겨 보던 쪽 그대로
|
||||
*/
|
||||
(async()=>{
|
||||
const w=ms=>new Promise(r=>setTimeout(r,ms));
|
||||
const q=s=>document.querySelector(s);
|
||||
const help=()=>({page:q('[data-help-page]').textContent,visible:[...document.querySelectorAll('[data-help-logic]')].filter(x=>!x.hidden).map(x=>x.dataset.helpLogic),on:[...document.querySelectorAll('[data-help-logic].is-on')].map(x=>x.dataset.helpLogic),prevDis:q('[data-help-nav="-1"]').disabled,nextDis:q('[data-help-nav="1"]').disabled,total:document.querySelectorAll('[data-help-logic]').length});
|
||||
const o={};
|
||||
o.start=help();
|
||||
for(let i=0;i<10;i++){q('[data-help-nav="-1"]').click()} o.firstPage=help();
|
||||
for(let i=0;i<10;i++){q('[data-help-nav="1"]').click()} o.lastPage=help();
|
||||
q('[data-help-nav="-1"]').click(); o.prev=help();
|
||||
// 렌더가 돌아도 넘겨 보던 쪽 유지 — 체크 토글로 render 유발
|
||||
q('[data-with-profile]').click(); await w(100); o.afterRender=help();
|
||||
q('[data-with-profile]').click();
|
||||
const a=q('.b05-routerec__help'); const r=a.getBoundingClientRect(); o.helpBox={w:Math.round(r.width),h:Math.round(r.height),scrollH:a.scrollHeight};
|
||||
const art=[...document.querySelectorAll('[data-help-logic]')].find(x=>!x.hidden); o.artText=art.innerText.slice(0,260);
|
||||
return JSON.stringify(o,null,1)})()
|
||||
Reference in New Issue
Block a user