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

This commit is contained in:
2026-10-06 00:26:10 +09:00
9 changed files with 3921 additions and 22 deletions
@@ -0,0 +1,46 @@
/* =============================================================================
* design_structure_ford_pavement.ts
* 횡단배수 › 물넘이 포장 명세 — 사용자 구조물 표 03(`docs/raw/구조물정의_표/03_횡단배수_물넘이포장.md`) 그대로.
*
* 컨테이너 둘: 경사각도(필수 · 각도 손 입력 · 도) · 도수로/돌붙임(선택 · 구조 = 도수로 · 돌붙임).
* 저장 = pipe_points.json `options` — 새 키 `ford_slope_deg` · 선택 컨테이너 `ford_chute_{n}_structure`.
* 경사각도는 옛 「바닥 경사(%)」(`ford_slope_pct` · B06 파임 기울기)와 뜻이 같은지 또렷하지 않아 잇지 않음.
* import 없음 — 시험이 이 파일만 옮겨 돌림.
* ========================================================================== */
import type { StructureSpec } from "./design_structure_spec";
export const FORD_PAVEMENT_SPEC: StructureSpec = {
typeId: "ford_pavement",
containers: [
{
id: "slope",
title: "경사각도",
required: true,
link: "gfordpaveslope",
items: [
{ id: "angle", label: "경사각도 (°)", kind: "number", key: "ford_slope_deg", step: 0.1 },
],
},
{
id: "chute",
title: "도수로/돌붙임",
required: false,
prefix: "ford_chute",
link: "gfordpavechute",
items: [
{
id: "structure",
label: "구조",
kind: "select",
choices: [
{ value: "도수로", label: "도수로" },
{ value: "돌붙임", label: "돌붙임" },
],
},
],
},
],
// 표 03 에 없는 옛 칸 — 화면에서 빼고 값은 남김(지난 정의 값)
retired: ["ford_width_m", "ford_height_m", "ford_slope_pct", "thickness_cm", "length_m"],
};
@@ -14,6 +14,7 @@
import type { SpecChoice, SpecItem, StructureSpec } from "./design_structure_spec";
import { BOX_SPEC } from "./design_structure_box";
import { FORD_SPEC } from "./design_structure_ford";
import { FORD_PAVEMENT_SPEC } from "./design_structure_ford_pavement";
import { ETC_SPEC, PAVEMENT_SPEC } from "./design_structure_road";
const SOIL = "기슭막이"; // 저장 값(옛 키 그대로) — 화면 이름은 흙막이
@@ -191,6 +192,7 @@ export const STRUCTURE_SPECS: Readonly<Record<string, StructureSpec>> = {
pipe: PIPE_SPEC,
box_culvert: BOX_SPEC,
ford_bridge: FORD_SPEC,
ford_pavement: FORD_PAVEMENT_SPEC,
pavement_concrete: PAVEMENT_SPEC,
etc: ETC_SPEC,
};
+11 -20
View File
@@ -62,7 +62,7 @@ import {
type GroupItems,
type SummaryItem,
} from "../common_util/common_util_summary_item";
import { createFordFields } from "./B05_Profile_UI_Drainage_Facility_Ford";
import { FORD_PAVEMENT_SPEC } from "../A00_Common/design/design_structure_ford_pavement";
import { createHiddenOptions } from "./B05_Profile_UI_Drainage_Facility_Hidden";
export type { InletStructureKind }; // 목록은 700줄 한계로 `_Fields` 로 옮김 — 부르던 곳은 그대로
@@ -369,9 +369,9 @@ export function createFacilityOptionsForm(
const hiddenOptions = createHiddenOptions(root);
// ── 물넘이·세월교 — 월류 폭·높이·경사·두께·길이·련 수(`_Ford`).
// 기본값(등록부)은 시설이 생길 때 칸에 실제 값으로 들어옴 — 제안값 단추 없음(PLAN 46-7).
const ford = createFordFields(() => emit());
// ── 물넘이 포장 = 사용자 구조물 표 03 명세(PLAN 52-3) — 경사각도 · 도수로/돌붙임 ·
// 옛 월류 폭 · 높이 · 바닥 경사 · 두께 · 길이 칸은 뺌(지난 정의 값) ──
const paveForm = createSpecForm(specLinks, { onChange: () => emit() });
// 10-A ⑲ 저장 규칙을 폼 머리에 드러냄 — 병합은 `_Drainage_Facility_Merge`(2026-09-14 브레인 판정).
const saveNote = document.createElement("p");
@@ -385,10 +385,7 @@ export function createFacilityOptionsForm(
saveNote,
pipeForm.root,
fordForm.root,
ford.widthRow,
ford.slopeRow,
ford.countRow,
ford.summary,
paveForm.root,
extraGroup.root,
revetTopRow,
revetInlet.root,
@@ -415,12 +412,7 @@ export function createFacilityOptionsForm(
wingOutFields.root.hidden = true;
(spec ? spec.slot("inlet") : wingInHome).append(wingInFields.slot);
(spec ? spec.slot("outlet") : wingOutHome).append(wingOutFields.slot);
const isFord = current === "ford_pavement";
ford.countRow.hidden = true;
ford.widthRow.hidden = !isFord;
// 바닥 경사는 물넘이포장만 쓴다 — 세월교는 구체 위 노면이라 파임이 없다.
ford.slopeRow.hidden = current !== "ford_pavement";
ford.summary.hidden = !isFord;
paveForm.root.hidden = current !== "ford_pavement";
const isRevet = current === "revetment";
revetTopRow.hidden = !isRevet;
// 설치 측이 한쪽이면 그쪽 칸만 남긴다 — 이름표는 측점이 정한 좌·우다(안 정함 = 양쪽처럼 보임).
@@ -432,7 +424,6 @@ export function createFacilityOptionsForm(
// 추가 기슭막이 칸은 **내용이 있을 때만** 선다 — 고른 다단 벽의 행이 여기 오면 뜨고,
// 선택이 풀리면 스스로 사라진다.
extraGroup.root.hidden = extraSlot.childElementCount === 0;
if (isFord) ford.sync();
}
function emit(): void {
@@ -443,7 +434,7 @@ export function createFacilityOptionsForm(
revetInlet.fields.onChange(emit);
revetOutlet.fields.onChange(emit);
revetSide.addEventListener("change", emit);
[...wingInFields.inputs, ...wingOutFields.inputs, ...ford.inputs].forEach((input) =>
[...wingInFields.inputs, ...wingOutFields.inputs].forEach((input) =>
input.addEventListener("change", emit),
);
@@ -477,11 +468,13 @@ export function createFacilityOptionsForm(
pipeForm.paintLinks();
boxForm.paintLinks();
fordForm.paintLinks();
paveForm.paintLinks();
},
paintLinks: () => {
pipeForm.paintLinks();
boxForm.paintLinks();
fordForm.paintLinks();
paveForm.paintLinks();
},
setChainage(chainageM) {
chainage = chainageM;
@@ -496,7 +489,6 @@ export function createFacilityOptionsForm(
setFacility(facility, options = {}, designFlow = null, definition = null) {
current = facility;
hiddenOptions.remember(options);
ford.setDesignFlow(designFlow ?? null);
fordFlow = designFlow ?? null;
if (facility === null) {
syncVisibility();
@@ -516,9 +508,9 @@ export function createFacilityOptionsForm(
}
if (facility === "ford_bridge")
fordForm.write(FORD_SPEC, withLegacyGuards(options), names, groups);
if (facility === "ford_pavement") paveForm.write(FORD_PAVEMENT_SPEC, options, names, groups);
wingInFields.write(options);
wingOutFields.write(options);
if (facility === "ford_pavement") ford.write(facility, options);
revetSide.value = text("side");
const spread = legacyRevetOptions(options);
revetInlet.fields.write(spread);
@@ -526,7 +518,6 @@ export function createFacilityOptionsForm(
syncVisibility();
},
setDesignFlow(designFlow) {
ford.setDesignFlow(designFlow ?? null);
fordFlow = designFlow ?? null;
if (current === "ford_bridge") fordForm.refresh(); // 높이 안내
syncVisibility();
@@ -540,7 +531,7 @@ export function createFacilityOptionsForm(
// 정본은 폭 · 높이 두 칸 — 규격이면 규격 수 · 사용자 입력이면 명세 숫자 칸(PLAN 52-2)
Object.assign(options, boxForm.read(), bodyOptions(boxForm.state().body?.[0]));
} else if (current === "ford_pavement") {
ford.read(current, options);
Object.assign(options, paveForm.read()); // 표 03 명세 값 · 옛 월류 · 경사 · 두께 · 길이는 병합이 이어 붙임(52-3)
} else if (current === "ford_bridge") {
Object.assign(options, fordForm.read()); // 표 04 명세 값 · 옛 관종 · 날개벽 치수는 병합이 이어 붙임(52-4)
} else if (current === "revetment") {
@@ -36,7 +36,9 @@ export const FORM_OPTION_KEYS: Readonly<Record<string, readonly string[]>> = {
],
// BOX = 표 02 명세 키(PLAN 52-2) — 옛 날개벽 치수는 폼에서 빠져 이어 붙임(지난 정의 값)
box_culvert: ["body_width_m", "body_height_m", "wing_in", "wing_out"],
ford_pavement: ["ford_width_m", "ford_height_m", "ford_slope_pct", "thickness_cm", "length_m"],
// 물넘이 포장 = 표 03 명세 키(PLAN 52-3 · `design_structure_ford_pavement`) — 옛 월류 · 경사 · 두께 · 길이는
// 폼에서 빠져 이어 붙임 · 도수로/돌붙임 키는 `FORM_OPTION_PREFIXES`
ford_pavement: ["ford_slope_deg"],
// 세월교 = 표 04 명세 키(PLAN 52-4 · `design_structure_ford`) — 옛 관종 · 날개벽 치수는 폼에서 빠져 이어 붙임
ford_bridge: [
"pipe_diameter_mm",
@@ -54,6 +56,7 @@ export const FORM_OPTION_KEYS: Readonly<Record<string, readonly string[]>> = {
/** 폼이 인스턴스 번호를 붙여 적는 키 머리(명세 선택 컨테이너 · `{머리}{n}_{항목}`) — 빼면 지워짐. */
export const FORM_OPTION_PREFIXES: Readonly<Record<string, readonly string[]>> = {
pipe: ["soil_add_", "chute_add_"],
ford_pavement: ["ford_chute_"],
};
/** 독립 기슭막이 레지스트리 키 → 폼은 유입·유출 두 채널 키로 적는다. */
@@ -22,6 +22,7 @@ SPEC_CONTAINERS: dict[str, tuple[tuple[str, str, str], ...]] = {
("soil_add", "gsoiladd", "흙막이 추가"),
("chute_add", "gchuteadd", "도수로/돌붙임"),
),
"ford_pavement": (("ford_chute", "gfordpavechute", "도수로/돌붙임"),),
"etc": (("etc_detail", "getcdetail", "상세 내용"),),
}
_TEXT = ("structure", "form", "note")
@@ -1,5 +1,5 @@
{
"판": 11,
"판": 12,
"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`가 새 키로 옮긴다.",
"구조물군": {
@@ -3509,6 +3509,14 @@
],
"형상": {}
}
],
"ford_pavement": [
{
"id": "gfordpaveslope",
"이름": "경사각도",
"옵션": [],
"형상": {}
}
]
},
"배치": {},
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,126 @@
# -*- coding: utf-8 -*-
"""횡단배수 › 물넘이 포장(표 03) 명세 — PLAN 52-3.
명세 · 저장 병합은 Node 로 그 자리에서 트랜스파일해 돌림 · 화면은 ORCA `ui_checks/52-3_물넘이포장_컨테이너.js`.
"""
import json
import os
import re
import subprocess
import pytest
from M02_MasterTemplete.M02_Table_Fill_Spec import SPEC_CONTAINERS, container_rows
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(HERE))
SCRIPT = r"""
const fs = require("fs"), path = require("path");
const ROOT = process.argv[1];
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
const load = (file) => {
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", "require", js)(box.exports, box, () => ({}));
return box.exports;
};
const s = load(path.join(ROOT, "A00_Common/design/design_structure_spec.ts"));
const { FORD_PAVEMENT_SPEC: spec } = load(path.join(ROOT, "A00_Common/design/design_structure_ford_pavement.ts"));
const merge = load(path.join(ROOT, "B05_Profile/B05_Profile_UI_Drainage_Facility_Merge.ts"));
const old = { ford_width_m: 6, ford_height_m: 0.3, ford_slope_pct: 3, thickness_cm: 20, length_m: 7 };
const state = s.readState(spec, { ...old, ford_slope_deg: 12.5, ford_chute_1_structure: "돌붙임" });
const edited = JSON.parse(JSON.stringify(state));
edited.slope[0].angle = "15";
edited.chute.push({ structure: "도수로" });
const written = s.writeOptions(spec, edited);
const prev = { facility: "ford_pavement", options: { ...old, ford_chute_2_structure: "도수로" } };
const saved = merge.mergeFacilityOptions(prev, { facility: "ford_pavement", options: written });
process.stdout.write(JSON.stringify({
containers: spec.containers.map((c) => [c.id, c.title, c.required, c.prefix ?? null, c.link]),
state, written, saved: saved.options, keys: s.specKeys(spec), retired: spec.retired,
links: spec.containers.map((c) => s.linkOf(c, 1)),
formKeys: merge.FORM_OPTION_KEYS.ford_pavement, heads: merge.FORM_OPTION_PREFIXES.ford_pavement,
}));
"""
@pytest.fixture(scope="module")
def out():
proc = subprocess.run(
["node", "-e", SCRIPT, ROOT],
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["containers"] == [
["slope", "경사각도", True, None, "gfordpaveslope"],
["chute", "도수로/돌붙임", False, "ford_chute", "gfordpavechute"],
]
assert out["links"] == ["gfordpaveslope", "gfordpavechute1"]
def test_저장_값은_도_와_인스턴스_키(out):
assert out["state"] == {"slope": [{"angle": "12.5"}], "chute": [{"structure": "돌붙임"}]}
assert out["written"] == {
"ford_slope_deg": 15,
"ford_chute_1_structure": "돌붙임",
"ford_chute_2_structure": "도수로",
}
def test_옛_칸은_지난_정의_값으로_남음(out):
assert out["retired"] == [
"ford_width_m",
"ford_height_m",
"ford_slope_pct",
"thickness_cm",
"length_m",
]
# 폼이 모르는 옛 칸은 그대로 · 폼이 아는 머리(빠진 인스턴스)는 폼 값으로 갈아 끼움
assert out["saved"] == {
"ford_width_m": 6,
"ford_height_m": 0.3,
"ford_slope_pct": 3,
"thickness_cm": 20,
"length_m": 7,
**out["written"],
}
assert out["formKeys"] == out["keys"]["keys"] == ["ford_slope_deg"]
assert out["heads"] == out["keys"]["prefixes"] == ["ford_chute_"]
def test_집계표_짝_명세와_같음():
text = open(
os.path.join(ROOT, "A00_Common/design/design_structure_ford_pavement.ts"), encoding="utf-8"
).read()
found = re.findall(
r'title: "([^"]+)",\s*required: false,\s*prefix: "(\w+)",\s*link: "(\w+)"', text
)
assert [(p, link, t) for t, p, link in found] == list(SPEC_CONTAINERS["ford_pavement"])
item = {
"type_id": "ford_pavement",
"options": {"ford_chute_1_structure": "도수로", "ford_slope_deg": 10},
"복사본": "pipe@50.0",
}
rows = container_rows(item, {"gfordpavechute1": "SS000001"})
assert [
(r["복사본"], r["그룹"], r["summary_item"], r["options"]["structure"]) for r in rows
] == [("pipe@50.0__gfordpavechute1", "도수로/돌붙임 1", "SS000001", "도수로")]
def test_정의_그룹_경사각도():
with open(
os.path.join(ROOT, "resources/master_template/구조물정의.json"), encoding="utf-8"
) as f:
groups = json.load(f)["그룹"]["ford_pavement"]
assert [(g["id"], g["이름"]) for g in groups] == [("gfordpaveslope", "경사각도")]
@@ -0,0 +1,206 @@
/* 52-3 — B05 · B06 좌측 「구조물 배치」 물넘이 포장 = 사용자 구조물 표 03 명세(경사각도 · 도수로/돌붙임)
* 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(sub7_검증)
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m523 = "단계";` 를 붙여
* orca eval --expression "window.__m523='setup'; $(cat 이 파일)" → 관 12+18(258.12)을 옛 값 든 물넘이 포장으로(PUT · 원래 값은 sessionStorage)
* (orca reload)
* orca eval --expression "window.__m523='edit'; $(cat 이 파일)" → 고치고 [저장]
* (orca reload)
* orca eval --expression "window.__m523='verify'; $(cat 이 파일)" → 다시 연 값 · B06 같은 그리기
* orca eval --expression "window.__m523='restore'; $(cat 이 파일)" → 원래 관 지점으로 되돌림(PUT) · 초안 비움
* 쓰는 것: 검증 프로젝트 pipe_points.json(되돌림 단계가 원래 값으로 다시 씀)
* 기대 수치(edit): 컨테이너 = 경사각도(필수) · [+ 도수로/돌붙임] · 옛 칸 안 보임 · 지난 정의 값 5 ·
* 구조 선택지 도수로/돌붙임 · 각도 12.5 · 선택 둘 추가(돌붙임 · 도수로) · 잇기 경사각도 · 둘째 → 첫째 [빼기] ·
* 남은 하나 = 도수로 · 잇기가 첫째로 당겨짐 · 저장 파일 = 도 · 인스턴스 키 · 옛 값 그대로
* 기대 수치(verify): 저장 값 그대로 · 같은 그리기(B05 = B06)
*/
(async () => {
const MODE = window.__m523 || "edit";
const AT = 258.12;
const NAME = /^12\+18물넘이포장/;
const OLD = {
ford_width_m: 6,
ford_height_m: 0.3,
ford_slope_pct: 3,
thickness_cm: 20,
length_m: 7,
};
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 KEYS = [
"chainage_m",
"source",
"facility",
"start_m",
"end_m",
"options",
"summary_item",
"group_items",
];
const plain = (list) =>
list.map((p) =>
Object.fromEntries(
KEYS.filter((k) => p[k] !== undefined && p[k] !== null).map((k) => [k, p[k]]),
),
);
const clearDrafts = () => {
for (const k of Object.keys(sessionStorage))
if (k.startsWith("aislo:draft:pipes:")) sessionStorage.removeItem(k);
};
const spec = () =>
[...document.querySelectorAll(".b05-structure__spec")].find((n) => n.offsetParent !== null);
const boxes = (c) => [...spec().querySelectorAll(`[data-spec-container="${c}"]`)];
const ctl = (b, c, item) =>
b.querySelector(`[data-spec-item="${c}.${item}"]`).querySelector("select, input");
const set = async (b, c, item, v) => {
const el = ctl(b, c, item);
el.value = v;
el.dispatchEvent(new Event("change", { bubbles: true }));
await w(250);
};
const pick = 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();
if (spec()) return; // 이미 열림(B06 은 늦게 열려 두 번 누르면 닫힘)
(li.querySelector("button") ?? li).click();
await until(() => !!spec(), 6000);
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")?.classList.contains("is-assigned"),
}));
const pickLink = async (b) => {
b.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((x) => x.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 staleCount = () => {
const legend = [...document.querySelectorAll("legend")].find(
(l) => l.offsetParent !== null && l.textContent.trim() === "지난 정의 값",
);
return legend ? legend.parentElement.querySelectorAll("input").length : 0;
};
const point = async () =>
(await fetchApi.fetchDetailPipePoints(projectId)).pipe_points.find(
(p) => Math.abs(p.chainage_m - AT) < 0.05,
);
const out = { mode: MODE };
if (MODE === "setup") {
const before = await fetchApi.fetchDetailPipePoints(projectId);
if (!sessionStorage.getItem("__523_original"))
sessionStorage.setItem("__523_original", JSON.stringify(before.pipe_points));
const points = plain(before.pipe_points).map((p) =>
Math.abs(p.chainage_m - AT) < 0.05
? { ...p, facility: "ford_pavement", options: { ...OLD } }
: p,
);
await fetchApi.saveDetailPipePoints(projectId, points);
clearDrafts();
out.ready = (await point())?.facility === "ford_pavement";
}
if (MODE === "edit") {
await pick();
out.containers = shape().map((b) => [b.c, b.legend]);
out.addButton = spec().querySelector('[data-spec-add="chute"]')?.textContent;
out.oldFieldsVisible = visibleKeys();
out.stale = staleCount();
await set(boxes("slope")[0], "slope", "angle", "12.5");
spec().querySelector('[data-spec-add="chute"]').click();
await w(300);
spec().querySelector('[data-spec-add="chute"]').click();
await w(300);
out.chuteChoices = [...ctl(boxes("chute")[0], "chute", "structure").options]
.filter((o) => o.value)
.map((o) => o.text);
await set(boxes("chute")[0], "chute", "structure", "돌붙임");
await set(boxes("chute")[1], "chute", "structure", "도수로");
await pickLink(boxes("slope")[0]);
await pickLink(boxes("chute")[1]);
out.beforeRemove = shape().map((b) => [b.legend, b.items, b.link]);
boxes("chute")[0].querySelector('[data-spec-remove="chute"]').click();
await w(400);
out.afterRemove = shape().map((b) => [b.legend, b.items, b.link]);
document.querySelector('[data-dock-action="save"]').click();
await w(500);
out.saved = await until(() => !document.querySelector(".ui-overlay.is-active"), 60000);
await w(1000);
const p = await point();
out.file = p && { options: p.options, group_items: Object.keys(p.group_items ?? {}).sort() };
}
if (MODE === "verify") {
await pick();
const b05 = shape();
out.reopened = b05.map((b) => [b.legend, b.items, b.link]);
out.stale = staleCount();
location.hash = "#/b06-section";
await until(() => location.hash.includes("b06"), 5000);
await w(6000);
await pick();
const b06 = shape();
out.sameDrawing = JSON.stringify(b05) === JSON.stringify(b06);
if (!out.sameDrawing) out.diff = { b05, b06 };
out.b06Stale = staleCount();
}
if (MODE === "restore") {
const original = JSON.parse(sessionStorage.getItem("__523_original") || "null");
if (!original) return JSON.stringify({ error: "원래 값 없음" });
await fetchApi.saveDetailPipePoints(projectId, plain(original));
clearDrafts();
sessionStorage.removeItem("__523_original");
const now = await fetchApi.fetchDetailPipePoints(projectId);
const key = (list) =>
JSON.stringify(
list.map((p) => [p.chainage_m, p.facility ?? null, p.options ?? {}, p.group_items ?? {}]),
);
out.restored = key(now.pipe_points) === key(original);
}
return JSON.stringify(out);
})();