fix(M02 실무자료): 361-1 지역만 다른 자재 = 전국 — 줄 글 지역 · (전국) 품목 · 없으면 키 없는 전국 자리표 · 고르기 창 지역 변형 접음 · 전국 되돌림 · 만들기 본뜬.전국자재

- 맞춤: 줄 글 지역 = 그 지역 변형(꼬리표 「지역 — 줄 글」) · 지역 없는 변형((전국) 품목 먼저) · 없으면(레미콘) 「전국」 자리표(바탕 이름(전국)-관급/사급 · 규격 · 단위 · 키 · 단가 없음 · 꼬리표 「전국 — 지역은 프로젝트에서」) · 옛 「못 가림 — N개 중 첫째」 없앰
- 후보 = 그 품목 변형 모두(`변형`) · 고르기 창 1위 / 전국 / 지역 변형(접힘) · 대상 꼴 `{전국: true}`
- 미리 셈 · 만들기: 전국 대상은 서버가 줄에서 다시 셈 · 일위대가 자재 칸이 M01 키만 받아 사전조합 `본뜬.전국자재` 에 적고 빠진 줄 까닭 · 장부엔 안 남김 · 만들기 몸 꼴에 `전국` 칸
- 시험: 전국 · 줄 글 지역 · (전국) 품목 · 고르기 지역 → 전국 되돌림 · 만들기 결과 · 화면 도우미

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
This commit is contained in:
2026-10-16 23:03:56 +09:00
co-authored by Claude Opus 5.5
parent 8799cf0c97
commit a2c2ad92de
14 changed files with 266 additions and 32 deletions
+47 -8
View File
@@ -6,8 +6,12 @@
자재 규격 수에 있는 몫(단위 배 1 · 10 · 1000 · 2 % 허용 · 면목 0.03 m = 30 mm) ·
줄 괄호 글 낱말(「몸통」)은 자재 이름에 들어야 함.
맞음 = ① 이름 · 규격 같음 하나(`Match._exact`) ② 바탕 이름 · 단위 · 규격 맞음 1 이 한 품목뿐
(지역 · 관급/사급만 다른 변형 = 한 품목 · 같은 단위 먼저 · 줄 글의 관급/사급 · 지역 먼저 ·
없으면 별도계상 줄 = 관급 · 재료비 줄 = 사급 · 지역 없는 것 · M01 차례 → 꼬리표 「못 가림」).
(지역 · 관급/사급만 다른 변형 = 한 품목 · 같은 단위 먼저 · 줄 글의 관급/사급 먼저 · 없으면
별도계상 줄 = 관급 · 재료비 줄 = 사급).
지역(361-1) = 줄 글에 지역 낱말 → 그 지역 변형(꼬리표 「지역 — 줄 글」) · 없으면 지역 없는
변형((전국) 품목) · 그것도 없으면(레미콘) 「전국」 = 키 · 단가 없는 자리표(`national` ·
바탕 이름(전국)-관급/사급 · 규격 · 단위 · 꼬리표 「전국 — 지역은 프로젝트에서」) · 자동 고름 없음 ·
지역 변형은 고르기 창에서 사용자가.
그 밖 = 후보 다섯(변형은 하나로 접고 글에 수). 곳 낱말 = M01 레미콘 이름에서(자료).
가져오기 세 칸(`marks`) = 별도계상(표시 → 별도계상 · 아니면 직접계산) · 자재구분(고른
자재 이름 · 줄 글) · 운반비(「운반비 미포함 · 별도」 → 미포함). 로직 판정과 무관.
@@ -33,6 +37,9 @@ REGIONS = (
*"여주 이천 가평 연천".split(),
)
SKIP_WORDS = frozenset({"조합공통품목"})
NATIONAL = "전국"
NATIONAL_TAG = "전국 — 지역은 프로젝트에서"
REGION_TAG = "지역 — 줄 글"
_APART = re.compile(r"\(?\s*별도\s*계상\s*\)?")
_NUM = re.compile(r"\d+(?:\.\d+)?")
_RANGE = re.compile(r"(\d+(?:\.\d+)?)\s*(<=|≤|<)\s*[A-Za-z]\s*(<=|≤|<)\s*(\d+(?:\.\d+)?)")
@@ -60,8 +67,8 @@ def _paren_words(name: str) -> set[str]:
return {w for g in re.findall(r"\(([^()]*)\)", name) for w in re.findall(r"[가-힣]{2,}", g)}
def base(name: Any) -> str:
"""바탕 이름 — 괄호 글 · 끝 지역 · 관급/사급 꼬리 뗌(정규화)."""
def _bare(name: Any) -> str:
"""괄호 글 · 끝 지역 · 관급/사급 꼬리 뗀 글(정규화 전 · 「레미콘(서울)-사급」 → 레미콘)."""
text = _nfkc(name)
while "(" in text and (cut := re.sub(r"\([^()]*\)", "", text)) != text:
text = cut
@@ -71,7 +78,12 @@ def base(name: Any) -> str:
text = text[: m.start()]
for w in SUPPLY:
text = text[: -len(w)] if text.endswith(w) and len(text) > len(w) else text
return mm.plain(text)
return text.strip()
def base(name: Any) -> str:
"""바탕 이름 — `_bare` 정규화."""
return mm.plain(_bare(name))
def clean(line: dict[str, Any]) -> tuple[dict[str, Any], bool]:
@@ -166,16 +178,21 @@ def _wants(line: dict[str, Any]) -> tuple[str | None, list[str]]:
return supply, [w for w in words if _region(w) and w not in NO_REGION]
def _hit(r: dict[str, Any], regions: list[str]) -> bool:
return any(w[:2] == x[:2] for w in regions for x in r["region"])
def _prefer(rows: list[dict[str, Any]], line: dict[str, Any]) -> list[dict[str, Any]]:
"""변형 차례 — 줄 글의 관급/사급 · 지역 먼저 · 지역 없는 것 · 엑셀(M01) 차례."""
supply, regions = _wants(line)
def rank(r: dict[str, Any]) -> tuple:
hit = any(w[:2] == x[:2] for w in regions for x in r["region"])
hit = _hit(r, regions)
return (
supply is not None and r["supply"] not in (None, supply),
bool(regions) and not hit,
bool(r["region"]),
not any(w in r["one"]["이름"] for w in NO_REGION), # 「(전국)」 품목 먼저(361-1)
r["at"],
)
@@ -201,8 +218,30 @@ def sure(line: dict[str, Any]) -> tuple[list[dict[str, Any]], list[str]]:
if not full or len({r["key"] for r in full}) > 1:
return [], []
full = _prefer(full, line)
tags = [f"지역 · 관급/사급 못 가림 — {len(full)}개 중 첫째"] if len(full) > 1 else []
return full, tags
supply, regions = _wants(line)
if any(_hit(r, regions) for r in full):
return full, [REGION_TAG]
if len(full) == 1 or any(not r["region"] for r in full):
return full, []
return [_national(full[0], supply), *full], [NATIONAL_TAG]
def _national(row: dict[str, Any], supply: str | None) -> dict[str, Any]:
"""「전국」 자리표(361-1) — 지역 변형만 있는 품목 · 키 · 단가 없음 · 이름(전국)-관급/사급."""
one = row["one"]
name = f"{_bare(one['이름'])}({NATIONAL})" + (f"-{supply}" if supply else "")
spec = _nfkc(one["규격"])
for w in SKIP_WORDS: # 「조합공통품목, 25-21-120」(관급) → 25-21-120
spec = re.sub(rf"{w}[\s,]*", "", spec)
return {"one": {"전국": True, "이름": name, "규격": spec.strip(" ,"), "단위": one["단위"]}}
def national(line: dict[str, Any]) -> dict[str, Any] | None:
"""자재 · 별도계상 줄(다듬기 전)의 「전국」 자리표 — 없으면 None(만들기 · 미리 셈 재검사)."""
if line.get("갈래") not in hm.MATERIAL_KINDS:
return None
rows, _ = sure(clean(line)[0])
return next((r["one"] for r in rows if r["one"].get("전국")), None)
def candidates(line: dict[str, Any], limit: int = 5) -> list[dict[str, Any]]: