From 80c3f72a248e52cbe2232b957107c5b931d8080b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 5 Nov 2026 18:44:03 +0900 Subject: [PATCH] =?UTF-8?q?perf(792-2):=20=EC=8B=9C=ED=8A=B8=20=EB=8B=A4?= =?UTF-8?q?=EC=8B=9C=20=EC=85=88=20=EC=84=9C=EB=B2=84=20=EB=8B=A4=EB=93=AC?= =?UTF-8?q?=EA=B8=B0=20=E2=80=94=20=EC=9E=90=EC=9E=AC=20=EC=83=89=EC=9D=B8?= =?UTF-8?q?=20=C2=B7=20=EC=B0=BE=EA=B8=B0=20=EB=8B=B5=EC=9D=84=20M01=20?= =?UTF-8?q?=EC=83=89=EC=9D=B8=EC=97=90=20=ED=95=9C=20=EB=B2=88=20=C2=B7=20?= =?UTF-8?q?=ED=95=A9=ED=8C=90=201.2~1.6=20s=20=E2=86=92=2062=20ms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `_Over` 덮개가 자재 색인(`_market` · `_by_class`) · 찾기 답(`_found`)을 속 M01 색인에 둠 — 셈마다 3만 줄 다시 안 지음 · M01 파일 자국이 바뀌면 새 색인 = 새로 - `master_pick.search` = 마스터 · 조건 · 지역 · 단위마다 한 번(`_search` 가 옛 몸) · 사본을 줌 - `calc_doc` = M01 `steady()` 안 — 한 셈 동안 파일 자국 한 번(37 번 → 1) - 측정(8001 · 10 번 중앙값 · 옵션/상수/식/변수 줄) 합판 62/62/62/66 · 지게 30 · 굴착기 31~36 · 봉화 27 ms · 금액 전후 같음 - 엔진 일곱 번은 남김 — 한 번 ~5 ms · 목표 아래 - 시험 test_792_2_calc_speed(색인 다시 안 지음 · 자국 바뀌면 다시 · 층 섞임 없음 · 셈 답 같음) Co-Authored-By: Claude Opus 5.5 --- M02_MasterTemplete/M02_Sangeun_Calc.py | 30 +++-- resources/master_data/scripts/master_pick.py | 13 ++ resources/tester/test_792_2_calc_speed.py | 122 +++++++++++++++++++ 3 files changed, 155 insertions(+), 10 deletions(-) create mode 100644 resources/tester/test_792_2_calc_speed.py diff --git a/M02_MasterTemplete/M02_Sangeun_Calc.py b/M02_MasterTemplete/M02_Sangeun_Calc.py index 8dd8e61db..ca53d62f8 100644 --- a/M02_MasterTemplete/M02_Sangeun_Calc.py +++ b/M02_MasterTemplete/M02_Sangeun_Calc.py @@ -41,6 +41,7 @@ from M02_MasterTemplete import M02_Sangeun_Views as views COSTS = labor.COSTS Ref = dict[str, Any] +_SHARED = ("_market", "_by_class", "_found") # 마스터에만 따르는 자재 색인 · 찾기 답 class _Over: @@ -55,6 +56,14 @@ class _Over: def __getattr__(self, name: str) -> Any: return getattr(self._whole, name) + def __setattr__(self, name: str, value: Any) -> None: + """자재 색인(`master_material`)은 속 M01 색인에 둠 — 셈마다 다시 안 지음(792-2) · + M01 파일 자국이 바뀌면 새 색인 = 새로 지음 · 그 밖은 이 덮개에만.""" + if name in _SHARED: + setattr(self._whole, name, value) + else: + super().__setattr__(name, value) + def _fail(e: sd.DocError) -> store.StoreError: return store.StoreError(e.status, str(e)) @@ -570,16 +579,17 @@ def calc_doc(doc: Any, given: dict | None = None, seen: bool = True) -> dict[str `seen` = 보기 덧칸(`글` · `중간`) 지음.""" from M01_MasterData import M01_MasterData_Store as m01 - whole = m01.loaded()[1] - try: - clean = sd.clean(doc, None, whole) - _check_refs(clean, refs.here()) # 저장 전 문서도 위 층만 · 있는 문서 · 순환 422 - me = refs.ref_id({"층": refs.here()[0], "주인": refs.here()[1], "키": clean["키"]}) - line = isinstance(doc, dict) and doc.get("줄모드") is True # 일위대가 줄 모달(750-1) - got = run(clean, whole, given, trail=(me,), detail=True, line=line, seen=seen) - except sd.DocError as e: - raise _fail(e) from e - return const.answer(got, clean, doc, given, (me,)) # 옛 답 + 원래 · 시트(790-2) + with m01.cache.steady(): # 한 셈 동안 M01 파일 자국 한 번(792-2) + whole = m01.loaded()[1] + try: + clean = sd.clean(doc, None, whole) + _check_refs(clean, refs.here()) # 저장 전 문서도 위 층만 · 있는 문서 · 순환 422 + me = refs.ref_id({"층": refs.here()[0], "주인": refs.here()[1], "키": clean["키"]}) + line = isinstance(doc, dict) and doc.get("줄모드") is True # 일위대가 줄 모달(750-1) + got = run(clean, whole, given, trail=(me,), detail=True, line=line, seen=seen) + except sd.DocError as e: + raise _fail(e) from e + return const.answer(got, clean, doc, given, (me,)) # 옛 답 + 원래 · 시트(790-2) def from_line( diff --git a/resources/master_data/scripts/master_pick.py b/resources/master_data/scripts/master_pick.py index 6b9b38599..6f6a2b7c1 100644 --- a/resources/master_data/scripts/master_pick.py +++ b/resources/master_data/scripts/master_pick.py @@ -67,6 +67,19 @@ def _words(term) -> tuple[list[str], list[str]]: def search(master, cond: dict, env: dict | None = None, unit: str | None = None) -> list[str]: + """`_search` 를 마스터 색인 하나에 조건마다 한 번만(792-2) — + 답은 마스터 · 조건 · 지역 · 단위만 따름.""" + place = mm.plain(env[mm.REGION]) if (env or {}).get(mm.REGION) else "" + memo = getattr(master, "_found", None) + if memo is None: + memo = master._found = {} + key = tuple(str(cond.get(k) or "") for k in ("검색어", "구분", "상세구분")) + (place, str(unit)) + if key not in memo: + memo[key] = _search(master, cond, env, unit) + return list(memo[key]) + + +def _search(master, cond: dict, env: dict | None = None, unit: str | None = None) -> list[str]: """검색어 안 자재품목 키 — 낱말이 모두 이름 또는 규격에 든 줄(요소의 구분 · 상세구분 안에서) · 단위는 같은 성격까지(하나도 없으면 다 — 경고가 붙음) · 고르는 차례: 값 있는 줄 → 줄 단위와 똑같은 단위 → 지역 낱말(입력 `자재지역` 포함)과 맞는 줄 → 키 차례.""" diff --git a/resources/tester/test_792_2_calc_speed.py b/resources/tester/test_792_2_calc_speed.py new file mode 100644 index 000000000..3c61d56a3 --- /dev/null +++ b/resources/tester/test_792_2_calc_speed.py @@ -0,0 +1,122 @@ +# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀 +"""792-2 시트 다시 셈 서버 다듬기 — 자재 색인 · 찾기 답을 M01 색인에 한 번만. + +- 셈마다 새 `_Over` 덮개를 지어도 자재 색인(`_market` · `_by_class`) · 찾기 답(`_found`)은 속 M01 + 색인에 붙음 — 두 번째 셈은 다시 안 지음 +- M01 파일 자국이 바뀌면 새 색인 = 새로 지음(지문 열쇠 = `M01_MasterData_Store_Cache.loaded`) +- 층 섞임 없음 — 기억은 M01 자재품목 키만 · 개인 자재(PM)는 그때 층에서만 찾음 +- 셈 답 같음 — 기억한 찾기 = 날 찾기 · SK 금액 견본(합판거푸집 51,419 · 공구손료 2 % 51,802) +""" + +from __future__ import annotations + +import json +import os +from decimal import Decimal + +from M01_MasterData import M01_MasterData_Store as m01 +from M01_MasterData import M01_MasterData_Store_Cache as cache +from M02_MasterTemplete import M02_Labor_Doc as labor +from M02_MasterTemplete import M02_MasterTemplete_Store as store +from M02_MasterTemplete import M02_Material_Own as own +from M02_MasterTemplete import M02_Sangeun_Calc as calc +from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층 + +mm = m01.cm.mf.mp.mm +mp = m01.cm.mf.mp +F211 = { + "자재지역": "포항", + "유형": "보통", + "사용횟수": "6회", + "높이감률": 0, + "지붕슬래브": "아님", + "폼타이": "아님", + "폼타이규격": "12.7㎜", + "측압": "3t/㎡", + "구멍땜": 0, +} + + +def _sk(**more): + doc = {"키": "SK-000001", "이름": "속도", "단위": "식", "바탕": {"로직": "GC000211"}} + return {**doc, "입력값": F211, **more} + + +def test_색인은_M01_색인에_한_번_두번째_셈은_안_지음(monkeypatch): + first = calc.calc_doc(_sk()) + whole = m01.loaded()[1] + held = {n: vars(whole)[n] for n in ("_market", "_found")} + fresh = mp._search + found = [] + monkeypatch.setattr(mp, "_search", lambda *a: found.append(a) or fresh(*a)) + again = calc.calc_doc(_sk()) + assert again["기준"] == first["기준"] and first["기준"]["계"] == 51419 + assert all(vars(whole)[n] is v for n, v in held.items()) + assert found == [] # 같은 조건 찾기 = 기억한 답(자재품목 3만 줄을 안 훑음) + over = calc._Over(whole, "SK-000001", {}) + assert over._market is whole._market and "_market" not in vars(over) + over.mine = 1 # 그 밖 칸은 덮개에만 + assert "mine" not in vars(whole) + + +def test_M01_파일이_바뀌면_새_색인_새로_지음(tmp_path): + def write(rows): + path = tmp_path / "자재품목.json" + path.write_text(json.dumps({"그룹": "재료", "줄": rows}, ensure_ascii=False), "utf-8") + return path + + one = {"키": "MT000001", "이름": "합판", "규격": "12mm", "단위": "장", "물가자료": 100} + two = {**one, "키": "MT000002", "이름": "각재"} + try: + write([one]) + old = cache.loaded(tmp_path)[1] + assert [k for *_, k in mm._market_index(old)] == ["MT000001"] + path = write([one, two]) + os.utime(path, ns=(1, 1)) # 자국(시각 · 크기)이 꼭 바뀌게 + new = cache.loaded(tmp_path)[1] + assert new is not old and not hasattr(new, "_market") + assert [k for *_, k in mm._market_index(new)] == ["MT000001", "MT000002"] + assert cache.loaded(tmp_path)[1] is new # 안 바뀌면 그대로 + finally: + cache.forget() # 다음 시험은 M01 폴더를 다시 읽음 + + +def test_기억한_찾기_답은_날_찾기와_같고_사본을_줌(): + whole = m01.loaded()[1] + conds = [ + ({"검색어": "합판 12mm", "구분": "목재"}, {}, "㎡"), + ({"검색어": "철선"}, {mm.REGION: "포항"}, "kg"), + ({"검색어": "레미콘"}, {mm.REGION: "서울"}, None), + ] + for cond, env, unit in conds: + got = mp.search(whole, cond, env, unit) + assert got == mp._search(whole, cond, env, unit) + got.append("MT999999") + assert "MT999999" not in mp.search(whole, cond, env, unit) + assert mp.search(whole, conds[1][0], {mm.REGION: "서울"}, "kg") == mp._search( + whole, conds[1][0], {mm.REGION: "서울"}, "kg" + ) + + +def test_층_섞임_없음_개인_자재는_그때_층에서만(world): + calc.calc_doc(_sk()) # M01 색인에 기억이 생긴 뒤 + whole = m01.loaded()[1] + assert all(k.startswith(mm.MARKET) for *_, k in whole._market) + assert all(k.startswith(mm.MARKET) for keys in whole._found.values() for k in keys) + with store.at(world["personal"]): + code = own.add(world["personal"], {"이름": "내 자재", "단위": "개", "값": 1234})["키"] + mine = labor.material_price(whole, code, "개") + other = world["personal"].parent.parent / "43" / world["personal"].name + other.mkdir(parents=True) + with store.at(other): + theirs = labor.material_price(whole, code, "개") + assert Decimal(str(mine["기준단가"])) == 1234 + assert theirs["기준단가"] is None + assert code not in whole.index.get(code[:2], {}) + + +def test_상수_덮개_셈_답_전후_같음(): + got = calc.calc_doc(_sk(상수덮개={"pct:공구손료 및 경장비#0": {"0": 2}})) + assert got["기준"]["계"] == 51802 + assert got["원래"]["계"] == 51419 + assert got["시트"]["자리"]["pct:공구손료 및 경장비#0"]["덮개"] == ["상수"]