feat(830-9): 재료 DB 항목 목록만 되돌림 — 지역 · 관급 · 사급 줄 다시 따로 · 상하차 글은 이름에서 뺌
- 요구 834 · 835 · 836 · 837 — 보기 · 관리 방식(열 구성 · 조달청 0 · 일반가격 · 상하차 고르기 · 가장 낮은 값 규칙) 그대로 · 항목 목록만 EST 그대로 - 빌더 통합 단계 걷음 — material_merge = 열 한 벌 · 상하차 판독(split_load) · place 만 남김(지역 · 관급/사급 묶기 걷음) - 재료_자재품목 17,692 → 24,027 줄 = 830-1 24,092 − 재료 아닌 줄 65 · 키 = 830-1 키 · 새 키 0 · 키대장 = 830-1 대장 − 65 · 「다음」 그대로 - 830-1 대조: 키 · 구분 · 상세구분 · 단위 · 물가지 넷 · 출처 · 면수 같음 · 다른 것 = 열 구성 · 조달청 값 0 · 비고(조달 4,552 · 가격정보 2) · 상하차 글 뺌 384(이름 297 · 규격 87 — 요구 836) - 상하차 = 이름 · 규격 끝 인도 조건 글(도착도 259 = 하차 · 상차도 류 125 = 상차)을 칸으로 옮기고 글은 뺌 · 나머지 - - 빌더가 손 값(상하차 · 일반가격)을 원문번호로 이어 받음 · 빌더 두 번 = 같은 파일 - 사급/관급 열은 두지 않음(요구 837 — 단가산출 쪽 몫) - 품셈재료 옮긴 줄 2(모래 · 자갈 건조상태 전국)의 830-2 통합 이름 · 비고를 830-1 글로 되살림 - 시험: test_830_2 를 이번 상태로(통합 규칙 시험 지움 · 상하차 글 뺌 · 줄 = EST 그대로 · 24,027) · 830-2 가 통합에 맞춰 바꾼 기대를 830-1 판으로 되돌림(814_1 · 815_1 · 827_2 · 827_3 · m02_hopyo_match · m01_pick · m01_api 「시멘트 전국」 xfail 걷음 · masterdata_check 자재지역) · 울진 레미콘 = 관급 줄뿐 → 「지역 값 없음」 · check_links 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,9 +4,9 @@ ESTX 자재 표 = StuffcDat(칸 뜻: 다산소프트/ESTX_값사전_자재노무
|
||||
열 한 벌 = material_merge.COLUMNS(키 · 구분 · 상세구분 · 이름 · 규격 · 단위 · 상하차 · 조달청 · 물가자료 · 유통물가 · 물가정보 · 거래가격 · 일반가격 · 출처 · 비고 · 면수).
|
||||
Cost2..5 = 물가지 넷 · Page2..5 = 면수. TC(채택) · Cost6(견적가)는 마스터 아님 — 뺌.
|
||||
Cost1(가격정보 = 관급 정보)은 넣지 않음 — 「조달청」 열은 자리만 두고 비움(요구 827 · 830) · 비고에도 안 적음.
|
||||
「일반가격」 = 시스템 관리자가 M01 에서 넣는 칸 — 지금 정본 값을 원문번호로 이어 받음(다시 지어도 안 사라짐).
|
||||
「일반가격」 = 시스템 관리자가 M01 에서 넣는 칸 · 손 값(상하차 · 일반가격)은 원문번호로 이어 받음.
|
||||
Cost1 출처 표기로 가린 재료 아닌 줄은 뺌(요구 832) — 「2026(경북)」 품질시험 수수료 · 「~26.10.16」 일반 도로용 블록.
|
||||
항목 통합(요구 819 · 827) = material_merge — 지역 · 관급/사급 · 제조사 · 조합 덧정보를 뗀 이름 · 규격 · 단위가 같은 줄을 한 줄로.
|
||||
줄 목록 = EST 그대로 — 지역 · 관급 · 사급 줄을 합치지 않음(요구 834) · 상하차 칸 = material_merge.place.
|
||||
0 · 미저장 = 값 없음(null). 이름·규격은 원문 그대로(띄어쓰기·오자 포함). 코드 없는 빈 줄은 뺌.
|
||||
ESTX 에 대·중분류 칸이 없어 구분·상세구분은 build_자재품목_분류.py 가 품명 글자 규칙으로 채움(빌더 끝에서 부름).
|
||||
조달청 줄 · 조달청 관급 값은 넣지 않음(요구 822 — EST 그대로) · update_관급단가*.py 는 지금 정본에 적용 안 함.
|
||||
@@ -62,13 +62,13 @@ def dropped(r: dict) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def own_prices() -> dict[str, object]:
|
||||
"""지금 정본의 일반가격(손 입력) — 원문번호 → 값."""
|
||||
def own_prices() -> dict[str, dict]:
|
||||
"""지금 정본의 손 값(상하차 · 일반가격) — 원문번호 → {칸: 값}."""
|
||||
if not OUT.is_file():
|
||||
return {}
|
||||
book = mk.load_book()["키"]
|
||||
rows = json.loads(OUT.read_text(encoding="utf-8"))["줄"]
|
||||
return {book[r["키"]]["원문번호"]: r[mm.OWN] for r in rows if r.get(mm.OWN) is not None}
|
||||
return {book[r["키"]]["원문번호"]: {c: r.get(c) for c in mm.HAND} for r in rows}
|
||||
|
||||
|
||||
def build(rows: list[dict], own: dict | None = None) -> list[dict]:
|
||||
@@ -90,10 +90,10 @@ def build(rows: list[dict], own: dict | None = None) -> list[dict]:
|
||||
"이름": r.get("Name", ""),
|
||||
"규격": r.get("Size", ""),
|
||||
"단위": r.get("Unite", ""),
|
||||
mm.LOAD: mm.LOADS[0],
|
||||
mm.LOAD: own.get(code, {}).get(mm.LOAD),
|
||||
mm.GOVT: None,
|
||||
**{slot: num(r.get(f"Cost{i}")) for i, slot in enumerate(MARKET, 2)},
|
||||
mm.OWN: own.get(code),
|
||||
mm.OWN: own.get(code, {}).get(mm.OWN),
|
||||
"출처": f"자재품목 {code}",
|
||||
"비고": 비고,
|
||||
"면수": 면수 if any(면수.values()) else None,
|
||||
@@ -126,19 +126,20 @@ def prune() -> int:
|
||||
return len(gone)
|
||||
|
||||
|
||||
def check(rows: list[dict], gone: int) -> None:
|
||||
"""원본 대조 — 줄 수(원본 − 재료 아닌 줄 − 통합으로 지운 줄) · 남는 줄 전부 물가지 넷 · 면수가 원본 그 줄 것 · 조달청 칸 빔."""
|
||||
def check(rows: list[dict]) -> None:
|
||||
"""원본 대조 — 줄 수(원본 − 재료 아닌 줄) · 줄마다 이름 · 규격(인도 조건 글 뺌) · 단위 · 물가지 넷 · 면수가 원본 그 줄 것 · 조달청 칸 빔."""
|
||||
data = json.loads(OUT.read_text(encoding="utf-8"))
|
||||
raw = {r["Code"]: r for r in rows if r.get("Code") and r["Code"] not in DROP}
|
||||
out = sum(1 for r in raw.values() if dropped(r))
|
||||
got = [r for r in data["줄"] if not str(r["출처"]).startswith(CARRY)]
|
||||
assert len(got) == len(raw) - out - gone, (len(got), len(raw), out, gone)
|
||||
assert len(got) == len(raw) - out, (len(got), len(raw), out)
|
||||
print(
|
||||
f"줄 {len(got)} = 원본 코드 있는 줄 {len(raw)} − 재료 아닌 줄 {out} − 통합으로 지운 줄 {gone}"
|
||||
f" (유류 {len(DROP)} 뺌)"
|
||||
f"줄 {len(got)} = 원본 코드 있는 줄 {len(raw)} − 재료 아닌 줄 {out} (유류 {len(DROP)} 뺌)"
|
||||
)
|
||||
for g in got:
|
||||
r = raw[g["출처"].removeprefix("자재품목 ")]
|
||||
name, spec, _, _ = mm.split_load({"이름": r.get("Name", ""), "규격": r.get("Size", "")})
|
||||
assert (g["이름"], g["규격"], g["단위"]) == (name, spec, r.get("Unite", "")), g["출처"]
|
||||
for i, slot in enumerate(MARKET, 2):
|
||||
assert (g[slot] or 0) == (r.get(f"Cost{i}") or 0), (g["출처"], slot)
|
||||
assert (g["면수"] or {}).get(slot) == (r.get(f"Page{i}") or None), (g["출처"], slot)
|
||||
@@ -148,7 +149,7 @@ def check(rows: list[dict], gone: int) -> None:
|
||||
for i, slot in enumerate(MARKET, 2):
|
||||
n = sum(1 for r in got if r[slot] is not None)
|
||||
print(f" {slot}: 값 {n}줄 · 합 {math.fsum(r[slot] or 0 for r in got):,.1f}")
|
||||
print(" 남는 줄 전부 물가지 넷 · 면수 = 원본 그 줄 · 조달청 칸 빔 · 열 한 벌")
|
||||
print(" 줄마다 이름 · 규격 · 단위 · 물가지 넷 · 면수 = 원본 그 줄 · 조달청 칸 빔 · 열 한 벌")
|
||||
load = {v: sum(1 for r in data["줄"] if r[mm.LOAD] == v) for v in mm.LOADS}
|
||||
print(f" {mm.LOAD}:", " · ".join(f"{k} {n}" for k, n in load.items()))
|
||||
|
||||
@@ -172,8 +173,8 @@ def main():
|
||||
"유류·전력은 뺌 — 유가·전기요금은 재료_유가전력(유류는 값이 비고 글뿐 · 중유는 받지 않음)",
|
||||
"원문번호(다산소프트 품번) 칸 없음 — 대조는 _키대장.json 에만",
|
||||
"조달청 칸 = 자리만 둠 · 값 없음 — 원문 「가격정보」(관급 정보) 값은 넣지 않음(요구 827 · 830)",
|
||||
"상하차 = 상차 · 하차 · -(기본 · 모름) · 단위 바로 뒤 — 이름 · 규격의 인도 조건 글(도착도 · 하차도 = 하차 · 채취장상차 · 하치장 상차도 = 상차)을 떼어 이 칸으로(요구 828 · 831) · 값이 아닌 표지",
|
||||
"항목 통합 = 지역 · 관급/사급 · 제조사 · 조합 덧정보를 뗀 이름 · 규격 · 단위가 같은 줄을 한 줄로(전국 › 서울 › 경기 › 그 밖 지역 값 중간) · 뗀 말은 비고 「통합: …」 (scripts/material_merge.py · 요구 819 · 827 · 831)",
|
||||
"상하차 = 상차 · 하차 · -(기본 · 모름) · 단위 바로 뒤 — 이름 · 규격 끝 인도 조건 글(도착도 · 하차도 = 하차 · 채취장상차 · 하치장 상차도 = 상차)을 떼어 이 칸으로(요구 828 · 831 · 836) · 값이 아닌 표지",
|
||||
"줄 목록 = EST 그대로 — 지역 · 관급 · 사급 · 제조사 · 조합 줄을 합치지 않음(요구 834)",
|
||||
"재료 아닌 줄은 뺌 — 품질시험 수수료(가격정보 출처 「2026(경북)」) · 일반 도로용 블록(「~26.10.16」)(요구 832)",
|
||||
"조달청 줄 · 조달청 관급 값은 넣지 않음 — EST 그대로(요구 822)",
|
||||
"품셈재료에서 옮긴 줄(출처 「품셈재료 …」 · 값 없음)은 ESTX 밖 — 지금 정본에서 이어 받음",
|
||||
@@ -182,10 +183,10 @@ def main():
|
||||
],
|
||||
}
|
||||
extra = carried()
|
||||
merged, log = mm.merge(build(rows, own_prices()) + extra) # 원문 차례 = 키 차례
|
||||
dump_master(OUT, head, merged)
|
||||
check(rows, sum(1 for x in log for o in x["지운"] if not str(o["출처"]).startswith(CARRY)))
|
||||
print("항목 통합 묶음", len(log), "· 지운 줄", sum(len(x["지운"]) for x in log))
|
||||
dump_master(
|
||||
OUT, head, [mm.place(r) for r in build(rows, own_prices()) + extra]
|
||||
) # 원문 차례 = 키 차례
|
||||
check(rows)
|
||||
print("ESTX 밖 줄 이어 받음", len(extra))
|
||||
print("새 틀로 받음", mk.adopt()) # 열쇠 → 키 — 분류가 「키」 줄을 읽으므로 먼저
|
||||
# adopt 의 keep_text 가 구분 · 상세구분을 한 줄에 두 번 씀 — 읽어서(뒤 값) 다시 씀
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
"""재료 자재품목 항목 통합(요구 819 · 827 · 828 · 830 ~ 832 · PLAN 830-2) — build_재료_자재품목 이 부름.
|
||||
시험 = test_830_2_material_merge.
|
||||
"""재료 자재품목 열 한 벌 · 상하차(요구 828 · 830 · 831 · 834 · 836 · 837 · PLAN 830-9).
|
||||
build_재료_자재품목 이 부름 · 시험 = test_830_2_material_merge.
|
||||
|
||||
묶음 = 이름에서 지역 · 관급/사급 꼬리표 · 제조사 꼬리(「-거양」) · 조합 이름 · 상하차 글을 뺀
|
||||
이름 · 규격 · 단위 · 상하차가 같은 줄.
|
||||
규격 = 앞말 「조합공통품목,」 · 규격 속 지역 괄호 뗌
|
||||
단위 = M=m · m²=㎡ · Ton=톤 · ㎏=kg 표기 하나로 · 비교는 공백을 뺀 글
|
||||
줄 목록 = EST 그대로(지역 · 관급 · 사급 · 제조사 · 조합 글이 든 원래 이름 · 줄마다 따로) — 합치지 않음.
|
||||
열 한 벌 = COLUMNS — 상하차 = 단위 바로 뒤 · 가격 열 = 조달청(관급) · 물가지 넷 · 일반가격(사급).
|
||||
상하차 칸(요구 828 · 831) = 상차 · 하차 · -(기본 · 모름) — 이름 · 규격의 인도 조건 글을 떼어 이 칸으로
|
||||
사급 / 관급 고르기는 재료 표 열이 아님 — 단가산출 쪽 몫(요구 837).
|
||||
상하차 칸 = 상차 · 하차 · -(기본 · 모름) — 이름 · 규격 끝 인도 조건 글로 채우고 그 글은 이름 · 규격에서 뺌(요구 836)
|
||||
「도착도」 · 「하차도」 = 하차 · 「채취장상차(도)」 · 「하치장 · 생산공장 상차도」 · 「(상차도)」 = 상차
|
||||
남는 줄 = 사급(관급 아닌) 줄 가운데 전국 › 지역 글 없음(전국으로 봄) › 서울 › 서울 권역 글 › 경기
|
||||
› 그 밖 지역(요구 830 · 831 — 조달청과 같은 차례) · 같은 차례 여럿(경기 · 그 밖)이면 값이 중간인 줄
|
||||
(짝수면 아래 중간 · 다시 지어도 같은 줄)
|
||||
· 남길 줄의 값 칸이 전부 비고 다른 줄에 값이 있으면 다음 차례 줄(브)
|
||||
· 관급 줄만 있는 묶음 = 가장 작은 키 한 줄(브) · 나머지 줄은 지움(이력 안 남김)
|
||||
덧정보가 하나도 없는 묶음(원래부터 겹친 줄 — 식생 · 종자 등)은 합치지 않음.
|
||||
조달청 칸 값은 모두 비움(EST 「가격정보」 — 요구 827 · 831) · 일반가격 = 시스템 관리자가 넣는 칸(빌더가 이어 받음).
|
||||
이름에서 뗀 말은 비고 「통합: …」 로.
|
||||
조달청 칸 값은 모두 비움(EST 「가격정보」 — 요구 827 · 831) · 일반가격 = 시스템 관리자가 넣는 칸.
|
||||
상하차 · 일반가격 손 값은 빌더가 이어 받음.
|
||||
"""
|
||||
|
||||
import re
|
||||
@@ -25,6 +17,7 @@ LOAD = "상하차" # 표지 칸(값 칸 아님)
|
||||
LOADS = ("-", "상차", "하차") # 첫째 = 기본(모름)
|
||||
GOVT = "조달청" # 관급 가격 칸 — 가장 낮은 값 고르기에 안 넣음(요구 826 · 830)
|
||||
OWN = "일반가격" # 시스템 관리자 손 입력 칸(사급)
|
||||
HAND = (LOAD, OWN) # 빌더가 이어 받는 손 값 칸
|
||||
COLUMNS = ( # 열 한 벌 · 차례(요구 830 · 831)
|
||||
"키",
|
||||
"구분",
|
||||
@@ -40,239 +33,35 @@ COLUMNS = ( # 열 한 벌 · 차례(요구 830 · 831)
|
||||
"비고",
|
||||
"면수",
|
||||
)
|
||||
GYEONGGI = {"경기", "수원", "용인", "포천", "가평", "여주", "화성"}
|
||||
CITY = set(
|
||||
"""서울 부산 대구 인천 광주 대전 울산 세종 제주 청주 전주 포항 창원 춘천 충주 강릉
|
||||
천안 원주 여수 구미 순천 수원 목포 금산 영천 논산 삼척 홍성 익산 청도 양양 남원
|
||||
공주 군산 김천 가평 용인 포천 경주 아산 제천 여주 보령 부여 당진 청원 음성 서산
|
||||
영동 진천 예산 장성 고흥 고창 광양 고령 성주 영주 안동 칠곡 상주 문경 합천 화성
|
||||
경산 전국""".split()
|
||||
)
|
||||
SIDO = "서울|부산|대구|인천|광주|대전|울산|세종|제주|경기|강원|충북|충남|전북|전남|경북|경남|충청|전라|경상"
|
||||
_CITY = "|".join(sorted(CITY, key=len, reverse=True))
|
||||
UNIT = {"M": "m", "m²": "㎡", "Ton": "톤", "ton": "톤", "㎏": "kg"}
|
||||
SUPPLY = ( # (꼬리표 글, 공급) — 이름 끝 · 괄호 꼴
|
||||
(r"\(관급\)", "관급"),
|
||||
(r"[-/ ]관급$", "관급"),
|
||||
(r"(?<=\))관급$", "관급"),
|
||||
(r",관급(?=\))", "관급"),
|
||||
(r"\(사급\)", "사급"),
|
||||
(r"-사급$", "사급"),
|
||||
(r",사급$", "사급"),
|
||||
)
|
||||
NAME_LOAD = re.compile(r",\s*(도착도|하차도|채취장상차도|채취장상차)$") # 모 래, 서울, 도착도
|
||||
SPEC_LOAD = re.compile(
|
||||
r"\s*(?:\((상차도)\)|((?:하치장|생산공장)\s*상차도))$"
|
||||
) # HD=10mm 하치장 상차도
|
||||
COOP = re.compile(r"^조합공통품목(?:\(([^)]*)\))?,?\s*")
|
||||
|
||||
|
||||
def load_of(text: str) -> str:
|
||||
"""인도 조건 글 → 상하차 값 — 도착 · 하차 = 하차 · 상차 = 상차."""
|
||||
return "하차" if text.startswith(("도착", "하차")) else "상차"
|
||||
|
||||
|
||||
def place(row: dict, load: str) -> dict:
|
||||
"""COLUMNS 차례의 줄 — 첫 칸(열쇠 · 키)은 그대로 · 상하차 = load · 조달청 = 빔 · 옛 칸(관급)은 뺌."""
|
||||
first = next(iter(row))
|
||||
out = {first: row[first]}
|
||||
for c in COLUMNS[1:]:
|
||||
out[c] = load if c == LOAD else None if c == GOVT else row.get(c)
|
||||
return out
|
||||
|
||||
|
||||
def ws(s: str | None) -> str:
|
||||
return re.sub(r"\s+", " ", s or "").strip()
|
||||
|
||||
|
||||
def nows(s: str) -> str:
|
||||
return re.sub(r"\s+", "", s)
|
||||
|
||||
|
||||
def _region(name: str, spec: str):
|
||||
"""(지역 글, 지역을 뗀 이름, 지역을 뗀 규격) — 지역 꼴 일곱(조사 818)."""
|
||||
m = re.match(r"^(.*?)\(([^()]+)\)-사급$", name) # 레미콘(서울)-사급
|
||||
if m and m[2] in CITY:
|
||||
return m[2], m[1] + "-사급", spec
|
||||
m = re.match(r"^(.*?)\((SD\d+),([^()]+)\)-관급$", name) # 이형철근(SD400,제주)-관급
|
||||
if m and m[3] in CITY:
|
||||
return m[3], f"{m[1]}({m[2]})-관급", spec
|
||||
m = re.match(r"^(.*?\(관급\))-(.+)$", name) # 레미콘(관급)-강원도(태백시)
|
||||
if m:
|
||||
tail = m[2]
|
||||
head = re.sub(r"[(\s,].*", "", tail)
|
||||
if re.match(rf"^({SIDO})", head) or head in CITY or "광역시" in head or head.endswith("도"):
|
||||
return tail, m[1], spec
|
||||
return None, name, spec
|
||||
m = re.match(r"^(.*?),\s*([가-힣]+)\s*\(관급\)$", name)
|
||||
if m and m[2] in CITY:
|
||||
return m[2], m[1] + "(관급)", spec
|
||||
if "(관급)" in name or name.endswith("-관급") or "-사급" in name:
|
||||
return None, name, spec
|
||||
parts = [p.strip() for p in name.split(",")] # 합판(내수), 서울 · 모 래, 여수, 도착도
|
||||
hit = [i for i, p in enumerate(parts) if i > 0 and p in CITY]
|
||||
if hit:
|
||||
return parts[hit[0]], ", ".join(p for j, p in enumerate(parts) if j != hit[0]), spec
|
||||
m = re.search(
|
||||
rf"\((?:([가-힣]+)·)?({_CITY}|충남)(지역)?(,관급)?\)", name
|
||||
) # H 형강(전국) · 모래(건조상태·전국)
|
||||
if m:
|
||||
keep = f"({m[1]})" if m[1] else ""
|
||||
rest = "(관급)" if m[4] else ""
|
||||
return m[2], name[: m.start()] + keep + rest + name[m.end() :], spec
|
||||
m = re.search(rf"\s?\(({_CITY})(지역)?\)", spec) # 콘크리트근가 0.7M(제주지역)
|
||||
if m:
|
||||
return m[1], name, spec[: m.start()] + spec[m.end() :]
|
||||
if spec in CITY: # 품질시험 현장출장비 · 포항
|
||||
return spec, name, ""
|
||||
return None, name, spec
|
||||
|
||||
|
||||
def parse(row: dict) -> dict:
|
||||
"""줄 → 덧정보를 뗀 이름 · 규격 · 상하차 · 묶음 열쇠 · 뗀 말."""
|
||||
def split_load(row: dict) -> tuple[str, str, str, str | None]:
|
||||
"""(인도 조건 글을 뺀 이름, 규격, 상하차 값, 뺀 글) — 도착 · 하차 = 하차 · 상차 = 상차 · 글 없음 = -."""
|
||||
name, spec = row.get("이름") or "", row.get("규격") or ""
|
||||
words = []
|
||||
region, base, spec2 = _region(name, spec)
|
||||
m = re.search(r"-거양$", base)
|
||||
m = NAME_LOAD.search(name)
|
||||
if m:
|
||||
base = base[: m.start()]
|
||||
words.append("제조사 거양")
|
||||
supply = ""
|
||||
for pat, kind in SUPPLY:
|
||||
if re.search(pat, base):
|
||||
supply = supply or kind
|
||||
base = re.sub(pat, "", base)
|
||||
if supply:
|
||||
words.append(supply)
|
||||
load = row.get(LOAD) or LOADS[0]
|
||||
m = NAME_LOAD.search(base)
|
||||
if m:
|
||||
load = load_of(m[1])
|
||||
words.append(m[1])
|
||||
base = base[: m.start()]
|
||||
base = re.sub(r"[\s,]+$", "", base)
|
||||
m = COOP.match(spec2)
|
||||
if m:
|
||||
spec2 = spec2[m.end() :]
|
||||
words.append(f"조합 {m[1]}" if m[1] else "조합공통품목")
|
||||
m = SPEC_LOAD.search(spec2)
|
||||
if m:
|
||||
load = load_of(m[1] or m[2])
|
||||
words.append(m[1] or m[2])
|
||||
spec2 = spec2[: m.start()]
|
||||
spec2 = spec2.strip()
|
||||
unit = ws(row.get("단위"))
|
||||
tagged = bool(region or words)
|
||||
return {
|
||||
"이름": base if tagged else name,
|
||||
"규격": spec2 if tagged else spec,
|
||||
LOAD: load,
|
||||
"지역": region or "",
|
||||
"공급": supply,
|
||||
"뗀 말": words,
|
||||
"덧정보": tagged,
|
||||
"열쇠": (nows(base), nows(spec2), UNIT.get(unit, unit), load),
|
||||
name, text = name[: m.start()], m[1]
|
||||
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)
|
||||
first = next(iter(row))
|
||||
fix = {
|
||||
"이름": name,
|
||||
"규격": spec,
|
||||
LOAD: row.get(LOAD) or load,
|
||||
GOVT: None,
|
||||
}
|
||||
|
||||
|
||||
def price(row: dict):
|
||||
v = [row[s] for s in MARKET if row.get(s) is not None]
|
||||
return min(v) if v else None
|
||||
|
||||
|
||||
RANKS = ("전국", "지역 없음", "서울", "서울 권역", "경기", "그 밖")
|
||||
|
||||
|
||||
def _rank(region: str) -> int:
|
||||
if region == "전국":
|
||||
return 0
|
||||
if not region:
|
||||
return 1
|
||||
if region == "서울":
|
||||
return 2
|
||||
if region.startswith("서울"):
|
||||
return 3
|
||||
if region in GYEONGGI or region.startswith("경기"):
|
||||
return 4
|
||||
return 5
|
||||
|
||||
|
||||
def _middle(pool: list) -> list:
|
||||
"""값이 중간인 줄부터 가까운 차례(짝수면 아래 중간 · 값 빈 줄은 끝) — 다시 지어도 같은 차례."""
|
||||
pool = sorted(pool, key=lambda p: (price(p[0]) is None, price(p[0]) or 0))
|
||||
mid = max((sum(1 for p in pool if price(p[0]) is not None) - 1) // 2, 0)
|
||||
return sorted(pool, key=lambda p: (abs(pool.index(p) - mid), pool.index(p)))
|
||||
|
||||
|
||||
def _order(pool: list) -> list:
|
||||
"""남길 차례 — 전국 › 지역 없음 › 서울 › 서울 권역 › 경기 › 그 밖 · 경기 · 그 밖은 값 중간부터 ·
|
||||
그 위 차례는 앞 줄(작은 키)부터."""
|
||||
out = []
|
||||
for rank in range(len(RANKS)):
|
||||
same = [p for p in pool if _rank(p[1]["지역"]) == rank]
|
||||
out += _middle(same) if rank >= 4 else same
|
||||
return out
|
||||
|
||||
|
||||
def merge(rows: list[dict]) -> tuple[list[dict], list[dict]]:
|
||||
"""통합한 줄(조달청 칸 비움 · COLUMNS 차례) · 기록(덧정보를 뗀 묶음마다 남은 줄 · 지운 줄 · 기준 지역 · 「브」 규칙).
|
||||
줄 차례 = 키 차례(빌더가 원문 차례로 짓고 대장 키도 그 차례) — 같은 순위 · 관급만 묶음은 앞 줄을 남김."""
|
||||
groups: dict = {}
|
||||
for r in rows:
|
||||
p = parse(r)
|
||||
groups.setdefault(p["열쇠"], []).append((r, p))
|
||||
new, log = {}, []
|
||||
for g in groups.values():
|
||||
if not any(p["덧정보"] for _, p in g):
|
||||
continue
|
||||
sup = [x for x in g if x[1]["공급"] != "관급"]
|
||||
rules = []
|
||||
if sup:
|
||||
order = _order(sup)
|
||||
best = next((x for x in order if price(x[0]) is not None), order[0])
|
||||
region = best[1]["지역"]
|
||||
if _rank(region) == 1 and len(g) > 1:
|
||||
rules.append("지역 없음 = 전국")
|
||||
if _rank(region) >= 4:
|
||||
rules.append(RANKS[_rank(region)])
|
||||
if sum(1 for x in sup if _rank(x[1]["지역"]) == _rank(region)) > 1:
|
||||
rules.append("값 중간 지역")
|
||||
if best is not order[0]:
|
||||
rules.append("값 빈 줄 건너뜀")
|
||||
else:
|
||||
best = g[0]
|
||||
region = ""
|
||||
if len(g) > 1:
|
||||
rules.append("관급만 = 작은 키")
|
||||
words = list(dict.fromkeys(w for _, p in g for w in p["뗀 말"]))
|
||||
regions = sorted({p["지역"] for _, p in g if p["지역"]})
|
||||
bits = [f"기준 지역 {region or '없음'}" if regions else ""] if sup else ["관급 줄만"]
|
||||
if len(g) > 1:
|
||||
bits.append(f"{len(g)}줄 합침")
|
||||
if len(regions) > 1 or (regions and region != regions[0]):
|
||||
bits.append(f"지역 {len(regions)}곳")
|
||||
if words:
|
||||
bits.append("뗀 말 " + ", ".join(words))
|
||||
r, info = best
|
||||
note = "통합: " + " · ".join(b for b in bits if b)
|
||||
new[id(r)] = place(
|
||||
{
|
||||
**r,
|
||||
"이름": info["이름"],
|
||||
"규격": info["규격"],
|
||||
"비고": " · ".join(x for x in (r.get("비고"), note) if x),
|
||||
},
|
||||
info[LOAD],
|
||||
)
|
||||
gone = [o for o, _ in g if o is not r]
|
||||
for o in gone:
|
||||
new[id(o)] = None
|
||||
log.append(
|
||||
{"남은": new[id(r)], "원래": r, "지운": gone, "기준 지역": region, "규칙": rules}
|
||||
)
|
||||
out = []
|
||||
for r in rows:
|
||||
r = new.get(id(r), place(r, r.get(LOAD) or LOADS[0]))
|
||||
if r is not None:
|
||||
out.append(r)
|
||||
return out, log
|
||||
return {first: row[first], **{c: fix[c] if c in fix else row.get(c) for c in COLUMNS[1:]}}
|
||||
|
||||
Reference in New Issue
Block a user