feat(815-1 K1·K2·K3·K5): 불러오기 이음 — EST 코드 · 별칭 칸 · 표기 맞추기 · 견적 줄

- K1 EST 원파일 자재 = 우리 자재 출처 칸 EST 코드로 바로 이음(새 M02_Elem_Alias · 자재 자원 24,219 중 코드 같음 23,981 모두 그 줄)
- K2 M01 요소 줄 「별칭」 칸 읽기 — 기계 찾기(옛 별칭 장부 읽기 걷음) · 재료 · 인력 · 유가 · 일위대가 호표 줄(Units · Hopyo_Sangeun 장부 import 뗌)
- K3 재료 표기 정규화 정확 일치(M/M · 곱셈표 · 지름 · 괄호 · 빈칸 · 전각 · 지역 꼬리) — 바탕 이름 길보다 먼저(치즐(0.7m3) → 0.2㎥ 줄로 잘못 잇던 것 바로잡음)
- K5 견적 · 운임 · 수수료 줄 = 우리 자재면 재료 줄(우리 값) · 아니면 그대로 + 「견적」 표지 · 문서 설명 · 화면 「견적 n」 배지(못 이음 수에서 뺌)
- 시험 test_815_1_import_link 새로 · test_812_9 = 옛 장부 별칭 대신 기계 줄 별칭 칸(마스터 다섯은 칸이 비면 시험 동안만 넣음) · 「디젤 엔진」 은 마스터 별칭 묶음으로 옮김
- test_778_5 기계 표(162 · 3 · 25)는 고치기 전 판과 같음 — 812-9 뒤부터 굳힌 수와 다름(이번 고침 무관 · 안 고침)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-11 14:36:58 +09:00
co-authored by Claude Opus 5.5
parent 55ad1bfee0
commit 030a843f07
13 changed files with 449 additions and 129 deletions
+4 -1
View File
@@ -332,7 +332,10 @@ def _attach(doc: dict, row: dict, ids: list[str], src: dict[str, dict]) -> None:
if not note and not item.get("이음"): # 못 이은 까닭(814-1 — 흠 아님 · 우리 정보 없음)
from M02_MasterTemplete import M02_San_Link as san_link
var["설명"] = f"이음 없음 — {san_link.why(item)} · 엑셀 단가 그대로"
why = san_link.why(item)
var["설명"] = (
why if why == san_link.QUOTE else f"이음 없음 — {why} · 엑셀 단가 그대로"
)
continue
if (opt := link.pop(OPTION, None)) and opt["열쇠"] not in {
v.get("열쇠") for v in doc["변수"]
+10 -1
View File
@@ -274,6 +274,10 @@ class Block:
out["옮김"] = part if part != nat else None
return out
def quote(self, r: dict) -> tuple[str | None, str | None]:
"""견적 자원(갈래 고정) → 우리 자재 이음 · 표지(`M02_San_Link.quote`) · 그 밖 = (None, None)."""
return em._m01("고정", r) if self.keys and r.get("갈래") == "고정" else (None, None)
def register(self, ln: dict, call: str) -> None:
if ln.get("결과"):
self.done.append((ln["결과"][-1], call, ln))
@@ -314,6 +318,8 @@ class Block:
prices = {ln["비목"]: (ln.get("연결단가") or [0])[0] for ln in rows}
r = self.res(rows[0], None, None)
r |= {"단가": prices}
if quoted := self.quote(r)[1]: # 815-1 K5 — 세 줄 견적 = 값묶음 그대로 · 표지 한 번
self.mark(rows[0], quoted.removeprefix(NOTE))
parts = em.machine_parts(r) if self.keys else [] # 812-9 기계 찾기 · 조합 = 줄 둘
r |= {"찾기": parts[0]} if parts else {}
r |= {"조합첫": True} if len(parts) > 1 else {}
@@ -363,10 +369,13 @@ class Block:
parts = em.machine_parts(r) if self.keys else [] # 812-9 기계 찾기 · 조합 = 줄 둘
r |= {"찾기": parts[0]} if parts else {}
r |= {"조합첫": True} if len(parts) > 1 else {}
one = em.resource(name, r.get("단위"), qty, r, parts[0]["키"] if parts else None)
link, many = (parts[0]["키"], None) if parts else self.quote(r)
one = em.resource(name, r.get("단위"), qty, r, link)
em.first_spec(one, parts)
one |= {"자리": place} if place else {}
self.o.add_line(one, True, ln["행"], ln.get("보임") or "", ln["결과"][-1])
if many:
self.mark(ln, many.removeprefix(NOTE))
self.got["확정"] += bool(got)
self.register(ln, self.parts(one, parts, ln, ln))
+106
View File
@@ -0,0 +1,106 @@
"""M02 불러오기 요소 잇기 색인(PLAN 815-1 · 요구 755 ~ 764) — M01 요소 줄 「별칭」 칸 · EST 참조 코드 ·
재료 정확 일치. 서버 단독 · 읽기만(M01 판이 바뀌면 다시 읽음 · `id(whole)`).
- `keys(표, 이름, 규격)` = 별칭으로 이어지는 키들(풀이 = `master_alias` · 표 LB 인력 · EQ 기계 · MT 재료 ·
MO 유가) — 부르는 곳: 단가산출 불러오기(`M02_San_Link.m01`) · 1차 줄 판정(`M02_Hopyo_Row.m01_picks`) ·
기계 찾기(`M02_Machine_Find.alias`) · 일위대가 호표 줄(`M02_Hopyo_Units`)
- `est_code(코드)` = EST 원파일 자재 자원 코드(M…) ↔ 우리 자재 줄 `출처` 칸 「자재품목 M…」 같은 줄
- `exact(이름, 규격)` = 지역 꼬리 뗀 이름 · 규격이 `master_alias.norm` 으로 같은 자재 줄(가까운 규격 아님)
- `one(줄, 키들)` = 같은 품목(지역 꼬리 뗀 이름 · 규격 같음)의 지역 줄 여럿 → 줄 지역 › 서울 › 같은 단위 ›
키 차례 하나 · 다른 품목이 섞이면 None(후보 여럿)
"""
from __future__ import annotations
import re
from functools import lru_cache
from typing import Any
from M01_MasterData import M01_MasterData_Store as m01
import master_alias as am # noqa: E402 — M01 store 가 scripts 경로를 넣음
MARKET = "MT"
BASE_REGION = "서울"
_CODE = re.compile(r"\bM\d{10}\b")
_REGION = re.compile(r"\s*,\s*([가-힣]{2,4})\s*$") # 자재 이름 지역 꼬리 「, 서울」
def region_of(name: Any) -> tuple[str, str]:
"""자재 이름 → (지역 꼬리 뗀 이름, 지역 | 빈 글)."""
text = str(name or "")
m = _REGION.search(text)
return (text[: m.start()], m.group(1)) if m else (text, "")
def _same(name: Any, spec: Any) -> tuple[str, str]:
return am.norm(region_of(name)[0]), am.norm(spec)
@lru_cache(maxsize=2)
def _book(_whole: int) -> dict[str, Any]:
whole = m01.loaded()[1]
tables = {t: whole.numbers.get(t, {}) for t in ("LB", "EQ", "MO")}
tables[MARKET] = whole.index.get(MARKET, {})
rows = {t: {str(r.get("키") or k): r for k, r in got.items()} for t, got in tables.items()}
alias = {t: am.index({"키": k, "별칭": r.get("별칭")} for k, r in got.items()) for t, got in rows.items()} # fmt: skip
code: dict[str, list[str]] = {}
exact: dict[tuple[str, str], list[str]] = {}
for k, r in rows[MARKET].items():
for c in _CODE.findall(str(r.get("출처") or "")):
code.setdefault(c, []).append(k)
exact.setdefault(_same(r.get("이름"), r.get("규격")), []).append(k)
return {"줄": rows, "별칭": alias, "코드": code, "같음": exact}
def book() -> dict[str, Any]:
return _book(id(m01.loaded()[1]))
def row(table: str, key: Any) -> dict[str, Any]:
return book()["줄"].get(table, {}).get(str(key)) or {}
def keys(table: str, name: Any, spec: Any = "") -> list[str]:
"""별칭 칸으로 이어지는 키들(없음 = [])."""
b = book()
got = b["줄"].get(table, {})
return am.match(b["별칭"].get(table, {}), lambda k: (got.get(k) or {}).get("규격"), name, spec)
def est_code(code: Any) -> list[str]:
"""EST 자재 코드(M + 수 열) → 우리 자재 키들(출처 칸 같은 코드)."""
text = str(code or "").strip()
return list(book()["코드"].get(text, [])) if _CODE.fullmatch(text) else []
def exact(name: Any, spec: Any) -> list[str]:
"""지역 꼬리 뗀 이름 · 규격 정규화 같음 자재 키들."""
return list(book()["같음"].get(_same(name, spec), []))
def one(line: dict[str, Any], found: list[str]) -> str | None:
"""같은 품목 지역 줄 여럿 → 하나(줄 지역 › 서울 › 같은 단위 › 키) · 다른 품목 섞임 = None."""
rows = {k: row(MARKET, k) for k in found}
if not rows or len({_same(r.get("이름"), r.get("규격")) for r in rows.values()}) != 1:
return None
mine, unit = region_of(line.get("이름"))[1], am.norm(line.get("단위"))
def rank(k: str) -> tuple:
where = region_of(rows[k].get("이름"))[1]
return (where != mine if mine else True, where != BASE_REGION, am.norm(rows[k].get("단위")) != unit, k) # fmt: skip
return min(rows, key=rank)
def material(line: dict[str, Any], code: Any = None) -> tuple[str | None, str]:
"""재료 줄 → (우리 자재 키, 길 「코드」 · 「별칭」 · 「정확 일치」) · 별칭 여럿 = (None, 「별칭 후보 여럿」) ·
없음 = (None, "")."""
if hit := est_code(code):
return one(line, hit) or hit[0], "코드"
hit = keys(MARKET, line.get("이름"), line.get("규격"))
if hit:
got = hit[0] if len(hit) == 1 else one(line, hit)
return (got, "별칭") if got else (None, "별칭 후보 여럿")
got = one(line, exact(line.get("이름"), line.get("규격")))
return (got, "정확 일치") if got else (None, "")
+19
View File
@@ -77,6 +77,8 @@ def m01_picks(kind: str, line: dict[str, Any]) -> list[dict[str, Any]]:
@lru_cache(maxsize=4096)
def _m01_picks(kind: str, name: str, spec: str, unit: str, _whole: int) -> list[dict[str, Any]]:
if kind in ("인력", "재료") and (named := _alias(kind, name, spec)):
return named # 815-1 K2 — M01 줄 「별칭」 칸 먼저
if kind == "인력":
trade = est.trade(name)
hit = [r for r in _rows("LB").values() if trade and r.get("이름") == trade]
@@ -111,6 +113,23 @@ def _m01_picks(kind: str, name: str, spec: str, unit: str, _whole: int) -> list[
return []
def _alias(kind: str, name: str, spec: str) -> list[dict[str, Any]]:
"""M01 별칭 칸 이음(`M02_Elem_Alias`) — 인력 = 이름 · 재료 = 이름 + 규격 › 표기 정규화 정확 일치 ·
같은 품목 지역 줄은 하나."""
from M02_MasterTemplete import M02_Elem_Alias as ea
table = "LB" if kind == "인력" else ea.MARKET
keys = ea.keys(table, name, "" if kind == "인력" else spec)
if kind == "재료" and len(keys) > 1:
keys = [k] if (k := ea.one({"이름": name, "규격": spec}, keys)) else keys
if kind == "재료" and not keys: # 815-1 K3 — 표기 정규화 정확 일치(바탕 이름 길보다 먼저)
keys = [k] if (k := ea.one({"이름": name, "규격": spec}, ea.exact(name, spec))) else []
rows = [(k, ea.row(table, k)) for k in keys]
if kind == "인력":
return [pick("인력", k, r.get("이름"), "", "인", 단가=_plain(r.get("값"))) for k, r in rows]
return [pick("재료", k, r.get("이름"), r.get("규격"), r.get("단위")) for k, r in rows]
# ── M02 층 SK · SC(같은 이름 · 참고) ────────────────────────────────────────
+2 -4
View File
@@ -23,7 +23,6 @@ from common_util.common_util_time import utc_now_text
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Hopyo_Elem as elem
from M02_MasterTemplete import M02_Hopyo_Fixed as hfix
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
from M02_MasterTemplete import M02_Hopyo_Units as units
from M02_MasterTemplete import M02_Labor_Doc as labor
@@ -144,9 +143,8 @@ class Maker:
self._units: units.Units | None = None
def units(self) -> units.Units:
if self._units is None:
books = ledger.books(self.source)
self._units = units.Units(books, ledger.doc_id(self.source, self.key))
if self._units is None: # 815-1 — 옛 별칭 장부 안 읽음(M01 별칭 칸은 Units 가 읽음)
self._units = units.Units()
return self._units
def ref_of(self, sub: dict[str, Any]) -> dict[str, Any] | None:
+21 -13
View File
@@ -6,13 +6,13 @@
L 노무 = 직종 LB(이름 · 옛이름) · 같은 이름 여럿 = 건설업 먼저(작업반장 · 요구 624-1) · 단위 `{노임}`
X 기계 = EQ(`Fixed.Linker._machine` · 778-1 괄호 낱말) → 기계경비 기초 로직(`mach_logics` ·
778-2) · 단위 `{기계, 로직, 할증?}` · 없는 규격 = 가까운 규격 후보만(`Elem.machines` · 자동 고름 없음)
M 자재 = MT(`material` — 별칭 장부 › 이름 · 규격 같음 › 바탕 이름 · 숫자 규격 같음(치수 차례 · 구분자
M 자재 = MT(`material` — M01 별칭 칸 › 이름 · 규격 같음 › 바탕 이름 · 숫자 규격 같음(치수 차례 · 구분자
무관) · 글자 든 규격 = 후보로만) · 손료율 = 방법 「수량 × 손료율 × 단가」 · 별도 = 단위 `{자재?,
별도계상}` · 단위 `{자재}`
R 율 = 778-4 꼴 `{기준, 비목}` — 기준 = `기준꼴`(인력 · 재료 묶음 · 노무비 · 줄 목록 `{행: [...]}`) ·
우리에 없는 율 항목(설치비 · 제경비)도 율 줄 · 기준 못 찾음 = 고를 것(글 기준 제안)
S 하위 = `{하위 {종류 단산|일위, 번}}`(SK · SC 참조) · E 값 = `{값: 금액}`(고정 금액 줄)
별칭 장부 짝(자재) = 숫자보다 먼저. 서버 단독 · 읽기만.
M01 줄 「별칭」 칸(자재 · 노임 · 기계 = 기계 찾기 안) = 숫자보다 먼저(815-1). 서버 단독 · 읽기만.
"""
from __future__ import annotations
@@ -22,7 +22,7 @@ from typing import Any
from M02_MasterTemplete import M02_Hopyo_Elem as elem
from M02_MasterTemplete import M02_Hopyo_Fixed as hfix
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
from M02_MasterTemplete import M02_Elem_Alias as ea
from M02_MasterTemplete import M02_Hopyo_Match as hm
from M02_MasterTemplete import M02_Hopyo_Material as hmat
from M02_MasterTemplete import M02_Hopyo_Shares as shares
@@ -109,6 +109,16 @@ def material(line: dict[str, Any], got: dict[str, Any], full: bool = True) -> di
found = hm._exact(me, "자재", got)
if len(found) == 1:
return {"상태": LINKED, "찾음": found, "글": "이름 · 규격 같음", "꼬리표": tags, "후보": []}
hits = ea.exact(
me.get("이름"), me.get("규격")
) # 815-1 K3 표기 정규화 같음 한 줄(지역 변형 = 아래 길)
if len(hits) == 1:
key = hits[0]
one = ea.row(ea.MARKET, key)
found = [
{"자재": key, "이름": one.get("이름"), "규격": one.get("규격"), "단위": one.get("단위")}
]
return {"상태": LINKED, "찾음": found, "글": "표기 정규화 같음", "꼬리표": tags, "후보": []}
tries = [me, *_split(me)]
for one in tries:
sure, more = hmat.sure(one)
@@ -150,7 +160,7 @@ def _text_base(one: dict[str, Any]) -> str:
class Units:
"""한 번 잇기 — M01 · 목록 · 장부를 한 번 읽음(`books` = `Ledger.books` · 없으면 장부 안 봄)."""
"""한 번 잇기 — M01 · 목록을 한 번 읽음(`books` · `doc` = 옛 장부 자리 · 815-1 뒤 안 씀)."""
def __init__(self, books: list | None = None, doc: str | None = None) -> None:
self.linker = hfix.Linker(by_name=True)
@@ -168,6 +178,9 @@ class Units:
return {"방법": how, "후보": [], "꼬리표": [], "글": "", **got}
def _L(self, one: dict[str, Any]) -> dict[str, Any]:
named = ea.keys("LB", one.get("직종") or one.get("이름")) # 815-1 M01 별칭 칸 먼저
if len(named) == 1:
return {"상태": LINKED, "단위": {"노임": named[0]}, "꼬리표": ["별칭"]}
name = shares.trade(one.get("직종") or one.get("이름")) or ""
rows = self.lb.get(name, [])
cands = [{"키": str(r.get("키")), "갈래": "노임", "이름": r.get("이름"),
@@ -211,7 +224,7 @@ class Units:
pair = self._alias(line)
if pair:
unit = {"자재": pair} | ({"별도계상": True} if apart else {})
return {"상태": LINKED, "방법": how, "단위": unit, "꼬리표": ["별칭 장부 짝"]}
return {"상태": LINKED, "방법": how, "단위": unit, "꼬리표": ["별칭"]}
got = material(line, self.got)
apart = apart or hmat.APART in got["꼬리표"]
found = [r for r in got["찾음"] if r.get("자재")]
@@ -231,14 +244,9 @@ class Units:
"글": got["글"], "꼬리표": got["꼬리표"]} # fmt: skip
def _alias(self, line: dict[str, Any]) -> str | None:
"""별칭 장부 자재 짝(자동 정확 열쇠 · 하나) — 숫자보다 먼저."""
if self.books is None:
return None
got = ledger.recall(line, "", self.books, doc=self.doc)
if not got or got.get("제안") or got.get("규격다름") or got.get("갈래") != "짝":
return None
pairs = [p.get("자재") for p in got.get("짝") or []]
return pairs[0] if len(pairs) == 1 and pairs[0] else None
"""M01 자재 줄 「별칭」 칸(815-1 · 이름 + 규격 · 같은 품목 지역 줄은 하나) — 숫자보다 먼저."""
keys = ea.keys(ea.MARKET, line.get("이름"), line.get("규격"))
return keys[0] if len(keys) == 1 else ea.one(line, keys) if keys else None
def _R(self, one: dict[str, Any]) -> dict[str, Any]:
part = rate_part(one.get("이름"))
+19 -48
View File
@@ -3,8 +3,8 @@
과녁 = M01 기계표 전체(EQ) · 부르는 곳 = 불러오기 이음(`M02_Hopyo_Row.m01_picks` · `M02_San_Emit._m01`) ·
일위대가 호표(`M02_Hopyo_Elem.machine(s)` · `M02_Hopyo_Fixed._eqs`) · 연결 찾기(`M02_Doc_Match_Note`).
차례:
1. 별칭 장부(`M02_Hopyo_Ledger` · 마스터 층 + 지금 층 · 짝 갈래 기계) — 이름 + 규격 열쇠 = 그 EQ ·
이름만(규격 빈) 열쇠 = 그 EQ 이름으로 바꿔 2 로
1. 별칭(M01 기계 줄 「별칭」 칸 · 815-1 `M02_Elem_Alias`) — 「이름 규격」 통째 별칭 = 그 EQ(여럿 =
후보 여럿) · 이름만 별칭 = 그 EQ 기종으로 바꿔 2 로
2. 기종 = EQ 이름(괄호 뗌) · 괄호 붙임 · 상세구분과 같은 글 › 그 글을 품은 EQ(표기 정규화 `norm`)
3. 괄호 낱말 — 기계표 괄호에 있는 낱말(무한궤도 · 타이어 · 자주식 …)만 거름 · 표에 없는 낱말(발파 · 리핑)은 안 봄
4. 규격 = 수 + 단위(범위 · 여럿) 모두 EQ 규격에 같은 값(단위 한쪽 없음 = 봄) › 없으면 EQ 범위 안
@@ -20,7 +20,6 @@ import re
import time
import unicodedata
from functools import lru_cache
from pathlib import Path
from typing import Any
from M01_MasterData import M01_MasterData_Store as m01
@@ -222,38 +221,14 @@ def parts(name: Any, spec: Any = "") -> list[tuple[str, str]]:
return [(n, s or first) for n, s in out]
# ── 별칭 장부 ─────────────────────────────────────────────────────────
def _alias_rows() -> dict[tuple[str, str], list[str]]:
"""(이름, 규격) 정규화 → EQ 키들 — 마스터 층 + 지금 층 별칭 장부 기계 짝."""
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
from M02_MasterTemplete import M02_MasterTemplete_Store as store
here, top = Path(store.root()), Path(store.FOLDER)
folders = [here] + ([top] if here != top else [])
files = [(f, hopyo_store._dir(f) / f"{hopyo_store.MEMORY}.json") for f in folders]
stamp = tuple((str(f), p.stat().st_mtime_ns if p.is_file() else 0) for f, p in files)
return _alias_read(stamp)
@lru_cache(maxsize=8)
def _alias_read(stamp: tuple) -> dict[tuple[str, str], list[str]]:
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
out: dict[tuple[str, str], list[str]] = {}
for folder, _ in stamp:
for row in ledger.read(folder).values():
pairs = row.get("짝") or []
eqs = [str(p["키"]) for p in pairs if p.get("갈래") == "기계" and p.get("키")]
if row.get("갈래") != "짝" or not eqs:
continue
origin = row.get("원래") or {}
out.setdefault((norm(origin.get("품명")), norm(_clean(origin.get("규격")))), eqs)
return out
# ── 별칭(M01 기계 줄 「별칭」 칸 · 815-1) ───────────────────────────────────
def alias(name: Any, spec: Any) -> list[str]:
"""별칭 장부 기계 짝 — 이름 + 규격 열쇠(규격 속 할증 글은 뺌) · 없으면 []."""
return _alias_rows().get((norm(name), norm(_clean(spec))), [])
"""M01 기계 줄 별칭(`M02_Elem_Alias`) — 규격이 있으면 「이름 규격」 통째 별칭(규격 속 할증 글은 뺌) ·
규격 빔 = 이름 별칭 · 없으면 []."""
from M02_MasterTemplete import M02_Elem_Alias as ea
rows = ea.book()["별칭"].get("EQ", {})
return list(rows.get(ea.am.norm(name) + ea.am.norm(_clean(spec)), []))
# ── 찾기 ──────────────────────────────────────────────────────────────
@@ -280,18 +255,19 @@ def _ans(kind: str, rows: list[Row], query: str, why: str = "") -> dict[str, Any
def find(name: Any, spec: Any = "") -> dict[str, Any]:
"""기계 하나(조합 아님) → `{판정, 키, 후보, 찾기어, 까닭, 수}`."""
rows_by = table()[2]
hit = alias(name, spec)
if len(hit) == 1 and hit[0] in rows_by:
r = rows_by[hit[0]]
return _ans(ALIAS, [r], str(r.raw.get("이름")))
hit = [rows_by[k] for k in alias(name, spec) if k in rows_by]
if hit: # 별칭 하나 = 그 EQ · 여럿 = 후보 여럿
if len(hit) == 1:
return _ans(ALIAS, hit, str(hit[0].raw.get("이름")))
return _ans(MANY, hit, _query(hit, hit[0].base), f"후보 {len(hit)}")
base = split_name(name)[0]
_, groups, specs = split_name(f"{name or ''} {spec or ''}") # 규격 괄호 낱말 「3.6m(일반용)」
groups.append(
_words(_TOKEN.sub(" ", _PAREN.sub(" ", _clean(spec))))
) # 규격 글 낱말 「탑승유압식」
named = alias(name, "")
if len(named) == 1 and named[0] in rows_by: # 이름만 별칭 = 그 EQ 기종으로
base = rows_by[named[0]].base
named = [rows_by[k] for k in alias(name, "") if k in rows_by]
if named and len({r.base for r in named}) == 1: # 이름만 별칭 = 그 EQ 기종으로
base = named[0].base
mine = tokens(" ".join(specs))
kinds = kind_rows(base)
known = table()[1]
@@ -307,7 +283,7 @@ def find(name: Any, spec: Any = "") -> dict[str, Any]:
same = [r for r in rows if spec_fit(mine, r.spec) == "같음"]
got = same or [r for r in rows if spec_fit(mine, r.spec) == "안"]
if len(got) == 1: # 이름만 별칭으로 기종을 정했으면 「별칭」
return _ans(ALIAS if len(named) == 1 else LINKED, got, str(got[0].raw.get("이름")))
return _ans(ALIAS if named else LINKED, got, str(got[0].raw.get("이름")))
if got:
return _ans(MANY, got, _query(got, base), f"후보 {len(got)}")
if not kinds:
@@ -326,12 +302,7 @@ def _gap(mine: list, theirs: list) -> float:
def find_all(name: Any, spec: Any = "") -> list[dict[str, Any]]:
"""기계 글 → 부분마다 `find` + {이름, 규격}(조합 = 둘 이상) · 글 통째 별칭(짝 여럿)이 먼저."""
whole = alias(name, spec)
rows_by = table()[2]
if len(whole) > 1 and all(k in rows_by for k in whole):
made = [_ans(ALIAS, [rows_by[k]], str(rows_by[k].raw.get("이름"))) for k in whole]
return [{**x, "이름": str(name or ""), "규격": str(spec or "")} for x in made]
"""기계 글 → 부분마다 `find` + {이름, 규격}(조합 = 둘 이상)."""
out: list[dict[str, Any]] = []
for n, s in parts(name, spec):
if out and s and not split_name(n)[2] and s == out[0]["규격"] and out[0]["키"]:
@@ -61,7 +61,7 @@ interface EditAnswer {
문서: ImportedDoc;
전?: Money4 | null;
후?: Money4 | null;
이음없음?: { 자리: string; 이름: string; 까닭?: string }[];
이음없음?: { 자리: string; 이름: string; 까닭?: string; 견적?: boolean }[];
/** qty 답(797-2) */
줄?: QtyRow[];
집계?: QtyTally;
@@ -42,6 +42,7 @@ const TEXT = {
Excel: ["엑셀", "Excel"],
Now: ["지금", "Now"],
Unlinked: ["이음 없는 자원 {n}", "Unlinked resources {n}"],
Quote: ["견적 {n}", "Quotes {n}"],
Link: ["이음 고르기", "Pick link"],
Gap: ["바꾸기 전 {a} → 후 {b} ({d})", "Before {a} → after {b} ({d})"],
QtyLinks: ["소요량 · 계수 이음 {n}", "Quantity / coefficient links {n}"],
@@ -54,7 +55,7 @@ export const tx = (k: keyof typeof TEXT): string =>
export interface PanelState {
doc: ImportedDoc;
/** 이음 없는 자원(서버 price 답) */
unlinked: { 자리: string; 이름: string; 까닭?: string }[];
unlinked: { 자리: string; 이름: string; 까닭?: string; 견적?: boolean }[];
/** 소요량 · 계수 이음 집계(서버 qty 답 · 797-2) */
qty: QtyTally | null;
/** 마지막 단가 바꿈 전후 */
@@ -154,8 +155,13 @@ export function importedPanel(s: PanelState, a: PanelActs): HTMLElement {
b.dataset.importedLink = u.자리;
return b;
});
const tag = createBadge(tx("Unlinked").replace("{n}", String(s.unlinked.length)), "warning");
kids.push(createToolbar({ items: [tag, ...items] }));
const quotes = s.unlinked.filter((u) => u.견적).length; // 815-1 견적 = 그대로 쓰는 값 · 못 이음 수에서 뺌
const left = s.unlinked.length - quotes;
const tags = [
...(left ? [createBadge(tx("Unlinked").replace("{n}", String(left)), "warning")] : []),
...(quotes ? [createBadge(tx("Quote").replace("{n}", String(quotes)), "info")] : []),
];
kids.push(createToolbar({ items: [...tags, ...items] }));
}
if (here && s.qty?.자리) {
const open = createButton({ label: tx("QtyOpen"), size: 24, variant: "ghost", onClick: a.qty });
+29 -31
View File
@@ -6,21 +6,20 @@
from __future__ import annotations
import re
import unicodedata
NOTE = "참고 · " # `M02_San_Emit.NOTE` 와 같음(사람 확인 표지 머리)
OIL_BASE = ("전국평균", "일반용(갑)Ⅰ 저압") # 유가 · 전력 기본(요구 748 · 우리 기초 기본값)
BASE_REGION = "서울" # 같은 이름 · 규격 자재가 지역마다 있을 때 기본(우리 기초 검색어 꼴)
_REGION = re.compile(r"\s*,\s*([가-힣]{2,4})\s*$") # 자재 이름 지역 꼬리 「, 서울」 · 「,서울」
QUOTE = "견적 — 그대로(이름 · 값) · [단가 갱신] 대상 아님" # 815-1 K5 · 요구 762-3
def m01(kind: str, src: dict) -> tuple[str | None, str | None]:
"""1차 줄 판정 유일 짝 M01 키 → (키, 표지) — 없음 = (None, None) · 여럿 = (None, 표지 · 유가 ·
환율만 — 재료 · 인력은 조용히 이음 없음) · 기계 = 기계 찾기(812-9) 못 이으면 늘 표지. 재료 줄 이름이 M01 유가전력(MO)이면
유가 · 화폐 이름 그대로(달러 · USD …)면 환율(FX)로 판정(797-1 · 「유로폼」 같은 글 속 낱말은 아님)."""
if kind == "고정":
return quote(src)
if kind not in ("인력", "재료", "기계"):
return None, None
from M02_MasterTemplete import M02_Elem_Alias as ea
from M02_MasterTemplete import M02_Hopyo_Elem as elem
from M02_MasterTemplete import M02_Hopyo_Row as hrow
@@ -34,16 +33,17 @@ def m01(kind: str, src: dict) -> tuple[str | None, str | None]:
# 찾기 판정 없는 기계 줄 막음(814-1 · 줄 쌓기 표지 · 별칭)
src.setdefault("찾기", {**got, "이름": got.get("이름") or line["이름"], "규격": got.get("규격") or line["규격"]}) # fmt: skip
return got["키"], mfind.mark(got, line["이름"], line["규격"])
if kind == "재료" and src.get("종류") == "자재" and (hit := ea.est_code(src.get("코드"))):
return ea.one(line, hit) or hit[0], None # 815-1 K1 — EST 원파일 자재 = 우리 자재 출처 코드
oil = ea.keys("MO", line["이름"], line["규격"]) if kind == "재료" else []
if kind == "재료":
if elem.oil_rows(line["이름"]):
if oil or elem.oil_rows(line["이름"]):
kind = "유가"
elif line["이름"].strip().upper() in {x.upper() for pair in hrow.CURRENCY for x in pair}:
kind = "환율"
if kind == "유가": # MO 전부 → 줄 규격이 상세구분(전국 · 지역)이면 그것만
got = [
{"키": r["키"], "규격": str(r.get("상세구분") or "")}
for r in elem.oil_rows(line["이름"])
]
rows = [ea.row("MO", k) for k in oil] or elem.oil_rows(line["이름"]) # 815-1 별칭 먼저
got = [{"키": r["키"], "규격": str(r.get("상세구분") or "")} for r in rows]
spec = line["규격"].replace(" ", "")
got = (
[x for x in got if x["규격"] == spec]
@@ -59,6 +59,8 @@ def m01(kind: str, src: dict) -> tuple[str | None, str | None]:
keys = {x["키"] for x in got if x["규격"] in OIL_BASE} or keys
elif len(keys) > 1 and kind == "재료": # 814-1 — 지역 꼬리 뗀 이름 · 규격 정확 일치
keys = exact(line, got) or keys
if not keys and kind == "재료": # 815-1 K3 — 자재표 전체에서 표기 정규화 정확 일치
keys = {k} if (k := ea.one(line, ea.exact(line["이름"], line["규격"]))) else keys
if len(keys) == 1:
return keys.pop(), None
if len(keys) > 1 and kind in ("유가", "환율"):
@@ -66,32 +68,28 @@ def m01(kind: str, src: dict) -> tuple[str | None, str | None]:
return None, None
def _flat(text: object) -> str:
return re.sub(r"\s+", "", unicodedata.normalize("NFKC", str(text or ""))).lower()
def quote(src: dict) -> tuple[str | None, str | None]:
"""견적 · 운임 · 수수료 꼴 줄(갈래 밖 — EST 견적 · STmate 고정 코드 · 815-1 K5) → 우리 자재에 이어지면
(코드 · 별칭 · 정확 일치 · 단가 줄일 때) 그 키 + 갈래 재료(우리 값 · 엑셀 값은 검산) · 아니면 그대로 + 표지."""
from M02_MasterTemplete import M02_Elem_Alias as ea
def region_of(name: object) -> tuple[str, str]:
"""자재 이름 → (지역 꼬리 뗀 이름, 지역 | 빈 글)."""
text = str(name or "")
m = _REGION.search(text)
return (text[: m.start()], m.group(1)) if m else (text, "")
src["견적"] = True
line = {k: src.get(k) or "" for k in ("이름", "규격", "단위")}
key, _how = ea.material(line, src.get("코드"))
if key and src.get("비목"):
src["갈래"] = "재료"
return key, None
return None, f"{NOTE}{QUOTE}"
def exact(line: dict, got: list[dict]) -> set[str]:
"""재료 후보 가운데 지역 꼬리 뗀 이름 · 규격이 줄과 정확히 같은 것(빈칸 · 전각 · 대소문자 정규화) —
"""재료 후보 가운데 지역 꼬리 뗀 이름 · 규격이 줄과 정확히 같은 것(표기 정규화 `master_alias.norm`) —
지역마다 여럿이면 줄 지역 › 서울 › 같은 단위 › 키 차례 첫째 하나 · 없음 = 빈 모음(사람 확인)."""
name, mine = region_of(line.get("이름"))
want = (_flat(name), _flat(line.get("규격")))
hits = [x for x in got if x.get("키") and (_flat(region_of(x["이름"])[0]), _flat(x.get("규격"))) == want] # fmt: skip
if not hits:
return set()
unit = _flat(line.get("단위"))
from M02_MasterTemplete import M02_Elem_Alias as ea
def rank(x: dict) -> tuple:
where = region_of(x["이름"])[1]
return (where != mine if mine else True, where != BASE_REGION, _flat(x.get("단위")) != unit, str(x["키"])) # fmt: skip
return {min(hits, key=rank)["키"]}
want = ea._same(line.get("이름"), line.get("규격"))
hits = [x["키"] for x in got if x.get("키") and ea._same(x.get("이름"), x.get("규격")) == want]
return {k} if (k := ea.one(line, hits)) else set()
def hourly_text(key: str | None) -> str | None:
@@ -154,7 +152,7 @@ def why(item: dict) -> str:
return "노임 없음"
if kind == "단가산출":
return "안쪽 단가산출 못 이음"
return "갈래 밖(견적 · 고정 값)"
return QUOTE
def tally(logic: dict) -> dict:
+3 -1
View File
@@ -304,13 +304,15 @@ def same_part(doc: Any) -> dict | None:
def unlinked(doc: dict) -> list[dict]:
"""이음 없는 자원 줄(표지 · 사람이 고름) `[{자리, 이름, 까닭}]`(까닭 = 814-1 `M02_San_Link.why`)."""
"""이음 없는 자원 줄(표지 · 사람이 고름) `[{자리, 이름, 까닭, 견적?}]`(까닭 = 814-1 `M02_San_Link.why` ·
견적 = 815-1 K5 그대로 쓰는 견적 · 운임 · 수수료 줄 — 못 이음 수에서 뺌)."""
from M02_MasterTemplete import M02_San_Link as link
row = row_of(doc) or {}
ids = sd.line_ids(row)
return [
{"자리": ids[n], "이름": one.get("이름") or "", "까닭": link.why(one)}
| ({"견적": True} if (one.get("자원") or {}).get("갈래") == "고정" else {})
for n, one in enumerate(doc[KEY]["로직"].get("호표") or [])
if not one.get("이음") and not one.get("참조")
]