feat(master_data): 재료 고르기 후보를 줄 단위와 같은 자재품목으로만 거름
- 엔진 — candidates·pick·element 에 unit · 호표 줄 단위를 그대로 넘김(우드칩 ㎥ 에 원목후로링 ㎡ 가 섞이던 자리) - 후보 API — /materials 에 unit · 단가 미리보기도 줄 단위로 거름 - check_master 「단위」 검사 새로 — 조건 안 줄이 모두 다른 단위거나 대표 줄 단위가 다른 자리 37건(로직 검사와 따로 — 조건을 좁히라는 알림) - 품셈재료 고르기 칸이 채워진 호표 줄 24개를 고르기 조건 묶음으로 바꿈 - 일괄 시험 계산 통과 1,270 → 1,272 · 시험 둘 더함 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
This commit is contained in:
@@ -337,6 +337,25 @@ def check_links(whole: mf.Master) -> list[str]:
|
||||
return out
|
||||
|
||||
|
||||
def check_units(files: dict[str, dict], whole: mf.Master) -> list[str]:
|
||||
"""단위 경고 — 재료 고르기 조건의 후보 단위가 그 줄 단위와 다름(호표 줄 · 품셈재료 줄).
|
||||
데이터가 틀린 것이 아니라 조건을 더 좁혀야 하는 자리라 로직 검사와 따로 봄."""
|
||||
out = []
|
||||
for key, row in whole.index.get("MP", {}).items():
|
||||
cond = row.get("고르기")
|
||||
if isinstance(cond, dict):
|
||||
out += mf.check_unit(whole, whole.label(key) + " 고르기", cond, row.get("단위"))
|
||||
for name, data in files.items():
|
||||
if data.get("그룹") != "로직":
|
||||
continue
|
||||
for row in data.get("줄", []):
|
||||
for item in row.get("호표", []):
|
||||
if isinstance(item.get("요소"), dict):
|
||||
where = f"{name} · {row.get('키')} 호표 {item.get('이름', '')} 요소"
|
||||
out += mf.check_unit(whole, where, item["요소"], item.get("단위"))
|
||||
return out
|
||||
|
||||
|
||||
def _check_table(where: str, table: dict) -> list[str]:
|
||||
out, conds, cols = [], table.get("조건", {}), table.get("값칸", {})
|
||||
allowed = set(conds) | set(cols) | {"단위", "짝"} | {c + "원문" for c in cols}
|
||||
@@ -613,6 +632,8 @@ def main(argv: list[str]) -> int:
|
||||
report["본문"] = check_body(files)
|
||||
if mode in ("로직", "전부"):
|
||||
report["로직"] = check_logics(files, whole)
|
||||
if mode in ("단위", "전부"):
|
||||
report["단위"] = check_units(files, whole)
|
||||
for title, found in report.items():
|
||||
print(f"({title}) {len(found)}건")
|
||||
for line in found:
|
||||
|
||||
@@ -481,7 +481,7 @@ def run(master: Master, ref: str, given: dict, depth: int = 0) -> dict:
|
||||
# 고르기 조건 묶음은 그대로 넘김 — `element` 가 대표 줄·첫 줄로 풂
|
||||
ref = item["요소"] if isinstance(item["요소"], dict) else fill(item["요소"], env)
|
||||
try:
|
||||
price, source = element(master, ref, env)
|
||||
price, source = element(master, ref, env, item.get("단위"))
|
||||
except FormulaError:
|
||||
if qty != 0:
|
||||
raise
|
||||
@@ -703,6 +703,7 @@ from master_material import ( # noqa: E402, F401
|
||||
TARIFF,
|
||||
candidates,
|
||||
check_pick,
|
||||
check_unit,
|
||||
cond_text,
|
||||
element,
|
||||
pick,
|
||||
|
||||
@@ -111,35 +111,58 @@ def cond_text(cond: dict) -> str:
|
||||
return "|".join(str(cond.get(k) or "") for k in PICK_KEYS)
|
||||
|
||||
|
||||
def candidates(master: mf.Master, cond: dict, env: dict | None = None) -> list[str]:
|
||||
def candidates(
|
||||
master: mf.Master, cond: dict, env: dict | None = None, unit: str | None = None
|
||||
) -> list[str]:
|
||||
"""고르기 조건 안 자재품목 키 — 구분 · 상세구분 · 규격 낱말이 모두 든 줄 · 파일 차례대로.
|
||||
`unit` 을 주면 그 단위(호표 줄 단위)와 같은 줄만 — 단위가 다르면 다른 물건.
|
||||
입력 `자재지역` 이 있으면 이름에 그 지역이 든 줄만(그런 줄이 없으면 조건 안 전부)."""
|
||||
want = [plain_spec(w) for w in str(cond.get("규격") or "").split()]
|
||||
detail = cond.get("상세구분")
|
||||
need = plain_unit(unit) if unit else ""
|
||||
hits = [
|
||||
key
|
||||
for key, row in master.index.get(MARKET, {}).items()
|
||||
if row.get("구분") == cond.get("구분")
|
||||
and (not detail or row.get("상세구분") == detail)
|
||||
and all(w in plain_spec(row.get("규격")) for w in want)
|
||||
and (not need or plain_unit(row.get("단위")) == need)
|
||||
]
|
||||
region = plain((env or {}).get(REGION, ""))
|
||||
near = [k for k in hits if region in plain(master.get(k).get("이름"))] if region else []
|
||||
return near or hits
|
||||
|
||||
|
||||
def pick(master: mf.Master, cond: dict, env: dict) -> str:
|
||||
"""시험 계산이 쓸 줄 — 관리자가 정한 `대표`, 없으면 조건 안 값 있는 첫 줄."""
|
||||
def pick(master: mf.Master, cond: dict, env: dict, unit: str | None = None) -> str:
|
||||
"""시험 계산이 쓸 줄 — 관리자가 정한 `대표`, 없으면 조건 안 값 있는 첫 줄(단위 같은 줄만)."""
|
||||
if cond.get("대표"):
|
||||
return str(cond["대표"])
|
||||
keys = candidates(master, cond, env)
|
||||
keys = candidates(master, cond, env, unit)
|
||||
keys = [k for k in keys if _offers(master, k)] or keys
|
||||
if not keys:
|
||||
raise mf.FormulaError(f"재료 고르기 후보 없음 — {cond}")
|
||||
return keys[0]
|
||||
|
||||
|
||||
def check_pick(master: mf.Master, where: str, cond: dict) -> list[str]:
|
||||
def check_unit(master: mf.Master, where: str, cond: dict, unit: str | None) -> list[str]:
|
||||
"""단위 경고 — 호표 줄·품셈재료 줄 단위와 맞는 후보가 없거나 대표 줄 단위가 다름.
|
||||
틀린 데이터가 아니라 좁혀야 할 조건이라 `check_pick` 과 따로 봄(`check_master 단위`)."""
|
||||
rows = master.index.get(MARKET, {})
|
||||
rep = str(cond.get("대표") or "")
|
||||
if not unit or set(cond) - set(PICK_KEYS) or not cond.get("구분"):
|
||||
return []
|
||||
if rep in rows:
|
||||
if plain_unit(rows[rep].get("단위")) != plain_unit(unit):
|
||||
return [
|
||||
f"{where} · 대표 줄 단위 「{rows[rep].get('단위')}」 이 줄 단위 「{unit}」 과 다름"
|
||||
]
|
||||
return []
|
||||
if candidates(master, cond) and not candidates(master, cond, None, unit):
|
||||
return [f"{where} · 단위 「{unit}」 후보 0 — 조건 안 줄은 모두 다른 단위"]
|
||||
return []
|
||||
|
||||
|
||||
def check_pick(master: mf.Master, where: str, cond: dict, unit: str | None = None) -> list[str]:
|
||||
"""고르기 조건 검사 — 칸 이름 · 구분·상세구분이 자재품목에 있는지 · 대표 줄 · 후보 0."""
|
||||
if set(cond) - set(PICK_KEYS) or not cond.get("구분"):
|
||||
return [f"{where} · 재료 고르기 조건 모양 「{cond}」 — {' · '.join(PICK_KEYS)}"]
|
||||
@@ -157,11 +180,13 @@ def check_pick(master: mf.Master, where: str, cond: dict) -> list[str]:
|
||||
return out
|
||||
|
||||
|
||||
def element(master: mf.Master, ref, env: dict) -> tuple[object, str | None]:
|
||||
def element(
|
||||
master: mf.Master, ref, env: dict, unit: str | None = None
|
||||
) -> tuple[object, str | None]:
|
||||
"""요소 값과 출처 — 자재품목은 제 줄, 품셈재료는 연결을 따라가 낮은 값 · 그 밖은 줄의 `값`.
|
||||
`ref` 가 고르기 조건 묶음이면 대표 줄(없으면 조건 안 첫 줄)로 풂."""
|
||||
`ref` 가 고르기 조건 묶음이면 대표 줄(없으면 단위 같은 조건 안 첫 줄)로 풂."""
|
||||
if isinstance(ref, dict):
|
||||
ref = pick(master, ref, env)
|
||||
ref = pick(master, ref, env, unit)
|
||||
row = master.get(ref)
|
||||
if ref[:2] == MARKET: # 자재품목을 바로 가리키는 호표 줄
|
||||
return _lowest(master, ref)
|
||||
|
||||
@@ -63,7 +63,10 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": "MP000097",
|
||||
"요소": {
|
||||
"구분": "건축마감",
|
||||
"상세구분": "천장·바닥·마감재"
|
||||
},
|
||||
"이름": "외부PANEL(벽) 1,200×2,400㎜",
|
||||
"단위": "매",
|
||||
"수량": "찾기(QC000012, 구분='외부PANEL(벽)', 종류=주자재종류).수량 * 손율 / 100",
|
||||
@@ -71,7 +74,10 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": "MP000098",
|
||||
"요소": {
|
||||
"구분": "건축마감",
|
||||
"상세구분": "천장·바닥·마감재"
|
||||
},
|
||||
"이름": "외부PANEL (창문) 1,200×2,400㎜",
|
||||
"단위": "매",
|
||||
"수량": "찾기(QC000012, 구분='외부PANEL (창문)', 종류=주자재종류).수량 * 손율 / 100",
|
||||
@@ -79,7 +85,10 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": "MP000099",
|
||||
"요소": {
|
||||
"구분": "건축마감",
|
||||
"상세구분": "천장·바닥·마감재"
|
||||
},
|
||||
"이름": "외부PANEL (철재문) 1,200×2,400㎜",
|
||||
"단위": "매",
|
||||
"수량": "찾기(QC000012, 구분='외부PANEL (철재문)', 종류=주자재종류).수량 * 손율 / 100",
|
||||
@@ -87,7 +96,10 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": "MP000100",
|
||||
"요소": {
|
||||
"구분": "건축마감",
|
||||
"상세구분": "천장·바닥·마감재"
|
||||
},
|
||||
"이름": "내부PANEL(벽) 1,200×2,400㎜",
|
||||
"단위": "매",
|
||||
"수량": "만약(종류 == '사무실', 찾기(QC000012, 구분='내부PANEL(벽)', 종류=주자재종류).수량 * 손율 / 100, 0)",
|
||||
@@ -103,7 +115,10 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": "MP000102",
|
||||
"요소": {
|
||||
"구분": "건축마감",
|
||||
"상세구분": "천장·바닥·마감재"
|
||||
},
|
||||
"이름": "PANEL JOINT(AL-BAR) L=2,400㎜",
|
||||
"단위": "조",
|
||||
"수량": "찾기(QC000012, 구분='PANEL JOINT(AL-BAR)', 종류=주자재종류).수량 * 손율 / 100",
|
||||
@@ -111,7 +126,10 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": "MP000103",
|
||||
"요소": {
|
||||
"구분": "건축마감",
|
||||
"상세구분": "천장·바닥·마감재"
|
||||
},
|
||||
"이름": "CANOPY(출입구채양) 600×1,200㎜",
|
||||
"단위": "매",
|
||||
"수량": "찾기(QC000012, 구분='CANOPY(출입구채양)', 종류=주자재종류).수량 * 손율 / 100",
|
||||
@@ -119,7 +137,10 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": "MP000104",
|
||||
"요소": {
|
||||
"구분": "건축마감",
|
||||
"상세구분": "천장·바닥·마감재"
|
||||
},
|
||||
"이름": "박공PANEL",
|
||||
"단위": "매",
|
||||
"수량": "찾기(QC000012, 구분='박공PANEL', 종류=주자재종류).수량 * 손율 / 100",
|
||||
@@ -127,7 +148,11 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": "MP000105",
|
||||
"요소": {
|
||||
"구분": "건축마감",
|
||||
"상세구분": "천장·바닥·마감재",
|
||||
"대표": "MT007262"
|
||||
},
|
||||
"이름": "ROOFSHEET 0.5㎜COLORSHEET",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC000012, 구분='ROOFSHEET', 종류=주자재종류).수량 * 손율 / 100",
|
||||
@@ -3974,7 +3999,10 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": "MP000130",
|
||||
"요소": {
|
||||
"구분": "잡재료",
|
||||
"상세구분": "소모품"
|
||||
},
|
||||
"이름": "방진망",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC000133, 구분='방진망').수량",
|
||||
|
||||
@@ -140,7 +140,10 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": "MP000158",
|
||||
"요소": {
|
||||
"구분": "매트·토목섬유",
|
||||
"상세구분": "부직포·토목섬유"
|
||||
},
|
||||
"이름": "펠트",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC001251, 구분='펠트').수량",
|
||||
@@ -1106,7 +1109,11 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": "MP000168",
|
||||
"요소": {
|
||||
"구분": "건축마감",
|
||||
"상세구분": "천장·바닥·마감재",
|
||||
"대표": "MT008074"
|
||||
},
|
||||
"이름": "초배지",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC001269, 바탕면=바탕면).초배지",
|
||||
|
||||
@@ -407,7 +407,10 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": "MP000186",
|
||||
"요소": {
|
||||
"구분": "건축마감",
|
||||
"상세구분": "천장·바닥·마감재"
|
||||
},
|
||||
"이름": "폴리카보네이트",
|
||||
"단위": "㎡",
|
||||
"수량": "찾기(QC001319, 구분='폴리카보네이트').수량",
|
||||
|
||||
@@ -3205,7 +3205,10 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": "MP000251",
|
||||
"요소": {
|
||||
"구분": "잡재료",
|
||||
"상세구분": "소모품"
|
||||
},
|
||||
"이름": "Sheet Metal Screw",
|
||||
"단위": "개",
|
||||
"수량": "만약(보온두께 == '30mm이하', 찾기(QC001650, 'Pipe Size mm'=관경, 구분='직관의물량').'Sheet Metal Screw', 만약(보온두께 == '31mm∼40mm', 찾기(QC001651, 'Pipe Size mm'=관경, 구분='직관의물량').'Sheet Metal Screw', 만약(보온두께 == '41mm∼60mm', 찾기(QC001652, 'Pipe Size mm'=관경, 구분='직관의물량').'Sheet Metal Screw', 만약(보온두께 == '61mm∼75mm', 찾기(QC001653, 'Pipe Size mm'=관경, 구분='직관의물량').'Sheet Metal Screw', 찾기(QC001654, 'Pipe Size mm'=관경, 구분='직관의물량').'Sheet Metal Screw')))) * 만약(부위 == '관(m당)', 1, 0)",
|
||||
@@ -8709,7 +8712,11 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": "MP000289",
|
||||
"요소": {
|
||||
"구분": "잡재료",
|
||||
"상세구분": "고철·기타금속",
|
||||
"대표": "MT003499"
|
||||
},
|
||||
"이름": "함석",
|
||||
"단위": "매",
|
||||
"수량": "0.71",
|
||||
@@ -9009,7 +9016,11 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": "MP000289",
|
||||
"요소": {
|
||||
"구분": "잡재료",
|
||||
"상세구분": "고철·기타금속",
|
||||
"대표": "MT003499"
|
||||
},
|
||||
"이름": "함석",
|
||||
"단위": "매",
|
||||
"수량": "0.62",
|
||||
@@ -10252,7 +10263,11 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": "MP000292",
|
||||
"요소": {
|
||||
"구분": "잡재료",
|
||||
"상세구분": "고철·기타금속",
|
||||
"대표": "MT003499"
|
||||
},
|
||||
"이름": "함석",
|
||||
"단위": "대",
|
||||
"수량": "0.53",
|
||||
@@ -11595,7 +11610,11 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": "MP000307",
|
||||
"요소": {
|
||||
"구분": "잡재료",
|
||||
"상세구분": "고철·기타금속",
|
||||
"대표": "MT003499"
|
||||
},
|
||||
"이름": "함석",
|
||||
"단위": "매",
|
||||
"수량": "1.9",
|
||||
@@ -12095,7 +12114,11 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": "MP000307",
|
||||
"요소": {
|
||||
"구분": "잡재료",
|
||||
"상세구분": "고철·기타금속",
|
||||
"대표": "MT003499"
|
||||
},
|
||||
"이름": "함석",
|
||||
"단위": "매",
|
||||
"수량": "1.7",
|
||||
@@ -12516,7 +12539,11 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": "MP000312",
|
||||
"요소": {
|
||||
"구분": "잡재료",
|
||||
"상세구분": "고철·기타금속",
|
||||
"대표": "MT003499"
|
||||
},
|
||||
"이름": "함석(Template)",
|
||||
"단위": "매",
|
||||
"수량": "0.53",
|
||||
@@ -12552,7 +12579,10 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": "MP000316",
|
||||
"요소": {
|
||||
"구분": "기계·설비",
|
||||
"상세구분": "기계류"
|
||||
},
|
||||
"이름": "Bite",
|
||||
"단위": "개",
|
||||
"수량": "2.5",
|
||||
@@ -12570,7 +12600,10 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": "MP000316",
|
||||
"요소": {
|
||||
"구분": "기계·설비",
|
||||
"상세구분": "기계류"
|
||||
},
|
||||
"이름": "Bite",
|
||||
"단위": "개",
|
||||
"수량": "3.2",
|
||||
@@ -12797,7 +12830,11 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": "MP000307",
|
||||
"요소": {
|
||||
"구분": "잡재료",
|
||||
"상세구분": "고철·기타금속",
|
||||
"대표": "MT003499"
|
||||
},
|
||||
"이름": "함석",
|
||||
"단위": "매",
|
||||
"수량": "1.2",
|
||||
@@ -13044,7 +13081,11 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": "MP000318",
|
||||
"요소": {
|
||||
"구분": "잡재료",
|
||||
"상세구분": "고철·기타금속",
|
||||
"대표": "MT003499"
|
||||
},
|
||||
"이름": "함석",
|
||||
"단위": "매",
|
||||
"수량": "0.71",
|
||||
@@ -19220,7 +19261,11 @@
|
||||
},
|
||||
{
|
||||
"종류": "재료",
|
||||
"요소": "MP000339",
|
||||
"요소": {
|
||||
"구분": "잡재료",
|
||||
"상세구분": "고철·기타금속",
|
||||
"대표": "MT003499"
|
||||
},
|
||||
"이름": "함석",
|
||||
"단위": "매",
|
||||
"수량": "(만약(공사 == '기기', 0.05, 만약(공사 == '철골', 0.07, 만약(공사 == '배관', 0.05, 만약(공사 == 'Belt & Conveyor', 0.05, 만약(공사 == 'Heater & Tank', 0.05, 만약(공사 == 'Pump & Fan', 0.05, 만약(공사 == 'Crane 류', 0.07, 0))))))))",
|
||||
|
||||
Reference in New Issue
Block a user