Merge remote-tracking branch 'origin/sub_laptop_8' into main_laptop_1
This commit is contained in:
@@ -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<StructDef["종류"][string]> => (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 }),
|
||||
]),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -57,7 +57,8 @@ export interface StructDef {
|
||||
구조물군: Record<string, string>;
|
||||
동작: Record<string, string>;
|
||||
types: TypeDef[];
|
||||
종류: Record<string, { 기준측점?: boolean; 동작?: string[] }>;
|
||||
/** 이름 = 구조물 명세 이름 바꿈 표(PLAN 52 · 키 `{컨테이너}[.{항목}[={값}]]`) */
|
||||
종류: Record<string, { 기준측점?: boolean; 동작?: string[]; 이름?: Record<string, string> }>;
|
||||
표시: Record<string, Record<string, false>>;
|
||||
그룹: Record<string, GroupDef[]>;
|
||||
배치: Record<string, Record<string, Spot>>;
|
||||
@@ -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(", ")}`);
|
||||
|
||||
@@ -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<NonNullable<SpecNameRow["tag"]>, 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],
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -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<string>();
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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,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);
|
||||
})();
|
||||
Reference in New Issue
Block a user