From c1181bc9b5f8b8e09eb92972ce64b2fc9801e4c5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 24 Sep 2026 13:20:40 +0900 Subject: [PATCH] =?UTF-8?q?fix(m01):=20=EC=9E=90=EB=A6=AC=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20=EB=93=A0=20=EC=B0=B8=EC=A1=B0=EB=A5=BC=20=EC=A1=B0?= =?UTF-8?q?=EA=B0=81=20=C2=B7=20=EC=9D=B4=EB=A6=84=20=EC=8B=9D=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=9D=B4=EB=A6=84=20+=20=EC=9E=85=EB=A0=A5?= =?UTF-8?q?=EA=B0=92=EC=9C=BC=EB=A1=9C=20(PLAN=205=EC=9E=A5=20=EB=8B=A4?= =?UTF-8?q?=EC=8B=9C=20=EB=8F=8C=EB=A6=BC=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MO:휘발유:{지역} 날글 대신 「휘발유 · 전국평균」 · 입력이 비면 「휘발유 · 지역」 - 조각 참조 = 입력으로 풀린 실제 키 · 못 풀면 참조 없음 - 요소조각 · 조각 · 이름 식 같은 규칙 · 값 식 · 되돌리기는 그대로 - 자동값 · 빈 입력 14,490줄: 날글 48줄(로직 13) → 0 · 금액 변화 0 Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01V1MKKZKpUHTPKb513FneU8 --- resources/master_data/scripts/master_text.py | 56 ++++++++++++++++---- resources/tester/test_m01_text.py | 29 ++++++++++ 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/resources/master_data/scripts/master_text.py b/resources/master_data/scripts/master_text.py index d455f44d..84e30b4a 100644 --- a/resources/master_data/scripts/master_text.py +++ b/resources/master_data/scripts/master_text.py @@ -89,7 +89,7 @@ def named(show, real, env: dict, master, depth: int = 0) -> str: if kind == "name": return show[1] if kind == "ref": - return _elem_name(master, show[1]) + return _elem_name(master, show[1], env) if kind == "field": return f"{named(show[1], real[1], env, master, depth)}.{show[2]}" if kind == "neg": @@ -111,17 +111,48 @@ def named(show, real, env: dict, master, depth: int = 0) -> str: return _value(real, env, master, depth) # 별칭이 안 붙은 부르기 — 값으로 -def _elem_name(master, ref: str) -> str: +def _hole(master, ref: str, env: dict | None) -> tuple[list[str], dict | None]: + """자리 표시(`XX:품목:{입력}` · `XX:{입력}`) 든 참조 → (읽는 글 조각들, 입력으로 풀린 요소 줄). + 글 = 앞 글 + 지금 입력값(「휘발유 · 전국평균」) · 구멍뿐이면 입력 이름이 앞(「장비 · 굴착기」). + 입력이 비면 입력 이름만(「휘발유 · 지역」) · 못 풀면 줄은 None.""" + env = env or {} + tail = ref[3:] + holes = mf._HOLE.findall(tail) + words = [w.strip(": ") for w in mf._HOLE.split(tail)[::2] if w.strip(": ")] + try: + row = master.get(mf.fill(ref, env)) + except mf.FormulaError: + row = None + pure = not words and len(holes) == 1 + shown = [] + for name in holes: + value = env.get(name) + if value is None: + shown.append(name) + elif pure and row is not None: # 입력 하나가 요소 자리 전부 — 그 요소 이름 + 규격 + shown.append(f"{row.get('이름') or value} {row.get('규격') or ''}".strip()) + else: + shown.append(fmt(value) if isinstance(value, Decimal) else str(value)) + head = words or holes[:1] + return head + [one for one in shown if one not in head], row + + +def _elem_name(master, ref: str, env: dict | None = None) -> str: """식 안 요소 한 줄의 이름 — 「석공노임」 꼴(같은 이름의 중간 값과 헷갈리지 않게).""" if ( str(ref)[:2] not in ELEM_KEYS ): # 별칭 이름이 표 열쇠 그대로인 것(QC…) — 밑이름을 풀어쓰지 않음 return ref + tail = ELEM_TAIL.get(str(ref)[:2], "단가") try: - name = mc.short(master.get(ref).get("이름")) + if "{" in str(ref): # 입력으로 풀린 요소 이름 · 못 풀면 입력 이름 + parts, row = _hole(master, ref, env) + name = mc.short(row.get("이름")) if row is not None else mc.plain("".join(parts)) + else: + name = mc.short(master.get(ref).get("이름")) except mf.FormulaError: name = "" - return f"{name}{ELEM_TAIL.get(str(ref)[:2], '단가')}" if name else ref + return f"{name}{tail}" if name else ref def _value(node, env: dict, master, depth: int) -> str: @@ -168,12 +199,17 @@ def _alias_index(marks: list[dict]) -> dict[str, dict]: return {m["이름"]: m for m in marks} -def _elem_piece(master, ref: str) -> dict: - """요소 알약 — 글은 이름(못 찾아도 이름만). 자리 표시(`{…}`) 든 참조 14파일은 - 값(참조 키)을 못 구하므로 이름만 적고 참조 자리는 비움.""" - piece = {"kind": "요소", "글": _elem_name(master, ref)} +def _elem_piece(master, ref: str, env: dict | None = None) -> dict: + """요소 알약 — 글은 이름(못 찾아도 이름만). 자리 표시 든 참조는 글 = 이름 + 지금 입력값 · + 참조 = 입력으로 풀린 실제 키(못 풀면 참조 없음).""" if "{" in str(ref): + parts, row = _hole(master, ref, env) + piece = {"kind": "요소", "글": " · ".join(parts)} + if row is not None: + key = str(row.get("키") or ref) + piece["참조"] = {"테이블": key[:2], "키": key} return piece + piece = {"kind": "요소", "글": _elem_name(master, ref)} try: row = master.get(ref) except mf.FormulaError: @@ -298,7 +334,7 @@ def _pieces(show, real, env: dict, master, alias_by_name: dict, ifdepth: int = 0 if kind == "call" and show[1] == "만약": return _if_pieces(show, real, env, master, alias_by_name, ifdepth) if kind == "ref": - return [_elem_piece(master, show[1])] + return [_elem_piece(master, show[1], env)] if kind == "field" and show[1][0] == "name" and show[1][1] in alias_by_name: return [_alias_piece(alias_by_name[show[1][1]], env, master, show[2])] if kind == "name" and show[1] in alias_by_name: @@ -579,7 +615,7 @@ def _item_pieces(item: dict, env: dict, master) -> list[dict]: name = str(item.get("이름") or "") if isinstance(ref, dict): # 재료 고르기 조건 — 품명만 return [{"kind": "요소", "글": name}] - piece = _elem_piece(master, ref) + piece = _elem_piece(master, ref, env) return [{**piece, "글": name or piece["글"]}] try: node = mf.parse(ref) diff --git a/resources/tester/test_m01_text.py b/resources/tester/test_m01_text.py index 4a2f1e9c..3122a856 100644 --- a/resources/tester/test_m01_text.py +++ b/resources/tester/test_m01_text.py @@ -308,6 +308,35 @@ def test_조각_로직_부르는_줄_990개_세_비목에_다_걸치고_계산_ assert [g["소계"] for g in out["groups"]] == [None] +def test_자리_표시_든_참조는_이름_입력값으로_읽히고_참조는_풀린_키() -> None: + """GF000081 잡품 — 「MO:휘발유:{지역}」 날글 대신 「휘발유 · 전국평균」 · 참조 = 풀린 MO 키.""" + files = store.cm.load(folder=REAL) + master = store.cm.master(REAL) + row = master.logic("GF000081") + + def pills(given: dict) -> tuple[list, str]: + got = mt.lines(files, "GF000081", given) + one = next(o for o in _all_lines(got) if o["이름"] == "잡품") + return [p for p in one["조각"] if p["kind"] == "요소"], one["이름글"] + + filled, name_text = pills(dict(store.cm.시험입력(row))) + assert [p["글"] for p in filled] == ["휘발유 · 전국평균"] + assert filled[0]["참조"]["키"] == "MO000001" and "MO:" not in name_text + blank, name_text = pills({}) + assert [p["글"] for p in blank] == ["휘발유 · 지역"] and "참조" not in blank[0] + assert "{" not in name_text and "MO:" not in name_text + + +def test_자리_표시_참조가_조각_요소조각_이름식에_날글로_남지_않는다() -> None: + files = store.cm.load(folder=REAL) + master = store.cm.master(REAL) + for key in ("GC000138", "GC000211", "GC000267", "GC000559", "GF000081"): + for given in (dict(store.cm.시험입력(master.logic(key))), {}): + for one in _all_lines(mt.lines(files, key, given)): + shown = [one["이름글"], *(p["글"] for p in one["조각"] + one["요소조각"])] + assert not any(re.search(r"[A-Z]{2}:|[{}]", t) for t in shown), (key, shown) + + def test_표_조각_참조_줄은_엔진이_찾은_줄의_차례다() -> None: """GC000010 크레인 — 줄에 없는 조건 칸은 맞음이라 흉내 내면 틀림 · GF000160 합판도.""" files = store.cm.load(folder=REAL)