Merge remote-tracking branch 'origin/sub_laptop_5' into main_laptop_1

This commit is contained in:
2026-10-08 12:16:13 +09:00
11 changed files with 444 additions and 29 deletions
+54
View File
@@ -0,0 +1,54 @@
/* 노면공 확폭 구간 규칙(PLAN 87-3) — `design_structure_widening.ts` 를 그 자리에서 트랜스파일해 Node 로 돌림. */
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const file = path.join(
__dirname,
"..",
"..",
"A00_Common",
"design",
"design_structure_widening.ts",
);
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
new Function("exports", "module", js)(box.exports, box);
const w = box.exports;
// 곡선 둘(앞 No.1+0~No.3+0 · 뒤 No.5+0~No.6+0) 사이에 직선 측점 하나 — 확폭 좌우 합
const at = (chainage_m, left = 0, right = 0) => ({
chainage_m,
design: { widening_left_m: left, widening_right_m: right },
});
const stations = [
at(0),
at(20, 0.3, 0),
at(40, 0.75, 0),
at(60, 0.4, 0),
at(80),
at(100, 0, 0.5),
at(120, 0, 0.6),
at(140),
];
const runs = w.wideningRuns(stations, 0, 140);
const inner = w.wideningRuns(stations, 30, 110); // 범위가 곡선을 가로지름
const off = w.formatOff([[20, 60]]);
const out = {
runs,
inner,
none: w.wideningRuns(stations, 0, 10),
fallback: w.wideningOf({ chainage_m: 1, curve_widening_m: 0.9, curve_outer_side: "left" }),
fallbackStraight: w.wideningOf({ chainage_m: 1, curve_widening_m: 0.9, curve_outer_side: null }),
off,
parsed: w.parseOff("20-60, 100-120,bad,5"),
isOff: runs.map((r) => w.isRunOff(r, off)),
toggledOff: w.toggleRun(runs, "", runs[1]),
toggledBoth: w.toggleRun(runs, off, runs[1]),
toggledOn: w.toggleRun(runs, off, runs[0]),
stale: w.offForRuns(runs, "20-60,0-5,100-130"),
empty: w.offForRuns([], "20-60"),
};
process.stdout.write(JSON.stringify(out));
+60
View File
@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
"""노면공 확폭 구간(PLAN 87-3) — 구간 묶기 · `widening_off` 글 읽고 쓰기 · 안 맞는 값 무시.
규칙은 Node 헬퍼(`helper_87_widening.cjs`)가 `design_structure_widening.ts` 를 그대로 돌림.
"""
import json
import os
import subprocess
import pytest
HERE = os.path.dirname(os.path.abspath(__file__))
@pytest.fixture(scope="module")
def out():
proc = subprocess.run(
["node", os.path.join(HERE, "helper_87_widening.cjs")],
capture_output=True,
text=True,
encoding="utf-8",
timeout=60,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
def test_곡선_둘은_구간_둘_확폭은_구간_최대(out):
assert out["runs"] == [
{"start_m": 20, "end_m": 60, "widening_m": 0.75},
{"start_m": 100, "end_m": 120, "widening_m": 0.6},
]
def test_범위_안_측점만_묶음(out):
assert out["inner"] == [
{"start_m": 40, "end_m": 60, "widening_m": 0.75},
{"start_m": 100, "end_m": 100, "widening_m": 0.5},
]
assert out["none"] == []
def test_설계_없으면_곡선_확폭_바깥쪽만(out):
assert out["fallback"] == 0.9
assert out["fallbackStraight"] == 0
def test_끈_구간_글_왕복(out):
assert out["off"] == "20-60"
assert out["parsed"] == [[20, 60], [100, 120]] # 틀린 조각은 버림
assert out["isOff"] == [True, False]
assert out["toggledOff"] == "100-120"
assert out["toggledBoth"] == "20-60,100-120"
assert out["toggledOn"] == ""
def test_안_맞는_끈_값은_무시_저장_때_빠짐(out):
assert out["stale"] == "20-60"
assert out["empty"] == ""
@@ -8,8 +8,9 @@
* orca eval --expression "window.__m5210='restore'; $(cat 이 파일)" → 추가한 둘 삭제 · [저장]
* 쓰는 것: 검증 프로젝트 structures.json(되돌림 단계가 지움)
* 기대 수치:
* 포장 — containers ["detail"] · 상세 내용 · 포장사양 선택지 ["콘크리트"] · 높이 0.35 · 잇기 지정 ·
* 종류 단위 잇기 줄 가림 · 옛 상세 칸(두께 · 줄눈 …) 안 보임 · 길이 · 전 · 후 칸 보임
* 포장 — containers ["detail"] · 상세 내용 · 포장사양 선택지 [콘크리트 · 아스콘 · 쇄석] · 칸 차례 포장사양 → 길이(출력 20) →
* 높이 0.35 · 잇기 지정 · 종류 단위 잇기 줄 가림 · 옛 상세 칸(두께 · 줄눈 …) 안 보임 ·
* 기준 측점 칸 없음 · 시작 · 종료 칸 보임 · 길이 · 전 · 후 입력 칸 없음
* 기타 — 「상세 종류 이름」 글 칸 = 「52-10 시험 기타」 · 목록 이름도 그 글 ·
* 선택 컨테이너 「상세 내용」(52-14) 하나만(52-17) — 추가 → [추가] 없음 · 빼기 → 다시 보임 ·
* 다시 추가 · 비고 「52-14 비고」(길이 상한 100) · 잇기 · 하나 남음 · 종류 단위 잇기 줄 가림
@@ -37,7 +38,7 @@
};
const listItems = () => [...document.querySelectorAll(".b05-route__irregular-list li")];
const ITEMS = [
{ typeId: "pavement_concrete", group: "G", at: [4, 6], list: /콘크리트 포장/ },
{ typeId: "pavement_concrete", group: "G", at: [4, 6], to: [5, 6], list: /콘크리트 포장/ },
{ typeId: "etc", group: "호환", at: [5, 2], list: new RegExp(NAME) },
];
const typeSummary = () =>
@@ -76,6 +77,16 @@
: null,
paveSpec: specCtl("spec")?.value ?? null,
height: specCtl("height")?.value ?? null,
itemOrder: boxes[0]
? [...boxes[0].querySelectorAll("[data-spec-item]")].map((n) => n.dataset.specItem)
: [],
lengthOut:
spec()?.querySelector('[data-spec-item="detail.length"] output')?.textContent ?? null,
anchorHidden: !!sec().querySelectorAll(".b05-structure__position-row > label")[2]?.hidden,
startEndShown: [1, 3].every(
(n) => !sec().querySelectorAll(".b05-structure__position-row > label")[n]?.hidden,
),
wideningShown: vis(sec().querySelector(".b05-structure__widening")),
containerLink: !!boxes[0]?.querySelector(".b05-structure__summary-name.is-assigned"),
containerLinks: boxes.map(
(b) => !!b.querySelector(".b05-structure__summary-name.is-assigned"),
@@ -139,11 +150,20 @@
for (const item of ITEMS) {
await pickType(item.group, item.typeId);
const row = { lockedBeforePosition: shape(item.typeId).locked };
const [no, dist] = sec()
.querySelectorAll(".b05-structure__position-row > label")[2]
.querySelectorAll("input");
fire(no, String(item.at[0]));
fire(dist, String(item.at[1]));
const labels = sec().querySelectorAll(".b05-structure__position-row > label");
if (item.to) {
// 노면공 = 시작 · 종료 측점만(87-1) — 기준 측점 칸 없음
const [s0, s1] = labels[1].querySelectorAll("input");
const [e0, e1] = labels[3].querySelectorAll("input");
fire(s0, String(item.at[0]));
fire(s1, String(item.at[1]));
fire(e0, String(item.to[0]));
fire(e1, String(item.to[1]));
} else {
const [no, dist] = labels[2].querySelectorAll("input");
fire(no, String(item.at[0]));
fire(dist, String(item.at[1]));
}
await w(300);
if (item.typeId === "pavement_concrete") {
fire(specCtl("spec"), "콘크리트");
@@ -226,13 +246,17 @@
!pave.missing &&
JSON.stringify(pave.containers) === '["detail"]' &&
pave.legend[0] === "상세 내용" &&
JSON.stringify(pave.paveChoices) === '["콘크리트"]' &&
JSON.stringify(pave.paveChoices) === '["콘크리트","아스콘","쇄석"]' &&
JSON.stringify(pave.itemOrder) === '["detail.spec","detail.length","detail.height"]' &&
Number(pave.lengthOut) === 20 &&
pave.anchorHidden &&
pave.startEndShown &&
pave.paveSpec === "콘크리트" &&
Number(pave.height) === 0.35 &&
pave.containerLink &&
!pave.typeSummaryVisible &&
!pave.options.some((l) => /두께|줄눈|철망|비닐|포장 폭|확폭/.test(l)) &&
["길이", "기준측점 전", "기준측점 후"].every((l) => pave.options.some((o) => o.startsWith(l)));
!pave.options.some((l) => /^(길이|기준측점 전|기준측점 후)/.test(l)); // 87-1 입력 칸 없음
out.etcOk =
!!etc &&
!etc.missing &&
@@ -13,7 +13,7 @@
(async () => {
const MODE = window.__m5215 || "check";
const PIPE = /^22\+0 배관/;
const PAVE = /^21\+15~22\+5 콘크리트 포장/; // 기준 22+0 · 길이 10 m
const PAVE = /^21\+15~22\+5 콘크리트 포장/; // 시작 21+15 · 종료 22+5 (10 m)
const OTHER = /^4\+5 배관/;
const w = (ms) => new Promise((r) => setTimeout(r, ms));
const until = async (fn, ms = 15000) => {
@@ -85,13 +85,16 @@
await w(300);
fire(t, "pavement_concrete");
await w(1500);
const anchor = () =>
sec().querySelectorAll(".b05-structure__position-row > label")[2].querySelectorAll("input");
fire(anchor()[0], "22");
// 노면공 = 시작 · 종료 측점만(87-1) — 시작 21+15 · 종료 22+5(10 m)
const pos = (n) =>
sec().querySelectorAll(".b05-structure__position-row > label")[n].querySelectorAll("input");
fire(pos(1)[0], "21");
fire(pos(1)[1], "15");
fire(pos(3)[0], "22");
await w(300);
fire(anchor()[1], "0");
fire(pos(3)[1], "5");
await w(500);
out.anchor = [...anchor()].map((i) => i.value);
out.anchor = [...pos(1), ...pos(3)].map((i) => i.value);
fire(specCtl("spec"), "콘크리트");
await w(200);
fire(specCtl("height"), "0.35");