- 새 M02_MasterTemplete_DrawingCard — 고칠 권한 + 저장 길이면 CAD 를 고치기로(보기만 끔) · 제목줄 · 리본 · 패널 · 상태줄 숨김(같은 출처 스타일 · CAD 앱 안 고침) · 전체 편집기 [도면 수정]과 같은 단축키 - 카드 [저장] · Ctrl+S = 전체 편집기와 같은 저장 길(saveTemplate · 읽은 문서에 도면 칸만) - ItemSource write(M02 화면 자료 자리만 · 모달 복사본은 보기만) - 저장 안 한 카드 고침 — 다른 항목 열 때 · 페이지 떠날 때 물음(ItemCards · Structure dirty · UI_Main 두 줄) - 시험 test_m02_drawing_card(새) · structure_panel · item_picker_detail 모양 맞춤 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BU9gbNE3aWFUvE61pS2WGt
205 lines
9.4 KiB
Python
205 lines
9.4 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
|
|
assert out["calc"] == {"total": 12345.6, "gaps": 2, "stuck": 1}
|
|
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):
|
|
assert out["inputsHits"] == 1
|
|
assert out["inputsLogics"] == [["GA1", ["지역", "소량감률"]], ["GA2", ["지역"]]]
|
|
|
|
|
|
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["groups"] == [
|
|
["GA1", ["지역"]],
|
|
["GA2", ["지역"]],
|
|
] # 같은 이름도 로직마다 · 「선택」 만
|
|
assert model["noLink"] == 0 # 연결이 없으면 아무것도 안 보임
|
|
assert model["pills"][0]["키"] == "입력|GA1|소량감률" # 25-1b 알약 키
|
|
|
|
|
|
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_MasterTemplete_Inputs.ts`(M02 카드와 같은 파일)."""
|
|
assert not (M02 / "M02_MasterTemplete_InputsCard.ts").exists()
|
|
inputs = (M02 / "M02_MasterTemplete_Inputs.ts").read_text(encoding="utf-8")
|
|
assert "export function createInputsCard(" in inputs # M02 자리 고르기(link)
|
|
assert "export function createInputsValueCard(" in inputs # 모달 값 받기(view · value)
|
|
for name in ("ui_template_item_picker_detail.ts", "ui_template_item_picker_edit.ts"):
|
|
text = (UI / name).read_text(encoding="utf-8")
|
|
assert "M02_MasterTemplete_Inputs" in text and 'InputsCard"' not in text, name
|
|
|
|
|
|
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_MasterTemplete_Inputs.ts",
|
|
M02 / "M02_MasterTemplete_Inputs_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_를_모달에_넘김():
|
|
panel = (ROOT / "B05_Profile" / "B05_Profile_UI_Structures_Panel.ts").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
assert "pipeStructureId(selectedPipeChainage)" in panel and "editingId ??" in panel
|