- 사본 셈 = 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
249 lines
12 KiB
Python
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
|