diff --git a/resources/master_data/ref/_비교_estx.md b/resources/master_data/ref/_비교_estx.md new file mode 100644 index 00000000..789756f6 --- /dev/null +++ b/resources/master_data/ref/_비교_estx.md @@ -0,0 +1,125 @@ +# 상용 적산 자료(ESTX 2026-07) 대 마스터 + +- 대상: `resources/knowledge/original/원가계산/다산소프트/2026-07-main.estx` (다산 EST main 2026년 7월판) +- 스크립트: `resources/master_data/scripts/compare_estx.py` (읽기 전용 · 마스터 파일 안 고침) +- ESTX 비목: 노(NC) = 노무비 · 재(GC) = 재료비 · 경(JC) = 경비. 기계는 경 = 손료 · 재 = 연료+잡품 · 노 = 조종원. + +## 결론 + +1. **노임 — 건설 131 짝 중 117 같음.** 다른 14개는 모두 우리가 「미공표」로 비운 직종. ESTX 는 「2026년(상)-노임증가율반영」으로 값을 지어 넣음. 제조노임은 128 짝 모두 다름 — ESTX 는 중소기업중앙회 2026.1.1 판, 우리는 2026-07-01 판(기준일 차이 · 중앙값 -3.7%). +2. **자재 — 나라장터와 짝 210개뿐(품명 같음 72).** 짝 가운데 같은 값 2개. ESTX 값이 나라장터보다 중앙값 +20% 높음. 어느 출처도 나라장터와 가깝지 않음(출처별 중앙값 +23~+35%). +3. **재료_자체 빈 값 360 중 ESTX 에 같은 품명 98 · 비슷한 품명 43 · 없음 219.** 레미콘·시멘트·모래·이형철근·흄관·파형강관·폼타이는 ESTX 에 있음(지역·규격별 여러 줄). +4. **기계경비 — 손료는 197/198 같음 · 연료는 +1.5% · 조종원은 -40%.** 우리 조종원 = 일노임 ÷ 8. ESTX 조종원 = 일노임 × 1/8 × 16/12 × 25/20(= 0.2083). 그래서 기계경비 전체가 중앙값 -16.7%(임도 기계 -19.2%). +5. **일위대가 28 짝 — 14개 같음(±0.5%) · 17개 ±5% 안.** 인력만 쓰는 품은 거의 같음. 차이는 셋에서 남: ① 기계 조종원 계수 ② ESTX 가 붙인 재료(잔디·비계 자재·페인트) ③ 품 수치 자체가 다른 1건(천연섬유네트). +6. **비목 구분 다름** — 「공구손료 및 경장비」를 ESTX 는 재료비, 우리는 경비로 둠(합계는 같음). + +## (1) 노임 + +| 갈래 | 우리 | ESTX | 짝 | 같음 | 다름 | 한쪽만 | +|---|---|---|---|---|---|---| +| 건설노임 | 132 | 249 | 131 | 117 | 14 | 우리만 1(배관공(수도) 표기) · ESTX만 118(SW·엔지니어링·건설사업관리·원자력 등 다른 조사) | +| 제조노임 | 129 | 129 | 128 | 0 | 128 | 이름 표기 1쌍(금속재료제품시험원 ↔ 금속재료품질시험원) | + +- 건설 다름 14 = 우리 「미공표」(값 없음) 14 전부. 예: 연마공 — ESTX 211,645 · 도편수 516,565 · 준설선선장 244,498. ESTX 비고 「2026년(상)-노임증가율반영」 — 공표값 아님. +- 제조 다름 128: ±5% 안 61 · ±20% 안 111 · 중앙값 -3.7%. 우리 「미공표」 16개도 ESTX 는 값이 있음(옛 판 값 또는 증가율 반영). 예: CAD설계사(기계) 우리 160,065 · ESTX 153,213 · 공업배관원 우리 없음 · ESTX 120,639. +- ESTX 머리 「2026년 상반기 적용 노임단가 — 대한건설협회(2026.1.1) · 중소기업중앙회(2026.1.1)」. 건설은 같은 판, 제조는 앞 판 — 기준일 차이로 설명됨. + +## (2) 자재 + +| 항목 | 값 | +|---|---| +| 나라장터 줄 · 품명 | 6,999 · 371 | +| ESTX 자재 줄 | 23,881 | +| 품명 같음 | 72 | +| 품명·규격 짝 | 210 | +| ESTX 채택값(TC) 대 나라장터 | 같음 2 · ±5% 9 · ±20% 103 · 중앙값 +20.0% | + +출처별(ESTX 가격 칸) 대 나라장터: + +| 출처 | 짝 | ±5% | ±20% | 중앙값 | 가장 가까운 출처로 뽑힌 수 | +|---|---|---|---|---|---| +| 물가자료 | 201 | 6 | 53 | +34.4% | 118 | +| 유통물가 | 196 | 4 | 47 | +35.0% | 32 | +| 물가정보 | 87 | 0 | 33 | +22.7% | 32 | +| 거래가격 | 95 | 0 | 20 | +25.8% | 28 | +| 가격정보 · 견적가 | 0 | - | - | - | - | + +- 가장 가까운 출처는 「물가자료」가 많으나 값이 있는 줄이 많아서임 — 어느 출처도 나라장터 값과 붙지 않음. 나라장터(조달 계약가)가 물가지 값보다 낮은 구조. +- 예: 육각볼트 M6*50 나라장터 39 · ESTX 40(유통물가) · 육각볼트 M12*75 196 · 233.2(물가정보) · 육각볼트 M8*20 36 · 135 — 규격 글자가 같아도 재질·도금이 다른 줄이 섞임(짝 품질 한계). + +재료_자체 빈 값 360: + +| 판정 | 수 | 예 | +|---|---|---| +| ESTX 같은 품명 | 98 | 레미콘(1,071줄 · 서울 25-18-080 91,760/㎥) · 시멘트(43줄) · 모래(36줄 · 서울 강모래 37,000/㎥) · 이형철근(466줄) · 흄관(72줄) · 파형강관(81줄) · 폼타이(1줄) · 모르타르 1:3(서울 119,530/㎥) · 부직포 · 초류종자 · 복합비료 · 폭약 · 중유 | +| 품명 끝 같음(비슷) | 43 | 자갈 → 자연자갈·쇄석자갈 · 잡석 → 깬잡석 · 유공관 → THP유공관 · 뇌관 → 전기뇌관 · 쉬즈관 → 나선형쉬즈관 | +| 없음 | 219 | 원형철근 ∅29㎜ · 강봉 D32㎜ · 철재 거푸집 · 강관동바리 내관·외관 · 조립식 가설건축물 부재 · 조립말비계 부재 · 모래주머니 · 철망태 · 틈메우기돌 · 침식방지안정제 등 | + +- 같은 품명이어도 규격까지 맞는 것은 적음(모르타르 1:3 · 1:1 · 카트리지 화약 정도). 값을 옮기려면 규격·단위(ESTX 는 본·포·조 단위 섞임)를 한 줄씩 봐야 함. + +## (3) 기계경비 + +로직 「건설품셈:공통 8 기계 시간당 경비」(연료·조종원 없는 기계는 같은 절의 「연료 없음」·「조종원 없음」·「손료」 로직) · 지역 전국평균. + +| 범위 | 짝 | 계 ±5% | 계 중앙값 | 손료 같음 | 연료 중앙값 | 조종원 중앙값 | +|---|---|---|---|---|---|---| +| 임도 기계(굴착기 0.2·0.4·0.7 · 불도저 · 덤프 · 진동롤러 · 대형브레이커) | 29 | 7 | -19.2% | 29/29 | +1.5% | -40.0% | +| 전체 | 198(짝 207 중 계산됨) | 63 | -16.7% | 197/198 | +1.5% | -40.0% | + +| 기계 | ESTX 계 (노·재·경) | 우리 계 (노·재·경) | 차이 | +|---|---|---|---| +| 굴착기 0.2㎥ | 83,897 (59,020 · 11,004 · 13,873) | 60,456 (35,412 · 11,171 · 13,873) | -27.9% | +| 굴착기 0.4㎥ | 98,503 (59,020 · 21,969 · 17,514) | 75,227 (35,412 · 22,301 · 17,514) | -23.6% | +| 굴착기 0.7㎥ | 109,316 (59,020 · 25,742 · 24,554) | 86,096 (35,412 · 26,130 · 24,554) | -21.2% | +| 불도저 19ton | 147,655 (59,020 · 52,751 · 35,884) | 124,841 (35,412 · 53,545 · 35,884) | -15.5% | +| 덤프트럭 15ton | 119,591 (59,020 · 39,912 · 20,659) | 96,585 (35,412 · 40,513 · 20,659) | -19.2% | +| 진동롤러 10ton | 119,733 (59,020 · 34,051 · 26,662) | 96,639 (35,412 · 34,564 · 26,662) | -19.3% | +| 대형브레이커 0.7㎥(손료만) | 11,412 | 11,412 | 0 | + +차이 자리: + +- **조종원(노무비) — 차이의 거의 전부.** ESTX 기준값(MachGijunDat) 「1/8 × 16/12 × 25/20 = 0.20833」. 건설기계운전사 283,297 × 0.20833 = 59,020. 우리 로직은 「÷ 8」(= 35,412) — 로직 비고 「환산 근거 미확인 — 우선 하루 ÷ 8」 자리. 1.667배 차이. +- **연료(재료비) +1.5%** — 유가 기준 차이. ESTX 경유 1,819원/ℓ(한국석유공사 26년 6월 4주) · 우리 오피넷 전국평균 1,846.39원/ℓ. +- **손료(경비)** — 197/198 같음. 크레인(타이어) 200ton 만 +1.6%(308,010 · 312,932). +- 조종원 직종 다름 1: 진동롤러(핸드가이드식) 0.7ton — ESTX 일반기계운전사(173,995) · 우리 1048 건설기계운전사. +- 연료 유무 다름 2: 아스팔트 스프레이어 300ℓ·400ℓ — ESTX 재료비 있음(1,547 · 2,321) · 우리 주연료 없음. +- 짝 못 지은 기계 412(우리 619 중) — ESTX 이름·규격 표기가 달라 자동 짝 안 됨(수동 대조 필요). + +## (4) 일위대가 + +같은 조건으로 계산. 우리 로직에 빈 재료 줄이 있으면 ESTX 자재 단가를 계산에만 임시로 넣음(이번 짝에서는 쓰인 줄 없음). 벌목은 단위 환산(우리 1,000㎡ → ESTX a = 100㎡, × 0.1). + +| ESTX 일위대가 | 우리 로직 · 입력 | ESTX | 우리 | 차이 | 까닭 | +|---|---|---|---|---|---| +| 인력터파기 보통토사 ㎥ | 공통 3-2-1 · 보통토사 | 63,314 | 62,812 | -0.8% | ESTX 품 0.28(반올림) · 우리 1/3.6 | +| 인력터파기 경질토사 ㎥ | 공통 3-2-1 · 경질토사 | 83,665 | 83,749 | +0.1% | 같음 | +| 벌목 5m미만 a | 공통 3-9-1 · 높이 4 · 집재 100m | 92,874 | 86,520 | -6.8% | 굴착기+집게 조종원 | +| 벌목 5~8m a | 공통 3-9-1 · 높이 6 | 121,329 | 113,030 | -6.8% | 〃 | +| 벌목 8m이상 a | 공통 3-9-1 · 높이 10 | 158,303 | 147,495 | -6.8% | 〃 | +| 평떼붙임공 ㎡ | 공통 4-1-1 · 평떼 | 11,317 | 6,157 | -45.6% | 인력 같음 · ESTX 잔디 12매 × 430 = 5,160 붙임(우리 로직 잔디 줄 없음) | +| 줄떼붙임공 ㎡ | 공통 4-1-1 · 줄떼 | 6,860 | 5,432 | -20.8% | 〃 잔디 3.3매 = 1,419 | +| 식재(나무높이)(인력) H≤2.0 주 | 공통 4-3-4 · 인력 · 지주목 세움 | 32,123 | 32,124 | 0 | 같음 | +| 식재(나무높이)(인력) H 2.1~3.0 주 | 공통 4-3-4 · 3.0이하 | 64,247 | 64,248 | 0 | 같음 | +| 굴취(나무높이)(인력) H≤2.0 주 | 공통 4-3-2 · 인력 · 분 있음 | 18,610 | 18,356 | -1.4% | ESTX 녹화마대·녹화끈 등 255 | +| 굴취(나무높이)(기계) H≤2.0 주 | 공통 4-3-2 · 기계 | 18,761 | 16,439 | -12.4% | 굴착기 조종원 + 재료 255 | +| 관목식재(단식) H<0.3 주 | 공통 4-2-2 | 5,485 | 5,486 | 0 | 같음 | +| 관목식재(군식) H 1.2~1.5 주 | 공통 4-2-3 | 6,266 | 6,269 | 0 | 같음 | +| 초화류식재 보통 주 | 공통 4-1-4 | 487 | 488 | 0 | 같음 | +| 레미콘타설(인력운반) 무근 ㎥ | 공통 6-1-1 | 59,282 | 59,285 | 0 | 같음(공구손료 비목만 다름) | +| 레미콘타설(인력운반) 철근 ㎥ | 공통 6-1-1 | 68,177 | 68,178 | 0 | 〃 | +| 철근가공(현장) Type-Ⅰ ton | 공통 6-2-2 | 236,561 | 236,561 | 0 | 〃 | +| 합판거푸집 3회(복잡) ㎡ | 공통 6-3-1 · 복잡 · 3회 | 74,814 | 74,442 | -0.5% | 합판·각재 단가(ESTX 물가지 · 우리 나라장터) | +| 강관비계 10m이하 3개월 ㎡ | 공통 2-7-1 · 보호망 시공 | 20,920 | 18,877 | -9.8% | ESTX 비계 자재 손료 2,317 포함 · 우리는 2-2-5 따로 | +| 예초 연3회 이상 ㎡ | 유지관리 1-2-12 | 360 | 361 | 0 | 같음 | +| 천연섬유네트설치 ㎡ | 공통 3-7-3 | 10,389 | 3,526 | -66.1% | ESTX 코아네트·고정핀 1,586 + 인력품 다름(작업반장 0.002 · 특별인부 0.018 · 보통인부 0.025 대 우리 특별인부 0.0103 · 보통인부 0.0069) | +| 콘크리트 치핑 ㎡ | 공통 6-1-6 | 33,021 | 33,022 | 0 | 같음 | +| 콘크리트말뚝 두부정리 Φ400 본 | 공통 5-3-4 · ø400 | 19,953 | 17,600 | -11.8% | 굴착기 조종원 · 재료 구성 다름 | +| 강관말뚝 두부정리 Φ400 본 | 공통 5-3-3 · ø400 | 23,185 | 21,642 | -6.7% | 〃 | +| 타이튼 접합 및 부설(인력) D80 본 | 토목 6-2-1 · 시공조건 A | 65,269 | 65,267 | 0 | 같음 | +| 강관부설(인력) D80 본 | 토목 6-3-1 · 시공조건 A | 87,153 | 87,154 | 0 | 같음 | +| 제수밸브부설및접합(인력) D50 | 토목 6-8-1 · 시공조건 A | 30,941 | 30,336 | -2.0% | ESTX 공구손료 노무비의 2% 붙임 · 우리 없음 | +| 녹막이페인트칠 1회 ㎡ | 건축 11-2-6 · 벽 | 5,370 | 4,551 | -15.3% | ESTX 페인트 재료 909 · 우리는 설계자 입력 | + +- 28 짝: 같음(±0.5%) 14 · ±5% 17 · ±20% 25 · 중앙값 -0.6%. +- 인력 품과 노임은 거의 그대로 맞음 → 우리 로직의 품 해석이 상용 자료와 일치. +- 벌어지는 자리: 기계 조종원 계수(벌목 · 굴취 기계 · 두부정리) · ESTX 가 기본으로 붙이는 재료(잔디 · 비계 자재 · 페인트 · 공구손료). diff --git a/resources/master_data/scripts/compare_estx.py b/resources/master_data/scripts/compare_estx.py new file mode 100644 index 00000000..bb721c6a --- /dev/null +++ b/resources/master_data/scripts/compare_estx.py @@ -0,0 +1,521 @@ +"""상용 적산 자료(ESTX 2026-07) 와 마스터 비교 — 읽기 전용 · 결과는 화면에만. + +ESTX 구조: resources/knowledge/original/원가계산/다산소프트/ESTX_파일구조.md · ESTX_테이블사전.md. +비목: ESTX NC=노무비 · GC=재료비 · JC=경비. 기계 JC=손료 · GC=연료+잡품 · NC=조종원. + +쓰기: ./venv/Scripts/python.exe resources/master_data/scripts/compare_estx.py [노임|자재|기계|일위대가] +""" + +import copy +import re +import statistics +import struct +import sys +import zipfile +from decimal import Decimal +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) +import check_master as cm # noqa: E402 +import master_formula as mf # noqa: E402 + +ROOT = Path(__file__).resolve().parents[3] +ESTX = ROOT / "resources/knowledge/original/원가계산/다산소프트/2026-07-main.estx" + + +# ── ESTX 읽기 ───────────────────────────────────────────────────────── +class Estx: + def __init__(self, path=ESTX): + self.zip = zipfile.ZipFile(path) + self.cache = {} + + def table(self, name: str) -> list[dict]: + """고정 레코드 표 — 필드 설명자대로 풂 · 미저장 필드는 뺌.""" + if name in self.cache: + return self.cache[name] + b = self.zip.read(name + ".dat") + count, size, nfield = struct.unpack_from(" dict[str, list[dict]]: + """제목 코드 → 상세 줄(HeadMain · MachMain) — TitleCode 줄 뒤 이름 있는 줄.""" + out, cur = {}, None + for r in self.table(main): + if r.get("TitleCode"): + cur = r["TitleCode"] + out[cur] = [] + elif cur and r.get("Name"): + out[cur].append(r) + return out + + +# ── 공통 ────────────────────────────────────────────────────────────── +def norm(s) -> str: + s = str(s or "").lower() + for a, b in ( + ("×", "*"), + ("x", "*"), + ("㎜", "mm"), + ("φ", "ø"), + ("Φ", "ø"), + ("Ø", "ø"), + ("㎥", "m3"), + ): + s = s.replace(a, b) + return "".join(ch for ch in s if not ch.isspace() and ch not in ",·") + + +def rate(a, b) -> float: + """a 에 대한 b 의 차이율 % — (b-a)/a.""" + return (float(b) - float(a)) / float(a) * 100 if a else float("inf") + + +def spread(rates: list[float]) -> str: + if not rates: + return "-" + ab = [abs(x) for x in rates] + within = lambda t: sum(x <= t for x in ab) # noqa: E731 + return ( + f"{len(rates)}건 · 같음(±0.5%) {within(0.5)} · ±5% {within(5)} · ±20% {within(20)} · " + f"중앙값 {statistics.median(rates):+.1f}%" + ) + + +def load_master(): + files = cm.load() + return files, mf.Master(files) + + +# ── (1) 노임 ────────────────────────────────────────────────────────── +def labor(estx: Estx, files: dict): + rows = estx.table("LaborcDat") + heads = [r["Name"] for r in rows if r.get("Name") and not r.get("Unite")] + print("## 노임 — ESTX 머리줄", heads) + ours = {} + for fname, tag in (("인력_건설노임.json", "건설"), ("인력_제조노임.json", "제조")): + for r in files[fname]["줄"]: + ours.setdefault((tag, norm(r["이름"])), r) + theirs = {} + for r in rows: + if r.get("Unite") != "인": + continue + name = r["Name"] + tag = "제조" if name.endswith("(제조)") else "건설" + key = (tag, norm(name.removesuffix("(제조)"))) + theirs.setdefault(key, r) + for tag in ("건설", "제조"): + mine = {k for k in ours if k[0] == tag} + est = {k for k in theirs if k[0] == tag} + both = mine & est + same = [ + k + for k in both + if ours[k]["값"] is not None and float(ours[k]["값"]) == theirs[k].get("TC") + ] + diff = [k for k in both if k not in same] + print( + f"### {tag} — 우리 {len(mine)} · ESTX {len(est)} · 짝 {len(both)} · 같음 {len(same)} · 다름 {len(diff)}" + ) + print( + " 다름:", spread([rate(ours[k]["값"], theirs[k]["TC"]) for k in diff if ours[k]["값"]]) + ) + for k in sorted(diff)[:15]: + print(" ", k[1], ours[k]["값"], "→", theirs[k].get("TC")) + print(" 우리만:", sorted(k[1] for k in mine - est)[:40]) + print(" ESTX만:", sorted(k[1] for k in est - mine)[:40]) + + +# ── (2) 자재 ────────────────────────────────────────────────────────── +SLOTS = ["가격정보", "물가자료", "유통물가", "물가정보", "거래가격", "견적가"] + + +def base_name(name) -> str: + """품명 뿌리 — 「, 서울」 · 「(관급)」 · 「-사급」 같은 지역·조달 꼬리를 뗌.""" + s = re.sub(r"\(.*?\)", "", str(name or "").split(",")[0]) + return norm(re.sub(r"-(사급|관급)$", "", s.strip())) + + +def bare_spec(name, spec) -> str: + n, s = norm(name), norm(spec) + s = s[len(n) :] if s.startswith(n) else s + return "".join(ch for ch in s if ch not in "-()[]") + + +def spec_match(nara_spec: str, estx_spec: str) -> bool: + """규격 같음 — 같거나, ESTX 규격(3자 이상)이 나라장터 규격 끝과 같음(앞 수식어 「아연도」 등).""" + return nara_spec == estx_spec or (len(estx_spec) >= 3 and nara_spec.endswith(estx_spec)) + + +def material_pairs(estx: Estx, nara: list[dict]): + by_name = {} + for r in estx.table("StuffcDat"): + if r.get("Name") and r.get("TC"): + by_name.setdefault(base_name(r["Name"]), []).append(r) + pairs = [] + for n in nara: + cands = by_name.get(base_name(n["이름"])) if n.get("값") else None + if not cands: + continue + s = bare_spec(n["이름"], n["규격"]) + hit = next((e for e in cands if spec_match(s, bare_spec(e["Name"], e.get("Size")))), None) + if hit: + pairs.append((n, hit)) + return by_name, pairs + + +def material(estx: Estx, files: dict): + nara = files["재료_나라장터자재.json"]["줄"] + by_name, pairs = material_pairs(estx, nara) + rows = sum(by_name.values(), []) + names = {base_name(n["이름"]) for n in nara} + print( + f"## 자재 — 나라장터 {len(nara)}(품명 {len(names)}) · ESTX {len(rows)} · 품명 같음 {len(names & set(by_name))} · 품명·규격 짝 {len(pairs)}" + ) + print(" TC 대 나라장터:", spread([rate(n["값"], e["TC"]) for n, e in pairs])) + for i, slot in enumerate(SLOTS, 1): + got = [rate(n["값"], e[f"Cost{i}"]) for n, e in pairs if e.get(f"Cost{i}")] + print(f" 슬롯{i} {slot}:", spread(got)) + closest = {} + for n, e in pairs: + got = { + s: abs(rate(n["값"], e[f"Cost{i}"])) + for i, s in enumerate(SLOTS, 1) + if e.get(f"Cost{i}") + } + best = min(got, key=got.get) + closest[best] = closest.get(best, 0) + 1 + print(" 가장 가까운 슬롯:", closest) + for n, e in pairs[:8]: + print( + " 예", + n["이름"], + "|", + n["규격"], + n["값"], + "→", + e.get("TC"), + {s: e.get(f"Cost{i}") for i, s in enumerate(SLOTS, 1) if e.get(f"Cost{i}")}, + ) + have, near, none = [], [], [] + for r in files["재료_자체.json"]["줄"]: + if r.get("값") is not None: + continue + mine = {base_name(r["이름"]), base_name(r["열쇠"])} + hits = [e for m in mine for e in by_name.get(m, [])] + if hits: + have.append((r, hits)) + continue + tail = [ + e for k, v in by_name.items() for m in mine if len(m) >= 2 and k.endswith(m) for e in v + ] + (near if tail else none).append((r, tail)) + print( + f"### 재료_자체 빈 값 {len(have) + len(near) + len(none)} — ESTX 품명 같음 {len(have)} · 품명 끝 같음 {len(near)} · 없음 {len(none)}" + ) + for r, hits in near: + print(f" 비슷 {r['열쇠']} | {sorted({e['Name'] for e in hits})[:4]}") + for r, hits in have: + s = bare_spec(r["이름"], r.get("규격")) + same = [e for e in hits if s and spec_match(s, bare_spec(e["Name"], e.get("Size")))] + h = (same or hits)[0] + print( + f" 있음 {r['열쇠']} [{r.get('단위')}] | ESTX {len(hits)}줄 규격같음 {len(same)} | 예 {h['Name']} {h.get('Size')} {h.get('Unite')} {h['TC']:.0f}" + ) + print(" 없음", [r["열쇠"] for r, _ in none]) + + +# ── (3) 기계경비 ────────────────────────────────────────────────────── +MACH_WORDS = ( + ("불도우저", "불도저"), + ("굴삭기", "굴착기"), + ("(타이어형)", "(타이어)"), + ("브레이카", "브레이커"), + ("로울러", "롤러"), + ("크로울러", "크롤러"), + ("진동자주식", "진동 자주식"), +) +FIRST = ("0201-0020", "0201-0040", "0201-0070", "0101-", "0602-", "1306-", "1305-", "0230-") +LOGICS = [ + "공통 8 기계 시간당 경비", + "공통 8 기계 시간당 경비(연료 없음)", + "공통 8 기계 시간당 경비(조종원 없음)", + "공통 8 기계 시간당 손료", +] + + +def mach_key(name, size) -> str: + n = str(name or "") + for a, b in MACH_WORDS: + n = n.replace(a, b) + size = norm(size).replace("∼", "~").replace("ton", "").replace("톤", "") + return norm(n) + "|" + size + + +def machine_cost(master: mf.Master, mid: str, region="전국평균"): + """로직으로 계산 — 고르기 밖 기계는 메모리에서만 고르기에 더함.""" + for key in LOGICS: + row = master.logic("건설품셈:" + key) + spec = next(i for i in row["입력"] if i["이름"] == "기계") + added = mid not in spec["고르기"] + if added: + spec["고르기"].append(mid) + try: + given = {"기계": mid, "지역": region} + given = {k: v for k, v in given.items() if any(i["이름"] == k for i in row["입력"])} + return key, mf.run(master, "건설품셈:" + key, given) + except (mf.FormulaError, ArithmeticError, KeyError, TypeError): + continue + finally: + if added: + spec["고르기"].remove(mid) + return None, None + + +def machine(estx: Estx, files: dict, master: mf.Master): + est = {} + for r in estx.table("MachTitleDat"): + if r.get("Unite") == "hr" and r.get("TC"): + est.setdefault(mach_key(r.get("Name"), r.get("Size")), r) + ours = files["기계_건설품셈.json"]["줄"] + pairs = [(m, est[k]) for m in ours if (k := mach_key(m["이름"], m["규격"])) in est] + print(f"## 기계 — 우리 {len(ours)} · ESTX 시간당 {len(est)} · 기종·규격 짝 {len(pairs)}") + out = [] + for m, e in pairs: + key, res = machine_cost(master, m["열쇠"]) + if not res: + continue + out.append((m, e, key, res)) + print(f" 로직 계산됨 {len(out)}") + first = [x for x in out if x[0]["열쇠"].startswith(FIRST)] + for label, group in (("임도 기계", first), ("전체", out)): + print(f"### {label}") + print(" 계:", spread([rate(e["TC"], res["계"]) for m, e, k, res in group])) + for part, col in ( + ("손료(경비)", "JC"), + ("연료·잡품(재료비)", "GC"), + ("조종원(노무비)", "NC"), + ): + item = { + "손료(경비)": "경비", + "연료·잡품(재료비)": "재료비", + "조종원(노무비)": "노무비", + }[part] + print( + f" {part}:", + spread([rate(e[col], res[item]) for m, e, k, res in group if e.get(col)]), + ) + print("### 손료 ±0.5% 밖 · 연료 ±5% 밖") + for m, e, k, res in out: + if abs(rate(e["JC"], res["경비"])) > 0.5 or ( + e.get("GC") and abs(rate(e["GC"], res["재료비"])) > 5 + ): + print( + f" {m['열쇠']} {m['이름']} {m['규격']} {k} | ESTX 재 {e.get('GC', 0):.0f} 경 {e['JC']:.0f} | 우리 재 {res['재료비']:.0f} 경 {res['경비']:.0f}" + ) + for m, e, k, res in first: + print( + f" {m['열쇠']} {m['이름']} {m['규격']} | ESTX {e['TC']:.0f} (노 {e.get('NC', 0):.0f} 재 {e.get('GC', 0):.0f} 경 {e.get('JC', 0):.0f})" + f" | 우리 {res['계']:.0f} (노 {res['노무비']:.0f} 재 {res['재료비']:.0f} 경 {res['경비']:.0f}) {rate(e['TC'], res['계']):+.1f}%" + ) + return out + + +# ── (4) 일위대가 ────────────────────────────────────────────────────── +# (ESTX 제목 코드, 로직 열쇠, 입력[, 단위 환산 배수 — 우리 값 × 배수 = ESTX 단위]) +REGION = {"지역": "전국평균", "보정작업": "해당 없음"} +PAIRS = [ + ( + "U0000001217", + "공통 3-2-1 굴착(인력/토사)", + {"토질": "보통토사", "넓은구역가산": 0, "용수감": 0, "잔토처리": "없음"}, + ), + ( + "U0000001218", + "공통 3-2-1 굴착(인력/토사)", + {"토질": "경질토사", "넓은구역가산": 0, "용수감": 0, "잔토처리": "없음"}, + ), + ("U0000001665", "공통 3-9-1 벌목", {"나무높이": 4, "집재거리": 100, **REGION}, "0.1"), + ("U0000001666", "공통 3-9-1 벌목", {"나무높이": 6, "집재거리": 100, **REGION}, "0.1"), + ("U0000001667", "공통 3-9-1 벌목", {"나무높이": 10, "집재거리": 100, **REGION}, "0.1"), + ("U0000001260", "공통 4-1-1 잔디붙임", {"시공법": "평떼"}), + ("U0000001261", "공통 4-1-1 잔디붙임", {"시공법": "줄떼"}), + ( + "U0000002799", + "공통 4-3-4 식재(나무높이)", + {"구분": "인력시공", "나무높이": "2.0이하", "지주목": "세움", **REGION}, + ), + ( + "U0000002802", + "공통 4-3-4 식재(나무높이)", + {"구분": "인력시공", "나무높이": "3.0이하", "지주목": "세움", **REGION}, + ), + ( + "U0000002693", + "공통 4-3-2 굴취(나무높이)", + {"구분": "인력시공", "나무높이": "2.0이하", "야생감": 0, "분": "분 있음", **REGION}, + ), + ( + "U0000003316", + "공통 4-3-2 굴취(나무높이)", + {"구분": "기계시공", "나무높이": "2.0이하", "야생감": 0, "분": "분 있음", **REGION}, + ), + ("U0000002794", "공통 4-2-2 식재(단식)", {"나무높이": "0.3미만"}), + ("U0000002633", "공통 4-2-3 식재(군식)", {"나무높이": "1.2~1.5이하"}), + ("U0000002661", "공통 4-1-4 초화류 식재", {"등급": "보통", "특수화단감": 0}), + ( + "U0000001171", + "공통 6-1-1 레디믹스트콘크리트 타설", + { + "타설방법": "인력운반타설", + "구조물": "무근구조물", + "소량감률": 0, + "굴착기": "0201-0070", + "지역": "전국평균", + }, + ), + ( + "U0000001172", + "공통 6-1-1 레디믹스트콘크리트 타설", + { + "타설방법": "인력운반타설", + "구조물": "철근구조물", + "소량감률": 0, + "굴착기": "0201-0070", + "지역": "전국평균", + }, + ), + ("U0000003127", "공통 6-2-2 철근 현장가공", {"Type": "Type-Ⅰ"}), + ( + "U0000001068", + "공통 6-3-1 합판거푸집", + { + "유형": "복잡", + "사용횟수": "3회", + "합판": "20142452", + "각재": "20141038", + "높이감률": 0, + "지붕슬래브": "아님", + "폼타이": "아님", + "폼타이규격": "12.7㎜", + "측압": "3t/㎡", + "구멍땜": 0, + }, + ), + ("U0000001101", "공통 2-7-1 강관비계 설치 및 해체", {"높이": "10m이하", "보호망": "시공"}), + ( + "U0000001988", + "유지관리 1-2-12 예초", + {"연간시공횟수": "연3회 이상", "경사도": "25˚ 미만", "그물막": "필요 없음"}, + ), + ("U0000001958", "공통 3-7-3 천연섬유사면보호공 설치", {}), + ("U0000001668", "공통 6-1-6 콘크리트 치핑", {}), + ("U0000001703", "공통 5-3-4 말뚝두부정리(콘크리트)", {"규격": "ø400", "지역": "전국평균"}), + ("U0000001694", "공통 5-3-3 말뚝두부정리(강관)", {"규격": "ø400", "지역": "전국평균"}), + ("U0000003394", "토목 6-2-1 타이튼 접합 및 부설 인력", {"관경": 80, "시공조건": "시공조건 A"}), + ("U0000077626", "토목 6-3-1 부설 인력", {"관경": 80, "시공조건": "시공조건 A"}), + ( + "U0000003527", + "토목 6-8-1 주철제 게이트 제수밸브 부설 및 접합 인력", + {"관경": 50, "시공조건": "시공조건 A"}, + ), + ("U0000001289", "건축 11-2-6 녹막이 페인트칠", {"시공면": "벽", "비계층수": 0}), +] + + +def with_estx_materials(master: mf.Master, estx: Estx, code: str): + """ESTX 일위대가의 자재 단가를 품명으로 우리 빈 재료(값 없음)에만 임시로 넣은 사본 — 파일에 안 씀.""" + temp = copy.copy(master) + temp.index = {k: dict(v) for k, v in master.index.items()} + items = [ + d + for d in estx.details("HeadMainDat").get(code, []) + if str(d.get("Code", "")).startswith("M") + ] + put = [] + for book in (("재료", "자체"), ("재료", "나라장터자재")): + rows = temp.index.get(book, {}) + for key, row in list(rows.items()): + if row.get("값") is not None: + continue + hit = next( + (d for d in items if base_name(d["Name"]) == base_name(row.get("이름"))), None + ) + if hit: + rows[key] = {**row, "값": Decimal(str(hit["TU"]))} + put.append(f"{key}={hit['TU']:.0f}") + return temp, put + + +def unit_price(estx: Estx, master: mf.Master): + heads = {r["Code"]: r for r in estx.table("HeadTitleDat")} + print("## 일위대가") + rates = [] + for code, key, given, *scale in PAIRS: + h = heads[code] + k = Decimal(scale[0]) if scale else Decimal(1) + temp, put = with_estx_materials(master, estx, code) + row = temp.logic("건설품셈:" + key) + given = {k: v for k, v in given.items() if any(i["이름"] == k for i in row.get("입력", []))} + given = {k: Decimal(str(v)) if isinstance(v, (int, float)) else v for k, v in given.items()} + try: + res = mf.run(temp, "건설품셈:" + key, given) + res = {c: res[c] * k for c in ("계", *mf.COST_ITEMS)} + except (mf.FormulaError, ArithmeticError, KeyError, TypeError) as e: + print(f" {code} {h['Name']} {h.get('Size')} | {key} | 멈춤: {str(e)[:120]}") + continue + r = rate(h["TC"], res["계"]) + rates.append(r) + print( + f" {code} {h['Name']} {h.get('Size')} {h.get('Unite')} | ESTX {h['TC']:.0f} (노 {h.get('NC', 0):.0f} 재 {h.get('GC', 0):.0f} 경 {h.get('JC', 0):.0f})" + f" | {key} {row.get('결과단위')} | 우리 {res['계']:.0f} (노 {res['노무비']:.0f} 재 {res['재료비']:.0f} 경 {res['경비']:.0f}) {r:+.1f}%" + + (f" | 임시 재료 {put}" if put else "") + ) + print(" 계:", spread(rates)) + + +def main(): + only = sys.argv[1:] + estx = Estx() + files, master = load_master() + if not only or "노임" in only: + labor(estx, files) + if not only or "자재" in only: + material(estx, files) + if not only or "기계" in only: + machine(estx, files, master) + if not only or "일위대가" in only: + unit_price(estx, master) + + +if __name__ == "__main__": + main()