knowledge(마스터): 되풀이 기호 바로잡기 · 미공표 산정 버림 · 자재품목 유류·원문번호 정리

되풀이 기호(〃 · ″ · " · 上同)
- 값·칸이름이 통째로 되풀이 기호인 자리 167 곳을 윗줄 값으로 — 단위 · 장비구분 · 장비명 · 항목 · 비고 · 재질
- 소요량 13-6-3 · 13-6-5 · 13-6-9 의 직종 칸 「〃」·「"」 는 윗줄 직종(플랜트제관공)으로 합치고 로직 식 8 곳도 함께
- 인력 LB000399(이름 「〃」)는 직종이 아니라 되풀이 기호 — 줄을 떼고 대장에 폐기
- 인치 기호(1/2〃 · ø12〃)와 설명 글 속 기호는 그대로

건설노임 미공표 산정
- 회차마다 증가율을 곱할 때 원 미만 버림 — 다산소프트와 같은 방식 · 값은 원 단위 정수
- 비고 「4-라 산정: 2025.9 210,772원 × 증가율(원 미만 버림)」
- 미공표직종_추적.md 5절 「단계마다 버림(채택)」 열을 채택으로 바꾸고 산정값(처리 없음)은 견주기용으로 남김

자재품목
- 유류 5줄(휘발유 · 경유 · 선박용경유 · 중유 · 등유) 뗌 — 값이 비고 글뿐 · 대장에 폐기 · 빌더도 뺌
- 품셈재료 중유(MP000359)는 미연결 — 중유는 받지 않기로 함 · 전력 2줄은 그대로 둠
- 원문번호(다산소프트 품번 M000…) 열 삭제 — 대조는 _키대장.json 에만
- 로직·품셈재료의 품번 참조 330 곳을 키로 · master_formula 는 「ID:키」 도 받음
- check_master · master_keys.stamp · M01 저장은 NO_NUMBER 테이블을 알아봄 · _틀.md · _화면_계약.md 도 맞춤

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
This commit is contained in:
2026-09-20 18:36:08 +09:00
co-authored by Claude Opus 5
parent b94e17ef80
commit 991b671c32
24 changed files with 24494 additions and 24472 deletions
@@ -18,7 +18,7 @@ import json
import re
import sys
from collections import Counter
from decimal import ROUND_HALF_UP, Decimal
from decimal import ROUND_FLOOR, Decimal
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
@@ -369,16 +369,15 @@ def estimate(wage: dict) -> None:
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:
while start != until: # 회차마다 원 미만 버림 — 다산소프트와 같은 방식
_, b, _, top, bottom = next(s for s in steps if s[0] == start)
value = value * top / bottom
value = (value * top / bottom).to_integral_value(rounding=ROUND_FLOOR)
start = b
value = value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
want = shown[r["열쇠"]]
assert str(value) == want[4].replace(",", ""), (r["열쇠"], value, want[4])
r[""] = float(value)
assert str(value) == want[6].replace(",", ""), (r["열쇠"], value, want[6])
r[""] = int(value)
r["상태"] = "산정"
r["비고"] = f"4-라 산정: {at.rsplit('.', 1)[0]} {base:,}× 증가율"
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[""])
@@ -27,6 +27,14 @@ from compare_estx import ESTX, ROOT, SLOTS, Estx # noqa: E402
OUT = ROOT / "resources/master_data/재료_자재품목.json"
BASE = SLOTS[0] # Cost1 「가격정보」 — 관급이면 관급 열 · 아니면 비고
MARKET = SLOTS[1:5] # 물가지 넷 · 견적가(Cost6)는 마스터 아님
# 유류 — 값이 비고 글뿐이라 뺌 · 유가는 재료_오피넷유가 · 중유는 받지 않음(대장 「폐기」)
DROP = {
"M0003039885": "휘발유",
"M0003039886": "경유",
"M0004805582": "선박용경유",
"M0003039887": "중유",
"M0003039888": "등유",
}
def num(v):
@@ -48,7 +56,7 @@ def build(rows: list[dict]) -> list[dict]:
out = []
for r in sorted(rows, key=lambda r: r["Index"]):
code = r.get("Code")
if not code:
if not code or code in DROP:
continue
base, page = num(r.get("Cost1")), r.get("Page1") or None
public = "관급" in r.get("Name", "") and base is not None
@@ -75,10 +83,11 @@ def check(rows: list[dict]) -> None:
"""원본 대조 — 줄 수 · 값 열마다 합 · 관급·가격정보 가름 · 표본 쉰 줄."""
data = json.loads(OUT.read_text(encoding="utf-8"))
got = {r["열쇠"]: r for r in data[""]}
raw = {r["Code"]: r for r in rows if r.get("Code")}
raw = {r["Code"]: r for r in rows if r.get("Code") and r["Code"] not in DROP}
assert len(got) == len(data[""]) == len(raw), (len(got), len(raw))
print(
f"{len(got)} = 원본 코드 있는 줄 {len(raw)} (코드 없는 빈 줄 {len(rows) - len(raw)} 뺌)"
f"{len(got)} = 원본 코드 있는 줄 {len(raw)}"
f" (코드 없는 빈 줄 {sum(1 for r in rows if not r.get('Code'))} · 유류 {len(DROP)} 뺌)"
)
for i, slot in enumerate((*MARKET, "관급"), 2):
a = math.fsum(r.get(f"Cost{i}") or 0 for r in raw.values())
@@ -124,8 +133,10 @@ def main():
"비고": [
"다산소프트 EST 2026-07 자재 · 개발용 · 배포 전 판권 구매",
"값 열 다섯 = 물가지 넷(물가자료 · 유통물가 · 물가정보 · 거래가격) · 관급(조달) — 채택값·기본가 없음 · 쓰는 곳이 값을 고르고 출처를 밝힘",
"유류(휘발유 · 경유 · 선박용경유 · 중유 · 등유)는 값이 비고 글뿐이라 뺌 — 유가는 재료_오피넷유가 · 중유는 받지 않음",
"원문번호(다산소프트 품번) 칸 없음 — 대조는 _키대장.json 에만",
"관급 = 원문 「가격정보」 칸의 조달청 관급 단가(철근 · 레미콘 · 아스콘 · H형강 등) · 기간은 비고 「조달 26.7」",
"원문 「가격정보」 칸에 시중 값이 남은 72 줄은 값 열에 넣지 않고 비고 「가격정보 ○○」 로만 둠 — 출처가 조달인지 물가지인지 원문이 안 가름",
"원문 「가격정보」 칸에 시중 값이 남은 줄은 값 열에 넣지 않고 비고 「가격정보 ○○」 로만 둠 — 출처가 조달인지 물가지인지 원문이 안 가름",
"구분 · 상세구분 = 원문에 대·중분류 칸이 없어 비움(지어내지 않음)",
"면수 = 물가지 값의 원문 면수 · 화면에는 접어 둠",
],
+11 -4
View File
@@ -115,9 +115,9 @@ LABOR_COLS = (
LABOR_STATUS = ("공표", "산정", "미공표", "미확보")
# 원문별 열 한 벌 — 모든 줄이 이 칸을 이 차례로 가짐(없으면 null)
COLS = {
# 자재품목 — 원문번호(남의 품번)는 줄에 두지 않음 · 대조는 `_키대장.json` 에만
"자재품목": (
"",
"원문번호",
"구분",
"상세구분",
"이름",
@@ -241,10 +241,12 @@ def check_form(name: str, data: dict) -> list[str]:
else None
)
for row in rows:
key, number = str(row.get("")), str(row.get("원문번호"))
key, number = str(row.get("")), str(row.get("원문번호") or "")
where = f"{name} · {key} {row.get('이름') or number}"
if not mk.KEY.fullmatch(key) or key[:2] != tid:
out.append(f"{where} · 키 모양이 「{tid}」 + 6자리 아님")
if number and tid in mk.NO_NUMBER:
out.append(f"{where} · 원문번호는 줄에 두지 않음 — 대장에만")
if merged and row.get(merged) not in data.get(merged, {}):
out.append(f"{where} · {merged}{row.get(merged)}」 이 머리에 없음")
if number and mk.section_no(number) != number:
@@ -502,7 +504,7 @@ def duplicate_keys(files: dict[str, dict], book: dict | None = None) -> list[str
rows = data.get("") if data.get("그룹") in mf.TABLE_GROUPS else data.get("")
one_each = data.get("그룹") in ("인력", "재료")
for row in rows or []:
key, number = str(row.get("")), str(row.get("원문번호"))
key, number = str(row.get("")), str(row.get("원문번호") or "")
if key in first:
out.append(f"{name} · 키 겹침 「{key}」 — {first[key]} 에도 있음")
first.setdefault(key, name)
@@ -515,7 +517,12 @@ def duplicate_keys(files: dict[str, dict], book: dict | None = None) -> list[str
had = book[""].get(key)
if had is None:
out.append(f"{name} · {key} · 대장에 없는 키")
elif number not in (had["원문번호"], mk.section_no(had["원문번호"])):
continue
# 줄에 번호를 두지 않는 테이블(`mk.NO_NUMBER`)은 대조를 건너뜀
if key[:2] not in mk.NO_NUMBER and number not in (
had["원문번호"],
mk.section_no(had["원문번호"]),
):
out.append(f"{name} · {key} · 대장 원문번호 「{had['원문번호']}」 와 다름")
elif int(key[2:]) >= book["다음"].get(key[:2], 0):
out.append(f"{name} · {key} · 대장 다음 번호보다 큼")
@@ -228,7 +228,7 @@ def walk(node):
# ── 마스터 모음 ────────────────────────────────────────────────────────
class Master:
"""요소·표·로직 찾기 — `files` = {파일 이름: 읽은 JSON}(수는 Decimal).
`ref` = 키(「LB000123」) · 「ID:원문번호」(`{이름}` 을 끼운 원문번호)."""
`ref` = 키(「LB000123」) · 「ID:원문번호」(`{이름}` 을 끼운 원문번호 · 끼운 값이 키여도 됨)."""
def __init__(self, files: dict[str, dict]):
self.index: dict[str, dict[str, dict]] = {} # 테이블ID → {키: 줄}
@@ -248,7 +248,9 @@ class Master:
def get(self, ref: str) -> dict:
tid, _, number = ref.partition(":")
if number:
if number and mk.KEY.fullmatch(number) and number[:2] == tid:
row = self.index.get(tid, {}).get(number) # 「ID:키」 — 고르기 값이 키인 자리
elif number:
if (tid, number) in self.twice:
raise FormulaError(f"원문번호 겹침 「{ref}」 — 키로 적을 것")
row = self.numbers.get(tid, {}).get(number)
+6 -2
View File
@@ -34,6 +34,8 @@ TABLES = {
}
GROUP_OF = {tid: group for (group, _), tid in TABLES.items()}
IDS = tuple(GROUP_OF)
# 줄에 원문번호를 두지 않는 테이블 — 번호가 남의 품번이라 이름 몫을 못 함(대조는 대장에만)
NO_NUMBER = ("MT",)
def table_id(group: str, book: str | None) -> str:
@@ -107,14 +109,16 @@ def stamp(
book: dict, tid: str, rows: list[dict], file: str, extra: dict | None = None
) -> list[dict]:
"""빌더가 만든 줄(「열쇠」) → 「키」 · 「원문번호」(절 번호만) · extra 칸을 앞에 둔 줄.
대장은 열쇠(부문·제목까지) 그대로 지님 키를 다시 찾는 열쇠라서."""
대장은 열쇠(부문·제목까지) 그대로 지님 키를 다시 찾는 열쇠라서.
`NO_NUMBER` 테이블은 줄에 원문번호를 두지 않음 대장에만."""
out = []
for row in rows:
row = dict(row)
number = str(row.pop("열쇠", row.get("원문번호")))
row.pop("원문번호", None)
key = row.pop("", None) or issue(book, tid, number, file)
out.append({"": key, "원문번호": section_no(number), **(extra or {}), **row})
head = {"": key} if tid in NO_NUMBER else {"": key, "원문번호": section_no(number)}
out.append({**head, **(extra or {}), **row})
return out