Files
Aislo/resources/tester/test_m02_structure_panel.py
T
eomsangdonandClaude Opus 5.5 1bba901834 feat(M02): 면적산출 · 재료산출 오른쪽 조합 패널 — 조합 목록 · 이 열에 쓰기 · 조합 고치기
- 오른쪽 안쪽 패널: 공용 사이드바 카드 · 접기(attachCollapsible) · 공용 폭 손잡이(createPanelResizer · 폭 기억 · 최소 280 · 최대 60%) · 공용 접기 손잡이(createWorkflowPanelHandle)
- 「이 열의 조합」 칸: 면적 = 일위대가 조합 줄 + [조합 고치기] · 재료 = 담은 자재 표(옆 판 aside 없앰)
- 조합 목록: 면적 = 일위대가(UA) · 재료 = 자재 조합(UM) · 키 · 이름 · 수 · 본뜬키 · 찾기 · 이 열 조합 강조
- [이 열에 쓰기]: 서버 POST /api/m02/structures/{column}/use-combo(집계표 판 검사 · 그 열 키만 바꿈 · 조합은 그대로) → 알약 다시 받음
- 상태 등록표에 m02-combo-panel-width · collapsed · 시험 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Kyw9qnkJ94pC1A4PfpDtA
2026-09-29 09:04:19 +09:00

212 lines
11 KiB
Python

"""M02 구조물 설계 컨테이너(`M02_MasterTemplete_Structure.ts`) 붙는 자리 시험.
화면 조작은 ORCA 로 봄 — 여기서는 창끼리 계약이 안 어긋나는지만 봄.
· 집계표 이름이 서버(`STRUCTURE_TABLE`)와 같은 글인가
· 컨테이너가 쓰는 이름(표 부품 · 도면 부품 · 서버 호출)이 정말 내보내진 것인가
· 열 머리 글 잇기(`columnHead`)를 Node 로 바로 돌려 봄
"""
from __future__ import annotations
import json
import re
import shutil
import subprocess
from pathlib import Path
import pytest
from M02_MasterTemplete import M02_MasterTemplete_Store as store
ROOT = Path(__file__).resolve().parents[2]
M02 = ROOT / "M02_MasterTemplete"
PANEL = M02 / "M02_MasterTemplete_Structure.ts"
MAIN = M02 / "M02_MasterTemplete_UI_Main.ts"
SIDE = M02 / "M02_MasterTemplete_UI_Side.ts"
def _text(path: Path) -> str:
return path.read_text(encoding="utf-8")
def test_집계표_이름이_서버와_같음():
found = re.search(r'STRUCTURE_TABLE = "([^"]+)"', _text(PANEL))
assert found and found.group(1) == store.STRUCTURE_TABLE
def test_소스가_700줄을_넘지_않음():
for path in (PANEL, MAIN, M02 / "M02_MasterTemplete_Structure.css"):
assert len(_text(path).splitlines()) <= 700, path.name
@pytest.mark.parametrize(
("source", "names", "target"),
[
(
"M02_MasterTemplete_Api_Fetch.ts",
["createStructure", "fetchStructureNumbers", "readTemplate"],
PANEL,
),
# 산출근거 저장은 컨테이너가 안 함 — [산출근거 수정] 이 여는 메인 칸 전체 편집이 함
("M02_MasterTemplete_Api_Fetch.ts", ["saveTemplate"], MAIN),
# 좌측 구조물 도면 · 면적산출 · 재료산출 줄 번호는 한 번에 받음
# (`{열 id: {도번, 면적번, 재료번}}`)
("M02_MasterTemplete_Api_Fetch.ts", ["fetchStructureNumbers"], SIDE),
("M02_MasterTemplete_Structure.ts", ["structureLabel", "isStructureColumn"], SIDE),
("M02_MasterTemplete_Drawing.ts", ["mountDrawingTemplate"], PANEL),
("../A00_Common/spreadsheet/spreadsheet.ts", ["createSpreadsheet"], PANEL),
],
)
def test_컨테이너가_쓰는_이름이_내보내진_것(source: str, names: list[str], target: Path):
"""도면 부품 · 스프레드시트(D) 가 이름을 바꾸면 여기서 먼저 걸림."""
body = _text(M02 / source)
used = _text(target)
for name in names:
assert re.search(rf"export (const|function|interface) {name}\b", body), f"{source}:{name}"
assert name in used, name
def test_sub2_가_부르는_이름과_모양():
"""브레인 확정 이름 · 모양 — sub2 가 이대로 부름(바꾸면 브레인에게)."""
body = _text(M02 / "M02_MasterTemplete_Api_Fetch.ts")
for name in ("copyStructure", "copyArea", "copyMaterial"):
assert re.search(
rf"export const {name} = async \(column: string, from: string\): Promise<void>", body
), name
assert "fetchStructureNumbers = (): Promise<Record<string, StructureNumbers>>" in body
assert re.search(
r"interface StructureNumbers \{\s*도번: string;\s*면적번: string;\s*재료번: string;", body
)
assert re.search(r'export type Kind = [^;]*"area" \| "material"', body)
def test_집계표만_컨테이너를_붙임():
"""다른 표 양식에는 안 붙음 — 시스템 층 · 이름이 집계표일 때만."""
body = _text(MAIN)
assert 'at.layer === "system" && at.name === STRUCTURE_TABLE ? buildPanel() : null' in body
# 구조물 도면 저장은 `도면` 칸만 갈아 끼움 — 도번 · 산출근거는 읽은 그대로
assert '{ ...(loaded as StructureDoc), 도면: edited as StructureDoc["도면"] }' in body
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
def test_열_머리_글_잇기():
"""`columnHead` — 빈 층은 건너뛰고 빈칸으로 이음 · 다 비면 열 id ·
`structureLabel` — 번호가 맨 앞(「SD010101 돌기슭막이 …」) · 번호 없으면 이름만."""
funcs = [
re.search(rf"^export const {name}[\s\S]*?;$", _text(PANEL), re.M)
for name in ("columnHead", "structureLabel")
]
assert all(funcs), "columnHead · structureLabel 을 못 찾음"
# 부품 파일은 화면 모듈을 부르므로 통째로 못 싣음 — 그 함수만 떼어 Node 가 타입을 벗겨 돌림
script = (
"interface StructureColumn { id: string; 머리: (string | null)[] }\n"
+ "\n".join(f.group(0).replace("export ", "") for f in funcs)
+ "\nconsole.log(JSON.stringify(process.argv.slice(1)"
+ ".map((a) => columnHead(JSON.parse(a)))"
+ ".concat([structureLabel('SD010101', {id: 'rv', 머리: ['돌기슭막이', null, 'H=1.0']}),"
+ " structureLabel('', {id: 'rv', 머리: ['돌기슭막이', null, null]})])));"
)
cases = [
{"id": "pv_l", "머리": ["콘크리트포장", "T=(두께별)", "L"]},
{"id": "no", "머리": ["NO", None, None]},
{"id": "etc", "머리": [None, " ", None]},
]
result = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings",
"--input-type=module-typescript", "-e", script,
*[json.dumps(c, ensure_ascii=False) for c in cases]],
capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == [
"콘크리트포장 T=(두께별) L",
"NO",
"etc",
"SD010101 돌기슭막이 H=1.0",
"돌기슭막이",
]
def test_미리보기_CAD_패널은_숨기지_않고_접힌_채로_엶():
"""특성 · 도면층 패널을 숨기지 않음 — 곁값 panelsCollapsed 로 접어 실음(B07 은 안 보냄)."""
panel = _text(PANEL)
assert ".cad-inspector" not in panel and "panelsCollapsed: true" in panel
drawing = _text(M02 / "M02_MasterTemplete_Drawing.ts")
assert "panelsCollapsed: options.panelsCollapsed" in drawing
assert "panelsCollapsed?: boolean" in _text(ROOT / "ui_template/cad_host/cad_host.ts")
cad = ROOT / "B07_DesignDetail/openwebcad/src"
bridge = _text(cad / "integration/aislo-drawing-bridge.ts")
assert "if (event.data.panelsCollapsed) setInspectorCollapsed(true);" in bridge
assert "isInspectorCollapsed()" in _text(cad / "components/Toolbar.tsx")
def test_좌측_구조물_집계_양식_아래_하위_넷_새로_본떠_지우기는_도면_양식만():
side = _text(SIDE)
assert 'actionRow("table")' not in side and 'actionRow("drawing")' in side
assert "createSelectField" not in side # 새로 = 종류 고르기 없이 이름만
assert "cur?.종류 !== kind" in side # 본떠 · 지우기 = 그 컨테이너에서 고른 것이 있을 때만
assert "i.이름 === STRUCTURE_TABLE" in side # 표는 구조물집계표 한 줄만
# 구조물 도면 · 면적산출 · 재료산출 · 자재 조합 = 구조물 집계 양식 컨테이너 안(같이 접힘)
summary = side.split("const summary = section(")[1].split(");")[0]
assert "COLUMN_KINDS.map" in summary and "matsetSection" in summary
assert 'M02_KindTable: ["구조물 집계 양식"' in _text(
ROOT / "ui_template" / "ui_template_locale_m2.ts"
)
def test_면적_재료_오른쪽_조합_패널은_공용_틀_손잡이():
"""좌측과 같은 사이드바 카드 · 접기 · 공용 폭 손잡이 · 접기 손잡이 — 새로 그리지 않음."""
cpanel = _text(M02 / "M02_MasterTemplete_ComboPanel.ts")
for name in ("createPanelResizer", "createWorkflowPanelHandle", "attachCollapsible"):
assert name in cpanel, name
assert "ui-sidebar-card" in cpanel and "setColumnCombo" in cpanel
main = _text(MAIN)
assert "comboSlot" not in main # 조합 줄은 패널 「이 열의 조합」 칸으로
assert "cpanel.setMine(matPanel.root)" in main # 담은 자재 표도 패널 안
assert '"aside"' not in _text(M02 / "M02_MasterTemplete_Materials.ts")
def test_첫_화면과_닫은_뒤는_구조물집계표():
page = _text(M02 / "M02_MasterTemplete_UI_Page.ts")
assert re.search(r'kind: "table",\s*name: STRUCTURE_TABLE', page)
assert "main.open(sel ?? home)" in page and "void main.open(home);" in page
def test_좌측_고름_표시는_열린_것_한_줄뿐():
"""카드 [도면 수정] 등 무엇으로 열든 편집 칸이 좌측 `paint` 를 부름 — 목록 · 열 줄을 함께 그림."""
side = _text(SIDE)
assert "picked" not in side # 따로 든 고름 상태 없음 — 편집 칸의 지금 열린 것만 봄
assert re.search(r"function paint\(\): void \{\s*paintList\(\);\s*paintColumns\(\);", side)
for fn in ("async function select(", "async function selectColumn("):
body = side.split(fn)[1].split("\n }\n")[0]
assert "paint" not in body, fn # 여기서 한쪽만 그리면 두 줄이 남음
assert re.search(r"sel = next;\s*onOpened\?\.\(\);", _text(MAIN))
# 자재 조합 상세도 같은 한 곳 — 열면 양식 · 열 줄 고름 지움 · 양식을 열면 자재 조합 고름 지움
assert re.search(r"sel = null;\s*onOpened\?\.\(\);", _text(MAIN))
assert "if (opt.getOpen()) matset.clear();" in side.split("function paint(): void {")[1]
assert "() => side.paint()" in _text(M02 / "M02_MasterTemplete_UI_Page.ts")
def test_리본_접힘_모드에서_탭으로_편_것은_밖을_누르면_다시_접힘():
ribbon = _text(ROOT / "B07_DesignDetail/openwebcad/src/ribbon/Ribbon.tsx")
assert "if (collapsed) setPeek(true);" in ribbon # 탭 = 잠깐 펼침(접힘 모드 유지)
assert "addEventListener('pointerdown', close, true)" in ribbon # 리본 밖 = 다시 접힘
assert "setPeek(false);" in ribbon and "setRibbonCollapsed(!collapsed);" in ribbon # 단추
def test_표_도구_막대에_줄_더하기_줄_지우기_손_입력_열_없음():
sheet = _text(ROOT / "ui_template/sheet/ui_template_sheet.ts")
text = _text(ROOT / "ui_template/sheet/ui_template_sheet_text.ts")
for key in ("Row_Add", "Row_Delete", "Col_Hand"):
assert key not in sheet and key not in text, key
assert "selectColumn: (colId) =>" in sheet
def test_상세_화면_돌아가기는_집계표로_그_열_고른_채():
main = _text(MAIN)
assert "children: [title, back, slot, reload, save]" in main # 제목 바로 오른쪽
assert "!(sel.kind in NUMBER_FIELD)" in main # 구조물 도면 · 면적산출 · 재료산출만
body = main.split('back.addEventListener("click"')[1]
assert "confirmLeave()" in body and "STRUCTURE_TABLE" in body
assert "sheet?.selectColumn(column)" in body