Files
Aislo/resources/tester/test_item_picker_detail.py
T
eomsangdonandClaude Opus 5.5 2fccd7c48a feat(M02): 구조물 조합 고르기 모달 = M02 일위대가 카드 · 사본 셈 = 층 셈 한 벌(142-1)
- 사본 셈 = M02_Labor_Doc.calc 한 벌(사본 단가 · 굳힌 참조사본 · 사본 알약 주입 · 빈 입력 = 자동값)
- 모달 일위대가 카드 = ItemCards labor(자료 자리 ItemSource.labor · 쓰는 길 없으면 읽기 전용)
- 사본 문서에 사본 입력값 · 고른자재를 줄 입력값 · 고름으로 얹음 · [수정] = 넣을 값 편집 화면
- 옛 요약 카드(picker_labor) · 넣을 값 카드 · 머리 옛 일위대가 이름 줄 지움 · 처음 펼침 = 개인
- 시험: 사본 셈 = 층 셈 · 모달 일위대가 카드 읽기 전용 · 새 셈 꼴로 고침

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

249 lines
12 KiB
Python

# -*- coding: utf-8 -*-
"""항목 고르기 모달 상세(계획서 25-2d) — 복사본 창구 · 넣을 값 셈 · 한 벌 지킴.
① 프로젝트 복사본 창구(`ui_template_item_picker_project.ts`)를 Node 로 돌려(sub6 길 모양의 가짜 서버 fetch 대역)
본다: 목록 `{항목}` · 가져오기 POST · 읽기 `{종류, 판, 내용}` · 고침 PUT `{판, 내용}`(판 · 409 · 403 까닭 그대로) ·
새로 가져오기 · 계산 한 줄 · 값 셈(범위 · 고르기 원래 모양).
② 모달이 카드 · 편집기를 M02 와 같은 한 벌로 쓰는지, 원본 층에 쓰는 길을 안 부르는지 소스로 지킨다.
"""
import json
import os
import re
import subprocess
from pathlib import Path
import pytest
from common_util.common_util_summary_item import snapshot_folder_name
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = Path(HERE).parents[1]
UI = ROOT / "ui_template"
M02 = ROOT / "M02_MasterTemplete"
@pytest.fixture(scope="module")
def out():
proc = subprocess.run(
["node", os.path.join(HERE, "helper_item_picker_project.cjs")],
capture_output=True,
text=True,
encoding="utf-8",
timeout=60,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
def test_구조물_id_는_복사본_폴더와_관_지점_id_로(out):
assert out["folders"] == ["st-1", "n3", "pipe@85.05"] # `#차례` = `n차례` (서버 folder_name)
# 서버 `pipe@{round(측점, 3)}` — 파이썬 float 글(정수도 `100.0`)
assert out["pipeIds"] == [
f"pipe@{round(x, 3)}" for x in (85.05, 100.0, 258.12, 0.1234, 12.0004)
]
def test_가져오기는_구조물마다_POST_본문에_structure(out):
assert out["listEmpty"] == []
assert out["importCall"] == {
"method": "POST",
"url": "/api/projects/P1/items",
"body": {"structure": "pipe@85.05", "layer": "personal", "owner": "1/3", "key": "SS000001"},
}
assert out["info"]["폴더"] == "pipe@85.05" # 폴더 = 구조물 id
assert out["list"] == ["pipe@85.05"]
assert out["noStructure"] == "미리 가져올 구조물(structure)이 필요합니다." # 서버 까닭 그대로
def test_같은_구조물이_다른_항목으로_바뀌었는지_원본_참조로_가림(out):
assert out["sameItem"] == [True, False, False]
def test_읽기는_내용만_돌려주고_없으면_null(out):
assert out["read"]["시트"][0]["칸"] == {"A1": {"값": 1}}
assert out["readNullContent"] is None # 파일이 아직 없음(내용 null)
assert out["readMissing"] is None # 없는 폴더 404
def test_고침은_읽은_판으로_쓰고_쓴_뒤_새_판을_들고_있음(out):
assert out["afterWrite"]["시트"][0]["칸"] == {"H10": {"값": 12345}}
assert out["secondWrite"] == {"값": 999} # 이어서 또 써도 판이 맞음
assert out["writeWithoutReadGets"] == 1 # 읽은 적 없는 부분은 쓰기 전에 판을 한 번 읽음
assert out["valuesSaved"]["GA1"]["지역"] == {"값": "서울", "확인": True}
def test_판이_달라지면_409_까닭_권한_없으면_서버_까닭_그대로(out):
assert out["stale"] == "다른 곳에서 먼저 고쳤습니다"
assert out["readOnly"] == "프로젝트 편집 권한이 없음"
def test_재적용은_덮임을_돌려주고_계산_한_줄(out):
assert out["reapplyCall"][0] == "POST /api/projects/P1/items/pipe%4085.05/reapply"
assert out["reapply"]["covered"] == ["면적"] # 덮이는 것을 알림으로
assert out["reapply"]["info"]["마스터바뀜"] is False
# 142-1 — 계 = 반영합 · 빈 값 = 빠진입력 · 못 돈 줄 = 까닭 있는 줄
assert out["calc"] == {"total": 300, "gaps": 2, "stuck": 1, "base": 100, "reflected": 300}
assert out["calcMissing"] is None # 없는 복사본 = 계산 줄 없음
def test_개인에_저장과_새_항목_본문_summary_item(out):
assert out["saveAsCall"] == {"to": "personal", "경로": ["sub5", "경로"], "이름": "내 항목"}
assert out["saveAs"]["summary_item"]["key"] == "SS900002"
assert out["newCall"] == {
"structure": "pipe@85.05",
"경로": ["개인", "폴더"],
"이름": "새 항목",
"단위": "m",
}
assert out["newItem"]["summary_item"]["name"] == "개인 폴더 새 항목"
def test_회사_없음_409_는_서버_까닭_그대로(out):
for key in ("noCompanySave", "noCompanyNew"):
assert out[key].startswith("회사부터 등록"), key
def test_넣을_값_셈(out):
model = out["model"]
assert model["range"] == [{"min": 0, "max": 50}, {"min": 1, "max": 2}, None]
assert [c["raw"] for c in model["choices"]] == [15, "16", 18] # 저장은 원래 모양
assert model["key"] == "GA1|지역" # 같은 이름도 로직마다 따로
def test_모달은_M02_와_같은_카드_한_벌을_씀():
cards = (M02 / "M02_MasterTemplete_ItemCards.ts").read_text(encoding="utf-8")
structure = (M02 / "M02_MasterTemplete_Structure.ts").read_text(encoding="utf-8")
detail = (UI / "ui_template_item_picker_detail.ts").read_text(encoding="utf-8")
assert "createItemCards(" in structure and "mountDrawingTemplate(" not in structure
assert 'import("../M02_MasterTemplete/M02_MasterTemplete_ItemCards")' in detail
assert "createItemCards(" in detail
# 카드를 모달에서 또 그리지 않음 — 미리보기 부품은 카드 파일 한 곳
for name in ("ui_template_item_picker_detail.ts", "ui_template_item_picker.ts"):
text = (UI / name).read_text(encoding="utf-8")
assert "mountDrawingTemplate(" not in text and "createSpreadsheet(" not in text, name
assert cards.count("mountDrawingCard(") == 1 # 도면 미리보기 = 카드 파일에서 뗀 부품 하나(31-4)
drawing_card = (M02 / "M02_MasterTemplete_DrawingCard.ts").read_text(encoding="utf-8")
assert drawing_card.count("mountDrawingTemplate(") == 1
def test_넣을_값_카드는_sub2_파일_한_벌(out):
"""두 벌 금지 — 모달의 값 받기 카드는 `M02_Labor_Values.ts`(39-3 · 일위대가 입력 모달과 같은
값 칸) · 옛 선택 입력(자리 고르기)은 없앰(39-7b)."""
for gone in ("M02_MasterTemplete_InputsCard.ts", "M02_MasterTemplete_Inputs.ts",
"M02_MasterTemplete_Inputs_Model.ts", "M02_MasterTemplete_Inputs.py"): # fmt: skip
assert not (M02 / gone).exists(), gone
values = (M02 / "M02_Labor_Values.ts").read_text(encoding="utf-8")
assert "export function createInputsValueCard(" in values # 모달 값 받기(view · value)
assert "seatsOf" not in values and "pickedGroups" not in values
for name in ("ui_template_item_picker_detail.ts", "ui_template_item_picker_edit.ts"):
text = (UI / name).read_text(encoding="utf-8") # 모달은 값 카드를 바로 받음(39-1c)
assert "M02_Labor_Values" in text and 'InputsCard"' not in text, name
def test_일위대가_카드_셈은_복사본_셈_답_통째_문서는_복사본_값(out):
"""142-1 — 모달 일위대가 카드 = M02 카드 · 셈 = 복사본 셈(층 셈 꼴) · 문서 = 복사본 입력값 ·
고른자재를 줄 입력값 · 고름으로(빈 값 = 자동값이라 안 얹음 · 서버 `with_values` 와 같은 규칙)."""
assert out["laborCalc"] == {"줄": 2, "반영합": {"계": 300}, "빠진입력": 2}
assert out["laborCalcMissing"] == "복사본이 없음" # 서버 까닭 그대로(카드 안 글)
lines = out["withValues"]["담은로직"]
assert lines[0] == {"로직": "GA1", "입력값": {"지역": 15}, "고름": {"0": "MT9"}}
assert lines[1] == {
"로직": "GA2",
"입력값": {},
"고름": {"0": "MT1"},
} # 복사본에 값 없음 = 자동값
assert lines[2] == {"참조": {"층": "system", "주인": None, "키": "SC-000001"}}
assert out["withValues"]["일위대가번"] == "SU-000007"
values = out["laborValues"]
assert values["keys"] == ["GA1", "GA2"]
assert values["logics"] == [["GA1", ["지역", "소량감률"]], ["GA2", ["지역"]]]
assert values["hasLink"] is False and values["values"] == {"GA1": {}}
def test_모달_카드_차례_도면_일위대가_면적():
"""142-1 — 일위대가 카드 = M02 카드 한 벌(`createItemCards` 의 `labor`) · 옛 요약 카드 · 넣을 값
카드 · 머리 옛 「일위대가 {조합 이름}」 줄 없음."""
detail = (UI / "ui_template_item_picker_detail.ts").read_text(encoding="utf-8")
assert not (UI / "ui_template_item_picker_labor.ts").exists()
assert "ui_template_item_picker_labor" not in detail
assert 'labor: () => void edit("inputs"),' in detail # [수정] = 넣을 값 편집 화면
assert "createInputsValueCard" not in detail and "extraCards" not in detail
assert "folderLines" not in detail and "unitPrice" not in detail
def test_모달_일위대가_카드_읽기_전용():
"""142-1 — 모달 자료 자리엔 쓰는 길(`write`)이 없고 카드는 그때 보기만(원본 층 [저장]을 안 엶)."""
detail = (UI / "ui_template_item_picker_detail.ts").read_text(encoding="utf-8")
body = detail[detail.index("const makeSource") : detail.index("const badge")]
assert "write" not in body and "labor: {" in body and "items.calcLabor(at)" in body
cards = (M02 / "M02_MasterTemplete_ItemCards.ts").read_text(encoding="utf-8")
assert "const laborWritable = (): boolean => opts.editable() && !!opts.source().write;" in cards
assert "readOnly: !laborWritable()," in cards
assert 'c.kind === "labor" ? laborWritable()' in cards
picker = (UI / "ui_template_item_picker.ts").read_text(encoding="utf-8")
assert '"personal" : "system"' in picker # 처음 펼침 = 개인(없으면 마스터)
def test_모달은_원본_층에_쓰는_길을_안_부름():
"""마스터 · 회사 · 개인 원본은 안 바뀜 — 저장 길은 프로젝트 복사본(`items.write`)뿐."""
for name in (
"ui_template_item_picker_detail.ts",
"ui_template_item_picker_edit.ts",
"ui_template_item_picker_project.ts",
):
text = (UI / name).read_text(encoding="utf-8")
for forbidden in (
"saveTemplate(",
"deleteTemplate(",
"deleteLayerTemplate(",
"copyToPersonal(",
"saveInputLinks(",
):
assert forbidden not in text, f"{name} 이 {forbidden} 를 부름"
edit = (UI / "ui_template_item_picker_edit.ts").read_text(encoding="utf-8")
assert edit.count("items.write(") >= 3 # 도면 · 시트 · 입력값 모두 복사본으로
def test_B05_구조물_폼이_projectId_를_모달에_넘김():
panel = (ROOT / "B05_Profile" / "B05_Profile_UI_Structures_Panel.ts").read_text(
encoding="utf-8"
)
assert re.search(r"projectId: sectionOptions\.projectId \?\? null", panel)
shared = (ROOT / "A00_Common" / "design" / "design_shared.ts").read_text(encoding="utf-8")
assert "projectId: shared.projectId" in shared
def test_소스가_700줄을_넘지_않음():
for path in (
*sorted(UI.glob("ui_template_item_picker*.ts")),
M02 / "M02_MasterTemplete_ItemCards.ts",
M02 / "M02_MasterTemplete_ItemSource.ts",
M02 / "M02_Labor_Values.ts",
M02 / "M02_Labor_Values_Model.ts",
):
assert len(path.read_text(encoding="utf-8").splitlines()) <= 700, path.name
def test_나무_오른쪽_클릭은_메뉴_항목을_밖에서_주는_자리():
tree = (UI / "ui_template_tree.ts").read_text(encoding="utf-8")
assert "menu?: () => TreeMenuItem[]" in tree and "export function openTreeMenu(" in tree
assert '"contextmenu"' in tree
picker = (UI / "ui_template_item_picker.ts").read_text(encoding="utf-8")
# 새 항목 메뉴는 개인 그룹 · 폴더에만(마스터 · 회사는 메뉴 없음) · 프로젝트 안에서만
assert 'folder.layer !== "personal" || !opts.projectId' in picker
assert "menu: menuOf(node)" in picker
def test_B05_구조물_폼이_구조물_id_를_모달에_넘김():
# 배선은 `_Structures_Groups.bindSummaryPickers`(PLAN 46-3) — 그룹은 `{id}__{그룹 id}`
picks = (ROOT / "B05_Profile" / "B05_Profile_UI_Structures_Groups.ts").read_text(
encoding="utf-8"
)
assert "opts.editingId() ?? (pipe !== null ? pipeStructureId(pipe) : null)" in picks
assert "`${id}${GROUP_JOIN}${groupId}`" in picks
panel = (ROOT / "B05_Profile" / "B05_Profile_UI_Structures_Panel.ts").read_text(
encoding="utf-8"
)
assert "bindSummaryPickers({" in panel