feat(M02): 관리 화면 「구조물 조합」 — 나무가 주 · 표는 참조표 · 비고 · 전구간 · 합계 숨김(PLAN 26-7a~e)
- 화면 이름 「구조물집계표」 · 「구조물 집계 양식」 → 「구조물 조합」(좌측 줄 · 상세 제목 · 안내글) · 파일 이름 · 양식 키 · API 길 그대로 - 좌측 패널 「항목 나무」 컨테이너 없앰 — 페이지가 나무를 만들어 편집 칸 왼쪽(aside)에 붙임 · 「구조물 조합」일 때만 보임 · 오른쪽 클릭 · 찾기 · 표와 서로 고름 · 고치기 전 저장 물음 그대로 - 표 부품 lockHead — 열 더하기 · 분류N 더하기 · 열 지우기 단추 없음 · 머리 · 층 이름 고치기 없음(단위 · 일위대가 · 조회 후 선택은 그대로) - 비고 열 · 전구간 줄 · 합계 줄은 보일 문서에서만 뺌(comboView) · [저장] 때 저장본 것을 다시 끼움(comboMerge) · 양식 파일 · M02_Table_Fill 그대로 - 서버 — 화면 표 [저장](store.write screen=True)은 있던 항목의 경로 · 층 이름 · 빈 폴더를 저장본 그대로(tree.keep_paths · 브라우저 머리 안 받음) · 머리는 경로에서 다시 만듦 · 나무 고침 줄기 그대로 - 나무 같은 이름 409 는 「이미 있는 항목」 까닭 그대로 보임(판 다름 안내와 가름) - 시험 test_m02_combo_screen 3(숨김 · 되끼움 Node · 잠금 · 나무 자리) · test_m02_tree 표 저장 약속 새로 · 이름 · 잠금 기대값 고침 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
"""관리 화면 「구조물 조합」(PLAN 26-7a~d) — 이름 · 숨김(비고 · 전구간 · 합계) · 참조표 잠금 ·
|
||||
나무 자리.
|
||||
|
||||
① `comboView` · `comboMerge`(Node) — 보일 문서에서 비고 열 · 줄 · 합계 줄을 빼고, [저장] 때
|
||||
저장본 것을 다시 끼움(열 차례 = 저장본 · 보인 고침은 받음).
|
||||
② 표 부품 `lockHead` — 열 더하기 · 지우기 · 머리 · 층 이름 고치기 없음(단위 · 조회 후 선택은
|
||||
그대로).
|
||||
③ 좌측 패널에 나무 없음 · 페이지가 나무를 편집 칸 왼쪽(`aside`)에 · 「구조물 조합」일 때만 보임.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
M02 = ROOT / "M02_MasterTemplete"
|
||||
SHEET = ROOT / "ui_template" / "sheet"
|
||||
|
||||
|
||||
def _text(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
def test_숨긴_것은_저장_때_저장본에서_다시_끼움():
|
||||
src = _text(M02 / "M02_MasterTemplete_Structure.ts")
|
||||
part = re.search(
|
||||
r"^const HIDDEN_COLUMNS[\s\S]*?^export function comboMerge[\s\S]*?^}", src, re.M
|
||||
)
|
||||
assert part, "comboView · comboMerge 를 못 찾음"
|
||||
script = (
|
||||
"interface StructureColumn { id: string; 머리: (string | null)[]; 꼴?: string | null;"
|
||||
" 단위?: string }\n"
|
||||
+ part.group(0).replace("export ", "")
|
||||
+ """
|
||||
const stored = {
|
||||
열: [{ id: "no", 머리: ["NO"] }, { id: "a", 머리: ["가"], 꼴: "수" },
|
||||
{ id: "b", 머리: ["나"], 꼴: "수" }, { id: "memo", 머리: ["비고"] }],
|
||||
줄: [{ id: "all", 값: { memo: "x" } }], 합계줄: [{ id: "sum" }], 변수: { k: 1 },
|
||||
};
|
||||
const view = comboView(stored);
|
||||
const edited = structuredClone(view);
|
||||
edited.열[2].단위 = "m";
|
||||
const saved = comboMerge(stored, edited);
|
||||
console.log(JSON.stringify([
|
||||
view.열.map((c) => c.id), view.줄, view.합계줄, stored.열.length,
|
||||
saved.열.map((c) => c.id), saved.열[2].단위, saved.줄, saved.합계줄, saved.변수,
|
||||
]));
|
||||
"""
|
||||
)
|
||||
result = subprocess.run(
|
||||
["node", "--experimental-strip-types", "--no-warnings",
|
||||
"--input-type=module-typescript", "-e", script],
|
||||
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
||||
) # fmt: skip
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert json.loads(result.stdout) == [
|
||||
["no", "a", "b"],
|
||||
[],
|
||||
[],
|
||||
4, # 저장본은 안 바뀜
|
||||
["no", "a", "b", "memo"],
|
||||
"m",
|
||||
[{"id": "all", "값": {"memo": "x"}}],
|
||||
[{"id": "sum"}],
|
||||
{"k": 1},
|
||||
]
|
||||
|
||||
|
||||
def test_참조표는_열_머리를_못_고침():
|
||||
sheet = _text(SHEET / "ui_template_sheet.ts")
|
||||
assert 'const editCols = opts.mode === "master" && !ro && !opts.lockHead;' in sheet
|
||||
assert "if (editCols) toolbar.append(colDelete);" in sheet
|
||||
assert "if (editCols) syncLevels(!!col);" in sheet
|
||||
assert "if (th && editCols) editHead(th);" in sheet
|
||||
assert "else if (layer && editCols) editLayer(layer);" in sheet
|
||||
assert 'ro || opts.lockHead ? { hidden: "" }' in sheet
|
||||
render = _text(SHEET / "ui_template_sheet_render.ts")
|
||||
assert 'const heads = mode === "master" && !state.headLocked;' in render
|
||||
main = _text(M02 / "M02_MasterTemplete_UI_Main.ts")
|
||||
assert "struct ? comboView(doc) : doc" in main
|
||||
assert "getDoc: () => comboMerge(loaded, shown.getDoc())" in main
|
||||
|
||||
|
||||
def test_나무는_편집_칸_왼쪽_좌측_패널엔_없음():
|
||||
side = _text(M02 / "M02_MasterTemplete_UI_Side.ts")
|
||||
assert "createTreePanel" not in side and "M02_TreeTitle" not in side
|
||||
assert 'info.이름 === STRUCTURE_TABLE ? L("M02_KindTable")' in side # 좌측 줄 이름
|
||||
page = _text(M02 / "M02_MasterTemplete_UI_Page.ts")
|
||||
assert "createTreePanel({" in page and "aside," in page and "tree.select(column)" in page
|
||||
main = _text(M02 / "M02_MasterTemplete_UI_Main.ts")
|
||||
assert 'at.kind === "table" && at.name === STRUCTURE_TABLE && at.layer !== "project"' in main
|
||||
assert 'isCombo(next) ? L("M02_KindTable")' in main # 상세 제목
|
||||
assert main.count("showAside();") == 3 # 열 때 · 양식 밖 화면 · 처음
|
||||
locale = _text(ROOT / "ui_template" / "ui_template_locale_m2.ts")
|
||||
assert 'M02_KindTable: ["구조물 조합"' in locale
|
||||
assert ".m02-shell__tree[hidden]" in _text(M02 / "M02_MasterTemplete_UI_Style.css")
|
||||
tree = _text(M02 / "M02_MasterTemplete_TreePanel.ts")
|
||||
assert (
|
||||
'error instanceof StaleError && error.message === "HTTP 409"' in tree
|
||||
) # 같은 이름 = 까닭 글
|
||||
@@ -147,5 +147,6 @@ def test_읽기만_표는_단추_고치기_모두_막음():
|
||||
assert sheet.count("if (ro") + sheet.count("!ro") + sheet.count("ro ||") >= 7
|
||||
# 열 0 = 「열 더하기」 하나만 켜짐 · 첫 열
|
||||
assert "b.disabled = !hasCol && !empty" in sheet and "addFirstColumn(" in sheet
|
||||
assert re.search(r"readOnly,\s*onChange", _text(M02 / "M02_MasterTemplete_UI_Main.ts"))
|
||||
main = _text(M02 / "M02_MasterTemplete_UI_Main.ts")
|
||||
assert re.search(r"readOnly,\s*lockHead: !!struct,\s*onChange", main)
|
||||
assert ".ui-sheet__toolbar[hidden]" in _text(SHEET / "ui_template_sheet.css")
|
||||
|
||||
@@ -153,7 +153,7 @@ def test_좌측_구조물_집계_양식_아래_하위_넷_새로_본떠_지우
|
||||
# 구조물 도면 · 면적산출 · 재료산출 · 자재 조합 = 구조물 집계 양식 컨테이너 안(같이 접힘)
|
||||
summary = side.split("const summary = section(")[1].split(");")[0]
|
||||
assert "COLUMN_KINDS.map" in summary and "matsetSection" in summary
|
||||
assert 'M02_KindTable: ["구조물 집계 양식"' in _text(
|
||||
assert 'M02_KindTable: ["구조물 조합"' in _text( # 화면 이름(26-7a)
|
||||
ROOT / "ui_template" / "ui_template_locale_m2.ts"
|
||||
)
|
||||
|
||||
|
||||
@@ -220,27 +220,32 @@ def test_판_409_권한_403(world):
|
||||
|
||||
|
||||
def test_표_저장과_나무가_한_줄기(world):
|
||||
"""집계표 표에서 머리를 고치면 경로가 따라오고, 나무 고침 뒤 표에도 같은 자료가 보임."""
|
||||
"""정본 = 나무(PLAN 26-7e) — 표 [저장]이 보낸 머리 · 층 이름 · 빈 폴더는 안 받음(저장본 경로
|
||||
그대로) · 나무 고침 뒤 표 머리가 경로에서 다시 만들어짐."""
|
||||
admin = _as(world, "SYSTEM_ADMIN")
|
||||
t = Tree(admin, "system")
|
||||
rv = next(r for r in t.now["items"] if r["열"] == "rv")
|
||||
path = rv["경로"]
|
||||
url = f"/api/m02/layers/system/templates/{TABLE}"
|
||||
got = admin.get(url).json()
|
||||
col = next(c for c in got["문서"]["열"] if c["id"] == "rv")
|
||||
col["머리"] = ["돌기슭막이", None, "찰쌓기", None] # 빈 칸은 건너뜀
|
||||
col["머리"] = ["돌기슭막이", None, "찰쌓기", None]
|
||||
col["단위"] = "㎡" # 머리 밖 칸은 받음
|
||||
got["문서"]["층"] = ["가", "나", "다", "라"]
|
||||
got["문서"]["폴더"] = [["표에서 만든 폴더"]]
|
||||
assert admin.put(url, json=got).status_code == 200
|
||||
table = admin.get(url).json()["문서"]
|
||||
col = next(c for c in table["열"] if c["id"] == "rv")
|
||||
assert col["경로"] == path and col["단위"] == "㎡"
|
||||
assert table["층"] == ["종류", "분류1", "분류2", "분류3"] and table["폴더"] == []
|
||||
t = Tree(admin, "system")
|
||||
assert t.item(["돌기슭막이", "찰쌓기"])["key"] == rv["key"]
|
||||
assert t.item(path)["key"] == rv["key"]
|
||||
t.do(op="rename", 키=rv["key"], 이름="메쌓기")
|
||||
table = admin.get(url).json()["문서"]
|
||||
col = next(c for c in table["열"] if c["id"] == "rv")
|
||||
assert col["경로"] == ["돌기슭막이", "메쌓기"] and col["머리"] == [
|
||||
"돌기슭막이",
|
||||
"메쌓기",
|
||||
None,
|
||||
None,
|
||||
]
|
||||
# 표에서 열을 지우면 나무에서도 빠지고 파일도 같이
|
||||
assert col["경로"] == [*path[:-1], "메쌓기"]
|
||||
assert col["머리"] == [*path[:-1], "메쌓기", *[None] * (4 - len(path))]
|
||||
# (화면엔 단추 없음 · API 호환) 표 문서에서 열을 빼고 저장하면 나무에서도 빠지고 파일도 같이
|
||||
table["열"] = [c for c in table["열"] if c["id"] != "rv"]
|
||||
assert (
|
||||
admin.put(url, json={"판": admin.get(url).json()["판"], "문서": table}).status_code == 200
|
||||
|
||||
Reference in New Issue
Block a user