merge: main_laptop_1 665ccf777 — 52-7 충돌 = 양쪽 다 살림
- Panel_Options = 메인 hidesTypeSummary 세 조건(명세 · 정의 그룹 · 종단배수) + 가드레일 solveSplit · 옛 hasSpecContainers 걷음 - 구조물정의.json = 대피로 · 가드레일 · 맹암거 그룹 다 · 판 9 - 이력 판_6 = 메인 것(바탕 판 6 그대로) · 네 판_6 은 판_8 로 - 52-8 숨김(군 단위)과 대피로 목록 빼기(종류 단위)는 자리가 달라 둘 다 둠 - 시험 수 = B05 선택지 28 · 정의 종류 42 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"판": 7,
|
||||
"판": 9,
|
||||
"schema_version": 1,
|
||||
"comment": "B05 구조물 타입 레지스트리 정본. 근거·옵션 상세는 PLAN.md 구조물 리스트와 resources/knowledge/technical_info/01_임도 참조. 2026-08-17 선택지 재편: B군(종단배수)·F군(생태/녹화)·G군(노면공)은 enabled:false 보존 — B05 선택지에서 빠지고 B06 개별 횡단도 옵션으로 재사용한다. 바닥막이(bed_sill)·생태연못(eco_pond)은 임도 미사용 확정으로 삭제. 배관·BOX암거·물넘이·세월교는 pipe_points.json 정본 관리(managed_by) — structures.json에 저장하지 않으며, 배관 부속(유형/집수정/기슭막이/돌붙임)·날개벽 옵션 정의는 B06/B07 필수 승격의 선언 근거다. ★기본값 원칙(2026-08-16 크로스체크 2차): default를 두는 것은 ①법정이 등급과 무관하게 못박은 단일 수치 ②사용자가 개발 단계에서 확정한 값 ③도메인 수치가 아닌 표시용 문자열, 이 셋뿐이다. 재료·형식 같은 설계자 판단 선택지와 미확정 제원은 default 없이 required로 두어 사용자가 직접 고르게 한다 (pipe_diameter_mm 1000 = 별표2 원칙값+사용자 확정 — 화이트리스트 등재). BOX암거 본체 2.0×2.0·날개벽 유입/유출 있음·짧은쪽 높이 1m·길이 2m·각도 45°는 2026-08-17 사용자 확정값이다 — 본체 규격은 실무 관측 규격(2.0×2.0·3.0×3.0)을 폼에서 프리셋 select로 받고 필요 시 사용자 지정으로 자유 입력하되, 저장 키는 body_width_m·body_height_m 숫자 그대로다(프리셋 전용 키를 만들지 않는다). 이 값들은 B05 서브폼이 실제로 받으므로 phase를 detail로 두지 않는다. ★보호공 개편(2026-08-17 사용자 지시): 구 `*_pitching`(있음/없음) + `*_pitching_finish`(찰/메) 두 축을 `*_protection` 한 축(돌붙임(찰)·돌붙임(메)·도수로)으로 합쳤다 — 도수로·산비탈수로(B4)는 유출부에서 돌붙임 대신 쓰는 공종이라 별도 구조물이 아니라 보호공 선택지로 받는다. \"없음\"은 삭제(물이 흐르는 자리라 보호공 필수). 치수는 배타 — 돌붙임이면 `*_protection_area_m2`(10㎡), 도수로면 `*_protection_width_m`(1m, 지식DB 수치 근거 없음·사용자 확정, 실무 확인 후 개선). 구 저장분은 `common_util_drainage_pipes.parse_pipe_points`가 새 키로 옮긴다.",
|
||||
"구조물군": {
|
||||
@@ -676,28 +676,41 @@
|
||||
"cross_section",
|
||||
"quantity"
|
||||
],
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"type_id": "cross_underdrain",
|
||||
"group": "A",
|
||||
"name": "맹암거",
|
||||
"placement": "point",
|
||||
"style": {
|
||||
"color": "#3aa0a8",
|
||||
"abbr": "횡맹"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan",
|
||||
"profile",
|
||||
"cross_section",
|
||||
"quantity"
|
||||
],
|
||||
"options": [
|
||||
{
|
||||
"key": "ditch_spec",
|
||||
"label": "규격",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"콘크리트 개거 150×200",
|
||||
"L형수로 H=0.2"
|
||||
],
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 실무 붙박이(콘크리트 개거)로 돎"
|
||||
},
|
||||
{
|
||||
"key": "length_m",
|
||||
"label": "연장",
|
||||
"key": "culvert_height_m",
|
||||
"label": "암거 높이",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": false,
|
||||
"empty_means": "비우면 표가 안 서고 「연장을 적어야 섬」 사유가 뜸 — 개거는 m당 원단위라 연장이 곧 밑수(2026-09-14 A2)"
|
||||
"empty_means": "비우면 값 없음 — 사용자 표 06 손 입력 칸(기본값 근거 없음)"
|
||||
},
|
||||
{
|
||||
"key": "culvert_slope_pct",
|
||||
"label": "암거 경사",
|
||||
"input": "number",
|
||||
"unit": "%",
|
||||
"default": null,
|
||||
"required": false,
|
||||
"empty_means": "비우면 값 없음 — 사용자 표 06 손 입력 칸(기본값 근거 없음)"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -3384,7 +3397,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": {
|
||||
"동작": [
|
||||
@@ -3451,6 +3482,17 @@
|
||||
],
|
||||
"형상": {}
|
||||
}
|
||||
],
|
||||
"cross_underdrain": [
|
||||
{
|
||||
"id": "gcrossud",
|
||||
"이름": "맹암거",
|
||||
"옵션": [
|
||||
"culvert_height_m",
|
||||
"culvert_slope_pct"
|
||||
],
|
||||
"형상": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"배치": {},
|
||||
|
||||
@@ -2732,12 +2732,26 @@
|
||||
"drawing_views": [
|
||||
"plan"
|
||||
],
|
||||
"options": []
|
||||
"options": [
|
||||
{
|
||||
"key": "facing",
|
||||
"label": "방향",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"좌",
|
||||
"우",
|
||||
"양면"
|
||||
],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type_id": "guardrail",
|
||||
"group": "E",
|
||||
"name": "가드레일",
|
||||
"name": "가드레일·경계석·위험표지",
|
||||
"placement": "interval",
|
||||
"style": {
|
||||
"color": "#1f9e46",
|
||||
@@ -2750,32 +2764,17 @@
|
||||
],
|
||||
"options": [
|
||||
{
|
||||
"key": "form",
|
||||
"label": "형태",
|
||||
"input": "text",
|
||||
"key": "kind",
|
||||
"label": "종류",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"가드레일",
|
||||
"경계석",
|
||||
"위험표지"
|
||||
],
|
||||
"default": null,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"key": "length_m",
|
||||
"label": "길이",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null
|
||||
},
|
||||
{
|
||||
"key": "before_m",
|
||||
"label": "기준측점 전",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null
|
||||
},
|
||||
{
|
||||
"key": "after_m",
|
||||
"label": "기준측점 후",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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,64 @@
|
||||
/* M02 구조물 명세 이름 고치기(`M02_SO_Def_Spec_Model.ts` · PLAN 52-11) 단위검증 헬퍼 —
|
||||
* TS 를 그 자리에서 트랜스파일해 Node 로 돌리고 결과를 JSON 으로 냄(pytest 가 판정).
|
||||
* 정의는 진짜 `구조물정의.json` 을 읽어 메모리 사본만 고침(파일 쓰기 없음). */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const load = (...parts) => {
|
||||
const js = ts.transpileModule(fs.readFileSync(path.join(ROOT, ...parts), "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, require);
|
||||
return box.exports;
|
||||
};
|
||||
const m = load("M02_MasterTemplete", "StructOptions", "M02_SO_Def_Spec_Model.ts");
|
||||
const { PIPE_SPEC } = load("A00_Common", "design", "design_structure_pipe.ts");
|
||||
const real = () =>
|
||||
JSON.parse(
|
||||
fs.readFileSync(path.join(ROOT, "resources", "master_template", "구조물정의.json"), "utf8"),
|
||||
);
|
||||
|
||||
const out = {};
|
||||
const def = real();
|
||||
const rows = m.specNameRows(def, PIPE_SPEC);
|
||||
out.levels = [0, 1, 2].map((lv) => rows.filter((r) => r.level === lv).length);
|
||||
out.unique = new Set(rows.map((r) => r.nameKey)).size === rows.length;
|
||||
out.pipeTitle = rows.find((r) => r.nameKey === "pipe");
|
||||
out.optional = rows.find((r) => r.nameKey === "soil_add");
|
||||
out.basinForm = rows.find((r) => r.nameKey === "inlet.form=I");
|
||||
|
||||
// 이름 고침 — 그룹 제목 · 항목 · 값 · 선택 컨테이너
|
||||
const work = real();
|
||||
const pick = (k) => m.specNameRows(work, PIPE_SPEC).find((r) => r.nameKey === k);
|
||||
m.setSpecName(work, "pipe", pick("pipe"), "관 본체");
|
||||
m.setSpecName(work, "pipe", pick("pipe.diameter"), "관 지름");
|
||||
m.setSpecName(work, "pipe", pick("pipe.diameter=800"), "Ø800");
|
||||
m.setSpecName(work, "pipe", pick("soil_add"), "흙막이 더");
|
||||
out.groupName = work.그룹.pipe.find((g) => g.id === "grv6lny").이름;
|
||||
out.names = ["pipe", "pipe.diameter", "pipe.diameter=800", "soil_add"].map(
|
||||
(k) => work.종류.pipe.이름[k] ?? null,
|
||||
);
|
||||
out.reread = ["pipe", "pipe.diameter", "pipe.diameter=800", "soil_add"].map((k) => pick(k).label);
|
||||
// 구조는 그대로 — types · 동작 · 다른 종류 · 이름 표 키 수(새 키 없음)
|
||||
const before = real();
|
||||
out.sameTypes = JSON.stringify(work.types) === JSON.stringify(before.types);
|
||||
out.sameKeys =
|
||||
Object.keys(work.종류.pipe.이름).length === Object.keys(before.종류.pipe.이름).length;
|
||||
|
||||
// 이름 표가 빈 종류 — 명세 글로 읽고 고치면 이름 표가 생김
|
||||
const bare = real();
|
||||
delete bare.종류.pipe.이름;
|
||||
const bareRows = m.specNameRows(bare, PIPE_SPEC);
|
||||
out.bareLabel = bareRows.find((r) => r.nameKey === "inlet.structure=기슭막이").label;
|
||||
m.setSpecName(
|
||||
bare,
|
||||
"pipe",
|
||||
bareRows.find((r) => r.nameKey === "inlet"),
|
||||
"유입",
|
||||
);
|
||||
out.bareGroup = bare.그룹.pipe.find((g) => g.id === "gnbf9m9").이름;
|
||||
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -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,91 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""52-6 횡단배수 › 맹암거(표 06 · 새 종류) — 정의 · 컨테이너 · 서버 저장 왕복.
|
||||
|
||||
새 키 `cross_underdrain`(횡단배수 · 점형) — 종단배수 맹암거(`underdrain`)와 따로.
|
||||
컨테이너 = 정의 그룹 「맹암거」(필수 · 늘 한 벌) — 암거 높이(m) · 암거 경사(%) 손 입력(비워 둘 수 있음) ·
|
||||
그룹 잇기(`group_items`)로 구조물 조합 항목. 저장은 임시 storage 에서만.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from B05_Profile.B05_Profile_Structures_Schema import (
|
||||
StructureInstance,
|
||||
is_station_planting_type,
|
||||
load_definition,
|
||||
structure_type_map,
|
||||
)
|
||||
|
||||
KEY = "cross_underdrain"
|
||||
GROUP = "gcrossud"
|
||||
|
||||
|
||||
def test_새_종류는_횡단배수_점형_종단배수_맹암거와_따로():
|
||||
types = structure_type_map()
|
||||
new, old = types[KEY], types["underdrain"]
|
||||
assert (new.group, new.placement, new.name, new.enabled) == ("A", "point", "맹암거", True)
|
||||
assert (old.group, old.placement) == ("B", "interval")
|
||||
assert new.style["abbr"] != old.style["abbr"] and new.style["color"] != old.style["color"]
|
||||
assert is_station_planting_type(new) # 횡단배수 = 그 측점에 횡단도
|
||||
|
||||
|
||||
def test_컨테이너_맹암거_높이_경사_손_입력():
|
||||
opts = {o.key: o for o in structure_type_map()[KEY].options}
|
||||
assert list(opts) == ["culvert_height_m", "culvert_slope_pct"]
|
||||
for key, label, unit in (
|
||||
("culvert_height_m", "암거 높이", "m"),
|
||||
("culvert_slope_pct", "암거 경사", "%"),
|
||||
):
|
||||
o = opts[key]
|
||||
assert (o.label, o.input, o.unit, o.default, o.required) == (
|
||||
label,
|
||||
"number",
|
||||
unit,
|
||||
None,
|
||||
False,
|
||||
)
|
||||
assert o.empty_means # 손 입력 · 기본값 근거 없음 — 비워 둘 수 있음(B05 단계 필수 금지)
|
||||
groups = load_definition()["그룹"][KEY]
|
||||
assert groups == [
|
||||
{"id": GROUP, "이름": "맹암거", "옵션": list(opts), "형상": {}},
|
||||
]
|
||||
|
||||
|
||||
def test_정의_검사_통과():
|
||||
from M02_MasterTemplete.StructOptions import M02_SO_Definition as definition
|
||||
|
||||
assert definition.check(load_definition()) == []
|
||||
|
||||
|
||||
def _item(**options):
|
||||
return StructureInstance(
|
||||
type_id=KEY,
|
||||
placement="point",
|
||||
chainage_m=63.5,
|
||||
options=options,
|
||||
group_items={
|
||||
GROUP: {"layer": "system", "owner": None, "key": "SS000001", "name": "맹암거"}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_저장_다시_읽기_그대로(tmp_path):
|
||||
from B05_Profile.B05_Profile_Structures_Repository import load_structures, save_structures
|
||||
|
||||
item = _item(culvert_height_m=0.6, culvert_slope_pct=2.5)
|
||||
save_structures(str(tmp_path), [item], base_revision=0)
|
||||
_, loaded = load_structures(str(tmp_path))
|
||||
assert [(s.type_id, s.chainage_m, s.options) for s in loaded] == [
|
||||
(KEY, 63.5, {"culvert_height_m": 0.6, "culvert_slope_pct": 2.5})
|
||||
]
|
||||
assert loaded[0].group_items == item.group_items
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"options",
|
||||
[{"culvert_height_m": -1}, {"culvert_slope_pct": -0.5}],
|
||||
)
|
||||
def test_음수는_서버가_막음(tmp_path, options):
|
||||
from B05_Profile.B05_Profile_Structures_Repository import save_structures
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
save_structures(str(tmp_path), [_item(**options)], base_revision=0)
|
||||
@@ -74,7 +74,7 @@ def test_정의_덧칸_얹기(out):
|
||||
|
||||
|
||||
def test_판1_정의는_지금_화면과_같음(out):
|
||||
assert out["v1Types"] == 37
|
||||
assert out["v1Types"] == 42 # 52-6 맹암거 · 52-7 대피로 · 경계석 · 위험표시 · 취수정
|
||||
assert out["v1Same"] is True
|
||||
assert out["v1Columns"] == [2]
|
||||
assert out["v1Anchor"] is True
|
||||
|
||||
@@ -189,7 +189,7 @@ def test_quick_add_types_exclude_pipe_family():
|
||||
|
||||
|
||||
def test_all_enabled_non_managed_types_are_quick_addable():
|
||||
"""잔존 B05 선택지 = A군 2(노출형·개거) + C군 5 + D군 3 + E군 14 + G군 1(콘크리트 포장) + 기타 1.
|
||||
"""잔존 B05 선택지 = A군 3(노출형·개거·맹암거) + C군 5 + D군 3 + E군 14 + G군 1(콘크리트 포장) + 기타 1.
|
||||
|
||||
콘크리트 포장은 2026-08-28 사용자 지시로 되살렸다 — 포장 구간을 기준측점+길이+전/후로
|
||||
받는 정본이 됐다(종단경사 자동 포장 적용을 대체).
|
||||
@@ -204,7 +204,7 @@ def test_all_enabled_non_managed_types_are_quick_addable():
|
||||
|
||||
E군이 10에서 14로 늘었다 — 표 07(PLAN 52-7) 대피로 · 경계석 · 위험표시 · 취수정.
|
||||
"""
|
||||
assert len(_quick_add_types()) == 27
|
||||
assert len(_quick_add_types()) == 28
|
||||
|
||||
|
||||
def test_removed_types_are_absent():
|
||||
@@ -368,6 +368,7 @@ def test_a6_a7_types_exist_for_manual_placement():
|
||||
item = by_id[type_id]
|
||||
assert item.group == "A" and item.managed_by is None
|
||||
assert item.placement == "point"
|
||||
assert not item.options, f"{type_id} 옵션 없음 — 기준 측점 + 종류 단위 잇기만(PLAN 52-5)"
|
||||
|
||||
|
||||
def test_ford_bridge_shares_pipe_kind_and_diameter():
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"""M02 구조물 명세 이름 고치기(`M02_SO_Def_Spec_Model.ts` · PLAN 52-11) 단위검증.
|
||||
|
||||
Node 헬퍼(`helper_m02_so_def_spec.cjs`)가 TS 를 트랜스파일해 진짜 정의 사본(메모리)으로 돌리고
|
||||
pytest 는 그 결과(JSON)를 판정.
|
||||
|
||||
못박는 것
|
||||
- 배관 명세 나무: 컨테이너 5 · 항목 · 값 줄 · 이름 키 겹침 없음 · 필수/선택 · 꼴 표시.
|
||||
- 필수 컨테이너 제목은 정의 그룹 이름에 씀 · 항목 · 값 · 선택 컨테이너는 이름 표에 씀.
|
||||
- 이름만 바뀜 — types(옵션 · 키) 그대로 · 이름 표에 없는 키를 새로 만들지 않음.
|
||||
- 이름 표가 빈 종류는 명세 글로 읽음.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def r() -> dict:
|
||||
proc = subprocess.run(
|
||||
["node", str(HERE / "helper_m02_so_def_spec.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) -> None:
|
||||
assert r["levels"][0] == 5
|
||||
assert r["levels"][1] > 5 and r["levels"][2] > 10
|
||||
assert r["unique"] is True
|
||||
assert r["pipeTitle"]["tag"] == "required" and r["pipeTitle"]["group"] == "grv6lny"
|
||||
assert r["optional"]["tag"] == "optional" and r["optional"]["group"] is None
|
||||
assert r["basinForm"]["label"] == "I형" and r["basinForm"]["key"] == "I"
|
||||
|
||||
|
||||
def test_이름_고침_자리(r) -> None:
|
||||
assert r["groupName"] == "관 본체"
|
||||
assert r["names"] == [None, "관 지름", "Ø800", "흙막이 더"]
|
||||
assert r["reread"] == ["관 본체", "관 지름", "Ø800", "흙막이 더"]
|
||||
|
||||
|
||||
def test_구조는_그대로(r) -> None:
|
||||
assert r["sameTypes"] is True and r["sameKeys"] is True
|
||||
|
||||
|
||||
def test_이름_표_없는_종류(r) -> None:
|
||||
assert r["bareLabel"] == "흙막이"
|
||||
assert r["bareGroup"] == "유입"
|
||||
@@ -0,0 +1,155 @@
|
||||
/* 52-11 — M02 「구조물 옵션 정의」 = 구조물 명세 이름 고치기(명세 있는 종류) · B05 가 그 이름으로 그림
|
||||
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m5211 = "단계";` 를 붙여
|
||||
* M02 마스터 층(#/m02-master-template?root=master) · 시스템 관리자:
|
||||
* orca eval --expression "window.__m5211='m02'; $(cat 이 파일)"
|
||||
* B05 종단설계(#/b05-profile) · 목록에 「배관 1」:
|
||||
* orca eval --expression "window.__m5211='b05'; $(cat 이 파일)"
|
||||
* 쓰는 것: 없음 — M02 는 화면 사본만 고치고 [저장] 안 누름 · 되돌린 뒤 판 그대로를 GET 으로 봄 ·
|
||||
* B05 는 받아 둔 종류 객체(메모리)만 고침 · 끝나면 orca reload 로 사본을 버림
|
||||
* 기대 수치(m02):
|
||||
* - spec = 배관 나무 줄(컨테이너 5) · old = 옛 편집기 0(옵션 · 배치 · 그룹 · 규칙 · 기준 측점) · other = 명세 없는 종류는 옛 편집기
|
||||
* - 이름 칸만 고침: inputs = 줄 수 · 키 · 꼴은 글(입력 칸 아님)
|
||||
* - 컨테이너 제목 · 항목 이름 · 값 이름 고침 → 미리보기 legend · 칸 이름 · 선택지 글 따라감 · [저장] 켜짐
|
||||
* - 빈 이름 → issues 「명세 이름」 · [저장] 꺼짐 · 되돌리면 이름 그대로 · 판 그대로
|
||||
* 기대 수치(b05): 사본 이름(컨테이너 · 항목 · 값)이 배관 폼에 그대로 → named.* = true
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m5211 || "m02";
|
||||
const W = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const $ = (s, root = document) => root.querySelector(s);
|
||||
const $$ = (s, root = document) => [...root.querySelectorAll(s)];
|
||||
const until = async (fn, ms = 10000) => {
|
||||
for (let t = 0; t < ms; t += 150) {
|
||||
const v = fn();
|
||||
if (v) return v;
|
||||
await W(150);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const put = (box, value) => {
|
||||
box.value = value;
|
||||
box.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
box.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
};
|
||||
const NEW = { pipe: "관 본체X", "pipe.diameter": "관 지름X", "pipe.diameter=800": "Ø800X" };
|
||||
const out = { mode: MODE };
|
||||
|
||||
if (MODE === "m02") {
|
||||
const getDef = async () =>
|
||||
(await fetch("/api/m02/structure-definition", { credentials: "include" })).json();
|
||||
const first = await getDef();
|
||||
out.판 = first.판;
|
||||
const row = await until(() => $("[data-options-row]"));
|
||||
if (!row) return JSON.stringify({ error: "정의 줄 없음" });
|
||||
row.click();
|
||||
if (!(await until(() => $$("[data-sod-kind]").length)))
|
||||
return JSON.stringify({ error: "종류 목록 없음" });
|
||||
// 명세 없는 종류 — 옛 편집기 그대로
|
||||
$('[data-sod-kind="retaining_wall"]').click();
|
||||
await W(200);
|
||||
out.other = { base: !!$("[data-sod-base]"), spec: !!$("[data-sod-spec-block]") };
|
||||
// 배관 — 명세 이름 나무
|
||||
$('[data-sod-kind="pipe"]').click();
|
||||
await W(200);
|
||||
const block = $("[data-sod-spec-block]");
|
||||
const detail = $("[data-sod-detail]");
|
||||
const rows = $$("[data-sod-spec]", block);
|
||||
out.spec = {
|
||||
rows: rows.length,
|
||||
containers: $$(".is-level-0", block).length,
|
||||
inputs: $$("input, select, textarea", block).filter(
|
||||
(n) => !n.closest("[data-sod-spec-preview]"),
|
||||
).length,
|
||||
nameInputs: $$("[data-sod-spec-name]", block).length,
|
||||
keysReadOnly: rows.every((r) => r.querySelector("small.m02-opts__key")),
|
||||
};
|
||||
out.old = {
|
||||
base: !!$("[data-sod-base]", detail),
|
||||
blocks: detail.children.length, // 머리 칸 + 명세 칸 = 2
|
||||
};
|
||||
const save = $("[data-sod-save]");
|
||||
out.saveBefore = save.disabled;
|
||||
const box = (k) => $(`[data-sod-spec="${k}"] [data-sod-spec-name]`);
|
||||
const orig = Object.fromEntries(Object.keys(NEW).map((k) => [k, box(k).value]));
|
||||
for (const [k, v] of Object.entries(NEW)) {
|
||||
put(box(k), v);
|
||||
await W(150);
|
||||
}
|
||||
const preview = $("[data-sod-spec-preview]");
|
||||
const pipeBox = $('[data-spec-container="pipe"]', preview);
|
||||
out.preview = {
|
||||
title: pipeBox.querySelector("legend").textContent.includes(NEW.pipe),
|
||||
item: pipeBox
|
||||
.querySelector('[data-spec-item="pipe.diameter"]')
|
||||
.textContent.includes(NEW["pipe.diameter"]),
|
||||
choice: [...pipeBox.querySelectorAll('[data-spec-item="pipe.diameter"] option')].some(
|
||||
(o) => o.textContent === NEW["pipe.diameter=800"],
|
||||
),
|
||||
};
|
||||
out.saveAfterEdit = $("[data-sod-save]").disabled;
|
||||
// 빈 이름 → 저장 막힘
|
||||
put(box("pipe.diameter"), "");
|
||||
await W(150);
|
||||
out.blank = {
|
||||
issue: $("[data-sod-issues]").textContent.includes("명세 이름"),
|
||||
save: $("[data-sod-save]").disabled,
|
||||
};
|
||||
// 되돌림
|
||||
for (const [k, v] of Object.entries(orig)) {
|
||||
put(box(k), v);
|
||||
await W(150);
|
||||
}
|
||||
out.restored = Object.entries(orig).every(([k, v]) => box(k).value === v);
|
||||
out.판After = (await getDef()).판;
|
||||
out.ok =
|
||||
out.other.base &&
|
||||
!out.other.spec &&
|
||||
out.spec.containers === 5 &&
|
||||
out.spec.inputs === out.spec.rows &&
|
||||
out.spec.nameInputs === out.spec.rows &&
|
||||
out.spec.keysReadOnly &&
|
||||
!out.old.base &&
|
||||
out.old.blocks === 2 &&
|
||||
out.preview.title &&
|
||||
out.preview.item &&
|
||||
out.preview.choice &&
|
||||
!out.saveAfterEdit &&
|
||||
out.blank.issue &&
|
||||
out.blank.save &&
|
||||
out.restored &&
|
||||
out.판After === out.판;
|
||||
} else {
|
||||
const sec = $(".b05-structure-section");
|
||||
if (!sec) return JSON.stringify({ error: "구조물 배치 칸 없음" });
|
||||
const url = performance
|
||||
.getEntriesByType("resource")
|
||||
.map((e) => e.name)
|
||||
.filter((n) => n.includes("/B05_Profile/B05_Profile_Api_Structures.ts"))
|
||||
.pop();
|
||||
const api = await import(url);
|
||||
const pipe = (await api.fetchStructureTypes(true)).find((t) => t.type_id === "pipe");
|
||||
pipe.spec_names = { ...(pipe.spec_names ?? {}), ...NEW };
|
||||
await until(() => $$(".b05-route__irregular-list li").length);
|
||||
const li = $$(".b05-route__irregular-list li").find((l) =>
|
||||
/배관 1$/.test(l.textContent.trim()),
|
||||
);
|
||||
const spec = () => $(".b05-structure__spec", sec);
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await W(800);
|
||||
if (!(spec() && spec().offsetParent !== null)) (li.querySelector("button") ?? li).click();
|
||||
await until(() => spec() && spec().offsetParent !== null);
|
||||
const pipeBox = $('[data-spec-container="pipe"]', spec());
|
||||
out.named = {
|
||||
title: pipeBox.querySelector("legend").textContent.includes(NEW.pipe),
|
||||
item: pipeBox
|
||||
.querySelector('[data-spec-item="pipe.diameter"]')
|
||||
.textContent.includes(NEW["pipe.diameter"]),
|
||||
choice: [...pipeBox.querySelectorAll('[data-spec-item="pipe.diameter"] option')].some(
|
||||
(o) => o.textContent === NEW["pipe.diameter=800"],
|
||||
),
|
||||
};
|
||||
(li.querySelector("button") ?? li).click(); // 재선택 = 해제
|
||||
out.ok = Object.values(out.named).every(Boolean);
|
||||
}
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
@@ -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);
|
||||
})();
|
||||
@@ -0,0 +1,158 @@
|
||||
/* 52-5 — B05 · B06 좌측 「구조물 배치」 노출형 횡단수로 · 개거(겉도랑) = 사용자 구조물 표 05
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(랩탑_보조) · 폼 비어 있음 · 로그인 상태
|
||||
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m525 = "단계";` 를 붙여
|
||||
* orca eval --expression "window.__m525='edit'; $(cat 이 파일)" → 두 종류 모양 · 추가 · 잇기 · [저장]
|
||||
* (orca reload 로 다시 열기)
|
||||
* orca eval --expression "window.__m525='verify'; $(cat 이 파일)" → 저장 값 그대로 · 다시 고름
|
||||
* (B06 으로 옮겨 — 같은 모양)
|
||||
* orca eval --expression "window.__m525='verify'; $(cat 이 파일)" → B06 에서도 같은 값 · 같은 모양
|
||||
* orca eval --expression "window.__m525='restore'; $(cat 이 파일)" → 추가한 둘 삭제 · [저장]
|
||||
* 쓰는 것: 검증 프로젝트 structures.json(되돌림 단계가 지움)
|
||||
* 기대 수치:
|
||||
* shape(종류마다) — optionInputs 0 · containers 0 · summaryRows 1 · summaryVisible true · summaryRightAfterPosition true ·
|
||||
* anchor true · stale false
|
||||
* edit — added 둘 · 잇기 이름 값 있음 · 저장 뒤 목록 2 늘어남
|
||||
* verify — 같은 모양 · 잇기 이름 그대로 · 옵션 값 없음
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m525 || "edit";
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const until = async (fn, ms = 15000) => {
|
||||
for (let t = 0; t < ms; t += 200) {
|
||||
if (fn()) return true;
|
||||
await w(200);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const vis = (n) => !!n && n.offsetParent !== null;
|
||||
const sec = () => document.querySelector(".b05-structure-section");
|
||||
const body = () => sec().querySelector(".b05-route__panel-body");
|
||||
const selects = () => sec().querySelectorAll(".b05-structure__grid select");
|
||||
const fire = (el, value) => {
|
||||
el.value = value;
|
||||
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
};
|
||||
const listItems = () => [...document.querySelectorAll(".b05-route__irregular-list li")];
|
||||
const NAMES = { cross_drain_exposed: "노출형 횡단수로", open_ditch: "개거(겉도랑)" };
|
||||
const AT = { cross_drain_exposed: [2, 7], open_ditch: [6, 3] };
|
||||
|
||||
const shape = (typeId) => {
|
||||
const kids = [...body().children];
|
||||
const position = kids.find((c) => c.classList.contains("b05-structure__position-row"));
|
||||
const summary = kids.find((c) => c.classList.contains("b05-structure__summary"));
|
||||
const optionRow = kids.filter((c) => c.classList.contains("b05-structure__grid"))[1];
|
||||
const after = kids.slice(kids.indexOf(position) + 1).find((c) => vis(c));
|
||||
const anchorWrap = position.querySelectorAll(":scope > label")[2];
|
||||
return {
|
||||
typeId,
|
||||
optionInputs: vis(optionRow) ? optionRow.querySelectorAll("input, select").length : 0,
|
||||
containers: [...sec().querySelectorAll("[data-spec-container]")].filter(vis).length,
|
||||
summaryRows: [...sec().querySelectorAll("[data-b05-summary=row]")].filter(vis).length,
|
||||
summaryVisible: vis(summary),
|
||||
summaryRightAfterPosition: after === summary,
|
||||
anchor: vis(anchorWrap),
|
||||
stale: vis(sec().querySelector(".b05-structure__stale")),
|
||||
linked: summary.querySelector(".b05-structure__summary-name")?.textContent ?? "",
|
||||
};
|
||||
};
|
||||
const pickType = async (typeId) => {
|
||||
const [group, type] = selects();
|
||||
fire(group, "A");
|
||||
await w(300);
|
||||
fire(type, typeId);
|
||||
await w(500);
|
||||
};
|
||||
const linkFirstLeaf = async () => {
|
||||
sec().querySelector("[data-b05-summary=pick]").click();
|
||||
await until(() => document.querySelector(".ui-item-picker"));
|
||||
await w(600);
|
||||
const modal = document.querySelector(".ui-item-picker");
|
||||
// 끝 항목 = ▸ · ▾ 로 안 시작하고 모달 조작 단추가 아닌 첫 단추
|
||||
const ctl = ["닫기", "개인용으로 복사", "관리 화면에서 고치기", "해제", "적용"];
|
||||
const leaf = [...modal.querySelectorAll("button")].find((b) => {
|
||||
const t = b.textContent.trim();
|
||||
return t && !/^[▸▾]/.test(t) && !ctl.includes(t);
|
||||
});
|
||||
leaf?.click();
|
||||
await w(300);
|
||||
[...modal.querySelectorAll("button")].find((b) => b.textContent.trim() === "적용")?.click();
|
||||
await w(500);
|
||||
return !!leaf;
|
||||
};
|
||||
const clickBtn = async (text) => {
|
||||
[...document.querySelectorAll("button")]
|
||||
.find((b) => b.textContent.trim() === text && vis(b))
|
||||
?.click();
|
||||
await w(1500);
|
||||
};
|
||||
const out = { mode: MODE, items: [] };
|
||||
|
||||
if (MODE === "edit") {
|
||||
await until(() => listItems().length > 0);
|
||||
out.before = listItems().length;
|
||||
for (const typeId of Object.keys(NAMES)) {
|
||||
await pickType(typeId);
|
||||
const row = { ...shape(typeId), phase: "edit" };
|
||||
const [no, dist] = sec()
|
||||
.querySelectorAll(".b05-structure__position-row > label")[2]
|
||||
.querySelectorAll("input");
|
||||
fire(no, String(AT[typeId][0]));
|
||||
fire(dist, String(AT[typeId][1]));
|
||||
await w(200);
|
||||
row.picked = await linkFirstLeaf();
|
||||
row.linked = shape(typeId).linked;
|
||||
sec().querySelector(".is-primary").click(); // 추가
|
||||
await w(800);
|
||||
out.items.push(row);
|
||||
}
|
||||
out.after = listItems().length;
|
||||
await clickBtn("저장");
|
||||
out.listed = listItems()
|
||||
.map((l) => l.textContent.trim())
|
||||
.filter((t) => /횡단수로|개거/.test(t));
|
||||
} else if (MODE === "verify") {
|
||||
await until(() => listItems().length > 0);
|
||||
for (const typeId of Object.keys(NAMES)) {
|
||||
const li = listItems().find((l) => l.textContent.includes(NAMES[typeId]));
|
||||
if (!li) {
|
||||
out.items.push({ typeId, missing: true });
|
||||
continue;
|
||||
}
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await w(800);
|
||||
// B06 은 다른 항목이 골라져 있으면 첫 눌림이 풀기만 함 — 안 불러와졌으면 한 번 더
|
||||
if (selects()[1].value !== typeId) {
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await w(800);
|
||||
}
|
||||
out.items.push({ ...shape(typeId), phase: "verify", title: li.title || "" });
|
||||
(li.querySelector("button") ?? li).click(); // 재선택 = 해제
|
||||
await w(300);
|
||||
}
|
||||
} else if (MODE === "restore") {
|
||||
await until(() => listItems().length > 0);
|
||||
for (const typeId of Object.keys(NAMES)) {
|
||||
const li = listItems().find((l) => l.textContent.includes(NAMES[typeId]));
|
||||
if (!li) continue;
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await w(600);
|
||||
await clickBtn("삭제");
|
||||
}
|
||||
await clickBtn("저장");
|
||||
out.left = listItems().length;
|
||||
}
|
||||
const rows = out.items.filter((r) => !r.missing);
|
||||
out.shapeOk =
|
||||
rows.length === 2 &&
|
||||
rows.every(
|
||||
(r) =>
|
||||
r.optionInputs === 0 &&
|
||||
r.containers === 0 &&
|
||||
r.summaryRows === 1 &&
|
||||
r.summaryVisible &&
|
||||
r.summaryRightAfterPosition &&
|
||||
r.anchor &&
|
||||
!r.stale,
|
||||
);
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
@@ -0,0 +1,217 @@
|
||||
/* 52-6 — 횡단배수 › 맹암거(표 06 · 새 종류 `cross_underdrain`) · B05 · B06 「구조물 배치」
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 아무것도 안 고름 · 측점 3+7 이 비어 있음
|
||||
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m526 = "단계";` 를 붙여
|
||||
* orca eval --expression "window.__m526='edit'; $(cat 이 파일)" → 종류 목록 · 컨테이너 · 값 · 잇기 · [추가] · 다시 열기
|
||||
* orca eval --expression "window.__m526='b06'; $(cat 이 파일)" → B06 에서 같은 항목 다시 열기 · B05 와 같은 그리기
|
||||
* orca eval --expression "window.__m526='save'; $(cat 이 파일)" → [저장] 요청 가로챔(몸통만 봄)
|
||||
* orca eval --expression "window.__m526='restore'; $(cat 이 파일)" → sessionStorage 를 처음 그대로 · 뒤에 orca reload
|
||||
* 쓰는 것: 없음 — [저장] 은 fetch 를 가로채 서버로 안 보냄(공유 storage) · 서버 저장 왕복은 test_52_6_cross_underdrain
|
||||
* 기대 수치(edit): b05Types 에 cross_underdrain:맹암거 · underdrain 없음 · box 맹암거 · keys 높이 · 경사 ·
|
||||
* 잇기 줄 1(맹암거 상자 안만 · 종류 단위 줄 가림 · 52-6b) · pending 높이 0.6 · 경사 2.5 · 잇기 gcrossud 지정 · reopened 같음
|
||||
* 기대 수치(b06): b06Types A 에 맹암거 · B 에 종단배수 맹암거 따로 · sameDrawing · 값 같음
|
||||
* 기대 수치(save): saved = PUT 몸통에 같은 값(67 m · 0.6 · 2.5 · gcrossud)
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m526 || "edit";
|
||||
const KEY = "cross_underdrain";
|
||||
const GROUP = "gcrossud";
|
||||
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 vis = (n) => n && n.offsetParent !== null;
|
||||
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 api = await import(modUrl("/B05_Profile/B05_Profile_Api_Structures.ts"));
|
||||
const sec = () => document.querySelector(".b05-structure-section");
|
||||
const sels = () => sec().querySelectorAll(".b05-structure__grid select");
|
||||
const choose = async (sel, value) => {
|
||||
sel.value = value;
|
||||
sel.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await w(300);
|
||||
};
|
||||
const typesOf = async (group) => {
|
||||
await choose(sels()[0], group);
|
||||
return [...sels()[1].options].filter((o) => o.value).map((o) => `${o.value}:${o.text}`);
|
||||
};
|
||||
const box = () => sec().querySelector(`[data-b05-group="${GROUP}"]`);
|
||||
const input = (k) => box()?.querySelector(`[data-option-keys="${k}"] input`);
|
||||
const shape = () => {
|
||||
const b = box();
|
||||
return {
|
||||
legend: b?.querySelector("legend").textContent,
|
||||
keys: [...(b?.querySelectorAll("[data-option-keys]") ?? [])].map(
|
||||
(n) => `${n.dataset.optionKeys}|${n.querySelector("span").textContent}`,
|
||||
),
|
||||
values: ["culvert_height_m", "culvert_slope_pct"].map((k) => input(k)?.value),
|
||||
link: b?.querySelector(".b05-structure__summary-name").textContent,
|
||||
linked: b?.querySelector(".b05-structure__summary-name").classList.contains("is-assigned"),
|
||||
linkRows: [...sec().querySelectorAll("[data-b05-summary=row]")].filter(vis).length,
|
||||
};
|
||||
};
|
||||
const item = () =>
|
||||
[...document.querySelectorAll(".b05-route__irregular-list li")].find((l) =>
|
||||
/^3\+7.*맹암거/.test(l.textContent.trim()),
|
||||
);
|
||||
const openItem = async () => {
|
||||
await until(() => item());
|
||||
const li = item();
|
||||
(li.querySelector("button") ?? li).click();
|
||||
await w(600);
|
||||
// 이미 고른 채로 열렸으면 누르면 풀림 — 풀렸으면 한 번 더(재선택 = 해제)
|
||||
if (sels()[1].value !== KEY) (li.querySelector("button") ?? li).click();
|
||||
await until(() => vis(box()));
|
||||
await w(300);
|
||||
};
|
||||
const pickLink = async () => {
|
||||
box().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 pendingMine = () =>
|
||||
(api.readPendingStructures(projectId) ?? []).filter((s) => s.type_id === KEY);
|
||||
const out = { mode: MODE };
|
||||
|
||||
if (MODE === "edit") {
|
||||
sessionStorage.setItem("__526_session", JSON.stringify({ ...sessionStorage }));
|
||||
out.b05Types = await typesOf("A");
|
||||
out.b05HasLongUnderdrain = (await typesOf("B")).some((t) => t.startsWith("underdrain:"));
|
||||
await typesOf("A");
|
||||
await choose(sels()[1], KEY);
|
||||
out.boxBeforeStation = { shown: vis(box()), locked: !!box()?.querySelector(".is-locked") };
|
||||
const pos = [...sec().querySelectorAll(".b05-structure__position-row input")].filter(vis);
|
||||
for (const [el, v] of [
|
||||
[pos[0], "3"],
|
||||
[pos[1], "7"],
|
||||
]) {
|
||||
el.value = v;
|
||||
el.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
}
|
||||
await w(300);
|
||||
for (const [k, v] of [
|
||||
["culvert_height_m", "0.6"],
|
||||
["culvert_slope_pct", "2.5"],
|
||||
]) {
|
||||
input(k).value = v;
|
||||
input(k).dispatchEvent(new Event("input", { bubbles: true }));
|
||||
input(k).dispatchEvent(new Event("change", { bubbles: true }));
|
||||
}
|
||||
await w(200);
|
||||
await pickLink();
|
||||
out.form = shape();
|
||||
sec().querySelector(".is-primary").click(); // [추가]
|
||||
await w(800);
|
||||
const mine = pendingMine();
|
||||
out.pending = mine.map((s) => ({
|
||||
chainage_m: s.chainage_m,
|
||||
options: s.options,
|
||||
groups: Object.keys(s.group_items ?? {}),
|
||||
}));
|
||||
// 종단 알약 — 약자 · 색이 종단배수 맹암거(맹암거 · #1f7fb8)와 따로
|
||||
const types = await api.fetchStructureTypes(true);
|
||||
const ids = new Set(mine.map((s) => s.structure_id));
|
||||
if (!document.querySelector(".b05-structure__mark")) {
|
||||
[...document.querySelectorAll("button")]
|
||||
.find((b) => b.textContent.trim() === "▲종단")
|
||||
?.click();
|
||||
await until(() => document.querySelector(".b05-structure__mark"), 8000);
|
||||
}
|
||||
out.marks = [...document.querySelectorAll(".b05-structure__mark")]
|
||||
.filter((m) => ids.has(m.dataset.structureId))
|
||||
.map((m) => `${m.textContent}|${m.style.color}`);
|
||||
out.longStyle = types.find((t) => t.type_id === "underdrain")?.style;
|
||||
// 다시 열기 — 폼을 비우고 목록에서 다시 고름
|
||||
const li = item();
|
||||
(li.querySelector("button") ?? li).click(); // 재선택 = 해제
|
||||
await w(400);
|
||||
await openItem();
|
||||
out.reopened = shape();
|
||||
out.reopenSame = JSON.stringify(out.reopened) === JSON.stringify(out.form);
|
||||
}
|
||||
|
||||
if (MODE === "save") {
|
||||
// [저장] — 요청을 가로채 몸통만 봄(서버 · storage 안 감) · 뒤에 화면이 서버 목록을 다시 읽어 항목이 사라짐
|
||||
const real = window.fetch;
|
||||
const sent = [];
|
||||
window.fetch = async (url, init = {}) => {
|
||||
const method = (init.method || "GET").toUpperCase();
|
||||
if (method !== "GET" && String(url).includes(`/projects/${projectId}/`)) {
|
||||
sent.push({ url: String(url), method, body: init.body ? String(init.body) : "" });
|
||||
return new Response(JSON.stringify({ status: "success", revision: 0, count: 0 }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
return real(url, init);
|
||||
};
|
||||
try {
|
||||
document.querySelector('[data-dock-action="save"]').click();
|
||||
await w(3000);
|
||||
await until(() => !document.querySelector(".ui-overlay.is-active"), 20000);
|
||||
} finally {
|
||||
window.fetch = real;
|
||||
}
|
||||
const put = sent.find((r) => r.url.includes("/route/structures") && r.method === "PUT");
|
||||
const body = put ? JSON.parse(put.body) : null;
|
||||
const saved = body?.structures.filter((s) => s.type_id === KEY) ?? [];
|
||||
out.saved = saved.map((s) => ({
|
||||
chainage_m: s.chainage_m,
|
||||
options: s.options,
|
||||
groups: Object.keys(s.group_items ?? {}),
|
||||
}));
|
||||
out.sent = sent.map((r) => `${r.method} ${r.url.split("/api/")[1] ?? r.url}`);
|
||||
}
|
||||
|
||||
if (MODE === "b06") {
|
||||
await openItem();
|
||||
const b05 = shape();
|
||||
location.hash = "#/b06-section";
|
||||
await until(() => location.hash.includes("b06"), 5000);
|
||||
await w(6000);
|
||||
await openItem();
|
||||
const b06 = shape();
|
||||
out.sameDrawing = JSON.stringify(b05) === JSON.stringify(b06);
|
||||
out.b06 = b06;
|
||||
sels()[1].value = "";
|
||||
const li = item();
|
||||
(li.querySelector("button") ?? li).click(); // 고름 풂
|
||||
await w(300);
|
||||
out.b06TypesA = await typesOf("A");
|
||||
out.b06TypesB = await typesOf("B");
|
||||
}
|
||||
|
||||
if (MODE === "restore") {
|
||||
const raw = sessionStorage.getItem("__526_session");
|
||||
if (!raw) return JSON.stringify({ error: "처음 sessionStorage 없음" });
|
||||
const before = JSON.parse(raw);
|
||||
sessionStorage.clear();
|
||||
for (const [k, v] of Object.entries(before)) sessionStorage.setItem(k, v);
|
||||
out.restored = JSON.stringify({ ...sessionStorage }) === JSON.stringify(before);
|
||||
out.mine = pendingMine().length;
|
||||
}
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
@@ -0,0 +1,99 @@
|
||||
/* 52-8 — B05 · B06 좌측 「구조물 배치」 종단배수(B) · 계류사방(D) 고르기 목록에서 숨김
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(sub2_검증)
|
||||
* 부르는 법: 앞에 `window.__m528 = "단계";` 를 붙여
|
||||
* orca eval --expression "window.__m528='seed'; $(cat 이 파일)" → 옛 값 둘(골막이 D · 측구 B)을 세션 초안에만 둠
|
||||
* (orca reload)
|
||||
* orca eval --expression "window.__m528='verify'; $(cat 이 파일)" → 군 목록 · 우클릭 메뉴 · 옛 값 열기 (B05)
|
||||
* (#/b06-section 으로 가서 reload 후 verify 한 번 더)
|
||||
* orca eval --expression "window.__m528='clear'; $(cat 이 파일)" → 세션 초안 비움(서버 저장 안 함)
|
||||
* 기대 수치(verify): visibleGroups 에 B · D 없음(0) · hiddenGroups 가림만 · menuHasBD 0 ·
|
||||
* 옛 값 목록 둘 그대로 · 옛 값을 열면 군 = D / B 로 열림(editGroup) · 잇기 칸 pickLinks 골막이(D) 1 · 측구(B) 0
|
||||
*/
|
||||
(async () => {
|
||||
const MODE = window.__m528 || "verify";
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
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 api = await import(modUrl("/B05_Profile/B05_Profile_Api_Structures.ts"));
|
||||
const out = { mode: MODE, hash: location.hash };
|
||||
if (MODE === "seed") {
|
||||
const base = {
|
||||
placement_source: "manual",
|
||||
status: "draft",
|
||||
revision: 0,
|
||||
geometry: null,
|
||||
memo: "",
|
||||
options: {},
|
||||
};
|
||||
api.writePendingStructures(projectId, [
|
||||
{
|
||||
...base,
|
||||
structure_id: "m528d",
|
||||
type_id: "erosion_check",
|
||||
placement: "point",
|
||||
chainage_m: 40,
|
||||
},
|
||||
{
|
||||
...base,
|
||||
structure_id: "m528b",
|
||||
type_id: "ditch_side",
|
||||
placement: "interval",
|
||||
start_m: 60,
|
||||
end_m: 80,
|
||||
},
|
||||
]);
|
||||
out.seeded = api.readPendingStructures(projectId).length;
|
||||
return JSON.stringify(out);
|
||||
}
|
||||
if (MODE === "clear") {
|
||||
api.writePendingStructures(projectId, null);
|
||||
out.cleared = api.readPendingStructures(projectId) === null;
|
||||
return JSON.stringify(out);
|
||||
}
|
||||
const groupSel = [...document.querySelectorAll("select")].find((s) =>
|
||||
[...s.options].some((o) => o.text.includes("A 횡단배수")),
|
||||
);
|
||||
const isBD = (o) => /^(B|D) /.test(o.text);
|
||||
out.visibleGroups = [...groupSel.options].filter((o) => !o.hidden && o.value).map((o) => o.text);
|
||||
out.visibleBD = [...groupSel.options].filter((o) => !o.hidden && isBD(o)).length;
|
||||
out.hiddenGroups = [...groupSel.options].filter((o) => o.hidden).map((o) => o.text);
|
||||
// 우클릭 메뉴 목록(공용 만들기) — 종류 전부를 줘도 B · D 군이 안 나옴
|
||||
const menuMod = await import(modUrl("/B05_Profile/B05_Profile_UI_Profile_Structures.ts"));
|
||||
const types = (await api.fetchStructureTypes(true)).map((t) => ({
|
||||
type_id: t.type_id,
|
||||
group: t.group,
|
||||
name: t.name,
|
||||
}));
|
||||
const menu = menuMod.structureGroupMenuItems(types, () => {});
|
||||
out.menuGroups = menu.map((m) => m.label);
|
||||
out.menuHasBD = menu.filter((m) => /^(B|D) /.test(m.label)).length;
|
||||
// 옛 값 — 목록에 그대로 · 열면 그 군으로 열림
|
||||
const rows = [...document.querySelectorAll(".b05-route__irregular-list li")];
|
||||
out.rows = rows.map((r) => r.textContent.trim().replace(/\s+/g, " ").slice(0, 40));
|
||||
const old = rows.find((r) =>
|
||||
(window.__m528row ? new RegExp(window.__m528row) : /골막이/).test(r.textContent),
|
||||
);
|
||||
if (old) {
|
||||
(old.querySelector("button") ?? old).click();
|
||||
await w(1500);
|
||||
const sels = [...document.querySelectorAll("select")].filter((s) =>
|
||||
[...s.options].some((o) => o.text.includes("A 횡단배수")),
|
||||
);
|
||||
// 다시 열면 고른 것이 복원돼 있음 — 누르면 풀리므로 풀렸으면 한 번 더
|
||||
if (!sels[0]?.value) {
|
||||
(old.querySelector("button") ?? old).click();
|
||||
await w(1500);
|
||||
}
|
||||
out.editGroup = sels[0]?.value;
|
||||
// 종단배수(B)는 조합 잇기 칸이 없음(0) · 계류사방(D)은 옛 그대로(1)
|
||||
out.pickLinks = [...document.querySelectorAll('[data-b05-summary="pick"]')].filter(
|
||||
(n) => n.offsetParent,
|
||||
).length;
|
||||
}
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
Reference in New Issue
Block a user