Files
Aislo/resources/master_data/scripts/material_merge.py
T
eomsangdonandClaude Opus 5.5 86cf20d997 feat(830-10): 재료 표 지역 열 분리 · 남은 상하차 글 판독 — 이름 · 규격의 지역 글을 단위 뒤 지역 칸으로
- 지역 칸 = 단위 바로 뒤 · 상하차 앞 · 적힌 지역 글 그대로(꼴 일곱 F1 ~ F6 · 6,604 줄 · 242 종) · 없음 = -(17,423 줄)
- 이름 꼴: 레미콘(서울)-사급 → 레미콘-사급 · 레미콘(관급)-경상북도(칠곡군) → 레미콘(관급) · 이형철근(SD400,제주)-관급 → 이형철근(SD400)-관급 · 모래(건조상태·전국) → 모래(건조상태) · 관급 · 사급 글은 남김
- 지역 아님은 그대로: 업체 · 산지(농산개발(홍성) · 보령웅천석) · 수종 · 상표
- EST 이름 칸 30 자에서 잘린 지역 글 30 줄 = 잘린 그대로 · 비고 「지역 글 잘림」
- 상하차: 폐기물운반비(상차포함) = 상차 · (상차제외) = - · 글은 이름에서 뺌(상차 125 → 135)
- 줄 · 키 · 차례 · 물가지 값 · 면수 · 출처 그대로(24,027) · 새 겹침 0(겹침 422 묶음 = 830-9 때부터)
- 손 값(지역 · 상하차 · 일반가격) 빌더가 이어 받음 · 「-」 은 손 값 아님
- check_master 지역 빈 칸 검사 · test_830_2 기대를 지역 칸 꼴로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 20:26:53 +09:00

137 lines
7.2 KiB
Python

"""재료 자재품목 열 한 벌 · 지역 · 상하차(요구 828 · 830 · 831 · 834 · 836 · 837 · 839 · PLAN 830-10).
build_재료_자재품목 이 부름 · 시험 = test_830_2_material_merge.
줄 목록 = EST 그대로(지역 · 관급 · 사급 · 제조사 · 조합 줄마다 따로) — 합치지 않음.
열 한 벌 = COLUMNS — 지역 · 상하차 = 단위 바로 뒤 · 가격 열 = 조달청(관급) · 물가지 넷 · 일반가격(사급).
지역 칸 = 이름 · 규격에 적힌 지역 글 그대로(`split_region` 꼴 일곱 · 표기 안 고침 · 없음 = -) — 그 글은 이름 · 규격에서 뺌
· 관급 · 사급 글은 이름에 남김 · 상표 · 산지 · 수종 글(「보령웅천석」 · 「농산개발(홍성)」)은 지역 아님
· EST 이름 칸 30 자에서 잘린 지역 글은 잘린 그대로 · 비고에 CUT.
사급 / 관급 고르기는 재료 표 열이 아님 — 단가산출 쪽 몫(요구 837).
상하차 칸 = 상차 · 하차 · -(기본 · 모름) — 이름 · 규격 끝 인도 조건 글로 채우고 그 글은 이름 · 규격에서 뺌(요구 836)
「도착도」 · 「하차도」 = 하차 · 「채취장상차(도)」 · 「하치장 · 생산공장 상차도」 · 「(상차도)」 = 상차 ·
「(상차포함)」 = 상차 · 「(상차제외)」 = -(포함 안 됨 · 같은 이름의 상차 줄과 상하차 칸으로 갈림)
조달청 칸 값은 모두 비움(EST 「가격정보」 — 요구 827 · 831) · 일반가격 = 시스템 관리자가 넣는 칸.
지역 · 상하차 · 일반가격 손 값은 빌더가 이어 받음(지역 · 상하차 「-」 = 손 값 아님 — 판독 값).
"""
import re
MARKET = ("물가자료", "유통물가", "물가정보", "거래가격")
REGION = "지역" # 표지 칸 — 적힌 지역 글 그대로 · 없음 = NONE
NONE = "-"
LOAD = "상하차" # 표지 칸(값 칸 아님)
LOADS = ("-", "상차", "하차") # 첫째 = 기본(모름)
GOVT = "조달청" # 관급 가격 칸 — 가장 낮은 값 고르기에 안 넣음(요구 826 · 830)
OWN = "일반가격" # 시스템 관리자 손 입력 칸(사급)
HAND = (REGION, LOAD, OWN) # 빌더가 이어 받는 손 값 칸
COLUMNS = ( # 열 한 벌 · 차례(요구 830 · 831)
"키",
"구분",
"상세구분",
"이름",
"규격",
"단위",
REGION,
LOAD,
GOVT,
*MARKET,
OWN,
"출처",
"비고",
"면수",
)
NAME_LOAD = re.compile(
r",\s*(도착도|하차도|채취장상차도|채취장상차)$|\(((?:상|하)차(?:포함|제외))\)$"
) # 모 래, 서울, 도착도 · 폐기물운반비(상차포함)
SPEC_LOAD = re.compile(
r"\s*(?:\((상차도)\)|((?:하치장|생산공장)\s*상차도))$"
) # HD=10mm 하치장 상차도
def split_load(row: dict) -> tuple[str, str, str, str | None]:
"""(인도 조건 글을 뺀 이름, 규격, 상하차 값, 뺀 글) — 도착 · 하차 = 하차 · 상차 = 상차 · 글 없음 = -."""
name, spec = row.get("이름") or "", row.get("규격") or ""
m = NAME_LOAD.search(name)
if m:
name, text = name[: m.start()], m[1] or m[2]
if text.endswith("제외"):
return name, spec, LOADS[0], text
else:
m = SPEC_LOAD.search(spec)
if not m:
return name, spec, LOADS[0], None
spec, text = spec[: m.start()], m[1] or m[2]
return name, spec, "하차" if text.startswith(("도착", "하차")) else "상차", text
def place(row: dict) -> dict:
"""COLUMNS 차례의 줄 — 첫 칸(열쇠 · 키)은 그대로 · 인도 조건 글은 상하차 칸 · 지역 글은 지역 칸으로 ·
손 값(지역 · 상하차 — 「-」 아님)이 있으면 그 값 · 조달청 = 빔 · 옛 칸(관급)은 뺌."""
name, spec, load, _ = split_load(row)
name, spec, region = split_region(name, spec)
first = next(iter(row))
note = row.get("비고")
if (
region != NONE
and len(row.get("이름") or "") >= 30
and region.count("(") > region.count(")")
):
note = " · ".join(x for x in (note, CUT) if x)
fix = {
"이름": name,
"규격": spec,
REGION: row.get(REGION) if row.get(REGION) not in (None, "", NONE) else region,
LOAD: row.get(LOAD) if row.get(LOAD) not in (None, "", NONE) else load,
GOVT: None,
"비고": note,
}
return {first: row[first], **{c: fix[c] if c in fix else row.get(c) for c in COLUMNS[1:]}}
# ── 지역 글(요구 839 · 조사 818 2 절 꼴 일곱) ─────────────────────────────
CUT = "지역 글 잘림 — EST 이름 칸 30 자"
PLACES = frozenset(
"""전국 서울 부산 대구 인천 광주 대전 울산 세종 제주 경기 강원 충북 충남 전북 전남 경북 경남
청주 전주 포항 창원 춘천 충주 강릉 천안 원주 여수 구미 순천 수원 목포 금산 영천 논산 삼척 홍성
익산 청도 양양 남원 공주 군산 김천 가평 용인 포천 경주 아산 제천 여주 보령 부여 당진 청원 음성
서산 영동 진천 예산 장성 고흥 고창 광양 고령 성주 영주 안동 칠곡 상주 문경 합천 화성 경산""".split()
) # 콤마 · 괄호 꼴에 적힌 지역 낱말(자료에서) — 관급 꼬리(F3)는 낱말 표 없이 꼬리 전부
_P = "|".join(sorted(PLACES, key=len, reverse=True))
_F2 = re.compile(rf"^(.*?)\(({_P})\)-사급$") # 레미콘(서울)-사급
_F4 = re.compile(rf"^(.*?)\((SD\d+),({_P})\)-관급$") # 이형철근(SD400,제주)-관급
_F3 = re.compile(
r"^(.*?\(관급\))-(?!거양|SD\d)(.+)$"
) # 레미콘(관급)-경상북도(칠곡군) · 거양 = 제조사
_F1 = re.compile(rf"^({_P})\s*(\(관급\))?$") # 이름 콤마 조각 「, 서울」 · 「, 제주 (관급)」
_F5 = re.compile(
rf"\((?:([^()·]+)·)?((?:{_P})(?:지역)?)(,관급)?\)"
) # (서울) · (건조상태·전국) · (충남지역,관급)
_F6 = re.compile(
rf"(?<![가-힣])\s?\(((?:{_P})(?:지역)?)\)"
) # 규격 42C (서울) · 0.7M(제주지역) · 「농산개발(홍성)」 = 업체 · 지역 아님
def split_region(name: str, spec: str) -> tuple[str, str, str]:
"""(지역 글을 뺀 이름, 규격, 지역 글 | NONE) — 꼴 일곱(F1 이름 끝 콤마 · F2 (지역)-사급 · F3 (관급)-꼬리 ·
F4 (강종,지역)-관급 · F5 이름 속 괄호 · F6 규격 속 괄호 · F7 규격 전체) · 관급 · 사급 글은 남김."""
name, spec = name or "", spec or ""
if m := _F2.match(name):
return f"{m[1]}-사급", spec, m[2]
if m := _F4.match(name):
return f"{m[1]}({m[2]})-관급", spec, m[3]
if m := _F3.match(name):
return m[1], spec, m[2]
parts = name.split(",")
for i, part in enumerate(parts[1:], 1):
if m := _F1.match(part.strip()):
rest = ",".join(parts[:i] + parts[i + 1 :])
return (rest.rstrip() + m[2] if m[2] else rest), spec, m[1]
if m := _F5.search(name):
inner = ",".join(x for x in (m[1], (m[3] or "")[1:]) if x)
return name[: m.start()] + (f"({inner})" if inner else "") + name[m.end() :], spec, m[2]
if m := _F6.search(spec):
return name, spec[: m.start()] + spec[m.end() :], m[1]
if spec.strip() in PLACES:
return name, "", spec.strip()
return name, spec, NONE