Merge remote-tracking branch 'origin/sub_laptop_6' into sub_laptop_8
This commit is contained in:
@@ -3321,7 +3321,25 @@
|
||||
"계곡통과시설",
|
||||
"월류단면",
|
||||
"날개벽"
|
||||
]
|
||||
],
|
||||
"이름": {
|
||||
"ford.diameter": "관경",
|
||||
"ford.diameter=800": "800",
|
||||
"ford.diameter=1000": "1000",
|
||||
"ford.diameter=1200": "1200",
|
||||
"ford.diameter=1500": "1500",
|
||||
"ford.width": "월류 폭 (m)",
|
||||
"ford.height": "월류 높이 (m)",
|
||||
"ford.count": "수량 (련)",
|
||||
"inlet.structure": "구조",
|
||||
"inlet.structure=없음": "없음",
|
||||
"inlet.structure=날개벽": "날개벽",
|
||||
"inlet.structure=흙막이": "흙막이",
|
||||
"outlet.structure": "구조",
|
||||
"outlet.structure=없음": "없음",
|
||||
"outlet.structure=날개벽": "날개벽",
|
||||
"outlet.structure=흙막이": "흙막이"
|
||||
}
|
||||
},
|
||||
"revetment": {
|
||||
"동작": [
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/* 세월교 명세(표 04) · 월류 폭 ↔ 높이 계산 단위검증 헬퍼(PLAN 52-4) — `design_structure_spec.ts` ·
|
||||
* `design_structure_ford.ts` · `design_structure_ford_calc.ts`(+ `fordSection` 이 든 B05 칸 조각)를 그 자리에서
|
||||
* 트랜스파일해 Node 로 돌림. 결과 = JSON 한 벌. 인자 `names` 면 세월교 이름 표(정의 `종류.ford_bridge.이름`)만 냄.
|
||||
*/
|
||||
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();
|
||||
/** TS 한 파일을 CommonJS 로 — 상대 import 는 그 파일 자리 기준 · `@config/` 는 config 폴더 · .json 은 그대로. */
|
||||
const load = (file) => {
|
||||
if (cache.has(file)) return cache.get(file);
|
||||
if (file.endsWith(".json")) return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
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 near = (m) => {
|
||||
const base = m.startsWith("@config/")
|
||||
? path.join(ROOT, "config", m.slice(8))
|
||||
: path.join(path.dirname(file), m);
|
||||
return load(base.endsWith(".json") ? base : `${base}.ts`);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, near);
|
||||
cache.set(file, box.exports);
|
||||
return box.exports;
|
||||
};
|
||||
const D = (f) => path.join(ROOT, "A00_Common", "design", f);
|
||||
const s = load(D("design_structure_spec.ts"));
|
||||
const { FORD_SPEC, withLegacyGuards } = load(D("design_structure_ford.ts"));
|
||||
const calc = load(D("design_structure_ford_calc.ts"));
|
||||
|
||||
if (process.argv[2] === "names") {
|
||||
process.stdout.write(JSON.stringify(s.nameTable(FORD_SPEC)));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const old = {
|
||||
pipe_kind: "파형강관",
|
||||
pipe_diameter_mm: 1000,
|
||||
pipe_count: 2,
|
||||
ford_width_m: 10,
|
||||
ford_height_m: 0.3,
|
||||
wing_in: "있음",
|
||||
wing_in_height_m: 1,
|
||||
wing_out: "없음",
|
||||
};
|
||||
const legacy = withLegacyGuards(old);
|
||||
const state = s.readState(FORD_SPEC, legacy);
|
||||
const back = s.writeOptions(FORD_SPEC, state);
|
||||
const soil = JSON.parse(JSON.stringify(state));
|
||||
soil.inlet[0].structure = "흙막이";
|
||||
soil.outlet[0].structure = "날개벽";
|
||||
const soilOut = s.writeOptions(FORD_SPEC, soil);
|
||||
|
||||
const flows = [0.5, 2.54, 8];
|
||||
const widths = [3, 5, 10, 20];
|
||||
const table = flows.flatMap((q) => widths.map((b) => [q, b, calc.fordHeightM(q, b)]));
|
||||
|
||||
const out = {
|
||||
containers: FORD_SPEC.containers.map((c) => [c.id, c.title, c.required]),
|
||||
items: FORD_SPEC.containers.map((c) => c.items.map((i) => [i.id, i.kind, i.key ?? null])),
|
||||
choices: FORD_SPEC.containers.map((c) =>
|
||||
c.items.filter((i) => i.kind === "select").map((i) => s.choicesOf(i, {}).map((ch) => ch.label)),
|
||||
),
|
||||
legacy: { inlet: legacy.inlet_type, outlet: legacy.outlet_type },
|
||||
keepSoil: withLegacyGuards({ inlet_type: "흙막이", wing_in: "없음" }).inlet_type,
|
||||
b06Off: withLegacyGuards({ inlet_type: "날개벽", wing_in: "없음" }).inlet_type,
|
||||
b06On: withLegacyGuards({ inlet_type: "없음", wing_in: "있음" }).inlet_type,
|
||||
state,
|
||||
back,
|
||||
soilOut,
|
||||
links: FORD_SPEC.containers.map((c) => s.linkOf(c, 1)),
|
||||
keys: s.specKeys(FORD_SPEC),
|
||||
retired: FORD_SPEC.retired,
|
||||
table,
|
||||
none: [calc.fordHeightM(null, 10), calc.fordHeightM(2.54, 0)],
|
||||
hints: [
|
||||
calc.fordHeightHint(2.54, 10),
|
||||
calc.fordHeightHint(2.54, 0),
|
||||
calc.fordHeightHint(null, 10),
|
||||
],
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -13,7 +13,8 @@ const load = (file, deps = {}) => {
|
||||
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));
|
||||
const near = (m) => (m.startsWith("./") ? load(`${m.slice(2)}.ts`) : require(m)); // 같은 폴더 명세
|
||||
new Function("exports", "module", "require", js)(box.exports, box, (m) => deps[m] ?? near(m));
|
||||
return box.exports;
|
||||
};
|
||||
const s = load("design_structure_spec.ts");
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""횡단배수 › 세월교(표 04) 명세 + 월류 폭 ↔ 높이 연동 — PLAN 52-4.
|
||||
|
||||
순수 규칙은 Node 헬퍼(`helper_52_4_ford.cjs`)가 돌림 · 화면은 ORCA `ui_checks/52-4_세월교_컨테이너.js`.
|
||||
높이 거울 = 이 파일이 Manning(통수단면.md §2 · n 0.017 · S 0.10)을 따로 풀어 같은 값인지 봄.
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(HERE))
|
||||
N, S = 0.017, 0.10
|
||||
|
||||
|
||||
def _node(*args):
|
||||
proc = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_52_4_ford.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"] == [
|
||||
["ford", "세월교", True],
|
||||
["inlet", "유입 구조", True],
|
||||
["outlet", "유출 구조", True],
|
||||
]
|
||||
assert out["items"][0] == [
|
||||
["diameter", "select", "pipe_diameter_mm"],
|
||||
["width", "number", "ford_width_m"],
|
||||
["height", "number", "ford_height_m"],
|
||||
["count", "number", "pipe_count"],
|
||||
]
|
||||
assert out["choices"] == [
|
||||
[["800", "1000", "1200", "1500"]],
|
||||
[["없음", "날개벽", "흙막이"]],
|
||||
[["없음", "날개벽", "흙막이"]],
|
||||
]
|
||||
|
||||
|
||||
def test_옛_날개벽_칸으로_구조를_세우고_같은_키로_씀(out):
|
||||
assert out["legacy"] == {"inlet": "날개벽", "outlet": "없음"}
|
||||
assert out["keepSoil"] == "흙막이" # 흙막이는 설치 칸 없음이어도 그대로
|
||||
assert out["b06Off"] == "없음" and out["b06On"] == "날개벽" # B06 조정창 설치 칸을 따름
|
||||
back = out["back"]
|
||||
assert back["pipe_diameter_mm"] == 1000 and back["pipe_count"] == 2
|
||||
assert back["ford_width_m"] == 10 and back["ford_height_m"] == 0.3
|
||||
assert back["wing_in"] == "있음" and back["wing_out"] == "없음"
|
||||
# 표 04 에 없는 옛 칸은 폼이 안 적음(병합이 이어 붙임 = 지난 정의 값)
|
||||
assert "pipe_kind" not in back and "wing_in_height_m" not in back
|
||||
assert "pipe_kind" in out["retired"]
|
||||
|
||||
|
||||
def test_흙막이_날개벽_고르면_옛_설치_칸도_맞춰_씀(out):
|
||||
s = out["soilOut"]
|
||||
assert s["inlet_type"] == "흙막이" and s["wing_in"] == "없음"
|
||||
assert s["outlet_type"] == "날개벽" and s["wing_out"] == "있음"
|
||||
|
||||
|
||||
def test_잇기_키_컨테이너마다(out):
|
||||
assert out["links"] == ["gford", "gfordin", "gfordout"]
|
||||
|
||||
|
||||
def _manning_depth(q, b):
|
||||
"""넓은 직사각 수로 Manning 을 h 에 대해 이분법으로 풂 — 헬퍼와 다른 풀이."""
|
||||
|
||||
def cap(h):
|
||||
return b * h * (1 / N) * ((b * h) / (b + 2 * h)) ** (2 / 3) * math.sqrt(S)
|
||||
|
||||
lo, hi = 0.0, 5.0
|
||||
for _ in range(100):
|
||||
mid = (lo + hi) / 2
|
||||
lo, hi = (mid, hi) if cap(mid) < q else (lo, mid)
|
||||
return hi
|
||||
|
||||
|
||||
def test_월류_높이는_폭과_연동_필요_수심_올림(out):
|
||||
for q, b, h in out["table"]:
|
||||
need = _manning_depth(q, b)
|
||||
assert h == pytest.approx(math.ceil(need * 100 - 1e-9) / 100), (q, b)
|
||||
# 폭이 넓을수록 높이가 낮아짐
|
||||
row = [h for q, b, h in out["table"] if q == 2.54]
|
||||
assert row == sorted(row, reverse=True) and len(set(row)) == len(row)
|
||||
assert out["none"] == [None, None] # 설계유량 · 폭이 없으면 손 입력
|
||||
assert out["hints"] == ["필요 수심 0.08 이상", "월류 폭을 넣으면 셈", "설계유량 없음 — 손 입력"]
|
||||
|
||||
|
||||
def test_정의_파일에_세월교_이름_표가_올라_있음():
|
||||
names = _node("names")
|
||||
with open(
|
||||
os.path.join(ROOT, "resources", "master_template", "구조물정의.json"), encoding="utf-8"
|
||||
) as f:
|
||||
doc = json.load(f)
|
||||
assert doc["종류"]["ford_bridge"]["이름"] == names
|
||||
@@ -0,0 +1,181 @@
|
||||
/* 52-4 — B05 · B06 좌측 「구조물 배치」 세월교 = 사용자 구조물 표 04 명세(컨테이너 셋)
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(sub6_검증) · 목록에 「세월교 1」
|
||||
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m524 = "단계";` 를 붙여
|
||||
* orca eval --expression "window.__m524='edit'; $(cat 이 파일)" → 고치고 [저장] (원래 관 지점은 sessionStorage 에 둠)
|
||||
* (orca reload 로 다시 열기)
|
||||
* orca eval --expression "window.__m524='verify'; $(cat 이 파일)" → 다시 연 값 · B06 같은 그리기
|
||||
* orca eval --expression "window.__m524='restore'; $(cat 이 파일)" → 원래 관 지점으로 되돌림(PUT) · 초안 비움
|
||||
* 쓰는 것: 검증 프로젝트 pipe_points.json(되돌림 단계가 원래 값으로 다시 씀)
|
||||
* 기대 수치(edit): 컨테이너 3(세월교 · 유입 구조 · 유출 구조 · 모두 필수) · 관경 800/1000/1200/1500 ·
|
||||
* 구조 없음/날개벽/흙막이 · 옛 관종 · 날개벽 칸 안 보임 · 폭 8 → 높이 = 필요 수심(설계유량 있으면) ·
|
||||
* 낮게 적으면 필요 수심으로 되돌림 · 수량 3 · 잇기 세월교 · 유입 지정 · 유출 지정 뒤 풂
|
||||
* 기대 수치(verify): 저장 값 그대로 · 같은 그리기(B05 = B06)
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m524 || "edit";
|
||||
const NAME = /세월교 1$/;
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const until = async (fn, ms = 20000) => {
|
||||
for (let t = 0; t < ms; t += 200) {
|
||||
if (fn()) return true;
|
||||
await w(200);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const projectId = localStorage.getItem("frd_current_project_id");
|
||||
const modUrl = (part) =>
|
||||
performance
|
||||
.getEntriesByType("resource")
|
||||
.map((e) => e.name)
|
||||
.filter((n) => n.includes(part))
|
||||
.pop();
|
||||
const fetchApi = await import(modUrl("/B04_PreProcess/B04_PreProcess_Api_Fetch.ts"));
|
||||
const spec = () =>
|
||||
[...document.querySelectorAll(".b05-structure__spec")].find((n) => n.offsetParent !== null);
|
||||
const box = (c) => spec().querySelector(`[data-spec-container="${c}"]`);
|
||||
const ctl = (c, item) =>
|
||||
box(c)?.querySelector(`[data-spec-item="${c}.${item}"]`)?.querySelector("select, input");
|
||||
const set = async (c, item, v) => {
|
||||
const el = ctl(c, item);
|
||||
el.value = v;
|
||||
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await w(250);
|
||||
};
|
||||
const labels = (c, item) => [...ctl(c, item).options].filter((o) => o.value).map((o) => o.text);
|
||||
const pickFord = async () => {
|
||||
const find = () =>
|
||||
[...document.querySelectorAll(".b05-route__irregular-list li")].find(
|
||||
(l) => l.offsetParent !== null && NAME.test(l.textContent.trim()),
|
||||
);
|
||||
await until(() => !!find());
|
||||
const li = find();
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await w(800);
|
||||
if (!spec()) (li.querySelector("button") ?? li).click();
|
||||
await until(() => !!spec());
|
||||
await w(500);
|
||||
};
|
||||
const shape = () =>
|
||||
[...spec().querySelectorAll("[data-spec-container]")].map((b) => ({
|
||||
c: b.dataset.specContainer,
|
||||
legend: b.querySelector("legend").firstChild.textContent,
|
||||
items: [...b.querySelectorAll("[data-spec-item]")].map((n) => [
|
||||
n.dataset.specItem,
|
||||
n.querySelector("select, input").value,
|
||||
]),
|
||||
link: b.querySelector(".b05-structure__summary-name")?.textContent,
|
||||
}));
|
||||
const assigned = (c) =>
|
||||
box(c).querySelector(".b05-structure__summary-name").classList.contains("is-assigned");
|
||||
const pickLink = async (c) => {
|
||||
box(c).querySelector('[data-b05-summary="pick"]').click();
|
||||
await until(() => document.querySelector(".ui-item-picker__tree .ui-tree__row"));
|
||||
for (let k = 0; k < 8; k++) {
|
||||
const rows = [...document.querySelectorAll(".ui-item-picker__tree .ui-tree__row")];
|
||||
const leaf = rows.find((r) => !r.classList.contains("is-branch"));
|
||||
if (leaf) {
|
||||
leaf.click();
|
||||
break;
|
||||
}
|
||||
rows.find((r) => r.querySelector(".ui-tree__caret")?.textContent === "▸")?.click();
|
||||
await w(300);
|
||||
}
|
||||
await w(300);
|
||||
[...document.querySelectorAll(".ui-item-picker__actions button")]
|
||||
.find((b) => b.textContent.includes("적용"))
|
||||
.click();
|
||||
await w(500);
|
||||
};
|
||||
const visibleKeys = () =>
|
||||
[...document.querySelectorAll(".b05-drainage__facility [data-option-keys]")]
|
||||
.filter((n) => n.offsetParent !== null)
|
||||
.map((n) => n.dataset.optionKeys);
|
||||
const out = { mode: MODE };
|
||||
|
||||
if (MODE === "edit") {
|
||||
const before = await fetchApi.fetchDetailPipePoints(projectId);
|
||||
sessionStorage.setItem("__524_original", JSON.stringify(before.pipe_points));
|
||||
await pickFord();
|
||||
const s0 = shape();
|
||||
out.containers = s0.map((b) => [b.c, b.legend]);
|
||||
out.oldFieldsVisible = visibleKeys(); // 옛 관종 · 월류 · 날개벽 칸 — 빈 목록이어야
|
||||
out.diameters = labels("ford", "diameter");
|
||||
out.inletChoices = labels("inlet", "structure");
|
||||
out.outletChoices = labels("outlet", "structure");
|
||||
out.start = s0.map((b) => b.items);
|
||||
await set("ford", "diameter", "1200");
|
||||
await set("ford", "width", "8");
|
||||
out.heightAfterWidth8 = ctl("ford", "height").value;
|
||||
out.heightHint = ctl("ford", "height").placeholder;
|
||||
await set("ford", "width", "4");
|
||||
out.heightAfterWidth4 = ctl("ford", "height").value;
|
||||
await set("ford", "height", "0.01");
|
||||
out.heightLowBounced = ctl("ford", "height").value;
|
||||
const raised = (Number(out.heightAfterWidth4 || 0) + 0.2).toFixed(2);
|
||||
await set("ford", "height", raised);
|
||||
out.heightRaisedKept = ctl("ford", "height").value === raised;
|
||||
await set("ford", "count", "3");
|
||||
await set("inlet", "structure", "흙막이");
|
||||
await set("outlet", "structure", "날개벽");
|
||||
await pickLink("ford");
|
||||
await pickLink("inlet");
|
||||
await pickLink("outlet");
|
||||
out.linksPicked = ["ford", "inlet", "outlet"].map(assigned);
|
||||
box("outlet").querySelector('[data-b05-summary="clear"]').click();
|
||||
await w(300);
|
||||
out.linkOutletCleared = !assigned("outlet");
|
||||
out.edited = shape().map((b) => b.items);
|
||||
document.querySelector('[data-dock-action="save"]').click();
|
||||
await w(500);
|
||||
out.saved = await until(() => !document.querySelector(".ui-overlay.is-active"), 60000);
|
||||
const after = await fetchApi.fetchDetailPipePoints(projectId);
|
||||
const p = after.pipe_points.find((x) => x.chainage_m > 170 && x.chainage_m < 176); // 세월교 1 = 8+13
|
||||
out.file = p && { options: p.options, group_items: Object.keys(p.group_items ?? {}) };
|
||||
}
|
||||
|
||||
if (MODE === "verify") {
|
||||
await pickFord();
|
||||
const b05 = shape();
|
||||
out.reopened = b05.map((b) => b.items);
|
||||
out.links = ["ford", "inlet", "outlet"].map(assigned);
|
||||
location.hash = "#/b06-section";
|
||||
await until(() => location.hash.includes("b06"), 5000);
|
||||
await w(6000);
|
||||
await pickFord();
|
||||
const b06 = shape();
|
||||
out.sameDrawing = JSON.stringify(b05) === JSON.stringify(b06);
|
||||
if (!out.sameDrawing) out.diff = { b05, b06 };
|
||||
out.b06WingSlots = ["inlet", "outlet"].map(
|
||||
(c) => box(c).querySelector(".b05-structure__adjust-slot")?.childElementCount ?? -1,
|
||||
);
|
||||
}
|
||||
|
||||
if (MODE === "restore") {
|
||||
const original = JSON.parse(sessionStorage.getItem("__524_original") || "null");
|
||||
if (!original) return JSON.stringify({ error: "원래 값 없음" });
|
||||
const keys = [
|
||||
"chainage_m",
|
||||
"source",
|
||||
"facility",
|
||||
"start_m",
|
||||
"end_m",
|
||||
"options",
|
||||
"summary_item",
|
||||
"group_items",
|
||||
];
|
||||
const points = original.map((p) =>
|
||||
Object.fromEntries(
|
||||
keys.filter((k) => p[k] !== undefined && p[k] !== null).map((k) => [k, p[k]]),
|
||||
),
|
||||
);
|
||||
await fetchApi.saveDetailPipePoints(projectId, points);
|
||||
for (const k of Object.keys(sessionStorage))
|
||||
if (k.startsWith("aislo:draft:pipes:") || k === "__524_original")
|
||||
sessionStorage.removeItem(k);
|
||||
const now = await fetchApi.fetchDetailPipePoints(projectId);
|
||||
const pick = (list) =>
|
||||
JSON.stringify(list.map((p) => [p.chainage_m, p.options ?? {}, p.group_items ?? {}]));
|
||||
out.restored = pick(now.pipe_points) === pick(original);
|
||||
}
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
Reference in New Issue
Block a user