From 29265815b48e0c20c8ffa2da95ad3c9119e9c516 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 24 Sep 2026 10:07:32 +0900 Subject: [PATCH] =?UTF-8?q?fix(m01):=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=88=98=EC=8B=9D=20=E3=80=8C=EB=8B=A8=EA=B0=80=20*=20(?= =?UTF-8?q?=EC=88=98=EB=9F=89=20=ED=95=A9)=E3=80=8D=20=EA=B4=84=ED=98=B8?= =?UTF-8?q?=20=E2=80=94=20=EC=9D=B4=EB=A6=84=20=EC=8B=9D=20=C2=B7=20?= =?UTF-8?q?=EA=B0=92=20=EC=8B=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 수량식 맨 위가 덧셈·뺄셈이면 단가 곱 뒤에 괄호(GC000467 촌락지대가) - 번호 별칭은 이름글에서 그대로 둠 — 별칭표 이름이어야 되돌려짐(찾은 줄 이름은 되돌리기 안 됨) · 시험으로 고정 - 조각은 수량식만이라 그대로 검증: test_m01_text 25개 통과 · 1,354 금액 변화 0 · 값 없이 멈춤 0 · GC000467 식에 괄호. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01V1MKKZKpUHTPKb513FneU8 --- resources/master_data/scripts/master_text.py | 23 +++++++++++++++-- resources/tester/test_m01_text.py | 27 ++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/resources/master_data/scripts/master_text.py b/resources/master_data/scripts/master_text.py index d13492c3..ed121e79 100644 --- a/resources/master_data/scripts/master_text.py +++ b/resources/master_data/scripts/master_text.py @@ -352,11 +352,29 @@ def _shares(line: dict) -> list[tuple[str, Decimal]]: return [(part, money) for part, money in parts if money != 0] or parts[:1] +def _loose(node, env: dict, master) -> bool: + """수량식 맨 위가 덧셈·뺄셈이면(곱 뒤에 오면 괄호가 필요함) · 만약은 걸린 갈래 기준.""" + if node[0] == "bin": + return _PREC[node[1]] < _PREC["*"] + if node[0] == "call" and node[1] == "만약": + try: + at = 1 if mf.evaluate(node[2][0], env, master, 0) else 2 + except _CALC_ERRORS: + return False # 이름 꼴은 「만약(…)」 부르기 그대로라 괄호 불필요 + return _loose(node[2][at], env, master) + return False + + +def _times(text: str, item: dict, env: dict, master) -> str: + """「단가 * 수량식」 의 수량식 — 덧셈·뺄셈 묶음이면 괄호.""" + return f"({text})" if _loose(mf.parse(item["수량"]), env, master) else text + + def _line_text(item: dict, line: dict, env: dict, master, part: str | None) -> str: """「단가 * 수량식 = 금액」 — 비목이 나뉜 로직 줄은 그 비목 몫만 · 빈 줄(금액 null)은 빈 글.""" if line["금액"] is None: return "" - qty = render(mf.parse(item["수량"]), env, master) + qty = _times(render(mf.parse(item["수량"]), env, master), item, env, master) money = line["비목"][part] if part else line["금액"] price = line["단가"] if part is None or line["수량"] == 0 else money / line["수량"] return f"{fmt_money(price)} * {qty} = {fmt_money(money)}" @@ -368,7 +386,8 @@ def _line_names(item: dict, line: dict, env: dict, master, part: str | None, nam tail = PRICE_TAIL.get(str(item.get("종류") or ""), "단가") price = f"{name}{part or ''}{tail}" money = f"{name}{part}" if part else f"{name}비" - return f"{price} * {_names(item['수량'], env, master, name_of)} = {money}" + qty = _times(_names(item["수량"], env, master, name_of), item, env, master) + return f"{price} * {qty} = {money}" def _extra_text(extra: dict, line: dict, env: dict, master) -> str: diff --git a/resources/tester/test_m01_text.py b/resources/tester/test_m01_text.py index b00a43d4..a373a04c 100644 --- a/resources/tester/test_m01_text.py +++ b/resources/tester/test_m01_text.py @@ -331,3 +331,30 @@ def test_빈_줄은_빈_글이고_소계는_빈_줄_빼고_더한다(client: Tes blank = [one for one in _all_lines(got) if one["금액"] is None] assert blank and all(one["글"] == "" and one["까닭"] for one in blank) assert Decimal(str(got["계"])) == Decimal(str(calc["sums"]["계"])) + + +def test_곱_뒤_덧셈_묶음은_괄호로_이름_식_값_식_모두() -> None: + """GC000467 촌락지대가 — 노임 * (아홉 항 합) · 첫 항에만 곱하는 것처럼 읽히지 않게.""" + files = store.cm.load(folder=REAL) + row = store.cm.master(REAL).logic("GC000467") + given = {n: store._dec(v) for n, v in store.auto("GC000467")["값"].items()} + got = mt.lines(files, "GC000467", given) + one = _all_lines(got)[0] + assert one["글"].startswith("295,138 * (1 + 1 + 0.5") and " = 3,984,363" in one["글"] + left = one["이름글"].split(" = ")[0] + assert left.startswith("중급기술자건설노임 * (") and left.endswith(".수량)") + assert Decimal(str(one["금액"])) == Decimal("295138") * Decimal("13.5") + blank = mt.lines(files, "GC000467", {}) + assert _all_lines(blank)[0]["이름글"].split(" = ")[0].count("(") == 1 # 값 없이도 그대로 + assert row # 로직이 있음 + + +def test_이름글_번호_별칭은_별칭표로_되돌려진다() -> None: + """이름글 속 별칭(가1~가9)은 별칭표 이름 그대로여야 식으로 되돌려짐(찾은 줄 이름 X).""" + files = store.cm.load(folder=REAL) + given = {n: store._dec(v) for n, v in store.auto("GC000467")["값"].items()} + got = mt.lines(files, "GC000467", given) + left = _all_lines(got)[0]["이름글"].split(" = ")[0].split(" * ", 1)[1].strip("()") + raw_of = {m["이름"]: m["원문"] for m in got["별칭"]} + back = mt.mc._unmask(left, raw_of) + assert back.count("찾기(") == 9 and "촌락지대가" not in back