diff --git a/M02_MasterTemplete/M02_Item_Copy.py b/M02_MasterTemplete/M02_Item_Copy.py index 4117b2c5..f7e3cd8c 100644 --- a/M02_MasterTemplete/M02_Item_Copy.py +++ b/M02_MasterTemplete/M02_Item_Copy.py @@ -89,13 +89,16 @@ def list_items(project_root: str | Path) -> list[dict[str, Any]]: complete_old(child) manifest = read_json(child / MANIFEST_NAME) or {} origin = manifest.get("원본") or {} + changed = source_changed(manifest) out.append( { "폴더": child.name, "이름": origin.get("name"), "원본": origin, + "원본판": manifest.get("원본판"), "가져온날": manifest.get("가져온날"), - "마스터바뀜": source_changed(manifest), + "마스터바뀜": bool(changed), + "원본없음": changed is None, "구조물": manifest.get("구조물") or [], "고침": bool(manifest.get("고침")), } diff --git a/M02_MasterTemplete/M02_Item_Source.py b/M02_MasterTemplete/M02_Item_Source.py index e5c45c27..134c62ce 100644 --- a/M02_MasterTemplete/M02_Item_Source.py +++ b/M02_MasterTemplete/M02_Item_Source.py @@ -22,7 +22,6 @@ from M02_MasterTemplete import M02_Template_Layers as layers TABLE_NAME = "구조물집계표" UM_FILE = "자재조합.json" UA_FILE = "일위대가조합.json" # M01 시스템(`resources/master_data/`) -INPUTS_DIRNAME = "inputs" # 층의 항목 연결 파일 자리(25-1b · sub4 층 자료 틀) #: 열 id → 스냅샷 파일(구조물 도면 · 면적산출 · 재료산출) PART_FILES = (("structure", "도면.json"), ("area", "면적.json"), ("material", "재료.json")) LINK_FILE = "입력연결.json" @@ -55,6 +54,15 @@ def row_of(document: Any, key: str) -> dict[str, Any] | None: return None +def item_name(item: dict[str, Any]) -> str: + """층 SS 줄의 보이는 이름(종류 · 분류1~3) — 없으면 LookupError.""" + row = row_of(read_json(layer_root(item) / layers.COMBO_FILE), str(item.get("key"))) + if row is None: + raise LookupError(f"집계 항목 {item.get('key')} 이 {item.get('layer')} 층에 없음") + parts = (row.get(k) for k in ("종류", "분류1", "분류2", "분류3")) + return " ".join(str(p) for p in parts if p) or str(row["키"]) + + def version_of(content: Any) -> str: raw = json.dumps(content, ensure_ascii=False, sort_keys=True).encode("utf-8") return hashlib.sha256(raw).hexdigest()[:16] @@ -88,8 +96,14 @@ def labor_rows(ua_key: str) -> dict[str, Any] | None: def input_links(root: Path, column_id: str) -> dict[str, Any]: - """층의 항목 연결 `inputs/<열id>.json` — 없으면 빈 연결(같은 모양).""" - found = read_json(root / INPUTS_DIRNAME / f"{column_id}.json") if column_id else None + """층의 항목 연결 `inputs/<열id>.json`(sub4 `_Inputs.read_file`) · 없으면 빈 연결.""" + from M02_MasterTemplete import M02_MasterTemplete_Inputs as inputs + from M02_MasterTemplete import M02_MasterTemplete_Store as store + + if not column_id: + return {"판": None, "줄": []} + with store.at(root): + found = inputs.read_file(column_id) if isinstance(found, dict) and isinstance(found.get("줄"), list): return found return {"판": None, "줄": []} diff --git a/M02_MasterTemplete/M02_Project_Items_Router.py b/M02_MasterTemplete/M02_Project_Items_Router.py index 0b183cae..4ca0e2f7 100644 --- a/M02_MasterTemplete/M02_Project_Items_Router.py +++ b/M02_MasterTemplete/M02_Project_Items_Router.py @@ -1,7 +1,7 @@ """프로젝트 집계 항목 복사본 길 — `/api/projects/{id}/items…` (PLAN 25-1b · 25-2b · 25-2e). GET …/items 목록(이름 · 원본 · 가져온날 · 마스터 바뀜) - POST …/items {summary_item} 미리 가져오기(구조물에 안 이은 항목) + POST …/items {layer, owner, key} 미리 가져오기(구조물에 안 이은 항목) → {폴더, 만듦, 가져온날} GET …/items/{폴더}/{종류} {종류, 판, 내용} PUT …/items/{폴더}/{종류} {판, 내용} — 판이 다르면 409 · 면적 · 재료는 서버 계산값 POST …/items/{폴더}/refresh 새로 가져오기(입력값 · 고른자재는 같은 키면 살림) @@ -23,6 +23,7 @@ from common_util.common_util_auth import verify_session from common_util.common_util_summary_item import parse_summary_item from M02_MasterTemplete import M02_Item_Copy as copies from M02_MasterTemplete import M02_MasterTemplete_Router_Layers as layer_routes +from M02_MasterTemplete.M02_Item_Source import item_name router = APIRouter(prefix="/api/projects", tags=["M02 Project Items"]) @@ -35,7 +36,13 @@ class WriteBody(BaseModel): class PrefetchBody(BaseModel): - summary_item: dict[str, Any] + """`{layer, owner, key}`(이름은 층 SS 줄에서) — 옛 모양 `{summary_item}` 도 받음.""" + + layer: str | None = None + owner: str | None = None + key: str | None = None + name: str | None = None + summary_item: dict[str, Any] | None = None async def _root(session: dict[str, Any], project_id: str): @@ -66,10 +73,16 @@ async def list_items( async def prefetch_item( project_id: str, body: PrefetchBody, session: dict[str, Any] = Depends(verify_session) ) -> dict[str, Any]: - try: - item = parse_summary_item(body.summary_item) + raw = body.summary_item or {"layer": body.layer, "owner": body.owner, "key": body.key} + try: # 모양 먼저(이름은 자리만) — 그다음 층 SS 줄에서 이름 + item = parse_summary_item({**raw, "name": raw.get("name") or body.name or "-"}) except ValueError as error: raise HTTPException(status_code=400, detail=str(error)) from error + if not (raw.get("name") or body.name): + try: + item["name"] = item_name(item) + except LookupError as error: + raise HTTPException(status_code=404, detail=str(error)) from error return await _run(copies.prefetch, await _root(session, project_id), item) diff --git a/resources/tester/test_item_copy_complete.py b/resources/tester/test_item_copy_complete.py index 3367b7b8..1546f92c 100644 --- a/resources/tester/test_item_copy_complete.py +++ b/resources/tester/test_item_copy_complete.py @@ -119,8 +119,10 @@ def test_지정하면_새_파일까지_복사(world): "폴더": FOLDER, "이름": "관공 관매설", "원본": PIPE, + "원본판": manifest["원본판"], "가져온날": manifest["가져온날"], "마스터바뀜": False, + "원본없음": False, "구조물": ["pipe@40.0"], "고침": False, } @@ -330,6 +332,11 @@ def test_길_권한_남의_프로젝트는_403(world, monkeypatch): assert client.get(f"{base}/{FOLDER}/모름").status_code == 422 assert client.post(f"{base}/{FOLDER}/calc").json()["멈춤"] assert client.post(base, json={"summary_item": {"layer": "x"}}).status_code == 400 + made = client.post(base, json={"layer": "system", "owner": None, "key": "SS000022"}).json() + assert made["폴더"] == "system_none_SS000022" and made["만듦"] is True + names = {i["폴더"]: i["이름"] for i in client.get(base).json()["항목"]} + assert names["system_none_SS000022"] == "돌쌓기 (찰·메마다) H=(높이마다)" # 층 SS 줄 이름 + assert client.post(base, json={"layer": "system", "key": "SS999999"}).status_code == 404 session["company_id"] = 4 assert client.get(base).status_code == 403 assert client.post(f"{base}/{FOLDER}/refresh").status_code == 403