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)
|
||||
@@ -0,0 +1,113 @@
|
||||
"""서고 문서 내용 지문 규칙 · 같은 층 · 위 층 찾기(PLAN 733-1a · 요구서_572 3장 1).
|
||||
|
||||
지문 규칙 = 이름 · 본뜬 · 메모 · 바탕판 · 원값 · 고정 출처 뺌 · 수 · 단위 정규화 · 줄 차례 무시 · SC 수량 null ·
|
||||
빈 문서 None · 참조 = 가리킨 문서 지문 · 같은 층 찾기 · 위 층 찾기(임시 층).
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from M02_MasterTemplete import M02_Library_Same as same
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
from resources.tester.test_m02_hopyo_excel import client # noqa: F401
|
||||
|
||||
SK = {
|
||||
"종류": "단가산출",
|
||||
"키": "SK-000001",
|
||||
"이름": "유로폼(보통)",
|
||||
"단위": "m2",
|
||||
"구분": "실무자료",
|
||||
"상세구분": "가.xlsx",
|
||||
"본뜬": {"층": "실무자료", "키": "HP-000001 단산3"},
|
||||
"바탕": {"로직": "GF000113"},
|
||||
"입력값": {"횟수": 6},
|
||||
"열린입력": [],
|
||||
"고름": {},
|
||||
"덮개": {"labor:LB000002#0": {"qty": 0.20, "원값": 0.1}},
|
||||
"더한줄": [
|
||||
{"종류": "인력", "요소": "LB000001", "이름": "보통인부", "단위": "인", "수량": 0.5},
|
||||
{"종류": "로직", "로직": "GC000206", "입력값": {}, "수량": 1, "바탕판": "abc"},
|
||||
],
|
||||
"뺀줄": ["labor:LB000003#0", "machine:EQ000001#0"],
|
||||
"바탕판": "x1",
|
||||
}
|
||||
SC = {
|
||||
"종류": "사전조합",
|
||||
"키": "SC-000001",
|
||||
"이름": "수로BOX 4.0×2.0",
|
||||
"단위": "m",
|
||||
"본뜬": {"층": "실무자료", "키": "HP-000001 제3호표"},
|
||||
"담은로직": [
|
||||
{"로직": "GF000001", "입력값": {"두께": "0.20"}, "수량": {"손": 3}, "메모": "가"},
|
||||
{"고정": {"이름": "잡비", "단위": "%", "비율": True, "출처": {"파일": "가"}}, "수량": 3},
|
||||
],
|
||||
"자재": [{"코드": "MT000001", "수량": {"손": 2}}],
|
||||
}
|
||||
|
||||
|
||||
def _put(folder, kind, key, doc):
|
||||
(folder / kind).mkdir(parents=True, exist_ok=True)
|
||||
(folder / kind / f"{key}.json").write_text(json.dumps({**doc, "키": key}), encoding="utf-8")
|
||||
|
||||
|
||||
def test_지문_규칙():
|
||||
f = same.finger
|
||||
base = f("sangeun", SK)
|
||||
assert base and base.startswith("SK:")
|
||||
other = {**SK, "키": "SK-000009", "이름": "유로폼(Euro Form)(6회, 보통)", "상세구분": "나.xls"}
|
||||
other |= {"본뜬": {"층": "실무자료", "키": "HP-000002 단산7"}, "바탕판": "y2", "메모": "다"}
|
||||
other["단위"] = " M2 "
|
||||
other["입력값"] = {"횟수": "6.0"}
|
||||
other["덮개"] = {"labor:LB000002#0": {"qty": "0.2", "원값": 9}}
|
||||
other["더한줄"] = [
|
||||
{"종류": "로직", "로직": "GC000206", "입력값": {}, "수량": "1.00", "바탕판": "zz"},
|
||||
{"종류": "인력", "요소": "LB000001", "이름": "인부(보통)", "단위": "인", "수량": 0.50},
|
||||
]
|
||||
other["뺀줄"] = list(reversed(SK["뺀줄"]))
|
||||
assert f("sangeun", other) == base # 이름 · 본뜬 · 바탕판 · 원값 · 차례 · 수 꼴 · 단위 꼴 뺌
|
||||
for diff in (
|
||||
{"입력값": {"횟수": 4}},
|
||||
{"단위": "m3"},
|
||||
{"덮개": {}},
|
||||
{"뺀줄": []},
|
||||
{"바탕": {"로직": "GF000114"}},
|
||||
{"더한줄": SK["더한줄"][:1]},
|
||||
):
|
||||
assert f("sangeun", {**SK, **diff}) != base, diff
|
||||
assert f("sangeun", {**SK, "바탕": None, "더한줄": []}) is None # 빈 문서
|
||||
# SC — 수량 null(비율 줄 율만) · 고정 출처 · 메모 뺌
|
||||
sc = f("precombo", SC)
|
||||
size = {**SC, "이름": "수로BOX 2.5×2.5", "자재": [{"코드": "MT000001", "수량": {"손": 9}}]}
|
||||
size["담은로직"] = [
|
||||
{**SC["담은로직"][1], "고정": {**SC["담은로직"][1]["고정"], "출처": {"파일": "나"}}},
|
||||
{**SC["담은로직"][0], "수량": {"손": 7}, "메모": "나"},
|
||||
]
|
||||
assert f("precombo", size) == sc
|
||||
rate = [SC["담은로직"][0], {**SC["담은로직"][1], "수량": 5}]
|
||||
assert f("precombo", {**SC, "담은로직": rate}) != sc # 비율 줄 율은 넣음
|
||||
assert f("precombo", {**SC, "담은로직": [], "자재": []}) is None
|
||||
|
||||
|
||||
def test_같은층_위층_찾기_참조_지문(client):
|
||||
me, firm = layers.personal_dir(7, 42), layers.company_dir(7)
|
||||
_put(me, "sangeun", "SK-000001", SK)
|
||||
_put(me, "sangeun", "SK-000002", {**SK, "이름": "다른 이름"})
|
||||
_put(me, "sangeun", "SK-000003", {**SK, "입력값": {"횟수": 4}})
|
||||
_put(me, "sangeun", "SK-000004", {**SK, "바탕": None, "더한줄": []})
|
||||
_put(firm, "sangeun", "SK-000005", {**SK, "이름": "회사 것"})
|
||||
_put(client.system, "sangeun", "SK-000006", SK)
|
||||
new = {**SK, "이름": "새것", "본뜬": {"층": "실무자료", "키": "HP-000003 단산1"}}
|
||||
assert same.find_same(me, "sangeun", new) == ["SK-000001", "SK-000002"]
|
||||
assert same.find_same(me, "sangeun", new, skip="SK-000001") == ["SK-000002"]
|
||||
assert same.find_above(me, "sangeun", new) == [
|
||||
{"층": "company", "주인": "7", "키": "SK-000005"},
|
||||
{"층": "system", "주인": None, "키": "SK-000006"},
|
||||
]
|
||||
assert same.find_above(firm, "sangeun", new) == [
|
||||
{"층": "system", "주인": None, "키": "SK-000006"}
|
||||
]
|
||||
assert same.find_same(me, "sangeun", {**SK, "바탕": None, "더한줄": []}) == []
|
||||
# 참조 = 가리킨 문서 지문 — 층 넘어 같은 내용 사본을 가리키면 같음
|
||||
a = {**SK, "바탕": {"참조": {"층": "company", "주인": "7", "키": "SK-000005"}}, "덮개": {}}
|
||||
b = {**a, "바탕": {"참조": {"층": "system", "주인": None, "키": "SK-000006"}}}
|
||||
c = {**a, "바탕": {"참조": {"층": "personal", "주인": "7/42", "키": "SK-000003"}}}
|
||||
assert same.finger("sangeun", a) == same.finger("sangeun", b) != same.finger("sangeun", c)
|
||||
Reference in New Issue
Block a user