feat(795-4): B09 사본 규칙 — 만든 때 그대로 · [단가 갱신] 값만 · [재생성] 따로 · 시점 섞임 고침

- 구조물 · 서고 사본 단가 묶음 = M02 일위대가 · SC · SK 로직 사본 줄 먼저(`collect(over)`) · M01 에서 지워진 키 = `사본_남은줄.json`
- 단가 책 새 줄([저장] · 첫 굳힘) = 연결 사본 `단가.json` 자원 줄(구조물 지정 때 값 · `Linked.frozen_rows`) · 없는 자원만 지금 M01 + 책 `now_keys` · 입력 `now` · 알림 한 줄
- POST …/estimation/prices 기본 = 단가 갱신(값만 · 사본 재적용 없음) · job 재생성 = 옛 [업데이트] · 미리 보기 단가 갱신 = 바뀐 자원 · 총괄 · 대가 합 전 → 후
- B09 화면 단추 [업데이트] → [단가 갱신] · 미리 보기 창 [재생성] → 재생성 미리 보기 → [적용] · 안내 글 둘 이름 맞춤
- 옮기기 도구(보기만) — 프로젝트 층 못셈 14 = 「못셈(옛 SP 참조)」 갈래 · B09 구조물 · 서고 사본 대조(그대로 · 재생성 금액)
- 새 시험 test_795_4_b09_copy(8) · 낡은 시험 고침: test_203_1 · test_296 = 단추 이름 「업데이트」 · 호버 글 · 몸 `{ user }` 를 박아 둔 것 → 795-4 이름 · job 덧칸으로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-07 11:54:10 +09:00
co-authored by Claude Opus 5.5
parent 16d7eede50
commit 3e1dc294b2
14 changed files with 560 additions and 99 deletions
+70 -2
View File
@@ -5,7 +5,11 @@
SK = `calc_doc` 기준(노 · 재 · 경 · 계) · SC · 일위대가 = `Labor_Doc.calc` 기준합 · 반영합.
같음 = 파일에 `사본` 칸만 더해 씀(그 밖 칸 그대로) · 다름 · 못 셈 = 안 씀(목록) ·
닿을 것 없음(바탕 참조 SK · 참조 · 손 단가 줄만) = 그대로.
프로젝트 층 일위대가 = 세기 · 대조만(B09 사본 규칙 795-4 와 함께 옮김).
프로젝트 층 일위대가 = 세기 · 대조만(795-4 · 쓰지 않음) — 옛 SP 참조(프로젝트 층은 굳힌
사본만 읽음 · 마스터 SP 를 바로 가리키는 옛 꼴) = 「못셈(옛 SP 참조)」 갈래.
B09 사본(795-4 · 보기만) = 구조물 사본 · 서고 사본 — 만든 때 그대로 셈(옮길 것 없음 ·
상태 「그대로」) ·
`재생성` = 지금 원본으로 다시 가져오면 금액(바뀌면 `재생성다름`) · 못 셈 = 까닭.
되돌릴 길 = 쓰기 전 파일 그대로 `--backup` 폴더에(층 자리 그대로) · `--restore` = 다시 덮음.
./venv/Scripts/python.exe -m M02_MasterTemplete.M02_Copy_Migrate # 보기만(표)
@@ -100,6 +104,65 @@ def _backup_path(path: Path, backup: Path) -> Path:
raise ValueError(f"층 밖 파일 「{path}」")
OLD_SP = "위 층만 가리킴" # 프로젝트 층 문서가 마스터 SP 를 바로 가리킴(굳힌 사본 아님)
LOCAL = ("입력값.json", "고른자재.json", "면적.json") # 재생성 대조 때 사본 값 그대로
def _b09_files() -> list[tuple[str, str, Path]]:
"""(어디, 종류, 폴더) — 프로젝트 구조물 사본 · B09 서고 사본."""
root = layers.storage_root()
out = []
for kind, pattern in (
("구조물사본", "*/*/*/templates/structures/*/manifest.json"),
("서고사본", "*/*/*/B09_Estimation/v1/library/*/manifest.json"),
):
for p in sorted(root.glob(pattern)):
at = p.parents[3] if kind == "구조물사본" else p.parents[4]
out.append((f"B09 {at.name}", kind, p.parent))
return out
def _copy_money(files: dict[str, Any]) -> Any:
from M02_MasterTemplete import M02_Item_Prices as prices
labor = files.get("일위대가.json")
if not isinstance(labor, dict):
return None
got = prices.calc(
labor,
files.get("단가.json") or {},
files.get("입력값.json") or {},
files.get("고른자재.json") or {},
files.get("면적.json"),
)
return _round({k: got.get(k) for k in ("기준합", "반영합")})
def b09_check(kind: str, folder: Path) -> dict[str, Any]:
"""B09 사본 하나(보기만) — {상태: 그대로 | 필요없음 | 못셈, 전, 재생성?, 재생성다름?, 까닭?}."""
from B09_Estimation import B09_Estimation_Bill_Library as lib
from M02_MasterTemplete.M02_Item_Source import build_source, read_json
own = {p.name: read_json(p) for p in folder.glob("*.json")}
manifest = own.get("manifest.json") or {}
try:
before = _copy_money(own)
except Exception as e: # noqa: BLE001 — 못 셈 = 목록
return {"상태": "못셈", "까닭": str(e) or type(e).__name__}
if before is None:
return {"상태": "필요없음"}
try:
if kind == "서고사본":
fresh, _ = lib._build(*lib._source_of(manifest))
lib._apply(fresh, str(manifest.get("종류")), manifest.get("값") or {})
else:
fresh = build_source(manifest["원본"])
after = _copy_money({**fresh, **{n: own[n] for n in LOCAL if n in own}})
except Exception as e: # noqa: BLE001 — 원본 없음 · 못 셈 = 재생성 대조만 빠짐
return {"상태": "그대로", "전": before, "까닭": f"재생성 대조 못 함 — {e}"}
return {"상태": "그대로", "전": before, "재생성": after, "재생성다름": after != before}
def migrate(apply: bool = False, backup: Path | None = None) -> list[dict[str, Any]]:
"""모든 층 — 문서마다 {어디, 종류, 키, 상태, 씀, …} · `apply` = 같음만 씀(프로젝트 층 빼고)."""
if apply and backup is None:
@@ -107,6 +170,8 @@ def migrate(apply: bool = False, backup: Path | None = None) -> list[dict[str, A
out = []
for where_, kind, path, project in _files():
got = check(kind, path)
if project and got["상태"] == "못셈" and OLD_SP in got.get("까닭", ""):
got["상태"] = "못셈(옛 SP 참조)"
copy = got.pop("사본", None)
row = {"어디": where_, "종류": kind, "키": path.stem, **got, "씀": False}
if apply and copy and not project:
@@ -116,6 +181,9 @@ def migrate(apply: bool = False, backup: Path | None = None) -> list[dict[str, A
atomic_write_json(path, {**json.loads(path.read_bytes()), skc.KEY: copy})
row["씀"] = True
out.append(row)
for where_, kind, folder in _b09_files(): # 795-4 B09 사본 = 보기만
out.append({"어디": where_, "종류": kind, "키": folder.name, **b09_check(kind, folder)})
out[-1]["씀"] = False
return out
@@ -149,6 +217,6 @@ if __name__ == "__main__": # 브레인 · 사용자 승인 뒤만 --apply
at = Path(args[args.index("--backup") + 1]) if "--backup" in args else None
rows = migrate("--apply" in args, at)
for one in rows:
if one["상태"] not in ("있음", "필요없음", "같음"):
if one["상태"] not in ("있음", "필요없음", "같음") or one.get("재생성다름"):
print(json.dumps(one, ensure_ascii=False, default=str))
print(json.dumps(table(rows), ensure_ascii=False, indent=1))
+24 -3
View File
@@ -225,12 +225,25 @@ class _Collector:
self.pick(row, item["요소"], item.get("단위"))
#: M01 에서 지워진 사본 줄(795-4) 자리 — `Master` 가 그룹 아닌 「줄」 로 읽음
COPY_FILE = "사본_남은줄.json"
def collect(
logic_keys: list[str], materials: list[str] | None = None, extra: list[dict] | None = None
logic_keys: list[str],
materials: list[str] | None = None,
extra: list[dict] | None = None,
over: list[dict] | None = None,
) -> dict[str, Any]:
"""담은 로직 + 자재(MT)의 닫힌 단가 묶음 — `{파일 이름: 파일(줄은 모은 것만 · 차례 그대로)}` ·
`extra` = 마스터 밖 로직 줄(굳힌 단가산출 719-1) — 그 줄이 닿는 것만 모음(줄은 안 담음)."""
`extra` = 마스터 밖 로직 줄(굳힌 단가산출 719-1) — 그 줄이 닿는 것만 모음(줄은 안 담음) ·
`over` = M02 로직 사본 줄(795-4) — 같은 키는 M01 줄 대신 · M01 에 없는 키 = `COPY_FILE`."""
files, whole = master()
mine = {str(r.get("키")): r for r in over or []}
if mine:
from M02_MasterTemplete import M02_Sangeun_Copy as skc
whole = skc.Copy(whole, skc._rows({"로직": list(mine.values())}))
collector = _Collector(whole)
for key in logic_keys:
collector.logic(key)
@@ -239,11 +252,19 @@ def collect(
for code in materials or []:
collector.add(code)
out: dict[str, Any] = {}
seen: set[str] = set()
for name, data in files.items():
slot = m01.items_key(data)
rows = [row for row in data.get(slot) or [] if str(row.get("키")) in collector.keys]
rows = [
mine.get(str(row.get("키")), row)
for row in data.get(slot) or []
if str(row.get("키")) in collector.keys
]
seen.update(str(row.get("키")) for row in rows)
if rows:
out[name] = {**{k: v for k, v in data.items() if k != slot}, slot: rows}
if left := [r for k, r in mine.items() if k in collector.keys and k not in seen]:
out[COPY_FILE] = {"그룹": "사본", "줄": left}
if collector.missing:
out["_빠진"] = sorted(set(collector.missing))
return out
+17 -1
View File
@@ -180,8 +180,11 @@ def price_bundle(labor: dict[str, Any] | None, root: Path) -> dict[str, Any]:
materials += fixed.link_keys(doc) # 705-1 고정 줄이 잇는 노임 · 자재 · 기계
materials += sk.materials(copies)
wanted = logic_keys + [code for code in materials if code]
over = copy_rows([doc, *copies.values()]) # M02 로직 사본(795-4) = 만든 때 줄 먼저
bundle = (
prices.to_json(prices.collect(logic_keys, materials, sk_rows)) if wanted or sk_rows else {}
prices.to_json(prices.collect(logic_keys, materials, sk_rows, over))
if wanted or sk_rows
else {}
)
if sk_rows:
bundle[sk.FILE] = {"그룹": "로직", "원문": "자체", "줄": sk_rows}
@@ -190,3 +193,16 @@ def price_bundle(labor: dict[str, Any] | None, root: Path) -> dict[str, Any]:
): # 개인 자재(148-3 · 사전조합 것도 175-2) — 원본 목록이 바뀌어도 사본 그대로
bundle[own.BUNDLE] = mine
return bundle
def copy_rows(docs: list[Any]) -> list[dict[str, Any]]:
"""문서 · 굳힌 참조사본(SK 는 `원본`)의 로직 사본 줄(795-4) — 키 겹침 = 앞 문서 것."""
from M02_MasterTemplete import M02_Sangeun_Copy as skc
out: dict[str, dict] = {}
for doc in docs:
for one in (doc, doc.get("원본") if isinstance(doc, dict) else None):
if skc.has(one):
for row in [*one[skc.KEY]["로직"], *(one[skc.KEY].get("단가") or [])]:
out.setdefault(str(row.get("키")), row)
return list(out.values())