"""M02 항목 선택 입력(PLAN 25-1b · 25-2a) — 입력 목록 · 항목 연결 파일 · 알약. 항목 = 구조물집계표조합(SS) 한 줄 → 그 열의 일위대가(UA) 조합 → 담은 단가산출 로직마다 입력 전부. 로직마다 갈라 보이고 이름이 같아도 합치지 않음(로직마다 값이 다를 수 있음) · 입력 목록은 그 로직 줄의 `입력` 그대로(하위 로직으로 넘겨지는 입력은 맨 위 로직 입력으로만 · M01 조합 미리 보기와 같음) · 자동값 = `master_auto.values`(보기용 · 저장 안 함). 항목 연결 파일 = 층 폴더 `inputs/<열id>.json` = `{판, 줄: [{로직, 입력, 자리, 알약?}]}` — 판은 저장마다 1 씩 늘고, 연결만 적음(값은 프로젝트 몫). 자리 면적 · 재료 = 그 시트의 알약 (키 `입력|{로직}|{입력}`) · 선택 = 「선택 입력」 카드 · null = 안 씀. M01 은 읽기만(`M01_MasterData_Store` · `_Store_Combo` · `master_auto`) · 권한은 부르는 쪽 몫. """ from __future__ import annotations import json from pathlib import Path from typing import Any from common_util.common_util_json import atomic_write_json from M01_MasterData import M01_MasterData_Store_Combo as combo_store from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete.M02_Labor_Inputs import _num, combo_logics PLACES = ("면적", "재료", "선택", None) PILL_HEAD = "입력" def pill_key(logic: str, name: str) -> str: return f"{PILL_HEAD}|{logic}|{name}" def _item(key: str) -> dict[str, Any]: """지금 층(`store.at`)의 항목 줄 — 없으면 404.""" row = store.find_combo(key) # 옛 키(회사 · 개인 층 SS)로도 if row is None: raise store.StoreError(404, f"없는 항목 「{key}」") return row def _combo_row(ua: str) -> dict[str, Any] | None: return combos.find(ua) # 마스터 UA · 회사 UC · 개인 UP(27-2c) def inputs(key: str) -> dict[str, Any]: """25-1b 입력 목록 — `{일위대가, 이름, 로직: [{로직, 이름, 결과단위, 입력: [...]}], 빠진로직}`. 일위대가 없는 항목은 로직 빈 목록 · 몸은 `M02_Labor_Inputs`(일위대가 입력 모달과 한 벌).""" return labor_inputs.inputs_of(_item(key).get("일위대가")) # ── 항목 연결 파일 ───────────────────────────────────── def read_file(column: str) -> dict[str, Any] | None: """지금 층의 `inputs/<열id>.json` 그대로 — 없으면 None(열 지우기 · 복사 · 스냅샷이 씀).""" path = store.inputs_path(column) return json.loads(path.read_bytes()) if path.is_file() else None def write_file(column: str, doc: dict[str, Any]) -> None: """지금 층에 연결 파일을 그대로 씀(복사용 — 검사 · 판 비교 없음).""" path = store.inputs_path(column) path.parent.mkdir(parents=True, exist_ok=True) atomic_write_json(path, doc) def _known(key: str) -> dict[str, set[str]]: """{로직 키: {입력 이름}} — 그 항목 조합이 지금 담은 것.""" _, logics, _ = combo_logics(_item(key).get("일위대가")) return {str(lg.get("키")): {str(s.get("이름")) for s in lg.get("입력") or []} for lg in logics} def read_links(key: str) -> dict[str, Any]: """항목 연결 — 없으면 `{판: null, 줄: []}` · 조합이 바뀌어 사라진 로직 · 입력 줄은 `사라짐: true`.""" doc = read_file(_item(key)["열"]) or {"판": None, "줄": []} known = _known(key) rows = [] for row in doc.get("줄") or []: gone = str(row.get("입력")) not in known.get(str(row.get("로직")), set()) rows.append({**row, "사라짐": True} if gone else row) return {"판": doc.get("판"), "줄": rows} def _clean_rows(key: str, rows: Any) -> list[dict[str, Any]]: """없는 로직 · 입력 · 모르는 자리 · 같은 입력 두 번은 422 · 알약 키는 서버가 붙임.""" if not isinstance(rows, list): raise store.StoreError(422, "「줄」 이 목록 아님") known = _known(key) out, seen, errors = [], set(), [] for at, row in enumerate(rows, start=1): if not isinstance(row, dict): errors.append(f"줄{at} · 묶음 아님") continue logic, name, place = str(row.get("로직") or ""), str(row.get("입력") or ""), row.get("자리") if logic not in known: errors.append(f"줄{at} · 이 항목 일위대가에 없는 로직 「{logic}」") elif name not in known[logic]: errors.append(f"줄{at} · 로직 {logic} 에 없는 입력 「{name}」") elif place not in PLACES: errors.append(f"줄{at} · 모르는 자리 「{place}」 — 면적 · 재료 · 선택 · null") elif (logic, name) in seen: errors.append(f"줄{at} · 같은 입력 두 번 「{logic}|{name}」") else: seen.add((logic, name)) one: dict[str, Any] = {"로직": logic, "입력": name, "자리": place} if place in ("면적", "재료"): one["알약"] = pill_key(logic, name) out.append(one) if errors: raise store.StoreError(422, {"errors": errors}) return out def write_links(key: str, version: Any, rows: Any) -> dict[str, Any]: """항목 연결 저장 — 받은 `판` 이 지금 판(없으면 null)과 다르면 409 · 저장마다 판 + 1.""" column = _item(key)["열"] clean = _clean_rows(key, rows) with store._LOCK: have = (read_file(column) or {}).get("판") if version != have: raise store.StoreError(409, {"stale": [f"inputs/{column}"], "판": have}) doc = {"판": int(have or 0) + 1, "줄": clean} write_file(column, doc) return doc # ── 알약 — 면적산출 · 재료산출 시트 ─────────────────────── def pills(key: str, place: str) -> dict[str, Any]: """그 시트 알약 — 면적이면 조합 물량 알약(M01 `combo_pills` · 권한 무관 읽기) + 자리를 그 시트로 이은 입력 알약 · 재료면 이은 입력 알약만. 입력 알약 = `{키, 이름, 단위, 로직, 입력}` — 이름은 로직마다 갈라 「입력 · 로직 이름」(같은 이름 입력이 여러 로직에 있어도 안 합침).""" if place not in ("면적", "재료"): raise store.StoreError(422, f"모르는 자리 「{place}」 — 면적 · 재료") ua = _item(key).get("일위대가") quantity: list[dict[str, Any]] = [] if place == "면적" and ua and (combo := _combo_row(str(ua))): quantity = combo_store.combo_pills(combo)["물량"] _, logics, _ = combo_logics(ua) by_key = {str(lg.get("키")): lg for lg in logics} linked = [] for row in read_links(key)["줄"]: if row.get("자리") != place or row.get("사라짐"): continue logic = by_key[str(row["로직"])] spec = next(s for s in logic.get("입력") or [] if s.get("이름") == row["입력"]) short = combo_store._TAIL.sub("", str(logic.get("이름") or "")) or row["로직"] linked.append( { "키": pill_key(row["로직"], row["입력"]), "이름": f"{row['입력']} · {short}", "단위": spec.get("단위"), "로직": row["로직"], "입력": row["입력"], } ) return _num({"물량": quantity, "입력": linked}) def copy_between(source: Path, target: Path, source_column: str, target_column: str) -> bool: """다른 층(또는 같은 층) 열의 연결 파일을 새 열로 — 판은 1 부터 · 없으면 아무것도 안 함.""" with store.at(source): doc = read_file(source_column) if doc is None: return False with store.at(target): write_file(target_column, {"판": 1, "줄": doc.get("줄") or []}) return True