From 4e7ebf7f87ca90f33455b815a2cbc75e4904aef1 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 9 Nov 2026 13:24:28 +0900 Subject: [PATCH] =?UTF-8?q?feat(812-4):=20=EC=97=B0=EA=B2=B0=20=EC=B0=BE?= =?UTF-8?q?=EA=B8=B0=20=EC=9C=A8=20=EA=B0=92=20=3D=20=EC=B0=B8=EA=B3=A0=20?= =?UTF-8?q?=E2=80=94=20=EC=9C=A8=20=EC=A4=84=20=ED=8C=90=EC=A0=95=20=3D=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0(=EB=8C=80=EC=83=81=20=C2=B7=20=EB=B9=84?= =?UTF-8?q?=EB=AA=A9)=EB=A7=8C=20=C2=B7=20=EC=9C=A8=20=EA=B0=92=20%=20?= =?UTF-8?q?=EB=8B=A4=EB=A6=84=20=3D=20=ED=99=95=EC=A0=95=20+=20=EC=B0=B8?= =?UTF-8?q?=EA=B3=A0(=EB=B0=94=EA=BE=B8=EB=A9=B4=20=EC=9A=B0=EB=A6=AC=20?= =?UTF-8?q?=EA=B0=92)=20=C2=B7=20=EC=9C=A8=20=EB=8C=80=EC=83=81=20=C2=B7?= =?UTF-8?q?=20=EB=B9=84=EB=AA=A9=20=EB=8B=A4=EB=A6=84=20=3D=20=EB=8B=A4?= =?UTF-8?q?=EB=A5=B8=20=ED=95=AD=20=EA=B7=B8=EB=8C=80=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - M02_Doc_Match_Note.rate_pairs → (다른 항, 참고) · 「율 값」 다른 항 폐지(EXPLAINED 에서도 뺌) · 띠 참고 요약에 「율 값 다름 N 줄」 - 시험: test_812_2 율 값 = 참고로 고침 · test_812_4 새(리퍼 105 · 275 확정 + 참고 · 율 대상 다름 = 다른 항 · 바꾸기 뒤 M01 율 · 되돌리기) - 전수 2,220: 확정 152 → 163 · 근접 588 → 577 · 없음 1,480 · 확정 다시 셈 다름 0 --- M02_MasterTemplete/M02_Doc_Match.py | 7 +- M02_MasterTemplete/M02_Doc_Match_Note.py | 26 +++--- M02_MasterTemplete/logic/M02_Sangeun_Match.ts | 8 +- resources/tester/test_812_2_match_logic.py | 8 +- resources/tester/test_812_4_rate_value.py | 79 +++++++++++++++++++ 5 files changed, 110 insertions(+), 18 deletions(-) create mode 100644 resources/tester/test_812_4_rate_value.py diff --git a/M02_MasterTemplete/M02_Doc_Match.py b/M02_MasterTemplete/M02_Doc_Match.py index b17f70508..debdf6b48 100644 --- a/M02_MasterTemplete/M02_Doc_Match.py +++ b/M02_MasterTemplete/M02_Doc_Match.py @@ -8,7 +8,7 @@ 값이 가져온 줄 수량 · 단가 묶음에 있거나 0(꺼진 줄)인 값만 남김) → 남은 조합(상한 `CAP_ALL`)을 문서 셈 · 입력 변수 후보 = 기본값 · 0 · 가져온 식에 나온 수 · 선택은 가져온 글에 든 값부터. ③ 판정 = 줄 대조 = 로직(812-2 · 요구 712 — `M02_Doc_Match_Note`): 다른 항 = 자원 · 기계 규격 · 수량 · - 비목 · 율 구조(율 = 비율) · 줄 없음 · 이름 · 참고 = 단가 · 끝수(값 — 확정을 안 막음) — + 비목 · 율 구조(대상 · 비목) · 줄 없음 · 이름 · 참고 = 단가 · 끝수 · 율 값 %(값 — 확정을 안 막음) — 다른 항 0 = 「확정」 · 같은 줄이 하나 넘고 핵심 줄 반 넘게 짝 = 「근접」(다른 항 목록) · 그 밖 「없음」. 결과 `{판정, 키, 이름, 옵션, 다른, 참고, 금액 {가져온, 우리}, 후보수}` = 가져온 식 원본 `연결후보`. """ @@ -462,7 +462,9 @@ def compare(mine: list[dict], ours: list[dict]) -> dict[str, Any]: diffs.append( {"항": "기계 규격", "글": f"{a['이름']} {a['코드']} ↔ {b['코드']} · 수량 다름"} ) - diffs += note.rate_pairs(left, right, take) + rate_diffs, rate_notes = note.rate_pairs(left, right, take) + diffs += rate_diffs + notes += rate_notes for side, rows in (("가져온 쪽만", left), ("우리 쪽만", right)): for x in rows: if x["갈래"] == "율": @@ -486,7 +488,6 @@ EXPLAINED = ( "단위 환산", "기계 규격", "비목", - "율 값", "율 줄", "공구손료 유무", ) diff --git a/M02_MasterTemplete/M02_Doc_Match_Note.py b/M02_MasterTemplete/M02_Doc_Match_Note.py index 8b72ba8db..8a9f42e17 100644 --- a/M02_MasterTemplete/M02_Doc_Match_Note.py +++ b/M02_MasterTemplete/M02_Doc_Match_Note.py @@ -1,8 +1,8 @@ """연결 찾기 — 로직 차이(다른 항) 대 값 차이(참고) 가르기(PLAN 812-2 · 요구 712) — `M02_Doc_Match` 가 부름. 확정을 막는 「다른 항」 = 로직 차이만 — 자원 갈래 · 기계 규격 · 노임 종류 · 자재 이름 종류 · 비목 · 산출 단가 · - 율 구조(있고 없음 · 대상 비목 · 율 = 비율) · 수량(옵션 풀이 뒤 · 원 단위 같아야 함). -「참고」 = 값 차이 — 인력 · 재료 · 기계 시간당 단가 · 끝수(합만 다름) · 그로 인한 비목 금액 · 계. + 율 구조(있고 없음 · 대상 비목 — 율 값 % 는 참고) · 수량(옵션 풀이 뒤 · 원 단위 같아야 함). +「참고」 = 값 차이 — 인력 · 재료 · 기계 시간당 단가 · 끝수(합만 다름) · 율 값(%) · 그로 인한 비목 금액 · 계. 율 = 율 줄 셈값 ÷ 대상 금액 합 — 대상 = 율 식이 (거쳐서) 부르는 금액 줄 · 비목 이름이면 그 비목 몫 · 번지 합 = 핵심 줄 전부 · 대상 금액 끝수 자리 차이만 지움(유효 6 자리 · 두 바탕 중 짧은 꼴) — 지어 맞추기 아님. """ @@ -192,9 +192,10 @@ def _same_stuff(a: dict, b: dict) -> bool: return bool(_grams(a["이름"]) & _grams(b["이름"])) -def rate_pairs(left: list[dict], right: list[dict], take: Any) -> list[dict]: - """율 줄 짝 — 같은 구조(율 · 대상 · 비목) 먼저 · 그다음 비목만 · 대상만 · 율만 다름 순.""" +def rate_pairs(left: list[dict], right: list[dict], take: Any) -> tuple[list[dict], list[dict]]: + """율 줄 짝 → (다른 항, 참고) — 구조 = 대상 · 비목 · 같으면 로직 같음(율 값 % 다름 = 참고 · 값은 업데이트됨).""" diffs: list[dict] = [] + notes: list[dict] = [] def is_rate(a: dict, b: dict) -> bool: return a["갈래"] == b["갈래"] == "율" @@ -210,11 +211,18 @@ def rate_pairs(left: list[dict], right: list[dict], take: Any) -> list[dict]: def target(x: dict) -> str: return "·".join(f"{k} {c}" for k, c in sorted(x.get("대상") or ())) or "없음" - take(lambda a, b: same(a, b, "율", "대상", "비목")) # 로직 같음 - for a, b in take(lambda a, b: same(a, b, "율", "대상")): + def value(a: dict, b: dict) -> None: + if a.get("율") != b.get("율"): + notes.append({"항": "율 값", "글": f"{name(a, b)} — 바꾸면 우리 값"}) + + take(lambda a, b: same(a, b, "율", "대상", "비목")) # 로직 · 값 같음 + for a, b in take(lambda a, b: same(a, b, "대상", "비목")): # 로직 같음 · 율 값만 다름 + value(a, b) + for a, b in take(lambda a, b: same(a, b, "대상")): diffs.append({"항": "비목", "글": f"율 줄 {name(a, b)} 비목 다름"}) + value(a, b) for a, b in take(lambda a, b: same(a, b, "율")): diffs.append({"항": "율 대상", "글": f"{name(a, b)} 대상 {target(a)} ↔ {target(b)}"}) - for a, b in take(lambda a, b: same(a, b, "대상") or is_rate(a, b)): - diffs.append({"항": "율 값", "글": f"{name(a, b)} · 대상 {target(a)} ↔ {target(b)}"}) - return diffs + for a, b in take(is_rate): + diffs.append({"항": "율 대상", "글": f"{name(a, b)} 대상 {target(a)} ↔ {target(b)}"}) + return diffs, notes diff --git a/M02_MasterTemplete/logic/M02_Sangeun_Match.ts b/M02_MasterTemplete/logic/M02_Sangeun_Match.ts index 4d9beed2c..66da16d97 100644 --- a/M02_MasterTemplete/logic/M02_Sangeun_Match.ts +++ b/M02_MasterTemplete/logic/M02_Sangeun_Match.ts @@ -3,7 +3,7 @@ * 가져온 식 ↔ 우리 M01 기초 연결 판정 띠 자료(PLAN 810-4 · 요구 707 · 709 · 703) — 원문 패널 카드 위. * SK `가져온.원본.연결후보`(서버 `M02_Doc_Match` 가 적음) → 띠 글 · 다른 항 · 단추 하나: * 확정 = [우리 기초 사본으로 바꾸기] · 근접 · 없음 = 단추 없음(703) · 안 찾음 = [연결 찾기] · - * 다른 항 = 로직 차이(확정을 막음) · 참고 = 단가 · 끝수(값 차이 — 「우리 단가로 셈 ○○ 원」 · 812-2) · + * 다른 항 = 로직 차이(확정을 막음) · 참고 = 단가 · 끝수 · 율 값 %(값 차이 — 「우리 단가로 셈 ○○ 원」 · 812-2 · 812-4) · * 바꾼 SK(`가져온보관` 있음) = 「우리 기초 사본 ○○」 + [가져온 식으로 되돌리기] * 화면 · DOM · fetch 없음 * ========================================================================== */ @@ -16,7 +16,7 @@ export interface MatchFound { 이름?: string; 옵션?: Record; 다른?: { 항: string; 글: string }[]; - /** 값 차이(단가 · 끝수) — 확정을 안 막음 */ + /** 값 차이(단가 · 끝수 · 율 값) — 확정을 안 막음 */ 참고?: { 항: string; 글: string }[]; 금액?: { 가져온?: Record; 우리?: Record }; 후보수?: number; @@ -49,13 +49,15 @@ const lines = (d?: { 항: string; 글: string }[]): string[] => const won = (n?: number | null): string => n == null ? "" : `${Math.round(n).toLocaleString("ko-KR")} 원`; -/** 참고 요약 — 「참고: 단가 다름 3 줄 · 우리 단가로 셈 53,218 원」 · 참고 없음 = "" */ +/** 참고 요약 — 「참고: 단가 다름 3 줄 · 율 값 다름 1 줄 · 우리 단가로 셈 53,218 원」 · 참고 없음 = "" */ const noteText = (f: MatchFound): string => { const n = f.참고 ?? []; if (!n.length) return ""; const price = n.filter((x) => x.항 === "단가 차이").length; + const rate = n.filter((x) => x.항 === "율 값").length; const bits = [ price ? `단가 다름 ${price} 줄` : "", + rate ? `율 값 다름 ${rate} 줄` : "", n.some((x) => x.항 === "끝수") ? "끝수 다름" : "", ].filter(Boolean); const ours = won(f.금액?.우리?.계); diff --git a/resources/tester/test_812_2_match_logic.py b/resources/tester/test_812_2_match_logic.py index cb5ee9703..2e39ccc89 100644 --- a/resources/tester/test_812_2_match_logic.py +++ b/resources/tester/test_812_2_match_logic.py @@ -2,7 +2,7 @@ (a) 단가만 다름 = 확정 + 참고 「단가 차이」(원파일 기계경비 묶음 ↔ 우리 기계 = 이름 종류 같을 때) · 끝수만 다름 = 확정 + 참고 「끝수」 -(b) 로직 차이 = 다른 항 — 율 비율 다름(「율 값」) · 기계 규격 · 노임 종류(「자원」) · 산출 단가 · 비목 +(b) 로직 차이 = 다른 항 — 율 대상 · 기계 규격 · 노임 종류(「자원」) · 산출 단가 · 비목 (c) 율 = 비율 — 거창 합판거푸집(블록 41): 가져온 「주자재소계 × 9 %」 = M01 소모자재 9 % 짝 · 율_2 1 % 재료비 ↔ 공구손료 1 % 경비 = 「비목」 · 대전 합판 단가 = 참고 (d) 요구 704 배수관부설 D=800 ↔ GF000179 = 여전히 근접(단위 환산 × 6 · 기계 규격 · 율 줄) @@ -81,9 +81,11 @@ def test_rounding_only_is_sure_with_note() -> None: # ── (b) 로직 차이 = 다른 항 ────────────────────────────────────────────── -def test_rate_ratio_differs_blocks() -> None: +def test_rate_value_is_note_structure_is_logic() -> None: res = dm.verdict([*A, _rate("공구손료", "0.02")], [*A, _rate("공구손료", "0.03")], TOT, TOT) - assert [d["항"] for d in res["다른"]] == ["율 값"] and res["판정"] == dm.NEAR + assert not res["다른"] and res["판정"] == dm.SURE + assert [d["항"] for d in res["참고"]] == ["율 값"] # 율 값(%) 다름 = 참고(812-4) · 로직 같음 + assert "2 % ↔" in res["참고"][0]["글"] and "3 %" in res["참고"][0]["글"] same = dm.verdict([*A, _rate("율", "0.02")], [*A, _rate("공구손료", "0.02")], TOT, TOT) assert same["판정"] == dm.SURE # 율 · 대상 · 비목 같음 = 이름 달라도 같은 율 줄 tgt = dm.verdict( diff --git a/resources/tester/test_812_4_rate_value.py b/resources/tester/test_812_4_rate_value.py new file mode 100644 index 000000000..92acefd57 --- /dev/null +++ b/resources/tester/test_812_4_rate_value.py @@ -0,0 +1,79 @@ +# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀 +"""812-4 지킴 — 율 값(%) 다름 = 참고(값은 M01 값으로 업데이트됨) · 율 대상 · 비목 다름 = 다른 항(로직). + +(a) 원파일 main 블록 105(터파기 · 리퍼 2.42 % ↔ 우리 유압식 리퍼 2 %) · 275(절토면고르기 리퍼) = + 확정 + 참고 「율 값」 (다른 항 0) · 글에 「바꾸면 우리 값」 +(b) 확정 + 참고 블록 [바꾸기] → 금액 = M01 사본 셈(율도 M01 값) · [되돌리기] → 원래 금액 · M01 문서 파일 그대로 +(c) 대상이 다른 율(교량받침 1.14 % ↔ 공구손료 3 % 노임 노무비) = 여전히 다른 항 「율 대상」 +원파일 없으면 skip(다른 PC) — 합성 줄 시험은 test_812_2. +""" + +from __future__ import annotations + +import sys +from decimal import Decimal as D +from pathlib import Path + +import pytest + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[1] +for p in (ROOT, HERE): + if str(p) not in sys.path: + sys.path.insert(0, str(p)) + +import test_810_2_native_doc as t2 # noqa: E402 +import test_810_4_match as t4 # noqa: E402 + +from M02_MasterTemplete import M02_Doc_Match as dm # noqa: E402 +from M02_MasterTemplete import M02_Doc_MatchSk as msk # noqa: E402 +from M02_MasterTemplete import M02_MasterTemplete_Store as store # noqa: E402 +from M02_MasterTemplete import M02_Sangeun_FromHopyo as fh # noqa: E402 +from resources.tester.test_m02_labor_ref import world # noqa: E402, F401 — 같은 임시 층 + + +def _judge(no: int, tag: str = "main") -> dict: + whole, _, index, links = t4.ctx() + try: + out = t2.fn.emit(t2.native("EST", tag), no) + doc = t4.dim.derive(t2.sk_of(out), None, t2.whole()) + except FileNotFoundError: + pytest.skip("원파일 없음") + return dm.match(doc, t4.dim.Links(whole, "우리"), index, links) + + +@pytest.mark.parametrize(("no", "key"), [(105, "GC000089"), (275, "GC000111")]) +def test_rate_value_only_is_sure_with_note(no: int, key: str) -> None: + got = _judge(no) + assert got["판정"] == dm.SURE and got["키"] == key and not got["다른"] + rate = [d for d in got["참고"] if d["항"] == "율 값"] + assert len(rate) == 1 and "바꾸면 우리 값" in rate[0]["글"] and "2 %" in rate[0]["글"] + + +def test_rate_target_differs_stays_logic() -> None: + got = _judge(1533) # 교량받침설치 — 율 대상 기계 · 노임 ↔ 노임 + assert "율 대상" in {d["항"] for d in got["다른"]} and got["판정"] != dm.SURE + + +def test_swap_rate_note_block_uses_m01_rate(world) -> None: + folder = world["personal"] + est = t2._file(t2.EST["main"]) + before = {p.name: p.read_bytes() for p in dm.ml.FOLDER.glob("수식_*.json")} + with store.at(folder): + hp = fh.upload_native(folder, est.name, est.read_bytes(), t2.WHO)["키"] + name = fh.make(folder, hp, 105)["이름"] + old = store.read("sangeun", name) + money0 = t4._money(name) + found = msk.find(name, old["판"]) + meta = found[dm.KEY] + assert meta["판정"] == dm.SURE and any(d["항"] == "율 값" for d in meta["참고"]) + assert t4._money(name) == money0 # 찾기 = 기록만 + msk.swap(name, found["판"]) + doc = store.read("sangeun", name)["문서"] + want = dm._calc(dm.ml.load_all()[meta["키"]], dm.ml.Links(t2.whole()), dict(meta["옵션"])) + assert t4._money(name) == [want[k] for k in t4.COSTS] # 율도 M01 값 + assert doc["바탕"] == {"로직": meta["키"]} and sum(t4._money(name)) != sum(money0) + msk.back(name, store.read("sangeun", name)["판"]) + assert t4._money(name) == money0 # 되돌리기 = 원래 금액 + assert D(sum(money0)) > 0 + assert before == {p.name: p.read_bytes() for p in dm.ml.FOLDER.glob("수식_*.json")}