- 인력.json — 절단공·사진제판공·사진식자공·측량사·측량기사·시공측량기사·측량조수·시공측량조수·특수운전공 상태 「추정」 · 값은 준용 직종 노임 · 출처는 근거 문서 · 비고 한마디 - 구분 「미확보」 등록부·빌더 짝짓기 없앰 · check_master BOOKS 에 공간정보법 - 근거 md 에 「사용자 지정 추정」 절 - 로직 일괄 시험 계산 통과 1,109 → 1,119 · 멈춤 245 → 235 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
417 lines
18 KiB
Python
417 lines
18 KiB
Python
"""노임 공표 원문 md → 인력 요소 파일 다섯 · 건설노임 미공표 산정 · 추정 줄 되채움.
|
|
|
|
원문 = resources/knowledge/original/원가계산/노임단가_* (우리 md · ESTX 값 쓰지 않음).
|
|
- 모든 줄이 같은 열 한 벌(`_틀.md` 인력) · 제조업 업종·엔지니어링 부문·측량 분야는 「상세구분」.
|
|
- 인력_엔지니어링노임 · 측량노임 · 건설사업관리노임 · SW노임 · 산림노임 — md 표 그대로 · 줄 수·값을 md 와 기계 대조.
|
|
옛이름·비고·준용은 다시 지어도 그대로 이어받음(`carry`).
|
|
- 상태 「추정」 줄(옛·폐지 직종) — 관리자가 고른 준용 직종에서 값·단위·일시간을 되채움.
|
|
- 인력_건설노임 미공표 — 원문 4-라 산정값을 「값」 에 넣고 상태 「산정」 · 근거는 비고 한 줄.
|
|
이음 = 원문 회차의 전체직종 평균을 그대로 이음(미공표직종_추적.md 4절) · 산정값은 같은 md 5절과 대조.
|
|
- 인력_제조노임 미공표 — 조사월마다 전체 평균 증가율을 곱하며 원 미만 버림(건설 4-라 방식 준용 · 제조 원문에 규정 없음).
|
|
대조는 제조 미공표직종_추적.md 5절 「단계마다 버림(채택)」 열 · 옛 회차 md 는 구분 머리 「원천」(역할 산정근거)에.
|
|
|
|
쓰기: ./venv/Scripts/python.exe resources/master_data/scripts/build_인력_노임.py
|
|
옛 모양(원문별 파일 · 열쇠)으로 쓰고 끝에 `master_keys.adopt` 가 인력.json(키 · 구분)으로 받음.
|
|
"""
|
|
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import sys
|
|
from collections import Counter
|
|
from decimal import ROUND_FLOOR, Decimal
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
import master_keys as mk # noqa: E402
|
|
from _migrate_base import dump_master # noqa: E402
|
|
|
|
ROOT = Path(__file__).resolve().parents[3]
|
|
MD = ROOT / "resources/master_data"
|
|
COST = ROOT / "resources/knowledge/original/원가계산"
|
|
CAK = COST / "노임단가_건설업_대한건설협회"
|
|
KBIZ = COST / "노임단가_제조업_중소기업중앙회"
|
|
WAGE = "원/일"
|
|
_INT = re.compile(r"\d{1,3}(?:,\d{3})+")
|
|
|
|
|
|
def num(text: str) -> int:
|
|
return int(text.replace(",", ""))
|
|
|
|
|
|
def cells(line: str) -> list[str]:
|
|
return [c.strip() for c in line.strip().strip("|").split("|")]
|
|
|
|
|
|
def table_after(text: str, head: str) -> list[list[str]]:
|
|
"""`head` 로 시작하는 md 표 줄부터 표 끝까지 — 구분 줄 뺌."""
|
|
lines = text.splitlines()
|
|
i = next(n for n, x in enumerate(lines) if x.startswith(head))
|
|
out = []
|
|
for x in lines[i:]:
|
|
if not x.startswith("|"):
|
|
break
|
|
if not set(x) <= set("|-: "):
|
|
out.append(cells(x))
|
|
return out
|
|
|
|
|
|
def origin(path: Path) -> list[dict]:
|
|
return [
|
|
{
|
|
"경로": path.relative_to(ROOT).as_posix(),
|
|
"해시": hashlib.sha256(path.read_bytes()).hexdigest(),
|
|
"역할": "primary",
|
|
}
|
|
]
|
|
|
|
|
|
def row(name: str, value, source: str, detail=None, **more) -> dict:
|
|
"""열 한 벌(`_틀.md` 인력) — 키·원문번호·구분은 `master_keys.adopt` 가 앞에 붙임."""
|
|
return {
|
|
"열쇠": name,
|
|
"상세구분": detail,
|
|
"이름": name,
|
|
"규격": "",
|
|
"단위": WAGE,
|
|
"값": value,
|
|
"출처": source,
|
|
"일시간": 6 if name == "잠수부" else 8,
|
|
"상태": "공표",
|
|
"옛이름": [],
|
|
"비고": None,
|
|
"준용": None,
|
|
**more,
|
|
}
|
|
|
|
|
|
def details(rows: list[dict]) -> list[str]:
|
|
"""구분 묶음의 상세구분 목록 — 줄에 나온 차례대로."""
|
|
out = []
|
|
for r in rows:
|
|
if r["상세구분"] and r["상세구분"] not in out:
|
|
out.append(r["상세구분"])
|
|
return out
|
|
|
|
|
|
# ── 갈래별 노임 ──────────────────────────────────────────────────────
|
|
def engineering():
|
|
d = COST / "노임단가_엔지니어링_한국엔지니어링협회"
|
|
src = d / "공표문_2025 엔지니어링업체 임금실태조사결과 공표.md"
|
|
t = table_after(src.read_text(encoding="utf-8"), "| 구분 |")
|
|
parts = [p.replace(r"\*", "") for p in t[0][1:]] # 「기타**」 주석 기호
|
|
rows = [
|
|
row(f"{r[0]}({p})", num(v), f"엔지니어링노임 {r[0]} {p}", detail=p)
|
|
for r in t[1:]
|
|
for p, v in zip(parts, r[1:])
|
|
]
|
|
# 같은 게시물 xls(연도별) 2025 칸과 대조
|
|
y = table_after(
|
|
(d / "노임단가_연도별_엔지니어링(2025).md").read_text(encoding="utf-8"), "| 년도 |"
|
|
)
|
|
xls = {(r[0], p): num(v) for r in y[2:10] for p, v in zip(y[1][-7:], r[-7:])}
|
|
assert {(r["이름"].split("(")[0], r["상세구분"]): r["값"] for r in rows} == xls
|
|
head = {
|
|
"그룹": "인력",
|
|
"원문": "엔지니어링노임",
|
|
"기관": "한국엔지니어링협회",
|
|
"판": "2026-01-01",
|
|
"공표일": "2025-12-02",
|
|
"원천": origin(src),
|
|
"상세구분": details(rows),
|
|
"비고": [
|
|
"한국엔지니어링협회 2025 엔지니어링업체 임금실태조사결과 공표(기획홍보팀-1515호 · 국가승인통계 제372001호) · 7 기술부문 × 8 기술등급 · 1인 1일 평균임금 · 월평균 근무일수 20.3일",
|
|
"이름 = 기술등급(기술부문) · 기타 = 선박·항공우주·금속·화학·광업·농림·산업·해양·수산",
|
|
"보고서 표 2 「전체」(기술등급 단순평균)는 공표 노임단가가 아니라 넣지 않음",
|
|
],
|
|
}
|
|
return src, head, rows
|
|
|
|
|
|
def survey():
|
|
src = (
|
|
COST
|
|
/ "노임단가_측량_한국공간정보산업협회/공표문_2025년도 측량업체 임금실태조사 결과 공표.md"
|
|
)
|
|
t = table_after(src.read_text(encoding="utf-8"), "| 구분 |")
|
|
rows, top, field = [], "", ""
|
|
for r in t[1:]:
|
|
if r[0]:
|
|
top, field = r[0], r[1]
|
|
elif r[1]:
|
|
field = r[1]
|
|
grade = r[2]
|
|
if top == "기술계":
|
|
name = "측량기술사" if grade == "기술사" else f"측량{grade}기술자"
|
|
elif top == "기능계":
|
|
name = f"{field}{grade}기능사"
|
|
else:
|
|
name = grade
|
|
rows.append(
|
|
row(
|
|
name,
|
|
num(r[3]),
|
|
f"측량노임 {' '.join(x for x in (top, field if top == '기능계' else '', grade) if x)}",
|
|
detail=f"{top} {field}" if top == "기능계" else top,
|
|
)
|
|
)
|
|
head = {
|
|
"그룹": "인력",
|
|
"원문": "측량노임",
|
|
"기관": "한국공간정보산업협회",
|
|
"판": "2026-01-01",
|
|
"공표일": "2025-12-22",
|
|
"원천": origin(src),
|
|
"상세구분": details(rows),
|
|
"비고": [
|
|
"한국공간정보산업협회 2025년도 측량업체 임금실태조사 결과 공표(국가승인통계 제377001호) · 20 직종 · 1인 1일 · 월평균 근무일수 20.3일",
|
|
"이름 = 기술계 「측량<등급>기술자」·측량기술사 · 기능계 「<분야><등급>기능사」 · 기타 원문 그대로 — 원문 칸은 출처에",
|
|
],
|
|
}
|
|
return src, head, rows
|
|
|
|
|
|
def cm():
|
|
src = (
|
|
COST
|
|
/ "노임단가_건설사업관리_한국건설엔지니어링협회/2026년도_건설사업관리기술인_임금실태조사결과_공표_한국건설엔지니어링협회.md"
|
|
)
|
|
t = table_after(src.read_text(encoding="utf-8"), "| 구 분 |")
|
|
rows = []
|
|
for r in t[1:]:
|
|
grade = r[0].replace(" ", "")
|
|
rows.append(row(f"{grade}건설사업관리기술인", num(r[1]), f"건설사업관리노임 {grade}"))
|
|
head = {
|
|
"그룹": "인력",
|
|
"원문": "건설사업관리노임",
|
|
"기관": "한국건설엔지니어링협회",
|
|
"판": "2026-01-01",
|
|
"공표일": "2025-12-29",
|
|
"원천": origin(src),
|
|
"상세구분": [],
|
|
"비고": [
|
|
"한국건설엔지니어링협회 2026년도 건설사업관리기술인 노임가격(정책381-3804호 · 통계청승인 제378001호) · 일임금액",
|
|
"환산비 = 원문 Si(고급 = 1.000 기준 등급별 비율 · 소수 넷째 자리 반올림)",
|
|
],
|
|
}
|
|
return src, head, rows
|
|
|
|
|
|
def sw():
|
|
src = (
|
|
COST
|
|
/ "노임단가_SW기술자_한국인공지능소프트웨어산업협회/2026년_적용_SW기술자_평균임금_공표.md"
|
|
)
|
|
t = table_after(src.read_text(encoding="utf-8"), "| 구 분 | 월평균임금")
|
|
rows = []
|
|
for r in t[1:]:
|
|
mark, name = r[0].split(" ", 1)
|
|
n = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰".index(mark) + 1
|
|
rows.append(row(name, num(r[2]), f"SW노임 {n}"))
|
|
head = {
|
|
"그룹": "인력",
|
|
"원문": "SW노임",
|
|
"기관": "한국인공지능·소프트웨어산업협회",
|
|
"판": "2026-01-01",
|
|
"공표일": "2025-12-19",
|
|
"원천": origin(src),
|
|
"상세구분": [],
|
|
"비고": [
|
|
"한국인공지능·소프트웨어산업협회 2026년 적용 SW기술자 평균임금(통계승인 제375001호) · 값 = 일평균임금(M/D) · 적용 2026-01-01 ~ 2026-12-31",
|
|
"⚠ 기본급·제수당·상여금·퇴직급여충당금·법인부담금(4대보험) 모두 포함 — 건설노임(기본급 일급)과 성격 다름",
|
|
"일평균 = 월평균 ÷ 20.5일 · 시간평균 = 일평균 ÷ 8시간(원문)",
|
|
],
|
|
}
|
|
return src, head, rows
|
|
|
|
|
|
def forest():
|
|
src = COST / "노임단가_산림_산림청/2026년_나무의사_수목치료기술자_노임단가_알림.md"
|
|
text = src.read_text(encoding="utf-8")
|
|
rows = [
|
|
row(name, num(v), f"산림노임 {name}")
|
|
for name, v in re.findall(r"(나무의사|수목치료기술자) ([\d,]+)원", text)
|
|
]
|
|
head = {
|
|
"그룹": "인력",
|
|
"원문": "산림노임",
|
|
"기관": "산림청",
|
|
"판": "2026-01-01",
|
|
"공표일": "2026-01-06",
|
|
"원천": origin(src),
|
|
"상세구분": [],
|
|
"비고": [
|
|
"산림청 2026년 나무의사·수목치료기술자 노임단가 알림(수목진료 표준품셈 예정가격 작성용) · 2025년 소비자물가상승률 2.1% 반영(원문) · 통계 승인 노임 아님",
|
|
"판 = 2026년 적용(원문에 적용 시작일 없음 · 1월 1일로 적음)",
|
|
],
|
|
}
|
|
return src, head, rows
|
|
|
|
|
|
def check_md(src: Path, rows: list[dict], table_head: str | None = None) -> None:
|
|
"""md 의 그 표(없으면 본문) 안 큰 수 모음 ⊇ 파일 값 · 개수 같음."""
|
|
text = src.read_text(encoding="utf-8")
|
|
if table_head:
|
|
text = "\n".join("|".join(r) for r in table_after(text, table_head))
|
|
have = Counter(num(x) for x in _INT.findall(text))
|
|
got = Counter(r["값"] for r in rows)
|
|
assert not (got - have), (src.name, got - have)
|
|
print(f" {src.parent.name[5:]}: {len(rows)} 줄 · 값 합 {sum(got.elements()):,} · md 대조 같음")
|
|
|
|
|
|
# ── 관리자 칸 잇기 ────────────────────────────────────────────────────
|
|
def carry(rows: list[dict], kind: str, old: list[dict]) -> None:
|
|
"""관리자·근거가 채운 칸(옛이름 · 비고 · 준용)은 다시 지어도 그대로 — 이름으로 맞댐."""
|
|
had = {r["이름"]: r for r in old if r.get("구분") == kind}
|
|
for r in rows:
|
|
keep = had.get(r["이름"])
|
|
if not keep:
|
|
continue
|
|
for col in ("옛이름", "비고", "준용"):
|
|
if keep.get(col) not in (None, [], ""):
|
|
r[col] = keep[col]
|
|
|
|
|
|
def main():
|
|
books = {
|
|
"엔지니어링노임": engineering(),
|
|
"측량노임": survey(),
|
|
"건설사업관리노임": cm(),
|
|
"SW노임": sw(),
|
|
"산림노임": forest(),
|
|
}
|
|
table_heads = {
|
|
"엔지니어링노임": "| 구분 |",
|
|
"측량노임": "| 구분 |",
|
|
"건설사업관리노임": "| 구 분 |",
|
|
"SW노임": "| 구 분 | 월평균임금",
|
|
}
|
|
print("갈래별 노임")
|
|
for book, (src, head, rows) in books.items():
|
|
assert len({r["열쇠"] for r in rows}) == len(rows)
|
|
check_md(src, rows, table_heads.get(book))
|
|
expect = {
|
|
"엔지니어링노임": 56,
|
|
"측량노임": 20,
|
|
"건설사업관리노임": 4,
|
|
"SW노임": 17,
|
|
"산림노임": 2,
|
|
}
|
|
assert {b: len(v[2]) for b, v in books.items()} == expect
|
|
|
|
wage = mk.old_view("인력_건설노임.json")
|
|
make = mk.old_view("인력_제조노임.json")
|
|
now = json.loads((MD / "인력.json").read_text(encoding="utf-8"))["줄"]
|
|
|
|
for book, (src, head, rows) in books.items():
|
|
carry(rows, mk.LABOR[book], now)
|
|
# 상태 「추정」 줄은 갈래 md 밖(옛 직종을 그 구분으로 옮긴 것) — 그대로 이어감
|
|
rows += [r for r in mk.old_view(f"인력_{book}.json")["줄"] if r.get("상태") == "추정"]
|
|
dump_master(MD / f"인력_{book}.json", head, rows)
|
|
|
|
estimate(wage)
|
|
estimate_make(make)
|
|
print("새 틀로 받음", mk.adopt())
|
|
refill_estimated()
|
|
|
|
|
|
def refill_estimated() -> None:
|
|
"""상태 「추정」 줄 — 값·단위·일시간을 준용 직종에서 다시 채움(공표 회차가 바뀌면 같이 바뀜)."""
|
|
path = MD / "인력.json"
|
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
by_key = {r["키"]: r for r in data["줄"]}
|
|
n = 0
|
|
for r in data["줄"]:
|
|
if r.get("상태") != "추정":
|
|
continue
|
|
target = by_key[r["준용"]]
|
|
r["값"], r["단위"], r["일시간"] = target["값"], target["단위"], target["일시간"]
|
|
n += 1
|
|
head = {k: v for k, v in data.items() if k != "줄"}
|
|
path.write_text(mk.dump_rows(head, data["줄"]), encoding="utf-8", newline="\n")
|
|
print(f"추정 {n} 줄 값 = 준용 직종 값")
|
|
|
|
|
|
# ── 건설노임 미공표 산정 ─────────────────────────────────────────────
|
|
def estimate(wage: dict) -> None:
|
|
text = (CAK / "미공표직종_추적.md").read_text(encoding="utf-8")
|
|
steps = [
|
|
(r[0].split(" → ")[0], r[0].split(" → ")[1], r[1], num(r[2]), num(r[3]))
|
|
for r in table_after(text, "| 앞 → 뒤 |")[1:]
|
|
]
|
|
hist = {r[0].split()[0]: r[1:] for r in table_after(text, "| 직종 | 2020.1.1")[1:]}
|
|
dates = table_after(text, "| 직종 | 2020.1.1")[0][1:]
|
|
shown = {r[0].split()[0]: r for r in table_after(text, "| 직종 | 마지막 공표")[1:]}
|
|
until = "2026.1.1"
|
|
n = 0
|
|
for r in wage["줄"]:
|
|
if r.get("상태") not in ("미공표", "산정"):
|
|
continue
|
|
vals = hist[r["열쇠"]]
|
|
last = max(i for i, v in enumerate(vals) if v != "-" and dates[i] != "2026.9.1")
|
|
at, base = dates[last], num(vals[last])
|
|
value, start = Decimal(base), at
|
|
while start != until: # 회차마다 원 미만 버림 — 다산소프트와 같은 방식
|
|
_, b, _, top, bottom = next(s for s in steps if s[0] == start)
|
|
value = (value * top / bottom).to_integral_value(rounding=ROUND_FLOOR)
|
|
start = b
|
|
want = shown[r["열쇠"]]
|
|
assert str(value) == want[6].replace(",", ""), (r["열쇠"], value, want[6])
|
|
r["값"] = int(value)
|
|
r["상태"] = "산정"
|
|
r["비고"] = f"4-라 산정: {at.rsplit('.', 1)[0]} {base:,}원 × 증가율(원 미만 버림)"
|
|
n += 1
|
|
head = {k: v for k, v in wage.items() if k != "줄"}
|
|
dump_master(MD / "인력_건설노임.json", head, wage["줄"])
|
|
print(f"건설노임 미공표 산정 {n} 줄 · 미공표직종_추적.md 5절 값과 같음")
|
|
|
|
|
|
# ── 제조노임 미공표 산정 ─────────────────────────────────────────────
|
|
def estimate_make(make: dict) -> None:
|
|
"""제조 미공표 직종 — 조사월마다 평균 증가율을 곱하며 원 미만 버림(건설 4-라 방식 준용).
|
|
이음·대조는 제조 미공표직종_추적.md 4절(단계) · 5절(「단계마다 버림(채택)」 열)."""
|
|
track = KBIZ / "미공표직종_추적.md"
|
|
text = track.read_text(encoding="utf-8")
|
|
steps = { # 뒤 조사월 → (분자, 분모)
|
|
r[0].split(" → ")[1]: (num(r[1]), num(r[2])) for r in table_after(text, "| 앞 → 뒤 |")[1:]
|
|
}
|
|
hist = table_after(text, "| 직종 (최근 판 코드) |")
|
|
months = hist[0][1:]
|
|
vals = {r[0].split()[0]: r[1:] for r in hist[1:]}
|
|
shown = {r[0].split()[0]: r for r in table_after(text, "| 직종 | 마지막 공표 조사월 |")[1:]}
|
|
n = 0
|
|
for r in make["줄"]:
|
|
if r.get("상태") not in ("미공표", "산정"):
|
|
continue
|
|
cells_ = vals[r["열쇠"]] # 원문 표기 그대로(`**` = 공표값 · `*` · `-` = 값 없음)
|
|
last = max(i for i, v in enumerate(cells_) if v.replace("*", "").strip() not in ("", "-"))
|
|
at, base = months[last], num(cells_[last].replace("*", ""))
|
|
value = Decimal(base)
|
|
for b in months[last + 1 :]:
|
|
top, bottom = steps[b]
|
|
value = (value * top / bottom).to_integral_value(rounding=ROUND_FLOOR)
|
|
want = shown[r["열쇠"]]
|
|
assert str(value) == want[5].replace(",", ""), (r["열쇠"], value, want[5])
|
|
r["값"] = int(value)
|
|
r["상태"] = "산정"
|
|
r["비고"] = (
|
|
f"산정: {at} {base:,}원 × 평균 증가율(원 미만 버림)"
|
|
" — 건설노임 4-라 방식 준용 · 제조 원문에 규정 없음"
|
|
)
|
|
n += 1
|
|
make["원천"] = [s for s in make["원천"] if s["역할"] == "primary"] + [
|
|
{
|
|
"경로": p.relative_to(ROOT).as_posix(),
|
|
"해시": hashlib.sha256(p.read_bytes()).hexdigest(),
|
|
"역할": "산정근거",
|
|
}
|
|
for p in [*sorted(KBIZ.glob("20??-??-??/3. 직종별 조사노임.md")), track]
|
|
]
|
|
head = {k: v for k, v in make.items() if k != "줄"}
|
|
dump_master(MD / "인력_제조노임.json", head, make["줄"])
|
|
print(f"제조노임 미공표 산정 {n} 줄 · 미공표직종_추적.md 5절 버림 열과 같음")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|