Merge remote-tracking branch 'origin/sub_laptop_7' into sub_laptop_1
# Conflicts: # M02_MasterTemplete/StructOptions/M02_SO_Api.ts # M02_MasterTemplete/StructOptions/M02_SO_Options.ts
This commit is contained in:
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"pipe": [
|
||||
{
|
||||
"id": "grv6lny",
|
||||
"이름": "관",
|
||||
"옵션": [
|
||||
"pipe_kind",
|
||||
"pipe_diameter_mm",
|
||||
"inlet_type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "gnbf9m9",
|
||||
"이름": "관 보호공(유입)",
|
||||
"옵션": []
|
||||
},
|
||||
{
|
||||
"id": "g9lq9uk",
|
||||
"이름": "관 보호공(유출)",
|
||||
"옵션": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -192,10 +192,11 @@ def test_placement_mismatching_registry_is_rejected(project_root):
|
||||
save_structures(project_root, [wall_as_point], base_revision=0)
|
||||
|
||||
|
||||
def test_undefined_option_key_is_rejected(project_root):
|
||||
bad = _point(options={"form": "돌", "no_such_option": 1})
|
||||
with pytest.raises(ValueError, match="옵션"):
|
||||
save_structures(project_root, [bad], base_revision=0)
|
||||
def test_undefined_option_key_is_kept(project_root):
|
||||
"""지금 정의에 없는 키 = 지난 정의 값 — 오류 없이 그대로 남김(PLAN 48-1)."""
|
||||
old = _point(options={"form": "돌", "no_such_option": 1})
|
||||
assert save_structures(project_root, [old], base_revision=0) == 1
|
||||
assert load_structures(project_root)[1][0].options["no_such_option"] == 1
|
||||
|
||||
|
||||
def test_negative_number_option_is_rejected(project_root):
|
||||
|
||||
@@ -18,7 +18,7 @@ ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
def _box_options() -> dict[str, dict]:
|
||||
registry = json.loads(
|
||||
(ROOT / "B05_Profile" / "B05_Profile_Structure_Types.json").read_text(encoding="utf-8")
|
||||
(ROOT / "resources" / "master_template" / "구조물정의.json").read_text(encoding="utf-8")
|
||||
)
|
||||
types = registry if isinstance(registry, list) else registry.get("types") or []
|
||||
box = next(t for t in types if t["type_id"] == "box_culvert")
|
||||
|
||||
@@ -32,7 +32,7 @@ QUANTITY_ONLY = {
|
||||
|
||||
def _pipe_option_keys() -> list[str]:
|
||||
payload = json.loads(
|
||||
(ROOT / "B05_Profile" / "B05_Profile_Structure_Types.json").read_text(encoding="utf-8")
|
||||
(ROOT / "resources" / "master_template" / "구조물정의.json").read_text(encoding="utf-8")
|
||||
)
|
||||
types = payload if isinstance(payload, list) else payload["types"]
|
||||
pipe = next(item for item in types if item["type_id"] == "pipe")
|
||||
|
||||
@@ -20,7 +20,7 @@ from B08_Quantity.B08_Quantity_Engine_UnitQuantity import expand, stone_masonry
|
||||
from common_util.common_util_structure_face_role import structure_face_role_of # noqa: E402
|
||||
|
||||
REGISTRY = json.loads(
|
||||
(ROOT / "B05_Profile" / "B05_Profile_Structure_Types.json").read_text(encoding="utf-8")
|
||||
(ROOT / "resources" / "master_template" / "구조물정의.json").read_text(encoding="utf-8")
|
||||
)
|
||||
FIELDS = (ROOT / "B05_Profile" / "B05_Profile_UI_Structures_Fields.ts").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ KNOWN_ABSENT: dict[str, str] = {}
|
||||
|
||||
def _registry_keys() -> set[str]:
|
||||
payload = json.loads(
|
||||
(ROOT / "B05_Profile" / "B05_Profile_Structure_Types.json").read_text(encoding="utf-8")
|
||||
(ROOT / "resources" / "master_template" / "구조물정의.json").read_text(encoding="utf-8")
|
||||
)
|
||||
types = payload if isinstance(payload, list) else (payload.get("types") or [])
|
||||
return {option["key"] for item in types for option in item.get("options") or []}
|
||||
|
||||
@@ -90,7 +90,7 @@ def test_사방_종류_이름이_레지스트리와_맞을것() -> None:
|
||||
import json
|
||||
|
||||
registry = json.loads(
|
||||
(ROOT / "B05_Profile" / "B05_Profile_Structure_Types.json").read_text(encoding="utf-8")
|
||||
(ROOT / "resources" / "master_template" / "구조물정의.json").read_text(encoding="utf-8")
|
||||
)
|
||||
types = registry if isinstance(registry, list) else (registry.get("types") or [])
|
||||
known = {t["type_id"] for t in types}
|
||||
|
||||
@@ -68,7 +68,7 @@ def test_바닥막이는_제곱미터당() -> None:
|
||||
|
||||
def test_개거_L형수로는_등록부_글자로_선다() -> None:
|
||||
registry = json.loads(
|
||||
(ROOT / "B05_Profile" / "B05_Profile_Structure_Types.json").read_text(encoding="utf-8")
|
||||
(ROOT / "resources" / "master_template" / "구조물정의.json").read_text(encoding="utf-8")
|
||||
)
|
||||
choices = next(
|
||||
option["choices"]
|
||||
|
||||
@@ -29,7 +29,7 @@ from B08_Quantity.B08_Quantity_Engine_UnitQuantity import ( # noqa: E402
|
||||
|
||||
def _registry_choices() -> list[str]:
|
||||
payload = json.loads(
|
||||
(ROOT / "B05_Profile" / "B05_Profile_Structure_Types.json").read_text(encoding="utf-8")
|
||||
(ROOT / "resources" / "master_template" / "구조물정의.json").read_text(encoding="utf-8")
|
||||
)
|
||||
types = payload if isinstance(payload, list) else payload["types"]
|
||||
pipe = next(item for item in types if item["type_id"] == "pipe")
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
"""M02 구조물 정의 한 벌(`/api/m02/structure-definition` · PLAN 48-1).
|
||||
|
||||
못박는 것
|
||||
- 판 1 = 옛 레지스트리 그대로 옮김(37종 · 옵션 235 · 키 그대로) · 검사 통과.
|
||||
- 읽기 = 로그인 누구나 · 쓰기 = 시스템 관리자만(403).
|
||||
- 저장마다 판 +1 · 앞 판은 `구조물정의_이력/판_N.json` · 낡은 판 409 · 틀린 정의 422(안 씀).
|
||||
- 검사 = 키 겹침 · 꼴 · 규칙 대상 있음 · 그룹 형상.
|
||||
- 저장한 정의를 `/structure-types` 가 바로 읽음 · 프로젝트 저장에 정의 판 · 없는 키 값 보존.
|
||||
"""
|
||||
|
||||
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 B05_Profile.B05_Profile_Structures_Repository import load_structures, save_structures
|
||||
from B05_Profile.B05_Profile_Structures_Router import router as b05_router
|
||||
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance, load_structure_types
|
||||
from common_util.common_util_auth import verify_session
|
||||
from M02_MasterTemplete.StructOptions import M02_SO_Definition as definition
|
||||
from M02_MasterTemplete.StructOptions.M02_SO_Router import read_router
|
||||
|
||||
URL = "/api/m02/structure-definition"
|
||||
TYPES = "/api/projects/structure-types"
|
||||
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.include_router(b05_router)
|
||||
app.dependency_overrides[verify_session] = lambda: session
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def test_판1_은_옛_레지스트리_그대로_검사_통과() -> None:
|
||||
doc = definition.read()
|
||||
assert doc["판"] == 1 and len(doc["types"]) == 37
|
||||
assert sum(len(t["options"]) for t in doc["types"]) == 235
|
||||
assert len(load_structure_types()) == 37
|
||||
assert sum(len(t.options) for t in load_structure_types()) == 235
|
||||
assert definition.check(doc) == []
|
||||
assert [g["이름"] for g in doc["그룹"]["pipe"]] == ["관", "관 보호공(유입)", "관 보호공(유출)"]
|
||||
assert {t["group"] for t in doc["types"]} <= set(doc["구조물군"])
|
||||
|
||||
|
||||
def test_읽기는_누구나_쓰기는_시스템_관리자만(copy) -> None:
|
||||
user = _client(USER)
|
||||
got = user.get(URL)
|
||||
assert got.status_code == 200 and got.json()["판"] == 1
|
||||
assert user.put(URL, json={"판": 1, "정의": got.json()}).status_code == 403
|
||||
assert json.loads(copy.read_text("utf-8"))["판"] == 1 # 안 씀
|
||||
|
||||
|
||||
def test_저장마다_판_더하기_1_앞_판은_이력_structure_types_가_바로_읽음(copy) -> None:
|
||||
admin = _client(ADMIN)
|
||||
doc = admin.get(URL).json()
|
||||
tid = doc["types"][0]["type_id"]
|
||||
doc["types"][0]["name"] = "이름 바꿈"
|
||||
res = admin.put(URL, json={"판": 1, "정의": doc})
|
||||
assert res.status_code == 200, res.text
|
||||
assert res.json()["판"] == 2
|
||||
old = json.loads((copy.parent / "구조물정의_이력" / "판_1.json").read_text("utf-8"))
|
||||
assert old["판"] == 1 and old["types"][0]["name"] != "이름 바꿈"
|
||||
seen = {t["type_id"]: t for t in admin.get(TYPES).json()["types"]}
|
||||
assert seen[tid]["name"] == "이름 바꿈" and len(seen) == 37
|
||||
assert admin.put(URL, json={"판": 1, "정의": doc}).status_code == 409 # 낡은 판
|
||||
assert admin.put(URL, json={"판": 2, "정의": res.json()}).json()["판"] == 3
|
||||
assert (copy.parent / "구조물정의_이력" / "판_2.json").is_file()
|
||||
|
||||
|
||||
def _rule(doc: dict) -> tuple[str, dict, dict]:
|
||||
"""고르기 옵션 둘 이상 가진 종류 — (종류 키, 상위 옵션, 하위 옵션)."""
|
||||
for t in doc["types"]:
|
||||
picks = [o for o in t["options"] if o["input"] == "select"]
|
||||
if len(picks) >= 2:
|
||||
return t["type_id"], picks[0], picks[1]
|
||||
raise AssertionError("고르기 둘인 종류 없음")
|
||||
|
||||
|
||||
def test_검사_키_겹침_꼴_규칙_대상_형상(copy) -> None:
|
||||
admin = _client(ADMIN)
|
||||
base = admin.get(URL).json()
|
||||
tid, up, down = _rule(base)
|
||||
good = {
|
||||
"종류": tid,
|
||||
"만약": {"옵션": up["key"], "값들": [up["choices"][0]]},
|
||||
"그러면": {"옵션": down["key"], "선택지": down["choices"][:1], "보이기": True},
|
||||
}
|
||||
assert definition.check({**base, "규칙": [good]}) == []
|
||||
|
||||
def bad(**change) -> dict:
|
||||
return json.loads(json.dumps({**base, **change}))
|
||||
|
||||
dup = bad()
|
||||
dup["types"].append(dup["types"][0]) # 종류 키 겹침
|
||||
opt_dup = bad()
|
||||
opt_dup["types"][0]["options"].append(opt_dup["types"][0]["options"][0]) # 옵션 키 겹침
|
||||
shape = bad()
|
||||
shape["types"][0]["options"][0]["input"] = "날짜" # 없는 꼴
|
||||
cases = [
|
||||
dup,
|
||||
opt_dup,
|
||||
shape,
|
||||
bad(규칙=[{**good, "그러면": {**good["그러면"], "옵션": "없는키"}}]), # 대상 없음
|
||||
bad(규칙=[{**good, "종류": "없는종류"}]),
|
||||
bad(규칙=[{**good, "만약": {"옵션": up["key"], "값들": ["없는값"]}}]),
|
||||
bad(규칙=[{**good, "그러면": {"옵션": down["key"], "선택지": ["없는값"]}}]),
|
||||
bad(규칙=[{**good, "그러면": {"옵션": down["key"]}}]), # 효과 없음
|
||||
bad(배치={tid: {up["key"]: {"줄": 0, "칸": 1}}}),
|
||||
bad(배치={tid: {"없는키": {"줄": 1}}}),
|
||||
bad(종류={tid: {"동작": ["없는동작"]}}),
|
||||
bad(종류={"없는종류": {"기준측점": False}}),
|
||||
bad(표시={tid: {"없는키": False}}),
|
||||
bad(
|
||||
그룹={"pipe": [{"id": "g1", "이름": "가", "옵션": [], "형상": {"폭": {"받기": True}}}]}
|
||||
),
|
||||
bad(모르는칸=1),
|
||||
]
|
||||
for i, doc in enumerate(cases):
|
||||
res = admin.put(URL, json={"판": 1, "정의": doc})
|
||||
assert res.status_code == 422, (i, res.text)
|
||||
assert json.loads(copy.read_text("utf-8"))["판"] == 1 # 거절되면 안 씀
|
||||
assert not (copy.parent / "구조물정의_이력").exists()
|
||||
shaped = bad(종류={tid: {"기준측점": False}})
|
||||
shaped["그룹"]["pipe"][0]["형상"] = {"길이": {"받기": True, "뜻": "관 길이"}}
|
||||
assert admin.put(URL, json={"판": 1, "정의": shaped}).status_code == 200
|
||||
|
||||
|
||||
def test_프로젝트_저장에_정의_판_없는_키_값은_남김(copy, tmp_path) -> None:
|
||||
root = str(tmp_path / "project")
|
||||
item = StructureInstance.model_validate(
|
||||
{
|
||||
"type_id": "mirror",
|
||||
"placement": "point",
|
||||
"chainage_m": 10.0,
|
||||
"options": {"지난정의키": "옛값"},
|
||||
}
|
||||
)
|
||||
assert save_structures(root, [item], base_revision=0) == 1
|
||||
raw = json.loads(Path(root, "B05_Profile", "route", "structures.json").read_text("utf-8"))
|
||||
assert raw["definition_version"] == 1
|
||||
assert load_structures(root)[1][0].options["지난정의키"] == "옛값"
|
||||
@@ -1,12 +1,11 @@
|
||||
"""M02 구조물 옵션 그룹(`/api/m02/structure-option-groups` · `구조물옵션그룹.json` · PLAN 46-2).
|
||||
"""M02 구조물 옵션 그룹(`/api/m02/structure-option-groups` · 정의 「그룹」 칸 · PLAN 46-2).
|
||||
|
||||
못박는 것
|
||||
- 시드 = `{}` · 파일이 없으면 그룹 없음 · `/structure-types` 종류마다 `groups: []`.
|
||||
- 「그룹」 이 `{}` 면 그룹 없음 · `/structure-types` 종류마다 `groups: []`.
|
||||
- 저장한 그룹이 `/structure-types` `groups` 에 그대로(차례 · 이름 · 옵션) · 빈 그룹도 나감.
|
||||
- 옵션 없는 종류도 그룹을 둘 수 있음(관리 목록 `종류` 는 레지스트리 전부).
|
||||
- 없는 종류 · 키 · id 틀림 · 겹침 · 이름 빔 · 겹침 · 옵션 두 그룹은 422 · 낡은 판 409 ·
|
||||
거절되면 안 씀.
|
||||
- 파일이 깨져도 폼 응답은 그룹 없음(죽지 않음) · 관리 화면은 500.
|
||||
- 좌측 「구조물 옵션 보이기」 는 시스템 관리자만(화면 코드).
|
||||
"""
|
||||
|
||||
@@ -19,10 +18,10 @@ import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from B05_Profile import B05_Profile_Structures_Schema as schema
|
||||
from B05_Profile.B05_Profile_Structures_Router import router as b05_router
|
||||
from B05_Profile.B05_Profile_Structures_Schema import load_structure_types
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete.StructOptions import M02_SO_Groups as groups
|
||||
from M02_MasterTemplete.StructOptions.M02_SO_Router import router as m02_router
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
@@ -32,7 +31,12 @@ TYPES = "/api/projects/structure-types"
|
||||
|
||||
@pytest.fixture
|
||||
def client(tmp_path: Path, monkeypatch) -> TestClient:
|
||||
"""정의 사본(표시 · 그룹 빈 칸 · 판 1)으로 — 마스터 정본은 안 씀(PLAN 48-1)."""
|
||||
monkeypatch.setattr(store, "FOLDER", tmp_path)
|
||||
doc = json.loads(Path(schema.DEFINITION_PATH).read_text("utf-8"))
|
||||
copy = tmp_path / "구조물정의.json"
|
||||
copy.write_text(json.dumps({**doc, "판": 1, "표시": {}, "그룹": {}}), "utf-8")
|
||||
monkeypatch.setattr(schema, "DEFINITION_PATH", str(copy))
|
||||
app = FastAPI()
|
||||
app.include_router(m02_router)
|
||||
app.include_router(b05_router)
|
||||
@@ -48,11 +52,9 @@ def _groups(client: TestClient) -> dict[str, list[dict]]:
|
||||
return {t["type_id"]: t["groups"] for t in client.get(TYPES).json()["types"]}
|
||||
|
||||
|
||||
def test_시드는_빈_표_없으면_그룹_없음(client) -> None:
|
||||
seed = ROOT / "resources/master_template/구조물옵션그룹.json"
|
||||
assert json.loads(seed.read_text("utf-8")) == {}
|
||||
def test_빈_그룹_칸은_그룹_없음(client) -> None:
|
||||
got = client.get(URL).json()
|
||||
assert got["판"] == "" and got["그룹"] == {}
|
||||
assert got["판"] == "1" and got["그룹"] == {}
|
||||
assert len(got["종류"]) == len(load_structure_types()) # 옵션 없는 종류도
|
||||
assert all(v == [] for v in _groups(client).values())
|
||||
|
||||
@@ -67,7 +69,7 @@ def test_저장한_그룹이_폼_응답에_차례대로_빈_그룹도(client) ->
|
||||
],
|
||||
empty: [{"id": "gx", "이름": "부속", "옵션": []}],
|
||||
}
|
||||
res = client.put(URL, json={"판": "", "그룹": body})
|
||||
res = client.put(URL, json={"판": "1", "그룹": body})
|
||||
assert res.status_code == 200, res.text
|
||||
assert res.json()["그룹"][tid][1]["이름"] == "빈 그룹" # 앞뒤 빈칸 지움
|
||||
seen = _groups(client)
|
||||
@@ -92,20 +94,14 @@ def test_틀린_그룹은_422_낡은_판은_409(client) -> None:
|
||||
{tid: [ok, {"id": "g2", "이름": "나", "옵션": [a]}]}, # 옵션 두 그룹
|
||||
{tid: {"g1": ok}},
|
||||
):
|
||||
assert client.put(URL, json={"판": "", "그룹": bad}).status_code == 422, bad
|
||||
assert not (store.FOLDER / groups.FILE).exists() # 거절되면 안 씀
|
||||
v1 = client.put(URL, json={"판": "", "그룹": {tid: [ok]}}).json()["판"]
|
||||
assert client.put(URL, json={"판": "1", "그룹": bad}).status_code == 422, bad
|
||||
assert json.loads(Path(schema.DEFINITION_PATH).read_text("utf-8"))["판"] == 1 # 거절되면 안 씀
|
||||
v1 = client.put(URL, json={"판": "1", "그룹": {tid: [ok]}}).json()["판"]
|
||||
assert client.put(URL, json={"판": "낡음", "그룹": {}}).status_code == 409
|
||||
res = client.put(URL, json={"판": v1, "그룹": {}})
|
||||
assert res.status_code == 200 and res.json()["그룹"] == {}
|
||||
|
||||
|
||||
def test_파일이_깨져도_폼_응답은_그룹_없음(client) -> None:
|
||||
(store.FOLDER / groups.FILE).write_text("{깨짐", encoding="utf-8")
|
||||
assert all(v == [] for v in _groups(client).values())
|
||||
assert client.get(URL).status_code == 500
|
||||
|
||||
|
||||
def test_좌측_옵션_보이기는_시스템_관리자만() -> None:
|
||||
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
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"""M02 구조물 옵션 보이기(`/api/m02/structure-options` · 정본 `구조물옵션표시.json`).
|
||||
"""M02 구조물 옵션 보이기(`/api/m02/structure-options` · 정의 「표시」 칸 · PLAN 48-1).
|
||||
|
||||
못박는 것
|
||||
- 파일이 없거나 `{}` 면 `GET /api/projects/structure-types` 옵션이 모두 visible.
|
||||
- 「표시」 가 `{}` 면 `GET /api/projects/structure-types` 옵션이 모두 visible.
|
||||
- 저장한 숨김이 그 응답 visible 에 반영 · 다른 종류 · 옵션은 그대로 · 옵션 정의는 안 바뀜.
|
||||
- 저장은 숨김(false)만 남김 · [모두 보임] = `{}` · 없는 종류 · 키 · 참거짓 아님 422 · 낡은 판 409.
|
||||
- 파일이 깨져도 폼 응답은 모두 보임(죽지 않음).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -16,10 +15,10 @@ import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from B05_Profile import B05_Profile_Structures_Schema as schema
|
||||
from B05_Profile.B05_Profile_Structures_Router import router as b05_router
|
||||
from B05_Profile.B05_Profile_Structures_Schema import load_structure_types
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete.StructOptions import M02_SO_Options as opt
|
||||
from M02_MasterTemplete.StructOptions.M02_SO_Router import router as m02_router
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
@@ -33,7 +32,12 @@ def structure_type(type_id):
|
||||
|
||||
@pytest.fixture
|
||||
def client(tmp_path: Path, monkeypatch) -> TestClient:
|
||||
"""정의 사본(표시 · 그룹 빈 칸 · 판 1)으로 — 마스터 정본은 안 씀(PLAN 48-1)."""
|
||||
monkeypatch.setattr(store, "FOLDER", tmp_path)
|
||||
doc = json.loads(Path(schema.DEFINITION_PATH).read_text("utf-8"))
|
||||
copy = tmp_path / "구조물정의.json"
|
||||
copy.write_text(json.dumps({**doc, "판": 1, "표시": {}, "그룹": {}}), "utf-8")
|
||||
monkeypatch.setattr(schema, "DEFINITION_PATH", str(copy))
|
||||
app = FastAPI()
|
||||
app.include_router(m02_router)
|
||||
app.include_router(b05_router)
|
||||
@@ -51,13 +55,9 @@ def _visible(client: TestClient) -> dict[str, dict[str, bool]]:
|
||||
return {t["type_id"]: {o["key"]: o["visible"] for o in t["options"]} for t in types}
|
||||
|
||||
|
||||
def test_시드_파일은_빈_표이고_없어도_모두_보임(client) -> None:
|
||||
assert (
|
||||
json.loads((ROOT / "resources/master_template/구조물옵션표시.json").read_text("utf-8"))
|
||||
== {}
|
||||
)
|
||||
def test_빈_표시는_모두_보임(client) -> None:
|
||||
got = client.get(URL).json()
|
||||
assert got["판"] == "" and got["표시"] == {} # 파일 없음
|
||||
assert got["판"] == "1" and got["표시"] == {}
|
||||
assert all(all(v.values()) for v in _visible(client).values())
|
||||
assert client.get(TYPES).json()["schema_version"] == 1
|
||||
|
||||
@@ -65,7 +65,7 @@ def test_시드_파일은_빈_표이고_없어도_모두_보임(client) -> None:
|
||||
def test_숨김을_저장하면_폼_응답에_반영_정의는_그대로(client) -> None:
|
||||
tid, off, on = _pick()
|
||||
before = client.get(TYPES).json()["types"]
|
||||
res = client.put(URL, json={"판": "", "표시": {tid: {off: False, on: True}}})
|
||||
res = client.put(URL, json={"판": "1", "표시": {tid: {off: False, on: True}}})
|
||||
assert res.status_code == 200, res.text
|
||||
assert res.json()["표시"] == {tid: {off: False}} # 숨김만 남김
|
||||
seen = _visible(client)
|
||||
@@ -83,10 +83,10 @@ def test_숨김을_저장하면_폼_응답에_반영_정의는_그대로(client)
|
||||
|
||||
def test_모두_보임은_빈_표_판은_저장마다_바뀜(client) -> None:
|
||||
tid, off, _ = _pick()
|
||||
v1 = client.put(URL, json={"판": "", "표시": {tid: {off: False}}}).json()["판"]
|
||||
v1 = client.put(URL, json={"판": "1", "표시": {tid: {off: False}}}).json()["판"]
|
||||
res = client.put(URL, json={"판": v1, "표시": {}})
|
||||
assert res.status_code == 200 and res.json()["표시"] == {} and res.json()["판"] != v1
|
||||
assert json.loads((store.FOLDER / opt.FILE).read_text("utf-8")) == {}
|
||||
assert json.loads(Path(schema.DEFINITION_PATH).read_text("utf-8"))["표시"] == {}
|
||||
assert all(all(v.values()) for v in _visible(client).values())
|
||||
|
||||
|
||||
@@ -98,21 +98,15 @@ def test_없는_종류_키_참거짓_아님은_422_낡은_판은_409(client) ->
|
||||
{tid: {off: "no"}},
|
||||
{tid: [off]},
|
||||
):
|
||||
assert client.put(URL, json={"판": "", "표시": bad}).status_code == 422, bad
|
||||
assert client.put(URL, json={"판": "", "표시": []}).status_code == 422 # 묶음 아님(요청 모양)
|
||||
assert not (store.FOLDER / opt.FILE).exists() # 거절되면 안 씀
|
||||
v1 = client.put(URL, json={"판": "", "표시": {tid: {off: False}}}).json()["판"]
|
||||
assert client.put(URL, json={"판": "1", "표시": bad}).status_code == 422, bad
|
||||
assert client.put(URL, json={"판": "1", "표시": []}).status_code == 422 # 묶음 아님(요청 모양)
|
||||
assert json.loads(Path(schema.DEFINITION_PATH).read_text("utf-8"))["판"] == 1 # 거절되면 안 씀
|
||||
v1 = client.put(URL, json={"판": "1", "표시": {tid: {off: False}}}).json()["판"]
|
||||
assert client.put(URL, json={"판": "낡음", "표시": {}}).status_code == 409
|
||||
assert client.put(URL, json={"판": "", "표시": {}}).status_code == 409 # 처음 판으로 또 쓰기
|
||||
assert client.put(URL, json={"판": "1", "표시": {}}).status_code == 409 # 처음 판으로 또 쓰기
|
||||
assert client.get(URL).json()["판"] == v1
|
||||
|
||||
|
||||
def test_파일이_깨져도_폼_응답은_모두_보임(client) -> None:
|
||||
(store.FOLDER / opt.FILE).write_text("{깨짐", encoding="utf-8")
|
||||
assert all(all(v.values()) for v in _visible(client).values())
|
||||
assert client.get(URL).status_code == 500 # 관리 화면은 까닭을 그대로 올림
|
||||
|
||||
|
||||
def test_좌측_줄은_구조물_집계_양식_아래_한_줄_같은_한_곳에서_지움() -> None:
|
||||
side = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts").read_text("utf-8")
|
||||
assert "section(options.title, options.body)" in side
|
||||
@@ -165,7 +159,7 @@ def test_숨긴_옵션은_필수_아님_저장이_안_막힘(client, tmp_path, m
|
||||
root = str(tmp_path / "project")
|
||||
with pytest.raises(ValueError, match="필수"):
|
||||
repo.save_structures(root, [item({})], base_revision=0) # 보임 = 필수 그대로
|
||||
(store.FOLDER / opt.FILE).write_text('{"fake_hide": {"a": false}}', encoding="utf-8")
|
||||
monkeypatch.setattr(repo, "hidden_keys", lambda: {"fake_hide": {"a"}})
|
||||
with pytest.raises(ValueError, match="필수"): # a 는 숨겨 통과 · b 는 여전히 필수
|
||||
repo.save_structures(root, [item({})], base_revision=0)
|
||||
assert (
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/* 48-1 — 구조물 정의 한 벌(resources/master_template/구조물정의.json)로 옮긴 뒤 화면 변화 0
|
||||
* 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?root=master) · 시스템 관리자 · 저장 안 함
|
||||
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌)
|
||||
* 기대 수치:
|
||||
* - 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(옵션 없는 종류도 · 옛 화면 그대로)
|
||||
* · pipe 단추에 그룹 표지 「3」
|
||||
*/
|
||||
(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 += 150) {
|
||||
const v = fn();
|
||||
if (v) return v;
|
||||
await W(150);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const get = async (url) => {
|
||||
const r = await fetch(url, { credentials: "include" });
|
||||
return r.ok ? r.json() : { status: r.status };
|
||||
};
|
||||
const out = {};
|
||||
const def = await get("/api/m02/structure-definition");
|
||||
if (!def.types) return JSON.stringify({ error: "정의 못 읽음", def });
|
||||
out.def = {
|
||||
판: def.판,
|
||||
types: def.types.length,
|
||||
options: def.types.reduce((n, t) => n + t.options.length, 0),
|
||||
};
|
||||
const st = await get("/api/projects/structure-types");
|
||||
const pipe = st.types.find((t) => t.type_id === "pipe");
|
||||
out.types = {
|
||||
types: st.types.length,
|
||||
options: st.types.reduce((n, t) => n + t.options.length, 0),
|
||||
visibleAll: st.types.every((t) => t.options.every((o) => typeof o.visible === "boolean")),
|
||||
pipeGroups: pipe.groups.map((g) => g.name),
|
||||
};
|
||||
const row = await until(() => $("[data-options-row]"));
|
||||
if (!row) return JSON.stringify({ ...out, error: "옵션 보이기 줄 없음" });
|
||||
row.click();
|
||||
await until(() => $$("[data-options-kind]").length);
|
||||
const kinds = $$("[data-options-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,
|
||||
};
|
||||
out.ok =
|
||||
out.def.types === 37 &&
|
||||
out.def.options === 235 &&
|
||||
out.types.types === 37 &&
|
||||
out.types.options === 235 &&
|
||||
out.types.pipeGroups.length === 3;
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
Reference in New Issue
Block a user