Merge remote-tracking branch 'origin/dev' into sub_laptop_5
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
"""M02 서고 문서 내용 지문 · 같은 것 찾기(PLAN 733-1 · 요구서_572 3장 1 · 서버 단독).
|
||||
|
||||
지문 = 단가산출(SK) · 일위대가(SC) 셈에 닿는 내용만 정규화해 `derived_key` —
|
||||
뺌 = 키 · 이름 · 구분 · 상세구분 · 본뜬 · 메모 · 별칭 · 바탕판 · 원값 · 계산값 · 열 · 일위대가번 · 종류 ·
|
||||
날짜(수정일) · 고정 줄 출처
|
||||
수 = Decimal 정규화(0.20 = 0.2 = 「0.2」) · 글 = 겹 빈칸 정리 · 빈 값 칸 뺌 · 단위 = 소문자 + m3 → ㎥
|
||||
차례 무시 = 더한줄 · 담은로직 · 자재 · 뺀줄 · 열린입력 정렬 · 더한줄 요소 줄 이름 뺌(요소 키가 정체)
|
||||
SC 줄 · 자재 수량 = null(723-1 `refs.no_qty` · 비율 줄 율만) · SK 더한줄 수량은 넣음
|
||||
참조 = 가리킨 문서의 지문(깊이 `refs.DEPTH` · 층 넘어 사본도 같음)
|
||||
빈 문서(SK 바탕 · 더한줄 없음 · SC 줄 · 자재 없음) = 지문 None(검사 안 함).
|
||||
저장 칸 없음 — 그때그때 셈(`Same` 한 벌 = 한 요청 안 메모).
|
||||
`find_same(folder, kind, doc)` = 같은 층 같은 내용 키(작은 번호 = 먼저 만든 것 먼저) ·
|
||||
`find_above(folder, kind, doc)` = 위 층(회사 · 마스터) 같은 내용 참조 `[{층, 주인, 키}]`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_derived_store import derived_key
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
|
||||
DROP = {"키", "이름", "구분", "상세구분", "본뜬", "메모", "별칭", "바탕판", "원값", "계산값", "열"}
|
||||
DROP |= {"일위대가번", "종류", "수정일", "출처"}
|
||||
UNIT = {"m3": "㎥", "m2": "㎡", "m³": "㎥", "m²": "㎡"}
|
||||
EMPTY = (None, "", [], {})
|
||||
|
||||
|
||||
def unit(value: Any) -> str:
|
||||
text = "".join(str(value or "").split()).lower()
|
||||
return UNIT.get(text, text)
|
||||
|
||||
|
||||
def _num(text: str) -> str | None:
|
||||
try:
|
||||
got = Decimal(text.replace(",", ""))
|
||||
except (InvalidOperation, ValueError):
|
||||
return None
|
||||
return format(got.normalize(), "f") if got.is_finite() else None
|
||||
|
||||
|
||||
def norm(value: Any) -> Any:
|
||||
if isinstance(value, dict):
|
||||
out = {str(k): norm(x) for k, x in value.items() if k not in DROP}
|
||||
return {k: x for k, x in out.items() if x not in EMPTY}
|
||||
if isinstance(value, list):
|
||||
return [norm(x) for x in value]
|
||||
if isinstance(value, bool) or value is None:
|
||||
return value
|
||||
if isinstance(value, (int, float)):
|
||||
return _num(str(value))
|
||||
if isinstance(value, str):
|
||||
text = " ".join(value.split())
|
||||
got = _num(text) if text and (text[0].isdigit() or text[0] in "-.") else None
|
||||
return text if got is None else got
|
||||
return value
|
||||
|
||||
|
||||
def _sorted(lines: list) -> list:
|
||||
return sorted(lines, key=lambda x: json.dumps(x, ensure_ascii=False, sort_keys=True))
|
||||
|
||||
|
||||
def _bare(cover: Any) -> dict:
|
||||
return {
|
||||
c: {k: v for k, v in (d or {}).items() if k != "원값"} for c, d in (cover or {}).items()
|
||||
}
|
||||
|
||||
|
||||
class Same:
|
||||
"""지문 한 벌 — 참조 지문 · 층 목록 메모(한 요청 안)."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.memo: dict[str, str] = {}
|
||||
self.lists: dict[tuple[str, str], dict[str, str]] = {}
|
||||
|
||||
# ── 지문 ──────────────────────────────────────────────────────────
|
||||
def finger(self, kind: str, doc: Any, trail: tuple = ()) -> str | None:
|
||||
if not isinstance(doc, dict):
|
||||
return None
|
||||
body = self._sk(doc, trail) if kind == "sangeun" else self._sc(doc, trail)
|
||||
if kind == "sangeun" and not body.get("바탕") and not body.get("더한줄"):
|
||||
return None
|
||||
if kind != "sangeun" and not body.get("담은로직") and not body.get("자재"):
|
||||
return None
|
||||
return f"{refs.PREFIX.get(kind, kind)}:{derived_key(body)}"
|
||||
|
||||
def _ref(self, ref: Any, trail: tuple) -> Any:
|
||||
if not isinstance(ref, dict):
|
||||
return norm(ref)
|
||||
rid = refs.ref_id(ref)
|
||||
if rid not in self.memo:
|
||||
doc = refs.read_ref(ref) if rid not in trail and len(trail) <= refs.DEPTH else None
|
||||
got = self.finger(refs.kind_of(ref.get("키")), doc, (*trail, rid)) if doc else None
|
||||
self.memo[rid] = got or f"없음:{rid}"
|
||||
return {"내용": self.memo[rid]}
|
||||
|
||||
def _line(self, one: Any, trail: tuple, qty: bool) -> Any:
|
||||
if not isinstance(one, dict):
|
||||
return norm(one)
|
||||
line = dict(one)
|
||||
if isinstance(line.get("참조"), dict):
|
||||
line["참조"] = self._ref(line["참조"], trail)
|
||||
if not qty and not refs.is_rate(line):
|
||||
line["수량"] = None
|
||||
if line.get("요소"):
|
||||
line.pop("이름", None)
|
||||
if isinstance(line.get("덮개"), dict):
|
||||
line["덮개"] = _bare(line["덮개"])
|
||||
return norm(line)
|
||||
|
||||
def _sk(self, doc: dict, trail: tuple) -> dict:
|
||||
base = doc.get("바탕")
|
||||
if isinstance(base, dict) and isinstance(base.get("참조"), dict):
|
||||
base = {"참조": self._ref(base["참조"], trail)}
|
||||
body = {
|
||||
"단위": unit(doc.get("단위")),
|
||||
"바탕": norm(base),
|
||||
"입력값": norm(doc.get("입력값") or {}),
|
||||
"환산": norm(doc.get("환산")),
|
||||
"켜쌓기율": norm(doc.get("켜쌓기율")),
|
||||
"열린입력": sorted(map(str, doc.get("열린입력") or [])),
|
||||
"고름": norm(doc.get("고름") or {}),
|
||||
"자재표시": norm(doc.get("자재표시") or {}),
|
||||
"덮개": norm(_bare(doc.get("덮개"))),
|
||||
"더한줄": _sorted([self._line(x, trail, True) for x in doc.get("더한줄") or []]),
|
||||
"뺀줄": sorted(map(str, doc.get("뺀줄") or [])),
|
||||
}
|
||||
return {k: v for k, v in body.items() if v not in EMPTY}
|
||||
|
||||
def _sc(self, doc: dict, trail: tuple) -> dict:
|
||||
mats = [
|
||||
norm({k: v for k, v in m.items() if k != "수량"} if isinstance(m, dict) else m)
|
||||
for m in doc.get("자재") or []
|
||||
]
|
||||
body = {
|
||||
"단위": unit(doc.get("단위")),
|
||||
"담은로직": _sorted([self._line(x, trail, False) for x in doc.get("담은로직") or []]),
|
||||
"자재": _sorted(mats),
|
||||
"규격": norm(doc.get("규격")),
|
||||
}
|
||||
return {k: v for k, v in body.items() if v not in EMPTY}
|
||||
|
||||
# ── 층 훑기 ───────────────────────────────────────────────────────
|
||||
def listed(self, folder: str | Path, kind: str) -> dict[str, str]:
|
||||
"""층 `{키: 지문}`(빈 문서 뺌 · 키 차례)."""
|
||||
at = (str(Path(folder).resolve()), kind)
|
||||
if at not in self.lists:
|
||||
out = {}
|
||||
for path in sorted((Path(folder) / kind).glob(f"{refs.PREFIX[kind]}-*.json")):
|
||||
try:
|
||||
got = self.finger(kind, json.loads(path.read_bytes()))
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
if got:
|
||||
out[path.stem] = got
|
||||
self.lists[at] = out
|
||||
return self.lists[at]
|
||||
|
||||
def add(self, folder: str | Path, kind: str, key: str, doc: Any) -> None:
|
||||
"""새로 쓴 문서를 메모 목록에 더함(다시 훑지 않음)."""
|
||||
got = self.finger(kind, doc)
|
||||
if got:
|
||||
self.listed(folder, kind)[key] = got
|
||||
|
||||
def find_same(
|
||||
self, folder: str | Path, kind: str, doc: Any, skip: str | None = None
|
||||
) -> list[str]:
|
||||
"""같은 층 같은 내용 키(작은 번호 먼저 · `skip` = 자기 키 뺌)."""
|
||||
want = self.finger(kind, doc)
|
||||
if not want:
|
||||
return []
|
||||
return [k for k, v in self.listed(folder, kind).items() if v == want and k != skip]
|
||||
|
||||
def find_above(self, folder: str | Path, kind: str, doc: Any) -> list[refs.Ref]:
|
||||
"""위 층(회사 · 마스터) 같은 내용 참조 `[{층, 주인, 키}]`."""
|
||||
want = self.finger(kind, doc)
|
||||
out: list[refs.Ref] = []
|
||||
for layer, owner in above(refs.where(folder)) if want else []:
|
||||
here = refs.folder_of(layer, owner)
|
||||
for key, got in self.listed(here, kind).items():
|
||||
if got == want:
|
||||
out.append({"층": layer, "주인": owner, "키": key})
|
||||
return out
|
||||
|
||||
|
||||
def above(at: refs.Here) -> list[refs.Here]:
|
||||
"""위 층 — 개인 C/U → 회사 C · 마스터 / 회사 → 마스터 / 마스터 · 프로젝트 → 없음."""
|
||||
layer, owner = at
|
||||
if layer == "personal":
|
||||
return [("company", str(owner).split("/")[0]), ("system", None)]
|
||||
return [("system", None)] if layer == "company" else []
|
||||
|
||||
|
||||
def finger(kind: str, doc: Any) -> str | None:
|
||||
return Same().finger(kind, doc)
|
||||
|
||||
|
||||
def find_same(folder: str | Path, kind: str, doc: Any, skip: str | None = None) -> list[str]:
|
||||
return Same().find_same(folder, kind, doc, skip)
|
||||
|
||||
|
||||
def find_above(folder: str | Path, kind: str, doc: Any) -> list[refs.Ref]:
|
||||
return Same().find_above(folder, kind, doc)
|
||||
Reference in New Issue
Block a user