diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Def.ts b/M02_MasterTemplete/StructOptions/M02_SO_Def.ts index 26899fdc1..d16428ac1 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Def.ts +++ b/M02_MasterTemplete/StructOptions/M02_SO_Def.ts @@ -26,6 +26,8 @@ import { } from "./M02_SO_Def_Model"; import { buildOptionsBlock } from "./M02_SO_Def_Options"; import { buildRulesBlock } from "./M02_SO_Def_Rules"; +import { buildSpecBlock } from "./M02_SO_Def_Spec"; +import { STRUCTURE_SPECS } from "../../A00_Common/design/design_structure_pipe"; import { checkBox, textBox } from "./M02_SO_Def_Ui"; import { buildGroupBlock } from "./M02_SO_Groups"; import { to } from "./M02_SO_Text"; @@ -182,6 +184,8 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void { } const edit = canEdit(); const extra = (): NonNullable => (d.종류[type.type_id] ??= {}); + // 명세가 있는 종류(PLAN 52) = 이름만 고침 — 옛 편집기(옵션 · 배치 · 그룹 · 규칙 · 기준 측점)는 숨김 + const spec = STRUCTURE_SPECS[type.type_id]; detail.replaceChildren( el("div", { className: "m02-sod__kindhead", @@ -196,16 +200,20 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void { edit, attr: "data-sod-kind-off", }), - checkBox( - to("BaseStationShow"), - baseShown(d, type.type_id), - (v) => { - if (v) delete extra().기준측점; - else extra().기준측점 = false; - changed(); - }, - { edit, attr: "data-sod-base" }, - ), + ...(spec + ? [] + : [ + checkBox( + to("BaseStationShow"), + baseShown(d, type.type_id), + (v) => { + if (v) delete extra().기준측점; + else extra().기준측점 = false; + changed(); + }, + { edit, attr: "data-sod-base" }, + ), + ]), ...(d.종류[type.type_id]?.동작 ?? []).map((a) => { const tag = createTag(a, "neutral"); tag.title = d.동작[a] ?? ""; @@ -213,10 +221,14 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void { }), ], }), - buildOptionsBlock({ def: d, type, edit, open, changed }), - buildLayoutBlock({ def: d, type, edit, changed }), - buildGroupBlock({ list: (d.그룹[type.type_id] ??= []), edit, changed }), - buildRulesBlock({ def: d, type, edit, changed }), + ...(spec + ? [buildSpecBlock({ def: d, type, spec, edit, changed })] + : [ + buildOptionsBlock({ def: d, type, edit, open, changed }), + buildLayoutBlock({ def: d, type, edit, changed }), + buildGroupBlock({ list: (d.그룹[type.type_id] ??= []), edit, changed }), + buildRulesBlock({ def: d, type, edit, changed }), + ]), ); }; diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Def_Model.ts b/M02_MasterTemplete/StructOptions/M02_SO_Def_Model.ts index d45d9511b..2cb6355bb 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Def_Model.ts +++ b/M02_MasterTemplete/StructOptions/M02_SO_Def_Model.ts @@ -57,7 +57,8 @@ export interface StructDef { 구조물군: Record; 동작: Record; types: TypeDef[]; - 종류: Record; + /** 이름 = 구조물 명세 이름 바꿈 표(PLAN 52 · 키 `{컨테이너}[.{항목}[={값}]]`) */ + 종류: Record }>; 표시: Record>; 그룹: Record; 배치: Record>; @@ -257,6 +258,10 @@ export function problems(def: StructDef): string[] { if (!o.choices?.length) out.push(`${t.name}.${o.label} 고르기 꼴은 선택지가 있어야 함`); } const grp = badNames((def.그룹[t.type_id] ?? []).map((g) => g.이름)); + const spec = Object.entries(def.종류[t.type_id]?.이름 ?? {}) + .filter(([, v]) => !v.trim() || v.length > NAME_MAX) + .map(([k]) => k); + if (spec.length) out.push(`${t.name} 명세 이름: ${spec.join(", ")}`); if (grp.length) out.push(`${t.name} 그룹 이름: ${grp.join(", ")}`); const lap = overlaps(placeAll(t.options, def.배치[t.type_id])); if (lap.length) out.push(`${t.name} 배치 겹침: ${lap.join(", ")}`); diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec.ts b/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec.ts new file mode 100644 index 000000000..63157ee50 --- /dev/null +++ b/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec.ts @@ -0,0 +1,67 @@ +/* ============================================================================= + * M02_SO_Def_Spec.ts + * 구조물 명세가 있는 종류(PLAN 52 · `STRUCTURE_SPECS`)의 이름 고치기 칸(52-11) — 나무 꼴 + * 컨테이너 제목 › 항목 이름 › 값 이름 · 이름만 고침 · 키 · 필수/선택 · 꼴은 읽기만 · + * 아래 미리보기 = B05 · B06 이 그리는 같은 명세 폼(`createSpecForm`)에 고친 이름을 얹음. + * 구조물군 · 종류 이름은 왼쪽 목록 · 머리 칸이 그대로 고침. + * ========================================================================== */ + +import { createTag, el } from "@ui/ui_template_elements"; +import { createSpecForm } from "../../A00_Common/design/design_structure_form"; +import type { StructureSpec } from "../../A00_Common/design/design_structure_spec"; +import type { StructDef, TypeDef } from "./M02_SO_Def_Model"; +import { setSpecName, specNameRows, type SpecNameRow } from "./M02_SO_Def_Spec_Model"; +import { head, textBox } from "./M02_SO_Def_Ui"; +import { to, type OptionsTextKey } from "./M02_SO_Text"; + +const TAG: Record, OptionsTextKey> = { + required: "SpecRequired", + optional: "SpecOptional", + select: "FormSelect", + number: "FormNumber", + auto: "FormAuto", +}; + +export function buildSpecBlock(opt: { + def: StructDef; + type: TypeDef; + spec: StructureSpec; + edit: boolean; + changed: () => void; +}): HTMLElement { + const { def, type, spec, edit, changed } = opt; + const rows = specNameRows(def, spec).map((row) => + el("div", { + className: `m02-sod__specrow is-level-${row.level}`, + attrs: { "data-sod-spec": row.nameKey }, + children: [ + textBox(row.label, (v) => setSpecName(def, type.type_id, row, v), changed, { + edit, + attr: "data-sod-spec-name", + cls: "m02-sod__name", + }), + el("small", { className: "m02-opts__key", text: row.key }), + ...(row.tag ? [createTag(to(TAG[row.tag]), "neutral")] : []), + ], + }), + ); + // 미리보기 — 값 없는 새 구조물 폼 · 잇기 줄 없음(조합은 프로젝트 몫) + const preview = createSpecForm(null); + const groups = Object.fromEntries((def.그룹[type.type_id] ?? []).map((g) => [g.id, g.이름])); + preview.write(spec, {}, def.종류[type.type_id]?.이름 ?? {}, groups); + preview.root.inert = true; + return el("div", { + className: "m02-sod__block", + attrs: { "data-sod-spec-block": "" }, + children: [ + ...head(to("SpecHead"), to("SpecNote")), + ...rows, + ...head(to("SpecPreview")), + el("div", { + className: "m02-sod__specpreview", + attrs: { "data-sod-spec-preview": "" }, + children: [preview.root], + }), + ], + }); +} diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec_Model.ts b/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec_Model.ts new file mode 100644 index 000000000..bd6bc693b --- /dev/null +++ b/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec_Model.ts @@ -0,0 +1,91 @@ +/* ============================================================================= + * M02_SO_Def_Spec_Model.ts + * 구조물 명세(PLAN 52 · `design_structure_spec`) 이름 고치기(52-11)의 자료 손질 — 화면 그리기 없음. + * + * · 명세 = 구조 정본(키 · 컨테이너 · 필수/선택 · 꼴) — 여기서는 **이름만** 고침 + * · 이름 자리 = 정의 `종류.{종류}.이름`(키 `{컨테이너}` · `{컨테이너}.{항목}` · `{컨테이너}.{항목}={값}`) + * · 필수 컨테이너 제목 = 이름 표에 없으면 정의 그룹(`그룹.{종류}` 의 `id = link`) 이름 — B05 그리기 + * (`containerTitle`)와 같은 차례로 읽고 그 자리에 씀 + * import 는 꼴만 — 시험이 이 파일만 옮겨 돌림. + * ========================================================================== */ + +import type { StructureSpec } from "../../A00_Common/design/design_structure_spec"; +import type { StructDef } from "./M02_SO_Def_Model"; + +export interface SpecNameRow { + /** 0 = 컨테이너 · 1 = 항목 · 2 = 값 */ + level: 0 | 1 | 2; + /** 이름 표 키 */ + nameKey: string; + /** 읽기만 표시 — 컨테이너 id · 항목 id · 값 */ + key: string; + /** 지금 이름 */ + label: string; + /** 읽기만 표시 — 필수/선택 · 꼴 */ + tag: "required" | "optional" | "select" | "number" | "auto" | null; + /** 필수 컨테이너 제목이 정의 그룹 이름에서 옴 — 고치면 그 그룹 이름에 씀 */ + group: string | null; +} + +/** 명세 차례대로 이름 줄 — 같은 이름 키(갈래마다 겹친 값)는 한 줄. */ +export function specNameRows(def: StructDef, spec: StructureSpec): SpecNameRow[] { + const names = def.종류[spec.typeId]?.이름 ?? {}; + const groups = def.그룹[spec.typeId] ?? []; + const rows: SpecNameRow[] = []; + const seen = new Set(); + const push = (row: SpecNameRow): void => { + if (seen.has(row.nameKey)) return; + seen.add(row.nameKey); + rows.push(row); + }; + for (const c of spec.containers) { + const group = + c.required && names[c.id] === undefined ? groups.find((g) => g.id === c.link) : undefined; + push({ + level: 0, + nameKey: c.id, + key: c.id, + label: names[c.id] ?? group?.이름 ?? c.title, + tag: c.required ? "required" : "optional", + group: group ? group.id : null, + }); + for (const item of c.items) { + const itemKey = `${c.id}.${item.id}`; + push({ + level: 1, + nameKey: itemKey, + key: item.id, + label: names[itemKey] ?? item.label, + tag: item.kind, + group: null, + }); + const choices = [ + ...(item.choices ?? []), + ...Object.values(item.by?.map ?? {}).flatMap((b) => b.choices ?? []), + ]; + for (const ch of choices) { + const choiceKey = `${itemKey}=${ch.value}`; + push({ + level: 2, + nameKey: choiceKey, + key: ch.value, + label: names[choiceKey] ?? ch.label, + tag: null, + group: null, + }); + } + } + } + return rows; +} + +/** 이름 하나를 사본에 씀 — 그룹에서 온 제목은 그 그룹 이름 · 나머지는 이름 표. */ +export function setSpecName(def: StructDef, typeId: string, row: SpecNameRow, value: string): void { + const group = row.group ? def.그룹[typeId]?.find((g) => g.id === row.group) : undefined; + if (group) { + group.이름 = value; + return; + } + const kind = (def.종류[typeId] ??= {}); + (kind.이름 ??= {})[row.nameKey] = value; +} diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Style.css b/M02_MasterTemplete/StructOptions/M02_SO_Style.css index a8570c86a..492eb8c4f 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Style.css +++ b/M02_MasterTemplete/StructOptions/M02_SO_Style.css @@ -332,3 +332,28 @@ margin: 0; color: var(--color-danger); } + +/* 구조물 명세 이름 고치기(PLAN 52-11) — 컨테이너 › 항목 › 값 들여쓰기 */ +.m02-sod__specrow { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--spacing-8); +} + +.m02-sod__specrow.is-level-0 { + margin-top: var(--spacing-8); + font-weight: 600; +} + +.m02-sod__specrow.is-level-1 { + padding-left: 1.5rem; +} + +.m02-sod__specrow.is-level-2 { + padding-left: 3rem; +} + +.m02-sod__specpreview { + max-width: 28rem; +} diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Text.ts b/M02_MasterTemplete/StructOptions/M02_SO_Text.ts index 90037edd8..3235e7762 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Text.ts +++ b/M02_MasterTemplete/StructOptions/M02_SO_Text.ts @@ -45,6 +45,15 @@ const TEXT = { FormNumber: ["숫자", "Number"], FormText: ["글", "Text"], FormBool: ["예/아니오", "Yes/No"], + FormAuto: ["자동", "Auto"], + SpecHead: ["구조물 명세 이름", "Structure spec names"], + SpecNote: [ + "컨테이너 제목 › 항목 이름 › 값 이름만 고침 — 키(작은 글) · 필수/선택 · 꼴 · 구조는 구조물 명세가 정함 · [저장]하면 B05 · B06 이 이 이름으로 그림", + "Rename containers › items › values only — keys, required/optional, form and structure come from the spec", + ], + SpecRequired: ["필수", "Required"], + SpecOptional: ["선택", "Optional"], + SpecPreview: ["미리보기 — B05 · B06 폼", "Preview — B05 · B06 form"], Yes: ["예", "Yes"], No: ["아니오", "No"], Unit: ["단위", "Unit"], diff --git a/resources/tester/helper_m02_so_def_spec.cjs b/resources/tester/helper_m02_so_def_spec.cjs new file mode 100644 index 000000000..ede10f95b --- /dev/null +++ b/resources/tester/helper_m02_so_def_spec.cjs @@ -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)); diff --git a/resources/tester/test_m02_so_def_spec.py b/resources/tester/test_m02_so_def_spec.py new file mode 100644 index 000000000..3677816ff --- /dev/null +++ b/resources/tester/test_m02_so_def_spec.py @@ -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"] == "유입" diff --git a/resources/tester/ui_checks/52-11_정의_이름고치기.js b/resources/tester/ui_checks/52-11_정의_이름고치기.js new file mode 100644 index 000000000..6d4679afe --- /dev/null +++ b/resources/tester/ui_checks/52-11_정의_이름고치기.js @@ -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); +})();