Merge remote-tracking branch 'origin/sub_laptop_1' into sub_laptop_5
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
/* M02 구조물 정의 편집 화면 자료 손질(`M02_SO_Def_Model.ts` · PLAN 48-2) 단위검증 헬퍼 —
|
||||
* TS 를 그 자리에서 트랜스파일해 Node 로 돌리고 결과를 JSON 으로 냄(pytest 가 판정). */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, "..", "..", "M02_MasterTemplete", "StructOptions", "M02_SO_Def_Model.ts"),
|
||||
"utf8",
|
||||
);
|
||||
const js = ts.transpileModule(source, {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, require);
|
||||
const m = box.exports;
|
||||
|
||||
const sel = (key, choices) => ({ key, label: key, input: "select", choices: [...choices] });
|
||||
const type = () => ({
|
||||
type_id: "k1",
|
||||
group: "A",
|
||||
name: "배수관",
|
||||
options: [sel("a", ["x", "y"]), sel("b", ["p", "q"]), { key: "c", label: "c", input: "number" }],
|
||||
});
|
||||
const defOf = (over = {}) => ({
|
||||
판: 3,
|
||||
구조물군: { A: "A군", B: "B군" },
|
||||
동작: {},
|
||||
types: [
|
||||
type(),
|
||||
{ ...type(), type_id: "k2", name: "둘째" },
|
||||
{ ...type(), type_id: "k3", group: "B", name: "셋째" },
|
||||
],
|
||||
종류: {},
|
||||
표시: {},
|
||||
그룹: {},
|
||||
배치: {},
|
||||
규칙: [],
|
||||
...over,
|
||||
});
|
||||
const cells = (t, spots) => Object.fromEntries(m.placeAll(t.options, spots));
|
||||
|
||||
const out = {};
|
||||
// 자리 안 정한 옵션 = 차례대로 채움 · 두 칸이면 셋째는 둘째 줄
|
||||
out.auto = cells(type());
|
||||
// 정한 자리 먼저 · 남은 것은 빈 칸으로 · 너비는 칸 수를 안 넘음
|
||||
out.fixed = cells(type(), { c: { 줄: 1, 칸: 1, 너비: 5 } });
|
||||
// 안 씀은 격자에서 빠짐
|
||||
const off = type();
|
||||
off.options[0].enabled = false;
|
||||
out.off = cells(off);
|
||||
// 겹침
|
||||
out.overlap = m
|
||||
.overlaps(
|
||||
m.placeAll(type().options, { a: { 줄: 1, 칸: 1, 너비: 2 }, b: { 줄: 1, 칸: 2, 너비: 1 } }),
|
||||
)
|
||||
.sort();
|
||||
// 규칙 — 맞는 줄 · 없는 값 · 자기 자신 · 없는 하위 선택지 · 그러면 비었음 · 고르기 아닌 상위
|
||||
const rd = defOf();
|
||||
const R = (만약, 그러면) => ({ 종류: "k1", 만약, 그러면 });
|
||||
out.rules = [
|
||||
R({ 옵션: "a", 값들: ["x"] }, { 옵션: "b", 선택지: ["p"], 기본값: "p" }),
|
||||
R({ 옵션: "a", 값들: ["zz"] }, { 옵션: "b", 보이기: false }),
|
||||
R({ 옵션: "a", 값들: ["x"] }, { 옵션: "a", 보이기: false }),
|
||||
R({ 옵션: "a", 값들: ["y"] }, { 옵션: "b", 선택지: ["nope"] }),
|
||||
R({ 옵션: "a", 값들: ["x"] }, { 옵션: "b" }),
|
||||
R({ 옵션: "c", 값들: ["3"] }, { 옵션: "b", 보이기: false }),
|
||||
R({ 옵션: "a", 값들: ["x"] }, { 옵션: "b", 선택지: ["p"], 기본값: "q" }),
|
||||
].map((r) => m.ruleBad(rd, r));
|
||||
// 이름 — 빈 이름 · 겹침
|
||||
out.names = m.badNames(["가", " ", "가"]);
|
||||
// 저장 전 검사 — 깨끗한 정의는 빈 목록 · 겹침 · 이름 틀림 · 빈 선택지는 까닭
|
||||
out.clean = m.problems(defOf());
|
||||
const broken = defOf({
|
||||
배치: { k1: { a: { 줄: 1, 칸: 1, 너비: 2 }, b: { 줄: 1, 칸: 2, 너비: 1 } } },
|
||||
});
|
||||
broken.types[0].options[1].label = "a";
|
||||
broken.types[1].options[0].choices = [];
|
||||
out.broken = m.problems(broken);
|
||||
// 선택지 이름 바꿈 · 뺌 — 기본값 · 규칙 같이
|
||||
const rn = defOf({
|
||||
규칙: [R({ 옵션: "a", 값들: ["x", "y"] }, { 옵션: "b", 선택지: ["p"], 기본값: "p" })],
|
||||
});
|
||||
const a = rn.types[0].options[0];
|
||||
const b = rn.types[0].options[1];
|
||||
a.default = "x";
|
||||
m.renameChoice(rn, "k1", a, "x", "엑스");
|
||||
m.renameChoice(rn, "k1", b, "p", "피");
|
||||
out.rename = [
|
||||
a.choices,
|
||||
a.default,
|
||||
rn.규칙[0].만약.값들,
|
||||
rn.규칙[0].그러면.선택지,
|
||||
rn.규칙[0].그러면.기본값,
|
||||
];
|
||||
m.dropChoice(rn, "k1", b, "피");
|
||||
out.drop = [b.choices, rn.규칙[0].그러면.선택지, "기본값" in rn.규칙[0].그러면];
|
||||
// 순서 — 구조물군 키 차례 · 종류는 같은 군 안에서만
|
||||
const od = defOf();
|
||||
out.family = [m.stepFamily(od, "A", -1), m.stepFamily(od, "A", 1), Object.keys(od.구조물군)];
|
||||
out.type = [m.stepType(od, "k2", 1), m.stepType(od, "k1", 1), od.types.map((t) => t.type_id)];
|
||||
out.base = [
|
||||
m.baseShown(defOf(), "k1"),
|
||||
m.baseShown(defOf({ 종류: { k1: { 기준측점: false } } }), "k1"),
|
||||
];
|
||||
// 목록 순서 바꿈 · 끝은 그대로
|
||||
const list = [1, 2, 3];
|
||||
out.step = [m.step(list, 0, -1), m.step(list, 0, 1), list];
|
||||
// 새 키 — 머리글 · 안 겹침 · 영문 소문자 숫자
|
||||
const k = m.newKey("opt_", ["opt_aaaaaa"]);
|
||||
out.newKey = [/^opt_[0-9a-z]{4,}$/.test(k), k !== "opt_aaaaaa"];
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -0,0 +1,87 @@
|
||||
"""M02 구조물 정의 편집 화면 자료 손질(`M02_SO_Def_Model.ts` · PLAN 48-2) 단위검증.
|
||||
|
||||
프론트에 JS 테스트 러너가 없어 Node 헬퍼(`helper_m02_so_def_model.cjs`)가 TS 를 트랜스파일해 돌리고
|
||||
pytest 는 그 결과(JSON)를 판정.
|
||||
|
||||
못박는 것
|
||||
- 배치 격자: 안 정한 옵션은 차례대로 · 정한 자리 먼저 · 너비 ≤ 칸 수 · 안 씀 빠짐 · 겹침 찾음.
|
||||
- 규칙: 없는 값 · 자기 자신 · 없는 하위 선택지 · 그러면 비었음 · 좁힌 선택지 밖 기본값은 틀린 줄
|
||||
(서버 `M02_SO_Definition._rule_errors` 와 같은 뜻) · 고르기 아닌 상위는 값 검사 안 함.
|
||||
- 저장 전 검사: 깨끗한 정의 = 빈 목록 · 이름 겹침 · 배치 겹침 · 빈 선택지 = 까닭 글.
|
||||
- 선택지 이름 바꿈 · 뺌은 그 옵션 기본값 · 같은 종류 규칙도 같이 고침.
|
||||
- 순서: 구조물군 = 키 차례 · 종류 = 같은 군 안에서만.
|
||||
"""
|
||||
|
||||
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_model.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["auto"] == {
|
||||
"a": {"줄": 1, "칸": 1, "너비": 1},
|
||||
"b": {"줄": 1, "칸": 2, "너비": 1},
|
||||
"c": {"줄": 2, "칸": 1, "너비": 1},
|
||||
}
|
||||
|
||||
|
||||
def test_배치_정한_자리_먼저_너비는_칸수까지(r) -> None:
|
||||
assert r["fixed"]["c"] == {"줄": 1, "칸": 1, "너비": 2}
|
||||
assert r["fixed"]["a"] == {"줄": 2, "칸": 1, "너비": 1}
|
||||
assert r["fixed"]["b"] == {"줄": 2, "칸": 2, "너비": 1}
|
||||
|
||||
|
||||
def test_안씀은_격자에서_빠짐(r) -> None:
|
||||
assert set(r["off"]) == {"b", "c"}
|
||||
assert r["off"]["b"] == {"줄": 1, "칸": 1, "너비": 1}
|
||||
|
||||
|
||||
def test_겹침(r) -> None:
|
||||
assert r["overlap"] == ["a", "b"]
|
||||
|
||||
|
||||
def test_규칙_틀린_줄(r) -> None:
|
||||
assert r["rules"] == [False, True, True, True, True, False, True]
|
||||
|
||||
|
||||
def test_이름_검사(r) -> None:
|
||||
assert r["names"] == ["(빈 이름)", "가"]
|
||||
|
||||
|
||||
def test_저장_전_검사(r) -> None:
|
||||
assert r["clean"] == []
|
||||
assert any("옵션 이름" in t for t in r["broken"])
|
||||
assert any("배치 겹침" in t for t in r["broken"])
|
||||
assert any("선택지가 있어야" in t for t in r["broken"])
|
||||
|
||||
|
||||
def test_선택지_이름_바꿈_뺌은_기본값_규칙도_같이(r) -> None:
|
||||
assert r["rename"] == [["엑스", "y"], "엑스", ["엑스", "y"], ["피"], "피"]
|
||||
assert r["drop"] == [["q"], [], False]
|
||||
|
||||
|
||||
def test_순서_기준측점_새키(r) -> None:
|
||||
assert r["family"] == [False, True, ["B", "A"]]
|
||||
assert r["type"] == [False, True, ["k2", "k1", "k3"]]
|
||||
assert r["base"] == [True, False]
|
||||
assert r["step"] == [False, True, [2, 1, 3]]
|
||||
assert r["newKey"] == [True, True]
|
||||
@@ -0,0 +1,123 @@
|
||||
"""M02 구조물 정의 편집 화면(PLAN 48-2) — 지난 판 길 · 화면이 보내는 꼴을 서버가 받음.
|
||||
|
||||
못박는 것
|
||||
- 지난 판 목록 `[{판, 저장일}]` 새 판부터 · 한 벌 읽기 · 없는 판 404 · 읽기는 누구나.
|
||||
- 화면이 고친 정의(이름 · 순서 · 안 씀 · 기준측점 · 선택지 이름 · 배치 · 그룹 형상 · 규칙)를
|
||||
그대로 PUT 하면 판 +1 · 같은 값으로 다시 읽힘.
|
||||
- 시스템 관리자 아니면 PUT 403(화면은 읽기만).
|
||||
- 마스터 정본은 안 씀(사본).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from B05_Profile import B05_Profile_Structures_Schema as schema
|
||||
from common_util.common_util_auth import verify_session
|
||||
from M02_MasterTemplete.StructOptions.M02_SO_Router import read_router
|
||||
|
||||
URL = "/api/m02/structure-definition"
|
||||
ADMIN = {"role": "SYSTEM_ADMIN", "user_id": 1}
|
||||
USER = {"role": "USER", "user_id": 2, "company_id": 1}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def copy(tmp_path: Path, monkeypatch) -> Path:
|
||||
path = tmp_path / "구조물정의.json"
|
||||
path.write_text(Path(schema.DEFINITION_PATH).read_text("utf-8"), "utf-8")
|
||||
monkeypatch.setattr(schema, "DEFINITION_PATH", str(path))
|
||||
return path
|
||||
|
||||
|
||||
def _client(session: dict) -> TestClient:
|
||||
app = FastAPI()
|
||||
app.include_router(read_router)
|
||||
app.dependency_overrides[verify_session] = lambda: session
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def _screen_edit(doc: dict) -> dict:
|
||||
"""화면이 하는 고침 한 벌 — `M02_SO_Def*.ts` 가 만드는 꼴 그대로."""
|
||||
pipe = next(t for t in doc["types"] if t["type_id"] == "pipe")
|
||||
pipe["name"] = "배수관(고침)"
|
||||
kind = next(o for o in pipe["options"] if o["key"] == "pipe_kind")
|
||||
kind["label"] = "관 종류"
|
||||
kind["choices"] = ["흄관(고침)" if c == "흄관" else c for c in kind["choices"]]
|
||||
pipe["options"].append(
|
||||
{
|
||||
"key": "opt_ab12cd",
|
||||
"label": "새 옵션 1",
|
||||
"input": "select",
|
||||
"choices": ["새 선택지 1", "새 선택지 2"],
|
||||
"default": None,
|
||||
}
|
||||
)
|
||||
pipe["options"][1]["enabled"] = False # 안 씀
|
||||
first = list(doc["구조물군"])
|
||||
doc["구조물군"] = {k: doc["구조물군"][k] for k in [first[1], first[0], *first[2:]]}
|
||||
doc["종류"].setdefault("pipe", {})["기준측점"] = False
|
||||
doc["배치"]["pipe"] = {"pipe_kind": {"줄": 1, "칸": 1, "너비": 2}}
|
||||
doc["표시"]["pipe"] = {"inlet_type": False}
|
||||
doc["그룹"]["pipe"][1]["형상"] = {
|
||||
"길이": {"받기": True, "뜻": "유입 보호공 연장"},
|
||||
"높이": {"받기": False, "뜻": ""},
|
||||
}
|
||||
doc["규칙"] = [
|
||||
{
|
||||
"종류": "pipe",
|
||||
"만약": {"옵션": "pipe_kind", "값들": ["흄관(고침)"]},
|
||||
"그러면": {"옵션": "opt_ab12cd", "선택지": ["새 선택지 2"], "기본값": "새 선택지 2"},
|
||||
},
|
||||
{
|
||||
"종류": "pipe",
|
||||
"만약": {"옵션": "inlet_type", "값들": ["집수정"]},
|
||||
"그러면": {"옵션": "opt_ab12cd", "보이기": False},
|
||||
},
|
||||
]
|
||||
return doc
|
||||
|
||||
|
||||
def test_화면_고침을_그대로_받아_판_더하기_1_지난_판_길(copy) -> None:
|
||||
admin = _client(ADMIN)
|
||||
doc = admin.get(URL).json()
|
||||
assert admin.get(f"{URL}/history").json() == []
|
||||
sent = _screen_edit(json.loads(json.dumps(doc)))
|
||||
res = admin.put(URL, json={"판": doc["판"], "정의": sent})
|
||||
assert res.status_code == 200, res.text
|
||||
got = res.json()
|
||||
assert got["판"] == doc["판"] + 1
|
||||
for key in ("구조물군", "종류", "배치", "표시", "그룹", "규칙", "types"):
|
||||
assert got[key] == sent[key], key
|
||||
assert list(got["구조물군"]) == list(sent["구조물군"]) # 순서 그대로
|
||||
rows = admin.get(f"{URL}/history").json()
|
||||
assert [r["판"] for r in rows] == [doc["판"]] and len(rows[0]["저장일"]) == 16
|
||||
old = admin.get(f"{URL}/history/{doc['판']}").json()
|
||||
assert old == doc
|
||||
assert admin.get(f"{URL}/history/999").status_code == 404
|
||||
|
||||
|
||||
def test_지난_판은_누구나_읽고_쓰기는_시스템_관리자만(copy) -> None:
|
||||
user = _client(USER)
|
||||
doc = user.get(URL).json()
|
||||
assert user.get(f"{URL}/history").status_code == 200
|
||||
assert user.put(URL, json={"판": doc["판"], "정의": _screen_edit(doc)}).status_code == 403
|
||||
assert json.loads(copy.read_text("utf-8"))["판"] == doc["판"]
|
||||
|
||||
|
||||
def test_화면이_못_거른_틀린_규칙은_422_안_씀(copy) -> None:
|
||||
admin = _client(ADMIN)
|
||||
doc = admin.get(URL).json()
|
||||
doc["규칙"] = [
|
||||
{
|
||||
"종류": "pipe",
|
||||
"만약": {"옵션": "pipe_kind", "값들": ["없음"]},
|
||||
"그러면": {"옵션": "inlet_type"},
|
||||
}
|
||||
]
|
||||
assert admin.put(URL, json={"판": doc["판"], "정의": doc}).status_code == 422
|
||||
assert json.loads(copy.read_text("utf-8"))["판"] == doc["판"]
|
||||
@@ -102,8 +102,10 @@ def test_틀린_그룹은_422_낡은_판은_409(client) -> None:
|
||||
assert res.status_code == 200 and res.json()["그룹"] == {}
|
||||
|
||||
|
||||
def test_좌측_옵션_보이기는_시스템_관리자만() -> None:
|
||||
def test_좌측_정의_줄은_시스템_층에_늘_고쳐_씀은_시스템_관리자만() -> None:
|
||||
"""PLAN 48-2 — 다른 역할은 읽기만(화면이 칸을 잠금 · 서버 PUT 403)."""
|
||||
side = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts").read_text("utf-8")
|
||||
assert "옵션: canEdit(layer, opt.isAdmin) ? section(options.title, options.body) : null" in side
|
||||
ui = (ROOT / "M02_MasterTemplete/StructOptions/M02_SO_Options.ts").read_text("utf-8")
|
||||
assert "buildGroupBlock(" in ui and "buildPlaceSelect(" in ui
|
||||
assert "옵션: section(options.title, options.body)," in side
|
||||
folder = ROOT / "M02_MasterTemplete/StructOptions"
|
||||
assert "buildGroupBlock(" in (folder / "M02_SO_Def.ts").read_text("utf-8")
|
||||
assert "buildPlaceSelect(" in (folder / "M02_SO_Def_Options.ts").read_text("utf-8")
|
||||
|
||||
@@ -123,12 +123,10 @@ def test_좌측_줄은_구조물_집계_양식_아래_한_줄_같은_한_곳에
|
||||
assert "matset" not in side
|
||||
paint = side.split("function paint(): void {")[1].split("\n }\n")[0]
|
||||
assert "if (opt.getOpen()) options.clear();" in paint # 양식을 열면 이 줄 고름 지움
|
||||
# 화면은 정의 한 벌로 읽고 씀(PLAN 48-2) · 보이기 체크는 정의 화면 옵션 줄
|
||||
api = (ROOT / "M02_MasterTemplete/StructOptions/M02_SO_Api.ts").read_text("utf-8")
|
||||
assert (
|
||||
'call("/layers/system/structure-options")' in api
|
||||
and 'call("/structure-options", { method: "PUT"' in api
|
||||
)
|
||||
ui = (ROOT / "M02_MasterTemplete/StructOptions/M02_SO_Options.ts").read_text("utf-8")
|
||||
assert 'call("/structure-definition")' in api
|
||||
ui = (ROOT / "M02_MasterTemplete/StructOptions/M02_SO_Def_Options.ts").read_text("utf-8")
|
||||
assert 'o.phase === "detail"' in ui # B06 전용 표시
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* - def = { 판 ≥ 1, types: 37, options: 235 }(GET /api/m02/structure-definition)
|
||||
* - types = { types: 37, options: 235, visibleAll: true, pipeGroups: ["관", "관 보호공(유입)", "관 보호공(유출)"] }
|
||||
* (GET /api/projects/structure-types — 옛 꼴 그대로)
|
||||
* - 좌측 「구조물 옵션 보이기」 → 종류 단추 kinds = 37(옵션 없는 종류도 · 옛 화면 그대로)
|
||||
* - 좌측 「구조물 옵션 정의」 → 종류 단추 kinds = 37(옵션 없는 종류도 · 48-2 정의 화면)
|
||||
* · pipe 단추에 그룹 표지 「3」
|
||||
*/
|
||||
(async () => {
|
||||
@@ -41,14 +41,14 @@
|
||||
pipeGroups: pipe.groups.map((g) => g.name),
|
||||
};
|
||||
const row = await until(() => $("[data-options-row]"));
|
||||
if (!row) return JSON.stringify({ ...out, error: "옵션 보이기 줄 없음" });
|
||||
if (!row) return JSON.stringify({ ...out, error: "정의 줄 없음" });
|
||||
row.click();
|
||||
await until(() => $$("[data-options-kind]").length);
|
||||
const kinds = $$("[data-options-kind]");
|
||||
await until(() => $$("[data-sod-kind]").length);
|
||||
const kinds = $$("[data-sod-kind]");
|
||||
out.screen = {
|
||||
kinds: kinds.length,
|
||||
withOptions: def.types.filter((t) => t.options.length).length,
|
||||
pipeTag: $('[data-options-kind="pipe"]')?.innerText.replace(/\s+/g, " ").trim() ?? null,
|
||||
pipeTag: $('[data-sod-kind="pipe"]')?.innerText.replace(/\s+/g, " ").trim() ?? null,
|
||||
};
|
||||
out.ok =
|
||||
out.def.types === 37 &&
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/* 48-2 — M02 구조물 옵션 정의 편집 화면(군 · 종류 · 옵션 · 선택지 · 배치 격자 · 그룹 형상 · 규칙 · 지난 판)
|
||||
* 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?root=master) · 시스템 관리자
|
||||
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌 · 20초 안)
|
||||
* 쓰는 것: 없음 — 화면 사본만 고치고 [저장] 안 누름(저장 길은 test_m02_structure_def_screen.py 가 사본으로) ·
|
||||
* 끝에 판이 그대로인지 GET 으로 봄 · 화면을 다시 띄우면 고침은 사라짐
|
||||
* 기대 수치:
|
||||
* - families = 구조물군 수 · kinds = 37 · pipe 줄 options = pipe 옵션 수 · cells = 쓰는 옵션 수 · base = true
|
||||
* - 이름 바꿈 → 왼쪽 단추 글 따라감 · [저장] 켜짐
|
||||
* - 선택지 펼침 = 3(흄관 · VR관 · 파형강관) · 「흄관」 → 「흄관X」 = 기본값 고르기에도 「흄관X」
|
||||
* - 너비 2 → 미리보기 span 2 · 겹치게 두면 issues 에 「배치 겹침」 · [저장] 꺼짐 · 되돌리면 사라짐
|
||||
* - 기준 측점 끔 → 미리보기 기준 측점 칸 0
|
||||
* - 그룹 형상 「길이」 켬 → 뜻 칸 열림
|
||||
* - [규칙 더함] → 규칙 줄 1 · 빨강 아님 · 지우기 → 0
|
||||
* - [지난 판] → 목록 칸(판 단추 또는 「지난 판 없음」)
|
||||
* - 끝 판 = 처음 판(저장 안 함)
|
||||
*/
|
||||
(async () => {
|
||||
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 = 8000) => {
|
||||
for (let t = 0; t < ms; t += 120) {
|
||||
const v = fn();
|
||||
if (v) return v;
|
||||
await W(120);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
/** 칸 값 바꿈 — 글자 넣음(input) + 떠남(change) */
|
||||
const put = (box, value) => {
|
||||
box.value = value;
|
||||
box.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
box.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
};
|
||||
const tick = (box, on) => {
|
||||
if (box.checked !== on) box.click();
|
||||
};
|
||||
const def = await (
|
||||
await fetch("/api/m02/structure-definition", { credentials: "include" })
|
||||
).json();
|
||||
if (!def.types) return JSON.stringify({ error: "정의 못 읽음" });
|
||||
const pipe = def.types.find((t) => t.type_id === "pipe");
|
||||
const out = { 판: def.판 };
|
||||
|
||||
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: "종류 목록 없음" });
|
||||
out.families = [$$("[data-sod-family]").length, Object.keys(def.구조물군).length];
|
||||
out.kinds = $$("[data-sod-kind]").length;
|
||||
$('[data-sod-kind="pipe"]').click();
|
||||
await until(() => $('[data-sod-option="pipe_kind"]'));
|
||||
const liveOpts = pipe.options.filter((o) => o.enabled !== false).length;
|
||||
out.pipe = {
|
||||
options: [$$("[data-sod-option]").length, pipe.options.length],
|
||||
cells: [$$("[data-sod-cell]").length, liveOpts],
|
||||
base: !!$(".m02-sod__cell.is-base"),
|
||||
};
|
||||
|
||||
// 이름 바꿈 → 왼쪽 단추 · [저장] 켜짐
|
||||
put($("[data-sod-kind-name]"), "배수관(시험)");
|
||||
out.rename = {
|
||||
left: $('[data-sod-kind="pipe"]').innerText.includes("배수관(시험)"),
|
||||
save: !$("[data-sod-save]").disabled,
|
||||
};
|
||||
|
||||
// 선택지 펼침 · 이름 바꿈 → 기본값 고르기에도
|
||||
$('[data-sod-choices="pipe_kind"]').click();
|
||||
await until(() => $$("[data-sod-choice]").length);
|
||||
out.choices = $$("[data-sod-choice]").map((r) => $("input", r).value);
|
||||
put($('[data-sod-choice="흄관"] [data-sod-choice-name]'), "흄관X");
|
||||
const defaultPick = $('[data-sod-option="pipe_kind"] [data-sod-default]');
|
||||
out.choiceRename = [...defaultPick.options].some((o) => o.value === "흄관X");
|
||||
|
||||
// 배치 — 너비 2 · 겹침 · 되돌림
|
||||
put($('[data-sod-place-row="pipe_kind"] [data-sod-width]'), "2");
|
||||
const cell = $('[data-sod-cell="pipe_kind"]');
|
||||
out.width2 = cell.style.gridColumn.replace(/\s+/g, " ");
|
||||
const other = pipe.options.filter((o) => o.enabled !== false)[1].key;
|
||||
put($(`[data-sod-place-row="${other}"] [data-sod-row]`), "1");
|
||||
put($(`[data-sod-place-row="${other}"] [data-sod-col]`), "2");
|
||||
out.overlap = {
|
||||
issue: $("[data-sod-issues]").innerText.includes("배치 겹침"),
|
||||
red: $$(".m02-sod__cell.is-bad").length,
|
||||
saveOff: $("[data-sod-save]").disabled,
|
||||
};
|
||||
put($('[data-sod-place-row="pipe_kind"] [data-sod-width]'), "1");
|
||||
out.overlapGone = !$("[data-sod-issues]").innerText.includes("배치 겹침");
|
||||
|
||||
// 기준 측점 끔
|
||||
tick($("[data-sod-base]"), false);
|
||||
out.baseOff = $$(".m02-sod__cell.is-base").length;
|
||||
|
||||
// 그룹 형상 「길이」 켬 → 뜻 칸 열림
|
||||
const shape = $("[data-sog-shape]");
|
||||
const meanBefore = $(".m02-sog__mean", shape).disabled;
|
||||
tick($("[data-sog-shape-0]", shape), true);
|
||||
out.shape = { before: meanBefore, after: $(".m02-sog__mean", $("[data-sog-shape]")).disabled };
|
||||
|
||||
// 규칙 더함 · 지우기
|
||||
$("[data-sod-rule-add]").click();
|
||||
const rule = await until(() => $("[data-sod-rule]"));
|
||||
out.rule = {
|
||||
rows: $$("[data-sod-rule]").length,
|
||||
bad: rule.classList.contains("is-bad"),
|
||||
top: $("[data-sod-rule-top]", rule).value,
|
||||
target: $("[data-sod-rule-target]", rule).value,
|
||||
};
|
||||
[...$$("button", rule)].find((b) => b.innerText.trim() === "지우기").click();
|
||||
out.ruleGone = $$("[data-sod-rule]").length;
|
||||
|
||||
// 지난 판 목록
|
||||
$("[data-sod-history-toggle]").click();
|
||||
await until(() => $("[data-sod-history]").children.length);
|
||||
out.history = $("[data-sod-history]").innerText.replace(/\s+/g, " ").trim().slice(0, 80);
|
||||
|
||||
const after = await (
|
||||
await fetch("/api/m02/structure-definition", { credentials: "include" })
|
||||
).json();
|
||||
out.판그대로 = after.판 === def.판;
|
||||
out.ok =
|
||||
out.families[0] === out.families[1] &&
|
||||
out.kinds === 37 &&
|
||||
out.pipe.options[0] === out.pipe.options[1] &&
|
||||
out.pipe.cells[0] === out.pipe.cells[1] &&
|
||||
out.pipe.base &&
|
||||
out.rename.left &&
|
||||
out.rename.save &&
|
||||
out.choices.length === 3 &&
|
||||
out.choiceRename &&
|
||||
out.width2.includes("span 2") &&
|
||||
out.overlap.issue &&
|
||||
out.overlap.saveOff &&
|
||||
out.overlapGone &&
|
||||
out.baseOff === 0 &&
|
||||
out.shape.before === true &&
|
||||
out.shape.after === false &&
|
||||
out.rule.rows === 1 &&
|
||||
!out.rule.bad &&
|
||||
out.ruleGone === 0 &&
|
||||
out.history.length > 0 &&
|
||||
out.판그대로;
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
Reference in New Issue
Block a user