feat(구조물표): 322-2 M02 표 화면 — 탭 셋(한눈 표 · 종류별 · 부품) · 반영 · 부품 · 알약 색 저장 · 목록 + enabled 함께 · 검사 짝(화면 = 서버 · 1-3 규칙 · 만든 뒤 고정) · 명세 폼 보임 따름 · 판 알림
- 한눈 표(M02_SO_Def_Table): 군 · 종류 ▲▼ · 이름 · 목록 · 자리꼴 · 정본(고정) · 횡단도 · 카드 이름 · 형상(+ 벽 기본 형태 · 물빼기) · 알약 색 · 3D · 검사 · 처음 값 = 흐림 「기본」 · 바꿈 = 진함 · [처음 값으로] - 종류별: 반영 칸 + 명세 이름 나무 줄마다 보임(컨테이너 = 그 안 칸) · 못 숨김 · 미리보기 숨김 따름 · 「안 씀」 = 목록 B06 만 - 부품 탭(M02_SO_Def_Parts): 부품 · 읽는 칸 · 자리꼴 · 정본 · 쓰는 종류 · 켬/끔(정의 부품 = 끈 것만) - 편집 손질(M02_SO_Def_Reflect): 처음 값이면 반영에서 지움 · [저장] 때 enabled = 목록 both 일 때만 참 - 검사 짝 M02_SO_Def_Check.ts ↔ M02_SO_Check.py(글까지 같음): 반영 · 부품 꼴 · 형상 none → 횡단도 none · 부품 자리꼴 · 정본 · 기본값 없는 읽는 칸 새로 숨김 금지(앞 판 숨김 그대로) · 종류 지우기 · 자리꼴 · 정본 · 군 글자 · 선택지 값 고정 · 서버 check(doc, 지금 판) · 앞 판 없으면 꼴만 - 명세 폼: 쓰는 옵션 키가 모두 숨긴 칸 · 상자는 안 그림(값 그대로) · B05 · B06 시설 · 구조물 명세 폼이 정의 「표시」를 따름 - 판 알림: GET 구조물 목록에 definition_version · structures.json 판 ≠ 지금 판이면 「구조물 정의가 판 N 으로 바뀜 — 저장하면 반영」 한 번 - 시험: test_322_2_check_mirror(거울 · 규칙 · 반영 손질 · 서버 저장 · 422) · 판 알림 · 옛 정의 화면 시험 = 선택지 값 대신 이름 표 · 못 숨김 아닌 칸 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
@@ -223,6 +223,7 @@ const {
|
||||
fetchStructureTypes,
|
||||
onStaleStructureTypes,
|
||||
pipesToStructureMarks,
|
||||
staleDefinitionNotice,
|
||||
writePendingStructures,
|
||||
} = loadTs(B05("B05_Profile_Api_Structures"));
|
||||
const { loadSectionDetail } = loadTs(B06("B06_Section_Section_Store"));
|
||||
@@ -322,6 +323,14 @@ async function main() {
|
||||
await fetchStructureTypes(true);
|
||||
const staleCalls = server.typeCalls - sameCalls;
|
||||
off();
|
||||
// 판 알림(322-2) — 프로젝트 정본이 옛 판으로 적혔으면 한 번만 · 같은 판 · 판 없음은 안 알림
|
||||
const old = server.version - 1;
|
||||
out.notice = [
|
||||
staleDefinitionNotice(PROJECT, old),
|
||||
staleDefinitionNotice(PROJECT, old),
|
||||
staleDefinitionNotice(PROJECT, server.version),
|
||||
staleDefinitionNotice(PROJECT, null),
|
||||
];
|
||||
out.version = {
|
||||
same,
|
||||
sameCalls,
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/* PLAN 322-2 구조물 표 검사 · 편집 헬퍼 — 진짜 `M02_SO_Def_Check.ts` · `M02_SO_Def_Reflect.ts` ·
|
||||
* `design_structure_spec.ts` 를 그 자리에서 트랜스파일해 Node 로 돌림 · 결과 = 표준출력 JSON 한 벌.
|
||||
* 입력 = argv[2] JSON `{cases: {이름: [정의, 앞 판|null]}, definition}`. 짝 = `test_322_2_check_mirror.py`.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
|
||||
|
||||
const cache = new Map();
|
||||
function load(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
if (full.endsWith(".json")) return JSON.parse(fs.readFileSync(full, "utf8"));
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2020,
|
||||
esModuleInterop: true,
|
||||
},
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const need = (spec) => {
|
||||
const base = path.join(path.dirname(full), spec);
|
||||
const hit = [base, `${base}.ts`, base.replace(/\.js$/, ".ts")].find(
|
||||
(p) => fs.existsSync(p) && fs.statSync(p).isFile(),
|
||||
);
|
||||
if (!hit) throw new Error(`모름: ${spec} (${full})`);
|
||||
return load(hit);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, need);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const SO = (name) => path.join(ROOT, "M02_MasterTemplete", "StructOptions", `${name}.ts`);
|
||||
const check = load(SO("M02_SO_Def_Check"));
|
||||
const reflect = load(SO("M02_SO_Def_Reflect"));
|
||||
const spec = load(path.join(ROOT, "A00_Common", "design", "design_structure_spec.ts"));
|
||||
const { STRUCTURE_SPECS } = load(
|
||||
path.join(ROOT, "A00_Common", "design", "design_structure_pipe.ts"),
|
||||
);
|
||||
const input = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
||||
const clone = (v) => JSON.parse(JSON.stringify(v));
|
||||
const out = { cases: {} };
|
||||
|
||||
for (const [name, [doc, before]] of Object.entries(input.cases))
|
||||
out.cases[name] = check.catalogProblems(doc, before);
|
||||
|
||||
// 반영 손질 — 목록 + enabled 짝 · 처음 값이면 지움 · 형상칸 · 보임 · 부품 쓰는 종류
|
||||
const def = clone(input.definition);
|
||||
const enabled = (id) => def.types.find((t) => t.type_id === id).enabled;
|
||||
const own = (id) => clone(reflect.reflectOf(def)[id] ?? null);
|
||||
reflect.setReflect(def, "wildlife_path", "목록", "both");
|
||||
reflect.setReflect(def, "retaining_wall", "목록", "b06");
|
||||
reflect.setReflect(def, "open_ditch", "횡단도", "none");
|
||||
reflect.setReflect(def, "cross_drain_exposed", "횡단도", "anchor");
|
||||
const setBefore = reflect.isSet(def, "cross_drain_exposed", "횡단도");
|
||||
reflect.setShapeOpt(def, "soil_guard", "weep", true);
|
||||
const weepOn = own("soil_guard");
|
||||
reflect.setShapeOpt(def, "soil_guard", "weep", false);
|
||||
reflect.setShown(def, "pipe", ["inlet_type", "없는키"], false);
|
||||
out.reflect = {
|
||||
wildlife: [own("wildlife_path"), enabled("wildlife_path")],
|
||||
wall: [own("retaining_wall"), enabled("retaining_wall")],
|
||||
openDitch: own("open_ditch"),
|
||||
exposed: [own("cross_drain_exposed"), setBefore],
|
||||
weepOn,
|
||||
weepOff: own("soil_guard"),
|
||||
shown: clone(def.표시.pipe),
|
||||
readPipe: [...reflect.readKeys(def, "pipe")].sort(),
|
||||
wallUsers: reflect.usersOf(def, "형상", "wall").map((row) => row.typeId),
|
||||
};
|
||||
reflect.resetRow(def, "cross_drain_exposed");
|
||||
reflect.resetRow(def, "retaining_wall");
|
||||
out.reflect.reset = [own("cross_drain_exposed"), own("retaining_wall"), enabled("retaining_wall")];
|
||||
// 저장 전 목록 + enabled 함께 — 목록은 그대로 · enabled = both 일 때만 참
|
||||
const plain = clone(input.definition);
|
||||
const lists = (d) => [...reflect.rowsOf(d).values()].map((row) => `${row.typeId}:${row.list}`);
|
||||
const listBefore = lists(plain);
|
||||
reflect.syncEnabled(plain);
|
||||
out.sync = {
|
||||
same: JSON.stringify(lists(plain)) === JSON.stringify(listBefore),
|
||||
enabled: plain.types.filter((t) => t.enabled).map((t) => t.type_id),
|
||||
};
|
||||
reflect.setPartOn(plain, "형상.wall", false);
|
||||
out.partOff = [clone(plain.부품), reflect.rowsOf(plain).get("retaining_wall").shape];
|
||||
reflect.setPartOn(plain, "형상.wall", true);
|
||||
out.partOn = clone(plain.부품);
|
||||
|
||||
// 명세 폼 숨김 — 저장 · 갈래 키가 모두 숨김인 칸 · 키 있는 칸이 모두 숨긴 상자
|
||||
out.spec = {};
|
||||
for (const [tid, sp] of Object.entries(STRUCTURE_SPECS)) {
|
||||
const keys = sp.containers.flatMap((c) => c.items.flatMap((i) => spec.specItemKeys(i)));
|
||||
const hidden = new Set(keys.slice(0, 1));
|
||||
const all = new Set(keys);
|
||||
out.spec[tid] = {
|
||||
first: keys[0] ?? null,
|
||||
hiddenItems: sp.containers.flatMap((c) =>
|
||||
c.items.filter((i) => spec.specItemHidden(i, hidden)).map((i) => `${c.id}.${i.id}`),
|
||||
),
|
||||
allBoxes: sp.containers.filter((c) => spec.specContainerHidden(c, all)).map((c) => c.id),
|
||||
keyedBoxes: sp.containers
|
||||
.filter((c) => c.items.some((i) => spec.specItemKeys(i).length))
|
||||
.map((c) => c.id),
|
||||
noneHidden: sp.containers.filter((c) => spec.specContainerHidden(c, new Set())).length,
|
||||
};
|
||||
}
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -182,7 +182,7 @@ def _run(work: Path) -> dict:
|
||||
for index, sample in enumerate(samples):
|
||||
key = f"{sample['type_id']}/{sample['case']}"
|
||||
rows[key] = {**ts["samples"][key], **_python_row(sample, work / f"p{index}", plain)}
|
||||
return {"samples": rows, "pick": ts["pick"], "version": ts["version"]}
|
||||
return {"samples": rows, "pick": ts["pick"], "version": ts["version"], "notice": ts["notice"]}
|
||||
|
||||
|
||||
def test_type_cache_follows_definition_version(result: dict) -> None:
|
||||
@@ -196,6 +196,17 @@ def test_type_cache_follows_definition_version(result: dict) -> None:
|
||||
}
|
||||
|
||||
|
||||
def test_stale_definition_notice_once(result: dict) -> None:
|
||||
"""322-2 판 알림 — structures.json 판 ≠ 지금 판이면 한 줄 · 탭 수명 동안 한 번 · 같은 판 · 판 없음은 없음."""
|
||||
now = load_definition()["판"] + 1 # 시험 서버가 판을 하나 올린 뒤
|
||||
assert result["notice"] == [
|
||||
f"구조물 정의가 판 {now} 으로 바뀜 — 저장하면 반영",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
]
|
||||
|
||||
|
||||
def _cells(table: dict) -> dict[tuple[str, str, str], object]:
|
||||
return {
|
||||
(part, key, column): value
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
"""PLAN 322-2 구조물 표 화면 — 검사 짝(화면 = 서버) · 반영 손질 · [저장] 길 · 명세 폼 숨김.
|
||||
|
||||
- 거울: 검사 견본마다 TS `catalogProblems` = 파이썬 `catalog_errors`(글까지 같음).
|
||||
- 1-3 규칙: 반영 · 부품 꼴 · 형상 none → 횡단도 none · 부품 자리꼴 · 정본 · 부품이 읽는 칸 새로 숨김 금지 ·
|
||||
만든 뒤 고정(자리꼴 · 정본 · 군 글자 · 종류 지우기 · 선택지 값).
|
||||
- 반영 손질: 처음 값이면 반영에서 지움 · 목록 + enabled(both 일 때만 참) · 형상칸 · 보임 · 부품 켬끔.
|
||||
- 서버 [저장]: 반영 · 부품 · 알약 색 · enabled 를 받아 판 +1 · 표(`catalog()`)가 바로 따름 · 틀리면 422.
|
||||
- 명세 폼 숨김: 칸 = 쓰는 옵션 키가 모두 숨김 · 상자 = 키 있는 칸이 모두 숨김(지금 정의로는 숨는 칸 없음).
|
||||
TS 쪽 = `helper_322_2_check.cjs`(Node) · 마스터 정의는 읽기만(사본으로 고침).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import subprocess
|
||||
import tempfile
|
||||
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 common_util.common_util_structure_catalog import catalog
|
||||
from M02_MasterTemplete.StructOptions.M02_SO_Check import catalog_errors
|
||||
from M02_MasterTemplete.StructOptions.M02_SO_Router import read_router
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
ROOT = HERE.parent.parent
|
||||
URL = "/api/m02/structure-definition"
|
||||
ADMIN = {"role": "SYSTEM_ADMIN", "user_id": 1}
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules").is_dir(), reason="프론트엔드 의존성 미설치"
|
||||
)
|
||||
|
||||
|
||||
def _real() -> dict:
|
||||
return json.loads(Path(schema.DEFINITION_PATH).read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _type(doc: dict, type_id: str) -> dict:
|
||||
return next(t for t in doc["types"] if t["type_id"] == type_id)
|
||||
|
||||
|
||||
def _cases() -> dict[str, tuple[dict, dict | None]]:
|
||||
real = _real()
|
||||
cases: dict[str, tuple[dict, dict | None]] = {"real": (real, real)}
|
||||
bad = copy.deepcopy(real)
|
||||
bad["반영"] = {
|
||||
"nope": {},
|
||||
"pipe": {
|
||||
"모름": 1,
|
||||
"목록": "x",
|
||||
"횡단도": "zz",
|
||||
"형상": 3,
|
||||
"형상칸": {"form": " "},
|
||||
"3D": "none",
|
||||
},
|
||||
}
|
||||
bad["부품"] = {"형상.zz": False, "횡단도.span3": "no", "끔": False}
|
||||
cases["shape_bad"] = (bad, real)
|
||||
rule = copy.deepcopy(real)
|
||||
rule["반영"] = {"open_ditch": {"횡단도": "anchor"}}
|
||||
cases["rule218"] = (rule, real)
|
||||
mismatch = copy.deepcopy(real)
|
||||
mismatch["반영"] = {"retaining_wall": {"형상": "pipe_set"}, "berm": {"횡단도": "anchor"}}
|
||||
cases["mismatch"] = (mismatch, real)
|
||||
hide = copy.deepcopy(real)
|
||||
hide["표시"]["pipe"]["revet_foundation"] = False # 관 세트가 읽는 기본값 없는 칸 새로 숨김
|
||||
hide["표시"]["retaining_wall"]["height_m"] = False # 기본값 있는 칸 = 숨겨도 됨(늘 기본값)
|
||||
cases["hide_read"] = (hide, real)
|
||||
cases["hide_no_before"] = (hide, None)
|
||||
fixed = copy.deepcopy(real)
|
||||
_type(fixed, "box_culvert")["placement"] = "interval"
|
||||
_type(fixed, "revetment")["managed_by"] = None
|
||||
_type(fixed, "mirror")["group"] = "F"
|
||||
dia = next(o for o in _type(fixed, "pipe")["options"] if o["key"] == "pipe_diameter_mm")
|
||||
dia["choices"] = [*dia["choices"], "2000"]
|
||||
fixed["types"] = [t for t in fixed["types"] if t["type_id"] != "etc"]
|
||||
cases["fixed"] = (fixed, real)
|
||||
off = copy.deepcopy(real)
|
||||
off["부품"] = {"형상.wall": False}
|
||||
cases["parts_off"] = (off, real)
|
||||
return cases
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ts() -> dict:
|
||||
payload = {"cases": _cases(), "definition": _real()}
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path = Path(folder) / "input.json"
|
||||
path.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
|
||||
out = subprocess.run(
|
||||
["node", str(HERE / "helper_322_2_check.cjs"), str(path)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
check=True,
|
||||
)
|
||||
return json.loads(out.stdout)
|
||||
|
||||
|
||||
def test_check_mirror_screen_equals_server(ts: dict) -> None:
|
||||
for name, (doc, before) in _cases().items():
|
||||
assert catalog_errors(doc, before) == ts["cases"][name], name
|
||||
|
||||
|
||||
def test_check_rules(ts: dict) -> None:
|
||||
got = ts["cases"]
|
||||
assert got["real"] == []
|
||||
assert got["shape_bad"] == [
|
||||
"반영 「nope」 — 없는 종류이거나 묶음 아님",
|
||||
"반영 「pipe.모름」 — 모르는 칸",
|
||||
"반영 「pipe.목록」 — both · b06 · hidden 중 하나",
|
||||
"반영 「pipe.횡단도」 — 없는 부품 「zz」",
|
||||
"반영 「pipe.형상」 — 없는 부품 「3」",
|
||||
"반영 「pipe.형상칸」 — form 글 · weep 참거짓",
|
||||
"부품 「형상.zz」 — 없는 부품",
|
||||
"부품 「횡단도.span3」 — 참거짓",
|
||||
"부품 「끔」 — 없는 부품",
|
||||
]
|
||||
assert got["rule218"] == ["「개거(겉도랑)」 형상 none — 횡단도도 none 이어야 함"]
|
||||
assert got["mismatch"] == [
|
||||
"「옹벽」 형상 「pipe_set」 — 자리꼴 interval · 정본 structures 에 안 맞음"
|
||||
]
|
||||
assert got["hide_read"] == [
|
||||
"「배관」 형상 「pipe_set」 이 읽는 칸 「revet_foundation」 — 숨길 수 없음"
|
||||
]
|
||||
assert got["hide_no_before"] == [] # 앞 판 없음 = 새로 숨긴 것 없음
|
||||
assert got["fixed"] == [
|
||||
# 표 줄도 함께 걸림 — 자리꼴 · 정본을 바꾸면 그 종류 부품이 안 맞음
|
||||
"「BOX암거」 형상 「box」 — 자리꼴 interval · 정본 pipe_points 에 안 맞음",
|
||||
"「BOX암거」 3D 「box」 — 자리꼴 interval · 정본 pipe_points 에 안 맞음",
|
||||
"「기슭막이」 형상 「hidden_revet」 — 자리꼴 interval · 정본 structures 에 안 맞음",
|
||||
"「pipe.pipe_diameter_mm」 선택지 값 — 고칠 수 없음(이름은 이름 표로)",
|
||||
"「box_culvert」 자리꼴 — 만든 뒤 고정",
|
||||
"「revetment」 정본 — 만든 뒤 고정",
|
||||
"「mirror」 군 글자 — 만든 뒤 고정",
|
||||
"종류 「etc」 — 지울 수 없음",
|
||||
]
|
||||
# 형상 부품을 끄면 벽 다섯은 형상 none — 횡단도도 none 으로 바꿔야 저장됨
|
||||
assert len(got["parts_off"]) == 5 and all("횡단도도 none" in t for t in got["parts_off"])
|
||||
|
||||
|
||||
def test_reflect_edits(ts: dict) -> None:
|
||||
r = ts["reflect"]
|
||||
assert r["wildlife"] == [None, True] # b06 → both = enabled 참이 곧 처음 값 짝(반영 안 남음)
|
||||
assert r["wall"] == [None, False] # both → b06 = enabled 거짓
|
||||
assert r["openDitch"] is None # 처음 값(none)과 같음 — 반영 안 남음
|
||||
assert r["exposed"] == [{"횡단도": "anchor"}, True]
|
||||
assert r["weepOn"] == {"형상칸": {"weep": True}} and r["weepOff"] is None
|
||||
assert r["shown"] == {"pipe_kind": False, "inlet_type": False} # 정의 옵션만
|
||||
assert r["readPipe"] == ["inlet_basin_height_m", "revet_foundation"] # 기본값 없는 읽는 칸
|
||||
assert r["wallUsers"] == [
|
||||
"retaining_wall",
|
||||
"masonry_wet",
|
||||
"masonry_dry",
|
||||
"soil_guard",
|
||||
"boulder_masonry",
|
||||
]
|
||||
assert r["reset"] == [None, None, True]
|
||||
|
||||
|
||||
def test_save_writes_list_and_enabled_together(ts: dict) -> None:
|
||||
sync = ts["sync"]
|
||||
assert sync["same"] is True
|
||||
rows = catalog()
|
||||
assert set(sync["enabled"]) == {t for t, row in rows.items() if row["list"] == "both"}
|
||||
assert len(sync["enabled"]) == 23 # B05 목록 23(브레인 화면 확인)
|
||||
assert ts["partOff"] == [{"형상.wall": False}, "none"] and ts["partOn"] == {}
|
||||
|
||||
|
||||
def test_spec_form_hidden_rule(ts: dict) -> None:
|
||||
spec = ts["spec"]
|
||||
assert spec["pipe"]["hiddenItems"] == ["pipe.diameter"]
|
||||
assert spec["box_culvert"]["allBoxes"] == spec["box_culvert"]["keyedBoxes"]
|
||||
assert all(s["noneHidden"] == 0 for s in spec.values())
|
||||
assert spec["etc"]["keyedBoxes"] == [] # 선택 상자 인스턴스 키는 옵션 밖 — 숨김 대상 아님
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def copy_def(tmp_path: Path, monkeypatch: pytest.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() -> TestClient:
|
||||
app = FastAPI()
|
||||
app.include_router(read_router)
|
||||
app.dependency_overrides[verify_session] = lambda: ADMIN
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def test_server_save_reflect_parts_color_enabled(copy_def: Path) -> None:
|
||||
client = _client()
|
||||
doc = client.get(URL).json()
|
||||
sent = copy.deepcopy(doc)
|
||||
sent["반영"] = {"open_ditch": {"횡단도": "anchor", "형상": "underdrain", "목록": "b06"}}
|
||||
sent["부품"] = {"3D.chute": False}
|
||||
_type(sent, "mirror")["style"]["color"] = "#123456"
|
||||
_type(sent, "masonry_wet")["enabled"] = False # 목록 hidden ⇒ enabled 거짓(화면이 함께 씀)
|
||||
res = client.put(URL, json={"판": doc["판"], "정의": sent})
|
||||
assert res.status_code == 200, res.text
|
||||
got = res.json()
|
||||
assert got["판"] == doc["판"] + 1
|
||||
assert got["반영"] == sent["반영"] and got["부품"] == sent["부품"]
|
||||
rows = catalog() # 파일 시각으로 바로 새 표
|
||||
assert (rows["open_ditch"]["station"], rows["open_ditch"]["list"]) == ("anchor", "b06")
|
||||
assert rows["ford_pavement"]["model3d"] == "none"
|
||||
assert rows["mirror"]["color"] == "#123456"
|
||||
assert rows["masonry_wet"]["list"] == "hidden"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"change",
|
||||
[
|
||||
lambda d: d.update(반영={"open_ditch": {"횡단도": "anchor"}}), # 218
|
||||
lambda d: d["표시"].setdefault("pipe", {}).update(revet_foundation=False), # 못 숨김 칸
|
||||
lambda d: next(o for o in _type(d, "pipe")["options"] if o["key"] == "pipe_kind")[
|
||||
"choices"
|
||||
].append("새관"), # 선택지 값
|
||||
lambda d: _type(d, "berm").update(placement="point"), # 자리꼴 고정
|
||||
],
|
||||
)
|
||||
def test_server_rejects_rule_breaks(copy_def: Path, change) -> None:
|
||||
client = _client()
|
||||
doc = client.get(URL).json()
|
||||
sent = copy.deepcopy(doc)
|
||||
change(sent)
|
||||
res = client.put(URL, json={"판": doc["판"], "정의": sent})
|
||||
assert res.status_code == 422, res.text
|
||||
assert json.loads(copy_def.read_text("utf-8"))["판"] == doc["판"]
|
||||
@@ -47,7 +47,8 @@ def _screen_edit(doc: dict) -> dict:
|
||||
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"]]
|
||||
# 선택지 값은 만든 뒤 고정(322-2) — 이름은 이름 표(`종류.pipe.이름`)로
|
||||
doc["종류"].setdefault("pipe", {}).setdefault("이름", {})["pipe.kind=흄관"] = "흄관(고침)"
|
||||
pipe["options"].append(
|
||||
{
|
||||
"key": "opt_ab12cd",
|
||||
@@ -62,7 +63,7 @@ def _screen_edit(doc: dict) -> dict:
|
||||
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"] = {"wing_wall_type": False} # 부품이 안 읽는 칸(322-2 읽는 칸은 못 숨김)
|
||||
doc["그룹"]["pipe"][1]["형상"] = {
|
||||
"길이": {"받기": True, "뜻": "유입 보호공 연장"},
|
||||
"높이": {"받기": False, "뜻": ""},
|
||||
@@ -70,7 +71,7 @@ def _screen_edit(doc: dict) -> dict:
|
||||
doc["규칙"] = [
|
||||
{
|
||||
"종류": "pipe",
|
||||
"만약": {"옵션": "pipe_kind", "값들": ["흄관(고침)"]},
|
||||
"만약": {"옵션": "pipe_kind", "값들": ["흄관"]},
|
||||
"그러면": {"옵션": "opt_ab12cd", "선택지": ["새 선택지 2"], "기본값": "새 선택지 2"},
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user