diff --git a/resources/master_data/ref/_품셈재료_삭제조건.md b/resources/master_data/ref/_품셈재료_삭제조건.md index cc68c780..b00db5d9 100644 --- a/resources/master_data/ref/_품셈재료_삭제조건.md +++ b/resources/master_data/ref/_품셈재료_삭제조건.md @@ -2,7 +2,7 @@ `재료_품셈재료.json`(품셈재료)을 지울 수 있는 상태인지 정리 — 로직이 아직 이 테이블을 바로 가리키는 줄 세기 · 갈래별 조건 · 삭제 뒤 남길 정보 자리 제안. -## 지금 남은 옛 방식 줄 — 178줄(살아있음, 오늘 날짜로 다시 셈) +## 지금 남은 옛 방식 줄 — 178줄 → **158줄**(2026-09-22 일감24 뒤) `build_로직_재료_고르기.py --보기` 로 로직 파일을 다시 훑은 값(글 셈 · 데이터는 안 고침) — `ref/_남은_옛재료줄.md`(2026-09-21 작성) 과 같음, 드리프트 없음. @@ -10,17 +10,19 @@ |---|---|---| | ① 자재품목에 짝 없음 | 139 | `build_로직_재료_고르기.py --보기` 「옛 방식 그대로」 156 중 모래·자갈·전력 17 을 뺀 나머지 | | ② 지역·계약종별 계산 때 풀림(모래·자갈·전력) | 17 | 로직 키 고정 목록(`ref/_남은_옛재료줄.md` ②) — grep 으로 17줄 그대로 확인 | -| ③ 이름에 `{변수}` 든 참조 | 22 | `grep -o '"요소": "\(MP\|MT\):[^"]*"' 로직_*.json` — 오늘 다시 세도 22(파형강관 규격 `"{관경}"` 줄은 이 22 에 안 든 별개 사례) | +| ③ 이름에 `{변수}` 든 참조 | 22 → **2** | 2026-09-22 일감24 에서 20줄을 고르기 조건으로 옮김 — 아래 「일감24」 절 | ## 갈래별 — 지우려면 무엇이 있어야 하나 - **① 139줄** — `ref/_없는_재료.md`(111) · `ref/_관리자값_미확보.md`(45, 겹침 있음) 의 재료를 자재품목에 채워야(구분·상세구분 확보) `build_품셈재료_고르기.py` → `build_로직_재료_고르기.py` 를 다시 돌리면 자동으로 줄어듦 — 사람이 로직을 직접 고치는 갈래 아님. - **② 17줄** — 모래·자갈은 원문 단위(㎥↔㎏) 환산 계수를 사용자가 확정해야(산림 12장 방식을 건설품셈에 그대로 옮기는 안, `ref/_남은_옛재료줄.md` ② 에 이미 적음) · 전력은 계약종별 입력을 고르기 조건으로 옮길지부터 확정해야 — 둘 다 확정 뒤에 로직 17줄을 고르기 조건으로 바꾸는 작업이 남음. -- **③ 22줄** — 아래 절. +- **③ 22줄 → 2줄** — 아래 두 절. 세 갈래가 다 0 이 된 뒤에도 품셈재료 테이블 자체를 지우려면, 그 테이블이 쥔 「원문 이름 ↔ 자재품목 묶음」 대응표를 다른 자리로 옮겨야 함 — 맨 아래 절. -## ③ `{변수}` 22줄 — 왜 못 바꾸나(셋) · 제안(구현 안 함) +## ③ `{변수}` 22줄 — 2026-09-21 에 적은 까닭(뒤에 판정 바뀜) + +> 아래 절은 2026-09-21 판단 · **2026-09-22 일감24 에서 실제로 시험해 20줄이 바뀜** — 맨 아래 「일감24」 절이 최신. **못 바꾸는 까닭 셋** @@ -40,4 +42,39 @@ - **안 A** — `재료_자재품목.json` 에 「품셈이름」(별칭) 칸을 더해 원문 이름을 구분·상세구분 줄에 직접 적어 둠. 자재품목이 조달청·물가지 원자료 성격이라, 파생 정보(원문 이름표)를 섞으면 원자료 갱신(가격 새로 받기)때 같이 흔들릴 위험. - **안 B(권함)** — 별도 작은 매핑 파일(`재료_이름표.json` 류)을 새로 둠 — 키 = 원문이 부르는 이름, 값 = `{구분, 상세구분, 요구절}`. 자재품목 원자료는 안 건드리고, 품셈재료가 하던 「이름 대응」 역할만 옮김 — 품셈재료보다 훨씬 얇은 표(대응 정보만, 연결·고르기·비고 등 지금 품셈재료가 쥔 중간 작업용 칸은 삭제 시점엔 다 필요 없음). -값·데이터는 이번에 고치지 않음 — 세 갈래 모두 0 이 되기 전엔 품셈재료 테이블은 아직 못 지움(지금도 178줄이 씀). +세 갈래 모두 0 이 되기 전엔 품셈재료 테이블은 아직 못 지움(2026-09-22 현재 158줄이 씀 — 139 + 17 + 2). + + +## 일감24 — 실제로 바꿔 봄 (2026-09-22) + +**엔진은 이미 됨 — 넓힐 것이 없었음.** `master_formula.run` 이 고르기 조건 묶음의 글자 칸을 모두 `fill()` 로 채우고(`{관경}` → `800`), `master_material.static()` 이 `{…}` 든 칸을 검사에서 빼고 있었음 — 2026-09-21 에 「확인 안 함」으로 남긴 전례(GF000176 `"규격": "{관경}"`)가 실제로 도는 길이었음. 표본으로 확인한 뒤 22줄을 옮김. + +| 꼴 | 줄 | 어떻게 바꿨나 | +|---|---|---| +| `MT:{철선}` 류 | 19 | 그 입력의 「고르기」 후보(죄다 자재품목 키)들의 **공통 구분·상세구분**을 조건으로 두고 `"대표": "{철선}"` — 계산 때 채워져 옛 줄과 같은 줄을 고름 | +| `MP:고무링 ∅{관경}㎜` | 1 | `{"구분": "관·이음관", "상세구분": "PVC관", "규격": "D={관경}mm"}` — 관경 800·1000·1200 마다 후보 정확히 1줄(MT009721·23·25), 품셈재료 연결이 가리키던 줄과 같음 | +| `MP:VR관 ∅{관경}㎜` | 1 | **못 바꿈** — 아래 | +| `MP:커플링밴드 ∅{관경}㎜` | 1 | **못 바꿈** — 아래 | + +`MT:{…}` 류의 「구분이 품목마다 달라 한 규칙으로 못 묶음」 은 틀린 판단이었음 — 품목마다 후보 줄에서 구분을 **읽어 오면** 됨(용접봉만 상세구분이 갈려 구분만 씀). + +**아직 못 바꾸는 2줄** + +- **VR관(GF000173)** — 자재품목 VR관 줄의 단위가 「본」, 호표 줄 단위가 「m」. 품셈재료 비고가 「VR관 값은 「본」 — m 환산 안 함」 이라 지금도 값이 비어 **이 로직은 예전부터 멈춤 상태**. 본↔m 환산(관 1본 길이)을 사용자가 확정해야 풀림 — 조건을 넣어도 단위가 달라 후보 0. +- **커플링밴드(GF000176)** — 자재품목이 두 갈래로 적혀 있음: 「파형강관 커플링밴드(관급)」(품명이 **이름** 에, 규격은 `Φ800mm`)와 「파형강관이음관(관급)-거양」(품명이 **규격** 에, `커플링밴드, Φ800mm, 아연도금`). 규격 낱말로 좁히면 한쪽만 걸리고, 규격을 `Φ{관경}mm` 로만 두면 **접속티**(전혀 다른 물건, 값 10배)까지 후보 6줄에 섞임. 고르기 조건에 「이름」 칸이 없어 못 가름 — 스키마에 이름 칸을 더하거나 자재품목 표기를 한 갈래로 맞춰야 함. + +**검사 넓힌 것** + +- `master_formula.check_logic` — 고르기 조건 칸의 `{이름}` 이 그 로직의 입력·중간인지 봄(글자 요소는 원래 보던 것을 조건 묶음에도 붙임). 전에는 조건 묶음이면 그냥 지나가 오타가 계산 때까지 안 잡혔음. +- `master_material.check_pick` — 「구분」 자체가 `{입력}` 이면 좁힐 것이 없어 조건 검사를 건너뜀(옛 코드는 「후보 0」 을 잘못 냈음). + +**앞뒤 견주기** + +| | 앞 | 뒤 | +|---|---|---| +| 시험 계산 통과 | 1,291 | 1,291 | +| 시험 계산 멈춤 | 63 | 63 | +| 금액 달라진 로직 | — | **0** (로직 1,354개 계·결과 값 전수 대조) | +| `check_master 전부` | 틀0 · 본문2 · 로직0 · 조합0 · 단위14 | 같음 | +| 로직의 `MT……` 직접 참조 | 19 | **0** | +| 로직의 `MP……` 직접 참조 | 160 | 158 | diff --git a/resources/master_data/scripts/master_formula.py b/resources/master_data/scripts/master_formula.py index ca735e47..2453ff82 100644 --- a/resources/master_data/scripts/master_formula.py +++ b/resources/master_data/scripts/master_formula.py @@ -686,6 +686,11 @@ def check_logic(master: Master, row: dict) -> tuple[list[str], set]: if isinstance(item["요소"], dict): # 재료 고르기 조건 if item.get("종류") != "재료": out.append(f"{where} · 고르기 조건은 종류 「재료」 만") + # 조건 칸의 {이름} 은 계산 때 채워짐 — 그 이름이 실제 입력·중간인지만 봄 + for value in item["요소"].values(): + for hole in _HOLE.findall(value) if isinstance(value, str) else []: + if hole not in known: + out.append(f"{where} 요소 · 고르기 조건 {{{hole}}} 가 입력·중간 아님") out += check_pick(master, where + " 요소", item["요소"]) continue one(item["요소"], where + " 요소", known) diff --git a/resources/master_data/scripts/master_material.py b/resources/master_data/scripts/master_material.py index 96736bcf..d88a77cd 100644 --- a/resources/master_data/scripts/master_material.py +++ b/resources/master_data/scripts/master_material.py @@ -156,7 +156,7 @@ def check_unit(master: mf.Master, where: str, cond: dict, unit: str | None) -> l rows = master.index.get(MARKET, {}) rep = str(cond.get("대표") or "") if not unit or set(cond) - set(PICK_KEYS) or not cond.get("구분"): - return [] + return [] # 구분·규격이 `{입력}` 이면 좁힐 것이 없어 여기서 뺌 if rep in rows: if plain_unit(rows[rep].get("단위")) != plain_unit(unit): return [ @@ -174,6 +174,8 @@ def check_pick(master: mf.Master, where: str, cond: dict, unit: str | None = Non if set(cond) - set(PICK_KEYS) or not cond.get("구분"): return [f"{where} · 재료 고르기 조건 모양 「{cond}」 — {' · '.join(PICK_KEYS)}"] cond = static(cond) + if not cond.get("구분"): # 구분 자체가 `{입력}` — 계산 때 채워져 여기서 좁힐 것이 없음 + return [] out, rows = [], master.index.get(MARKET, {}) for slot in ("구분", "상세구분"): if cond.get(slot) and not any(r.get(slot) == cond[slot] for r in rows.values()): diff --git a/resources/master_data/로직_건설품셈_02장_가설공사.json b/resources/master_data/로직_건설품셈_02장_가설공사.json index 9c04cbb3..d213574e 100644 --- a/resources/master_data/로직_건설품셈_02장_가설공사.json +++ b/resources/master_data/로직_건설품셈_02장_가설공사.json @@ -2414,7 +2414,7 @@ }, { "종류": "재료", - "요소": "MT:{철선}", + "요소": {"구분": "철근·강재", "상세구분": "철선", "대표": "{철선}"}, "이름": "철선", "단위": "㎏", "수량": "찾기(QC000071, 구분='철선').수량", @@ -2974,7 +2974,7 @@ }, { "종류": "재료", - "요소": "MT:{결속선}", + "요소": {"구분": "철근·강재", "상세구분": "철선", "대표": "{결속선}"}, "이름": "결속선 # 10", "단위": "kg", "수량": "결속선량 / 10", @@ -4010,7 +4010,7 @@ }, { "종류": "재료", - "요소": "MT:{철선}", + "요소": {"구분": "철근·강재", "상세구분": "철선", "대표": "{철선}"}, "이름": "철선", "단위": "kg", "수량": "찾기(QC000133, 구분='철선').수량", @@ -4666,7 +4666,7 @@ }, { "종류": "재료", - "요소": "MT:{못}", + "요소": {"구분": "볼트·앵커·철물", "상세구분": "못", "대표": "{못}"}, "이름": "못", "단위": "kg", "수량": "만약(작업 == '목재 붙이기', 찾기(QC000144, 구분=작업, 자재='못').수량, 0)", diff --git a/resources/master_data/로직_건설품셈_03장_토공사.json b/resources/master_data/로직_건설품셈_03장_토공사.json index 56ea2ef3..c75ed296 100644 --- a/resources/master_data/로직_건설품셈_03장_토공사.json +++ b/resources/master_data/로직_건설품셈_03장_토공사.json @@ -3204,7 +3204,7 @@ }, { "종류": "재료", - "요소": "MT:{각재}", + "요소": {"구분": "목재", "상세구분": "각재·판재", "대표": "{각재}"}, "이름": "각재 10㎝×10㎝", "단위": "㎥", "수량": "찾기(QC000245).각재", diff --git a/resources/master_data/로직_건설품셈_06장_철근콘크리트공사.json b/resources/master_data/로직_건설품셈_06장_철근콘크리트공사.json index 72572211..8056f925 100644 --- a/resources/master_data/로직_건설품셈_06장_철근콘크리트공사.json +++ b/resources/master_data/로직_건설품셈_06장_철근콘크리트공사.json @@ -768,7 +768,7 @@ }, { "종류": "재료", - "요소": "MT:{결속선}", + "요소": {"구분": "철근·강재", "상세구분": "철선", "대표": "{결속선}"}, "이름": "결속선", "단위": "kg", "수량": "찾기(QC000421, Type=Type).사용량", @@ -1137,7 +1137,7 @@ }, { "종류": "재료", - "요소": "MT:{합판}", + "요소": {"구분": "목재", "상세구분": "합판", "대표": "{합판}"}, "이름": "합판(내수 12㎜)", "단위": "㎡", "수량": "찾기(QC000427).합판 * 합판율", @@ -1145,7 +1145,7 @@ }, { "종류": "재료", - "요소": "MT:{각재}", + "요소": {"구분": "목재", "상세구분": "각재·판재", "대표": "{각재}"}, "이름": "각재", "단위": "㎥", "수량": "찾기(QC000427).각재 * 합판율", @@ -2150,7 +2150,7 @@ }, { "종류": "재료", - "요소": "MT:{철선}", + "요소": {"구분": "철근·강재", "상세구분": "철선", "대표": "{철선}"}, "이름": "철선 #8", "단위": "kg", "수량": "찾기(QC000449, 구분='철선').수량", diff --git a/resources/master_data/로직_산림품셈_12장_철근콘크리트.json b/resources/master_data/로직_산림품셈_12장_철근콘크리트.json index d0ed411b..66329ad6 100644 --- a/resources/master_data/로직_산림품셈_12장_철근콘크리트.json +++ b/resources/master_data/로직_산림품셈_12장_철근콘크리트.json @@ -480,7 +480,7 @@ }, { "종류": "재료", - "요소": "MT:{결속선}", + "요소": {"구분": "철근·강재", "상세구분": "철선", "대표": "{결속선}"}, "이름": "결속선 0.9㎜", "단위": "kg", "수량": "만약(구조별 == '간단', 5, 만약(구조별 == '보통', 6.5, 8))", @@ -607,7 +607,7 @@ "호표": [ { "종류": "재료", - "요소": "MT:{합판}", + "요소": {"구분": "목재", "상세구분": "합판", "대표": "{합판}"}, "이름": "합판(내수 12㎜)", "단위": "㎡", "수량": "찾기(QF000357, 구분='합판').기준수량_1회사용 * 재료율", @@ -615,7 +615,7 @@ }, { "종류": "재료", - "요소": "MT:{각재}", + "요소": {"구분": "목재", "상세구분": "각재·판재", "대표": "{각재}"}, "이름": "각재", "단위": "㎥", "수량": "찾기(QF000357, 구분='각재').기준수량_1회사용 * 재료율", @@ -623,7 +623,7 @@ }, { "종류": "재료", - "요소": "MT:{철선}", + "요소": {"구분": "철근·강재", "상세구분": "철선", "대표": "{철선}"}, "이름": "철선", "단위": "kg", "수량": "찾기(QF000357, 구분='철선').기준수량_1회사용 * 재료율", @@ -631,7 +631,7 @@ }, { "종류": "재료", - "요소": "MT:{못}", + "요소": {"구분": "볼트·앵커·철물", "상세구분": "못", "대표": "{못}"}, "이름": "못", "단위": "kg", "수량": "찾기(QF000357, 구분='못').기준수량_1회사용 * 재료율", @@ -639,7 +639,7 @@ }, { "종류": "재료", - "요소": "MT:{박리제}", + "요소": {"구분": "거푸집·가설재", "상세구분": "거푸집", "대표": "{박리제}"}, "이름": "박리제", "단위": "ℓ", "수량": "찾기(QF000357, 구분='박리제').기준수량_1회사용 * 재료율", @@ -2089,7 +2089,7 @@ }, { "종류": "재료", - "요소": "MP:고무링 ∅{관경}㎜", + "요소": {"구분": "관·이음관", "상세구분": "PVC관", "규격": "D={관경}mm"}, "이름": "고무링", "단위": "개", "수량": "찾기(QF000370, 구분='고무링', 관경=관경).적용", @@ -4285,7 +4285,7 @@ "호표": [ { "종류": "재료", - "요소": "MT:{결속선}", + "요소": {"구분": "철근·강재", "상세구분": "철선", "대표": "{결속선}"}, "이름": "결속선(R=0.9㎜)", "단위": "kg", "수량": "찾기(QF000389, 구분='결속선(R=0.9mm)').적용", @@ -4814,7 +4814,7 @@ }, { "종류": "재료", - "요소": "MT:{용접봉}", + "요소": {"구분": "잡재료", "대표": "{용접봉}"}, "이름": "용접봉", "단위": "kg", "수량": "찾기(QF000399, 구분='자재 - 용접봉').적용", @@ -4822,7 +4822,7 @@ }, { "종류": "재료", - "요소": "MT:{철선}", + "요소": {"구분": "철근·강재", "상세구분": "철선", "대표": "{철선}"}, "이름": "철선(#8)", "단위": "kg", "수량": "찾기(QF000399, 구분='자재 - 철선(#8)').적용", @@ -5399,7 +5399,7 @@ "호표": [ { "종류": "재료", - "요소": "MT:{결속선}", + "요소": {"구분": "철근·강재", "상세구분": "철선", "대표": "{결속선}"}, "이름": "결속선(R-0.9㎜)", "단위": "kg", "수량": "찾기(QF000411, 구분='자재 - 결속선(R-0.9mm)').적용", diff --git a/resources/tester/test_masterdata_check.py b/resources/tester/test_masterdata_check.py index 823cd6b3..d8b8b4ef 100644 --- a/resources/tester/test_masterdata_check.py +++ b/resources/tester/test_masterdata_check.py @@ -1012,3 +1012,74 @@ def test_표_조건_조합이_없어_멈추지_않음(): files = cm.load() _, stuck = cm.calc_all(files, mf.Master(files)) assert [x for x in stuck if "맞는 줄" in x] == [] + + +def test_고르기_조건_규격에_입력_변수(whole): + """규격 칸의 `{관경}` 은 계산 때 그 입력값으로 바뀌어 관경마다 다른 줄을 고름(고무링).""" + cond = {"구분": "관·이음관", "상세구분": "PVC관", "규격": "D={관경}mm"} + for 관경, want in ((800, "MT009721"), (1000, "MT009723"), (1200, "MT009725")): + env = {"관경": Decimal(관경)} + filled = {k: mf.fill(v, env) for k, v in cond.items()} + assert mm.candidates(whole, filled, env, "개") == [want] + assert mm.element(whole, filled, env, "개")[1].startswith(want) + + +def test_고르기_조건_대표에_입력_변수(whole): + """대표 칸의 `{철선}` 도 계산 때 채워짐 — 입력이 고른 자재품목 줄이 그대로 단가가 됨.""" + cond = {"구분": "철근·강재", "상세구분": "철선", "대표": "{철선}"} + for key in ("MT000199", "MT000164"): + env = {"철선": key} + filled = {k: mf.fill(v, env) for k, v in cond.items()} + assert mm.pick(whole, filled, env, "kg") == key + assert mm.element(whole, filled, env, "kg") == mm.element(whole, key, env, "kg") + + +def test_고르기_조건_변수는_검사에서_뺌(whole): + """`{입력}` 이 든 칸은 계산 때 채워지므로 조건 검사·단위 검사가 안 잡음.""" + cond = {"구분": "관·이음관", "상세구분": "PVC관", "규격": "D={관경}mm"} + assert mm.check_pick(whole, "시험", cond) == [] + assert mm.check_unit(whole, "시험", cond, "개") == [] + assert mm.check_pick(whole, "시험", {"구분": "{품목}", "대표": "{품목줄}"}) == [] + + +def test_고르기_조건의_모르는_변수를_잡음(whole): + """조건 칸의 `{이름}` 이 그 로직의 입력·중간이 아니면 로직 검사가 잡음.""" + row = { + "키": "GF999998", + "원문번호": "시험", + "입력": [{"이름": "관경", "고르기": [800]}], + "호표": [ + { + "종류": "재료", + "요소": {"구분": "관·이음관", "상세구분": "PVC관", "규격": "D={구경}mm"}, + "이름": "고무링", + "단위": "개", + "수량": "1", + "비목": "재료비", + } + ], + "덧줄": [], + } + found, _ = mf.check_logic(whole, row) + assert any("{구경} 가 입력·중간 아님" in x for x in found) + row["호표"][0]["요소"]["규격"] = "D={관경}mm" + found, _ = mf.check_logic(whole, row) + assert [x for x in found if "입력·중간 아님" in x] == [] + + +def test_변수_참조_줄은_고르기_조건으로_바뀜(whole): + """일감24 — `MT:{철선}` 꼴 옛 참조 19줄을 고르기 조건으로 옮김(남은 2줄은 관·단위 사정).""" + 남음 = [ + (str(row["키"]), item["요소"]) + for data in cm.load().values() + if data.get("그룹") == "로직" + for row in data.get("줄", []) + for item in row.get("호표", []) + if isinstance(item.get("요소"), str) + and item["요소"][:2] in ("MP", "MT") + and "{" in item["요소"] + ] + assert sorted(남음) == [ + ("GF000173", "MP:VR관 ∅{관경}㎜"), + ("GF000176", "MP:커플링밴드 ∅{관경}㎜"), + ]