Merge remote-tracking branches 'origin/sub_laptop_1', 'origin/sub_laptop_2', 'origin/sub_laptop_3', 'origin/sub_laptop_5' and 'origin/sub_laptop_6' into CODEX
This commit is contained in:
@@ -320,7 +320,7 @@ const { buildDrawingSidePanel } = loadTs(path.join(B07, "B07_DesignDetail_UI_Pan
|
||||
confirms: confirms.length,
|
||||
};
|
||||
|
||||
// 미리보기 장 바꾸기 — 「지형 2장」 → 「노선배치도」(확정 → [확정 풀기])
|
||||
// 미리보기 장 바꾸기 — 「지형 2장」 → 「노선배치도」(확정 배지만 · [확정 풀기] 없음 140-1)
|
||||
const pickSheet = async (id) => {
|
||||
modal.body
|
||||
.querySelectorAll(".b07-plan-manage__sheets .b07-drawing-button")
|
||||
@@ -340,14 +340,6 @@ const { buildDrawingSidePanel } = loadTs(path.join(B07, "B07_DesignDetail_UI_Pan
|
||||
};
|
||||
await pickSheet("plan_k_route");
|
||||
out.routeHead = head().textContent;
|
||||
byText(head(), "BUTTON", "확정 풀기")[0].click();
|
||||
await settle();
|
||||
await settle();
|
||||
out.release = {
|
||||
call: calls.find((c) => c[0] === "invalidate"),
|
||||
head: head().textContent,
|
||||
preview: cad.loads.at(-1).drawing.entities[0].id,
|
||||
};
|
||||
|
||||
// 닫기 — 고친 게 있으니 changed = true · 미리보기 듣기 멈춤
|
||||
modal.opts.beforeClose();
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/* 136-1 폼 → 캐시 chute 헬퍼 — `applyPipeOptionsToCache`(B06_Section_UI_Page_Pipe_Options.ts)를 그 자리에서
|
||||
* 트랜스파일해 돌림 · 도수로/돌붙임 상자가 관 · BOX · 세월교 · 물넘이 포장 제원 `chute` 에 바로 얹히는지 ·
|
||||
* 다시 그린 카드. 짝 = `test_136_chute_form_cache.py`. 로더는 `helper_b06_draft_pipe_replay.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 cache = new Map();
|
||||
const session = new Map();
|
||||
const fakeStorage = {
|
||||
getItem: (k) => session.get(k) ?? null,
|
||||
setItem: (k, v) => session.set(k, String(v)),
|
||||
removeItem: (k) => session.delete(k),
|
||||
};
|
||||
globalThis.window = { sessionStorage: fakeStorage, localStorage: fakeStorage };
|
||||
globalThis.sessionStorage = fakeStorage;
|
||||
globalThis.localStorage = fakeStorage;
|
||||
|
||||
function loadTs(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const localRequire = (request) => {
|
||||
if (request.startsWith("@ui/")) return new Proxy({}, { get: () => () => {} });
|
||||
if (request.startsWith("@util/"))
|
||||
return loadTs(path.join(ROOT, "common_util", `${request.slice(6)}.ts`));
|
||||
if (request.startsWith("@config/"))
|
||||
return loadTs(path.join(ROOT, "config", `${request.slice(8)}.ts`));
|
||||
return request.startsWith(".")
|
||||
? loadTs(path.join(path.dirname(full), `${request}.ts`))
|
||||
: require(request);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
cache.set(full, box.exports);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const options = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Page_Pipe_Options.ts"));
|
||||
|
||||
const box = (at) => ({
|
||||
chainage_m: 120,
|
||||
top_thickness_m: 0.3,
|
||||
inner_height_m: 2,
|
||||
slab_thickness_m: 0.3,
|
||||
wing_in: {},
|
||||
wing_out: {},
|
||||
_at: at,
|
||||
});
|
||||
const detail = {
|
||||
cross_sections: [
|
||||
{ chainage_m: 85, culvert: { chainage_m: 85.05, diameter_m: 1, inlet: {}, outlet: {} } },
|
||||
{ chainage_m: 118, box: box(118) },
|
||||
{ chainage_m: 120, box: box(120) },
|
||||
{ chainage_m: 160, ford: { chainage_m: 160, diameter_m: 1, wing_in: {}, wing_out: {} } },
|
||||
{ chainage_m: 196, ford_pavement: { chainage_m: 200, span_m: 9, depth_m: 0.3 } },
|
||||
{ chainage_m: 200, ford_pavement: { chainage_m: 200, span_m: 9, depth_m: 0.3 } },
|
||||
],
|
||||
};
|
||||
let refreshed = [];
|
||||
const ctx = {
|
||||
detail: () => detail,
|
||||
ford: { setPipe: () => {}, setWing: () => {} },
|
||||
box: { setBody: () => {}, setWing: () => {} },
|
||||
revetOffset: { update: () => {}, adjustFor: () => ({ h: null, m: null }) },
|
||||
overrideOptions: () => {},
|
||||
refreshCard: (at) => refreshed.push(at),
|
||||
};
|
||||
const at = (i) => detail.cross_sections[i];
|
||||
const step = (chainage, patch, whole = true) => {
|
||||
refreshed = [];
|
||||
options.applyPipeOptionsToCache(ctx, chainage, patch, whole);
|
||||
return {
|
||||
pipe: at(0).culvert.chute ?? null,
|
||||
box: [at(1).box.chute ?? null, at(2).box.chute ?? null],
|
||||
ford: at(3).ford.chute ?? null,
|
||||
pave: [at(4).ford_pavement.chute ?? null, at(5).ford_pavement.chute ?? null],
|
||||
refreshed: [...new Set(refreshed)].sort((a, b) => a - b),
|
||||
};
|
||||
};
|
||||
|
||||
const out = {
|
||||
pipeAdd: step(85.05, { chute_add_1_structure: "돌붙임", chute_add_1_thickness: 40 }),
|
||||
pipeSame: step(85.05, { chute_add_1_structure: "돌붙임", chute_add_1_thickness: 40 }),
|
||||
replayNoChute: step(85.05, { pipe_diameter_mm: 1000 }, false),
|
||||
pipeRemove: step(85.05, { pipe_diameter_mm: 1000 }),
|
||||
boxAdd: step(120, { chute_add_1_structure: "도수로" }),
|
||||
fordAdd: step(160, { chute_add_1_structure: "돌붙임", chute_add_1_thickness: 50 }),
|
||||
paveAdd: step(200, { ford_chute_1_structure: "돌붙임", chute_add_1_structure: "도수로" }),
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -50,5 +50,18 @@ const out = {
|
||||
toggledOn: w.toggleRun(runs, off, runs[0]),
|
||||
stale: w.offForRuns(runs, "20-60,0-5,100-130"),
|
||||
empty: w.offForRuns([], "20-60"),
|
||||
// 확폭 면적(138-2) — 앞뒤 확폭 0 측점까지 사다리꼴 · 범위로 자름
|
||||
area: runs.map((r) => w.wideningArea(stations, r, { start: 0, end: 140 })),
|
||||
areaClipped: w.wideningArea(stations, runs[0], { start: 30, end: 140 }),
|
||||
areaSingle: w.wideningArea(
|
||||
[at(0), at(20, 1, 0), at(40)],
|
||||
{ start_m: 20, end_m: 20, widening_m: 1 },
|
||||
{ start: 0, end: 40 },
|
||||
),
|
||||
areaMissing: w.wideningArea(
|
||||
stations,
|
||||
{ start_m: 999, end_m: 999, widening_m: 1 },
|
||||
{ start: 0, end: 140 },
|
||||
),
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/* M02 일위대가 문서 화면 정리(`M02_MasterTemplete_Labor_Api.ts` `normalizeLabor`) 헬퍼(134-1) —
|
||||
* 그 자리에서 트랜스파일해 Node 로 돌림 · 가져오는 길은 빈 대역 · stdin = {doc, column} · stdout = 정리한 문서 JSON. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const file = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"M02_MasterTemplete",
|
||||
"M02_MasterTemplete_Labor_Api.ts",
|
||||
);
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const stub = new Proxy({}, { get: () => function () {} });
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, () => stub);
|
||||
|
||||
const input = JSON.parse(fs.readFileSync(0, "utf8"));
|
||||
process.stdout.write(JSON.stringify(box.exports.normalizeLabor(input.doc, input.column)));
|
||||
@@ -82,7 +82,14 @@ POINTS = [
|
||||
{
|
||||
"chainage_m": 500.0,
|
||||
"facility": "ford_pavement",
|
||||
"options": {"ford_width_m": 8, "ford_height_m": 1.0},
|
||||
"options": {
|
||||
"ford_width_m": 8,
|
||||
"ford_height_m": 1.0,
|
||||
# 물넘이 포장 도수로/돌붙임(136-2) — 관 · 박스 · 세월교와 같은 열
|
||||
"ford_chute_1_structure": "돌붙임",
|
||||
"ford_chute_1_bond": "찰붙임",
|
||||
"ford_chute_1_area": 12,
|
||||
},
|
||||
},
|
||||
]
|
||||
STRUCTURES = [
|
||||
@@ -177,6 +184,7 @@ def test_물넘이_H별_월류폭(out):
|
||||
pave = _col(out, "pipe:pave|1.0")
|
||||
assert pave["group"] == ["물넘이포장", "월류 폭", "H=1.0"] and pave["label"] == "m"
|
||||
assert _cell(out, 500, "pipe:pave|1.0") == 8
|
||||
assert _cell(out, 500, "pipe:chute|찰붙임") == 12
|
||||
|
||||
|
||||
def test_개거_노출형_맹암거_개소(out):
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
- 좌측 「평면도」 = 장 단추 + [평면도 관리] 하나 · 종류 폼 · 대피소 칸 · × 없음.
|
||||
- 단추 → 공용 모달(큰 창) · 안에 종류 목록 · 종류 폼 · 대피소 · 사토장 자리 · 장 목록 · 첫 장 미리보기(보기 전용).
|
||||
- 종류 추가 · 업데이트(축척 포함) · 지우기 = 지금 서버 길 · 고칠 때마다 모달 안을 다시 받음.
|
||||
- 미리보기 장 바꾸기 · 확정이면 [확정 풀기](invalidate) · 닫으면 changed=True · [CAD 에서 열기] = 그 장 id.
|
||||
- 미리보기 장 바꾸기 · 캔버스만(bare) · 확정 배지만(확정 풀기 없음 140-1) · 닫으면 changed=True · [CAD 에서 열기] = 그 장 id.
|
||||
|
||||
Node 헬퍼 `helper_130_b07_plan_manage.cjs` 가 화면 그릇 흉내로 돌림. 화면은 사용자 확인.
|
||||
"""
|
||||
@@ -54,6 +54,7 @@ def test_첫_장_미리보기는_보기_전용(out):
|
||||
assert out["open"]["preview"] == "plan_k_terrain"
|
||||
extra = out["open"]["extra"]
|
||||
assert extra["readOnly"] is True and extra["viewOnly"] is True
|
||||
assert extra["bare"] is True # 140-1 캔버스만(제목줄 · 리본 · 상태막대 없음)
|
||||
assert extra["recoveryScope"] == "b07-plan-preview" # 본 CAD 자동백업 칸과 안 겹침
|
||||
|
||||
|
||||
@@ -82,12 +83,10 @@ def test_미리보기_장_바꾸기(out):
|
||||
assert "확정 전" in s2["head"] and "확정 풀기" not in s2["head"]
|
||||
|
||||
|
||||
def test_확정이면_확정_풀기(out):
|
||||
assert "확정 풀기" in out["routeHead"]
|
||||
rel = out["release"]
|
||||
assert rel["call"] == ["invalidate", "plan_k_route"]
|
||||
assert "확정 풀기" not in rel["head"]
|
||||
assert rel["preview"] == "plan_k_route" # 보던 장 그대로
|
||||
def test_확정이면_배지만(out):
|
||||
# 140-1 확정 풀기는 메인 [수정] — 모달엔 배지 · [CAD 에서 열기]만
|
||||
assert "확정" in out["routeHead"] and "CAD 에서 열기" in out["routeHead"]
|
||||
assert "확정 풀기" not in out["routeHead"]
|
||||
|
||||
|
||||
def test_닫으면_좌측_다시_읽기_신호(out):
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"""136-1 폼 → 캐시 chute — 좌측 패널 도수로/돌붙임 상자가 [저장] 전에 횡단 제원에 바로 얹힘.
|
||||
|
||||
관 · BOX(폭 걸친 측점 둘) · 세월교 · 물넘이 포장(`ford_chute_*`) · 지움 · 일부만 온 되얹기.
|
||||
화면 TS 를 그 자리에서 돌린 값(`helper_136_chute_form_cache.cjs`).
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
ROOT = HERE.parents[1]
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules").is_dir(), reason="프론트엔드 의존성 미설치"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out() -> dict:
|
||||
done = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_136_chute_form_cache.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert done.returncode == 0, done.stderr
|
||||
return json.loads(done.stdout)
|
||||
|
||||
|
||||
def test_관_상자_넣으면_바로_chute_와_카드(out: dict) -> None:
|
||||
assert out["pipeAdd"]["pipe"] == {"structure": "돌붙임", "thickness_m": 0.4}
|
||||
assert 85 in out["pipeAdd"]["refreshed"]
|
||||
# 같은 값이면 chute 때문에 다시 그리진 않음(관 길의 카드 갱신만)
|
||||
assert out["pipeSame"]["pipe"] == out["pipeAdd"]["pipe"]
|
||||
|
||||
|
||||
def test_되얹기는_상자_없으면_그대로_칸_전부면_지움(out: dict) -> None:
|
||||
assert out["replayNoChute"]["pipe"] == {"structure": "돌붙임", "thickness_m": 0.4}
|
||||
assert out["pipeRemove"]["pipe"] is None
|
||||
|
||||
|
||||
def test_박스는_걸친_측점_모두_세월교도(out: dict) -> None:
|
||||
assert out["boxAdd"]["box"] == [{"structure": "도수로", "thickness_m": 0.3}] * 2
|
||||
assert {118, 120} <= set(out["boxAdd"]["refreshed"])
|
||||
assert out["fordAdd"]["ford"] == {"structure": "돌붙임", "thickness_m": 0.5}
|
||||
assert out["fordAdd"]["refreshed"] == [160]
|
||||
# 다른 관 지점은 안 건드림
|
||||
assert out["fordAdd"]["pipe"] is None and out["fordAdd"]["box"][0] is not None
|
||||
|
||||
|
||||
def test_물넘이_포장은_ford_chute_만(out: dict) -> None:
|
||||
assert out["paveAdd"]["pave"] == [{"structure": "돌붙임", "thickness_m": 0.3}] * 2
|
||||
assert out["paveAdd"]["refreshed"] == [196, 200]
|
||||
@@ -131,3 +131,70 @@ def test_정의_그룹_경사각도():
|
||||
) as f:
|
||||
groups = json.load(f)["그룹"]["ford_pavement"]
|
||||
assert [(g["id"], g["이름"]) for g in groups] == [("gfordpaveslope", "경사각도")]
|
||||
|
||||
|
||||
OVERLAY = r"""
|
||||
const fs = require("fs"), path = require("path");
|
||||
const ROOT = process.argv[1];
|
||||
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
|
||||
const cache = new Map();
|
||||
const load = (file) => {
|
||||
if (cache.has(file)) return cache.get(file);
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(file, box.exports);
|
||||
const req = (r) => (r.startsWith(".") ? load(path.join(path.dirname(file), `${r}.ts`)) : ({}));
|
||||
new Function("exports", "module", "require", js)(box.exports, box, req);
|
||||
return box.exports;
|
||||
};
|
||||
class Node {
|
||||
constructor(tag) { this.tag = tag; this.attrs = {}; this.children = []; }
|
||||
setAttribute(k, v) { this.attrs[k] = String(v); }
|
||||
getAttribute(k) { return this.attrs[k] ?? null; }
|
||||
append(...kids) { this.children.push(...kids); }
|
||||
set textContent(v) { this.text = v; }
|
||||
}
|
||||
globalThis.document = { createElementNS: (_ns, tag) => new Node(tag) };
|
||||
const pave = load(path.join(ROOT, "B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts"));
|
||||
// +offset = 좌측 · 좌 성토 1:1.5 · 우 절토 오름 — 유입 = 좌(left_cut 아님) → 유출 = 우
|
||||
const design = (mode) => ({
|
||||
section_mode: mode, cross_slope_pct: 2, design_elevation_m: 100, pavement_thickness_m: 0.2,
|
||||
road_edges: { left: { offset_m: 2.5, elevation_m: 100 }, right: { offset_m: -2.5, elevation_m: 100 } },
|
||||
design_line: [
|
||||
{ offset_m: 7, elevation_m: 97 }, { offset_m: 2.5, elevation_m: 100 },
|
||||
{ offset_m: -2.5, elevation_m: 100 }, { offset_m: -5.5, elevation_m: 98 },
|
||||
{ offset_m: -6.5, elevation_m: 98 }, { offset_m: -8, elevation_m: 99 },
|
||||
],
|
||||
});
|
||||
const spec = { span_m: 9, depth_m: 0.3, slope_pct: 2, chute: { structure: "돌붙임", thickness_m: 0.3 } };
|
||||
const run = (mode, withChute = true) => {
|
||||
const svg = new Node("g");
|
||||
pave.appendFordPavementOverlay(svg, withChute ? spec : { ...spec, chute: undefined }, design(mode), (o) => o, (e) => e);
|
||||
const band = svg.children.find((k) => k.attrs.class === "b06-chart__culvert-pitching");
|
||||
return band ? band.attrs.points : null;
|
||||
};
|
||||
process.stdout.write(JSON.stringify({
|
||||
outLeft: pave.outletFillLine(design("left_cut")), outRight: pave.outletFillLine(design("right_cut")),
|
||||
band: run("left_cut"), none: run("left_cut", false),
|
||||
}));
|
||||
"""
|
||||
|
||||
|
||||
def test_유출측_성토사면_띠_136_2():
|
||||
proc = subprocess.run(
|
||||
["node", "-e", OVERLAY, ROOT], capture_output=True, text=True, encoding="utf-8", timeout=60
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
got = json.loads(proc.stdout)
|
||||
# 유입 = 좌 → 유출 = 우(− 바깥) · 노견에서 내려가는 동안만(평지 · 오름에서 멈춤)
|
||||
assert got["outLeft"] == [
|
||||
{"offset": -2.5, "elevation": 100},
|
||||
{"offset": -5.5, "elevation": 98},
|
||||
{"offset": -6.5, "elevation": 98},
|
||||
]
|
||||
# right_cut = 유입 우 → 유출 좌 성토 1:1.5
|
||||
assert got["outRight"] == [{"offset": 2.5, "elevation": 100}, {"offset": 7, "elevation": 97}]
|
||||
assert got["band"] and got["band"].startswith("-2.5,100 -5.5,98")
|
||||
assert got["none"] is None
|
||||
|
||||
@@ -25,7 +25,8 @@ def test_이월은_사용자_입력만() -> None:
|
||||
"ditch_side",
|
||||
):
|
||||
assert result_key not in CARRY_INPUT_KEYS
|
||||
assert {"ground_type", "paved", "ditch_choice", "cut_slope_ratio_user"} <= set(CARRY_INPUT_KEYS)
|
||||
assert {"ground_type", "ditch_choice", "cut_slope_ratio_user"} <= set(CARRY_INPUT_KEYS)
|
||||
assert "paved" not in CARRY_INPUT_KEYS # 포장 = 노면공 범위로 새로 정함(138-1)
|
||||
|
||||
|
||||
def test_체인이_옛_설계를_통째로_옮기지_않는다() -> None:
|
||||
|
||||
@@ -58,3 +58,19 @@ def test_끈_구간_글_왕복(out):
|
||||
def test_안_맞는_끈_값은_무시_저장_때_빠짐(out):
|
||||
assert out["stale"] == "20-60"
|
||||
assert out["empty"] == ""
|
||||
|
||||
|
||||
def test_확폭_면적_사다리꼴_앞뒤_0_측점까지(out):
|
||||
# 0~20: (0+0.3)/2×20=3 · 20~40: 10.5 · 40~60: 11.5 · 60~80: 4 → 29
|
||||
# 80~100: 5 · 100~120: 11 · 120~140: 6 → 22
|
||||
assert [round(a, 6) for a in out["area"]] == [29.0, 22.0]
|
||||
|
||||
|
||||
def test_확폭_면적_노면공_범위로_자름(out):
|
||||
# 30 에서 자름: w(30)=0.525 → (0.525+0.75)/2×10=6.375 + 11.5 + 4 = 21.875
|
||||
assert round(out["areaClipped"], 6) == 21.875
|
||||
|
||||
|
||||
def test_한_측점_구간도_면적_자료_없으면_0(out):
|
||||
assert round(out["areaSingle"], 6) == 20.0 # (0+1)/2×20 × 2
|
||||
assert out["areaMissing"] == 0
|
||||
|
||||
@@ -100,7 +100,14 @@ POINTS = [
|
||||
{
|
||||
"chainage_m": 140.0,
|
||||
"facility": "ford_pavement",
|
||||
"options": {"ford_height_m": 0.3, "ford_slope_deg": 5},
|
||||
"options": {
|
||||
"ford_height_m": 0.3,
|
||||
"ford_slope_deg": 5,
|
||||
# 물넘이 포장 도수로/돌붙임(136-2) — `ford_chute_*` · 관 키(`chute_add_*`)는 안 읽음
|
||||
"ford_chute_1_structure": "돌붙임",
|
||||
"ford_chute_1_thickness": 40,
|
||||
"chute_add_1_structure": "도수로",
|
||||
},
|
||||
},
|
||||
{"chainage_m": 160.0, "facility": "ford_pavement", "options": {"ford_height_m": 0.2}},
|
||||
]
|
||||
@@ -213,3 +220,5 @@ def test_ford_box_guard_attached_as_culvert() -> None:
|
||||
assert sections[60.0]["ford"]["chute"] == {"structure": "도수로", "thickness_m": 0.3}
|
||||
assert box["chute"] == {"structure": "돌붙임", "thickness_m": 0.45}
|
||||
assert "chute" not in sections[40.0]["culvert"]
|
||||
assert sections[140.0]["ford_pavement"]["chute"] == {"structure": "돌붙임", "thickness_m": 0.4}
|
||||
assert "chute" not in sections[160.0]["ford_pavement"]
|
||||
|
||||
@@ -126,17 +126,42 @@ def test_missing_truth_files_are_not_an_error(tmp_path):
|
||||
# ── 포장 판정 규칙 ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_outside_range_keeps_user_value_and_defaults_unpaved():
|
||||
"""경사 제안은 더 이상 포장을 켜지 않는다 — 구간 밖은 사용자 값(없으면 비포장)."""
|
||||
def test_outside_range_is_unpaved():
|
||||
"""포장 = 노면공 · 물넘이 범위만(138-1) — 밖은 비포장(옛 저장값 인자 없음)."""
|
||||
ranges = [(10.0, 20.0)]
|
||||
assert paved_at(30.0, ranges) is False
|
||||
assert paved_at(30.0, ranges, True) is True
|
||||
assert paved_at(30.0, ranges, False) is False
|
||||
assert paved_at(15.0, ranges) is True
|
||||
assert paved_at(20.02, ranges) is True # 끝 허용오차 = 브라우저 짝 0.02 m
|
||||
assert paved_at(20.03, ranges) is False
|
||||
assert paved_at(15.0, []) is False
|
||||
|
||||
|
||||
def test_inside_range_overrides_user_off_switch():
|
||||
"""구간 안은 강제 포장 — 물넘이는 콘크리트 노면이고 지정 구간은 자동 반영이다."""
|
||||
assert paved_at(15.0, [(10.0, 20.0)], False) is True
|
||||
def test_enforce_turns_paved_on_and_off(monkeypatch):
|
||||
"""재계산은 켬↔끔 양쪽 — 범위 밖 옛 `paved=true` 는 비포장으로 되돌림(138-1)."""
|
||||
from pathlib import Path
|
||||
|
||||
from B06_Section import B06_Section_Router_Design as design_mod
|
||||
|
||||
monkeypatch.setattr(design_mod, "pavement_ranges", lambda root: [(0.0, 50.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": 20.0, "design": {"paved": False}}, # 범위 안 · 끔 → 켬
|
||||
{"chainage_m": 80.0, "design": {"paved": True}}, # 범위 밖 · 켬 → 끔
|
||||
{"chainage_m": 30.0, "design": {"paved": True}}, # 그대로
|
||||
{"chainage_m": 90.0, "design": {"paved": False}}, # 그대로
|
||||
]
|
||||
assert design_mod.enforce_pavement_ranges({}, sections, Path("."), None) == 2
|
||||
assert [s["design"]["paved"] for s in sections] == [True, False, True, False]
|
||||
|
||||
# 노면공을 다 지워도(범위 없음) 옛 켬은 꺼진다
|
||||
monkeypatch.setattr(design_mod, "pavement_ranges", lambda root: [])
|
||||
assert design_mod.enforce_pavement_ranges({}, sections, Path("."), None) == 2
|
||||
assert not any(s["design"]["paved"] for s in sections)
|
||||
|
||||
|
||||
# ── 독립 기슭막이 ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""일위대가 로직 줄 단가승격 표시(PLAN 134-1) — 줄에 `승격: true` · 금액 무관.
|
||||
|
||||
- 서버 `clean` = `true` 만 남김(거짓 · 글 · 참조 줄은 버림) · 저장 왕복에 남음 · 금액 그대로
|
||||
- 사전조합 안 로직 줄엔 승격 없음(정리 때 버림)
|
||||
- 화면 `normalizeLabor` 도 `true` 만 통과(Node 헬퍼)
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from resources.tester.test_m02_structure import write_sample_table
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
NET = "GC000337"
|
||||
BARE = "GC000994"
|
||||
|
||||
|
||||
def _doc(lines):
|
||||
return {"열": "rv", "담은로직": lines, "자재": []}
|
||||
|
||||
|
||||
def test_clean_은_참만_남김():
|
||||
got = labor.clean(_doc([{"로직": NET, "승격": True}, {"로직": BARE, "승격": "yes"}]))[
|
||||
"담은로직"
|
||||
]
|
||||
assert got[0]["승격"] is True and "승격" not in got[1]
|
||||
|
||||
|
||||
def test_승격은_금액을_안_바꿈():
|
||||
plain = labor.calc(labor.clean(_doc([{"로직": NET, "수량": {"손": 2}}])), {})
|
||||
flag = labor.calc(labor.clean(_doc([{"로직": NET, "수량": {"손": 2}, "승격": True}])), {})
|
||||
assert flag["반영합"] == plain["반영합"] and flag["기준합"] == plain["기준합"]
|
||||
|
||||
|
||||
def test_사전조합_안_로직_줄엔_승격_없음(monkeypatch):
|
||||
monkeypatch.setattr(refs, "here", lambda: ("system", None))
|
||||
lines = refs._combo_lines({"담은로직": [{"로직": NET, "승격": True}]}, "SC-000009")
|
||||
assert "승격" not in lines[0]
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def system(tmp_path, monkeypatch):
|
||||
write_sample_table(tmp_path)
|
||||
monkeypatch.setattr(store, "FOLDER", tmp_path)
|
||||
store.create_structure("rv")
|
||||
return tmp_path
|
||||
|
||||
|
||||
def test_저장_왕복에_승격_남음(system):
|
||||
have = store.read("labor", "rv")
|
||||
sent = {
|
||||
**have["문서"],
|
||||
"담은로직": [
|
||||
{"로직": NET, "수량": None, "승격": True},
|
||||
{"로직": BARE, "수량": None},
|
||||
],
|
||||
}
|
||||
store.write("labor", "rv", have["판"], sent)
|
||||
rows = store.read("labor", "rv")["문서"]["담은로직"]
|
||||
assert rows[0]["승격"] is True and "승격" not in rows[1]
|
||||
# 내리면(드롭다운 단가산출) 칸째 사라짐
|
||||
have = store.read("labor", "rv")
|
||||
store.write("labor", "rv", have["판"], {**have["문서"], "담은로직": [{"로직": NET}]})
|
||||
assert "승격" not in store.read("labor", "rv")["문서"]["담은로직"][0]
|
||||
|
||||
|
||||
def test_화면_정리도_참만_통과():
|
||||
doc = {
|
||||
"담은로직": [
|
||||
{"로직": NET, "승격": True},
|
||||
{"로직": BARE, "승격": False},
|
||||
{"로직": "", "참조": {"층": "system", "주인": None, "키": "SC-000001"}, "승격": True},
|
||||
]
|
||||
}
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_m02_labor_normalize.cjs")],
|
||||
input=json.dumps({"doc": doc, "column": "rv"}, ensure_ascii=False),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
check=True,
|
||||
)
|
||||
rows = json.loads(proc.stdout)["담은로직"]
|
||||
assert [r.get("승격") for r in rows] == [True, None, None]
|
||||
Reference in New Issue
Block a user