- 새 `M02_Machine_Find` — 기종 = 기계표 이름 · 괄호 붙임 · 상세구분(낱말표 24 안 씀) · 소리 표기 한 글(로우더 = 로더 · 로울러 = 롤러 · 브레이카 = 브레이커 · 굴삭기 = 굴착기 · 램 머 = 래머 …) · 기계표 괄호 낱말로 좁힘(표에 없는 낱말 발파 · 리핑 = 안 봄) · 규격 = 수 + 단위 전부(범위 · 복수 · 인치 = ㎝) · 하나 = 이음 · 여럿 = 「기계 후보 n — 고르기」 · 0 = 「기계 못 찾음」 표지(가까운 규격으로 대신 잇지 않음) - 별칭 먼저 — 마스터 층 + 지금 층 별칭 장부 기계 짝(이름 + 규격 · 이름만 · 규격 속 할증 글 뺌) · `M02_Doc_Match_Note._ALIAS` 여섯 쌍 걷음(소리 표기 = 정규화 · 같은 기종 = 기계표 · 별칭 장부) - 조합 기계 = 기계마다 항목 하나(요구 738-3 · 제목 = 그 기계 · 줄 쌓기 차림 `M02_Doc_NativeLayout`) · 줄 둘(「+」 · 괄호 속 다른 기종 · 「…조합」 · 덤프트럭(자동덮개) = 덤프트럭 + 자동덮개시설) — 같은 수량 식 · 엑셀 값은 첫 줄 · 다음 줄 0 + 표지 「조합 기계 — 경비는 한 덩이」 · 우리 단가로 = 각자 시간당 경비 - 원파일 EST · STmate · 엑셀 읽기기 · 일위대가 호표(`Hopyo_Elem` · `Hopyo_Fixed._eqs` · `Hopyo_Row.m01_picks`)가 같은 찾기 · STmate 묶음 기계 줄도 시간당 경비 이음 - 문서 머리 `가져온.기계`(기호마다 판정 · 후보 · 찾기어) · 못 이은 기계 줄도 기계 줄(이름 단추 · `owned_of` 파이썬 · TS 짝) · 이름 단추 조회어 = 기종 이름만(규격 뺌) · 고르면 엑셀 값 기억 그대로(금액 안 바뀜) · 문서 [저장] = 고른 EQ → 이 층 별칭 장부 줄 → M01 별칭 장부 후보(승인 흐름 그대로) · 장부 짝 글 = 「기계 EQ…」 - 마스터 층 별칭 장부(resources/master_template/실무자료/맞춤.json) = 사용자 승인 다섯(요구 735-5 · 736) — 아스팔트페이버(피니셔) = 아스팔트 피니셔 · 디젤 엔진 = 엔진(디젤) · 워터제트 = 워터젯트 · 버킷준설기 = 버킷식준설기 · 바이브레이트 봉상후렉시블 = 콘크리트 진동기 엔진식 ø45(2.6㎾) - 시험 — 새 test_812_9(예 전부 · 정규화 · 괄호 · 범위 · 조합 둘 · 별칭 먼저 · 표지 · 고르면 별칭 줄 · 금액 전후) · test_812_3 전석쌓기 · 측구터파기 연암 항목을 기계마다 항목으로 고침(조합 기계 둘로 가름 · 요구 738-3 이 바꾼 까닭) - 전수 목록 docs/raw/요구_1104/_목록_812-9_기계이음.xlsx Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
396 lines
19 KiB
Python
396 lines
19 KiB
Python
"""M02 기계 찾기(PLAN 812-9 · 요구 718 · 719) — 가져온 기계 글(이름 · 규격) → 우리 EQ · 서버 단독.
|
||
|
||
과녁 = 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 로
|
||
2. 기종 = EQ 이름(괄호 뗌) · 괄호 붙임 · 상세구분과 같은 글 › 그 글을 품은 EQ(표기 정규화 `norm`)
|
||
3. 괄호 낱말 — 기계표 괄호에 있는 낱말(무한궤도 · 타이어 · 자주식 …)만 거름 · 표에 없는 낱말(발파 · 리핑)은 안 봄
|
||
4. 규격 = 수 + 단위(범위 · 여럿) 모두 EQ 규격에 같은 값(단위 한쪽 없음 = 봄) › 없으면 EQ 범위 안
|
||
하나 = 이음 · 여럿 = 「후보 여럿」 · 0 = 「못 찾음」(가까운 규격으로 대신 잇지 않음)
|
||
조합 기계(「+」 · 괄호 속 다른 기종 · 「…조합」 · 「덤프트럭(자동덮개)」) = `parts` 로 갈라 각각 찾음 ·
|
||
규격 없는 쪽은 앞 쪽 규격.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import math
|
||
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
|
||
|
||
# 소리만 다른 표기 → 한 글(양쪽에 같이 씀 · 차례대로)
|
||
SOUND = (
|
||
("로우더", "로더"), ("로울러", "롤러"), ("브레이카", "브레이커"), ("브레카", "브레이커"),
|
||
("굴삭기", "굴착기"), ("굴삭", "굴착"), ("불도우저", "불도저"), ("램머", "래머"),
|
||
("크로울러", "크롤러"), ("콤프레샤", "공기압축기"), ("컴프레셔", "공기압축기"),
|
||
("컴프레서", "공기압축기"), ("컴팩터", "콤팩터"), ("타이어형", "타이어"), ("젯트", "제트"),
|
||
("후렉시블", "플렉시블"), ("스프레야", "스프레이어"), ("코어", "코아"),
|
||
) # fmt: skip
|
||
_UNIT = (
|
||
("m3/min", "m3/min"), ("m3/분", "m3/min"), ("m3/hr", "m3/hr"), ("m3/h", "m3/hr"),
|
||
("km/hr", "km/hr"), ("km/h", "km/hr"), ("l/min", "l/min"), ("t/hr", "t/hr"),
|
||
("kg/cm2", "kg/cm2"), ("kg/min", "kg/min"), ("m3", "m3"), ("톤용", "ton"), ("ton", "ton"),
|
||
("톤", "ton"), ("t", "ton"), ("kw", "kw"), ("mm", "mm"), ("cm", "cm"), ("kg", "kg"),
|
||
("km", "km"), ("m", "m"), ("l", "l"), ("hp", "hp"), ("amp", "a"), ("cc", "cc"), ('"', "in"),
|
||
("inch", "in"), ("인치", "in"),
|
||
) # fmt: skip
|
||
_NUM = r"\d+(?:\.\d+)?"
|
||
_TOKEN = re.compile(
|
||
rf"({_NUM})(?:\s*([~∼\-])\s*({_NUM}))?\s*("
|
||
+ "|".join(re.escape(u) for u, _ in _UNIT)
|
||
+ r")?(?![a-z])"
|
||
)
|
||
_PAREN = re.compile(r"[(\[]([^()\[\]]*)[)\]]")
|
||
_EXTRA = re.compile(
|
||
r"[:,]?\s*\(?할증율?[^)]*\)?"
|
||
) # 「0.2㎥:할증120%」 · 「(할증125%)」 = 율(규격 아님)
|
||
NOTE = "참고 · "
|
||
LINKED, ALIAS, MANY, NONE = "이어짐", "별칭", "후보 여럿", "못 찾음"
|
||
SHOW = 8 # 후보 글 · 목록 상한
|
||
|
||
|
||
def norm(text: Any) -> str:
|
||
"""표기 정규화 — NFKC · 작은 글자 · 빈칸 뺌 · 소리 표기 한 글."""
|
||
t = re.sub(r"\s+", "", unicodedata.normalize("NFKC", str(text or "")).lower())
|
||
for old, new in SOUND:
|
||
t = t.replace(old, new)
|
||
return t
|
||
|
||
|
||
def _clean(text: Any) -> str:
|
||
t = unicodedata.normalize("NFKC", str(text or "")).lower()
|
||
t = _EXTRA.sub(" ", t)
|
||
return re.sub(r"(?<=\d),(?=\d{3}(?!\d))", "", t) # 천 자리 쉼표
|
||
|
||
|
||
def tokens(text: Any) -> list[tuple[float, float, str | None]]:
|
||
"""규격 글 → [(아래, 위, 단위)] — 범위 「10~12」 · 여럿 「32m,80~95㎥/hr」 · 단위 없음 = None."""
|
||
out = []
|
||
for m in _TOKEN.finditer(_clean(text)):
|
||
lo, sep, hi, unit = float(m.group(1)), m.group(2), m.group(3), m.group(4)
|
||
top = float(hi) if hi else lo
|
||
if sep == "-" and top <= lo: # 「50-2.6m」 = 범위 아님 — 앞 수만
|
||
top = lo
|
||
canon = dict(_UNIT).get(unit) if unit else None
|
||
if canon == "in": # 인치 = ㎝(×2.54 · 우리 표는 ㎝)
|
||
lo, top, canon = round(lo * 2.54, 6), round(top * 2.54, 6), "cm"
|
||
out.append((lo, top, canon))
|
||
return out
|
||
|
||
|
||
def _unit_ok(a: str | None, b: str | None) -> bool:
|
||
return a is None or b is None or a == b or {a, b} <= {"ton", "t/hr"}
|
||
|
||
|
||
def spec_fit(mine: list, theirs: list) -> str | None:
|
||
"""가져온 규격 수가 EQ 규격에 다 있음 = 「같음」 · EQ 범위 안 = 「안」 · 아니면 None."""
|
||
if not mine:
|
||
return "같음" if not theirs else None
|
||
same = all(
|
||
any(abs(a - c) < 1e-9 and abs(b - d) < 1e-9 and _unit_ok(u, w) for c, d, w in theirs)
|
||
for a, b, u in mine
|
||
)
|
||
if same:
|
||
return "같음"
|
||
inside = all(
|
||
any(c - 1e-9 <= a and b <= d + 1e-9 and c < d and _unit_ok(u, w) for c, d, w in theirs)
|
||
for a, b, u in mine
|
||
)
|
||
return "안" if inside else None
|
||
|
||
|
||
def _words(inner: str) -> list[str]:
|
||
return [norm(w) for w in re.split(r"[\s,;·/]+", inner) if w.strip()]
|
||
|
||
|
||
def split_name(text: Any) -> tuple[str, list[list[str]], list[str]]:
|
||
"""이름(+규격) 글 → (기종 글 정규화, 괄호 낱말 묶음들, 규격 조각들) — 괄호 속 수 = 규격."""
|
||
raw = _clean(text)
|
||
groups, specs = [], []
|
||
for inner in _PAREN.findall(raw):
|
||
bits = [b for b in re.split(r"[,;]", inner) if b.strip()]
|
||
specs += [b for b in bits if re.search(r"\d", b)]
|
||
groups.append([w for b in bits if not re.search(r"\d", b) for w in _words(b)])
|
||
base = _PAREN.sub(" ", raw)
|
||
specs += [m.group(0) for m in _TOKEN.finditer(base)]
|
||
base = _TOKEN.sub(" ", base)
|
||
return norm(base), groups, specs
|
||
|
||
|
||
# ── 기계표 ────────────────────────────────────────────────────────────
|
||
class Row:
|
||
__slots__ = ("raw", "key", "base", "detail", "joined", "full", "words", "spec", "said")
|
||
|
||
def __init__(self, r: dict) -> None:
|
||
name = str(r.get("이름") or "")
|
||
self.raw, self.key = r, str(r.get("키"))
|
||
inner = _PAREN.findall(unicodedata.normalize("NFKC", name))
|
||
self.words = {w for x in inner for w in _words(x)}
|
||
self.base = norm(_PAREN.sub("", unicodedata.normalize("NFKC", name)))
|
||
self.detail = norm(r.get("상세구분"))
|
||
self.joined = {w + self.base for w in self.words} | {self.base + w for w in self.words}
|
||
self.full = norm(name)
|
||
self.spec = tokens(r.get("규격"))
|
||
self.said = norm(
|
||
_TOKEN.sub(" ", _clean(r.get("규격")))
|
||
) # 규격 글 낱말(「더블」 · 「엔진식」)
|
||
|
||
|
||
@lru_cache(maxsize=2)
|
||
def _table(_whole: int) -> tuple[list[Row], frozenset[str], dict[str, Row]]:
|
||
rows = [Row(r) for r in m01.loaded()[1].numbers.get("EQ", {}).values()]
|
||
words = frozenset(w for r in rows for w in r.words)
|
||
return rows, words, {r.key: r for r in rows}
|
||
|
||
|
||
_SEEN: list = [0.0, None] # (때, 표) — M01 판 확인(파일 도장)은 1 초에 한 번
|
||
|
||
|
||
def table() -> tuple[list[Row], frozenset[str], dict[str, Row]]:
|
||
now = time.monotonic()
|
||
if _SEEN[1] is None or now - _SEEN[0] > 1.0:
|
||
_SEEN[:] = [now, _table(id(m01.loaded()[1]))]
|
||
return _SEEN[1]
|
||
|
||
|
||
def by_code(code: Any) -> str | None:
|
||
"""원문번호 → EQ 키."""
|
||
want = str(code or "").strip("'\"")
|
||
return next((r.key for r in table()[0] if r.raw.get("원문번호") == want), None)
|
||
|
||
|
||
def kind_rows(base: str) -> list[Row]:
|
||
"""기종 글 → EQ 줄 — 이름 · 괄호 붙임 같음 › 상세구분 같음 › 그 글을 품음(첫 층만)."""
|
||
if len(base) < 2:
|
||
return []
|
||
rows = table()[0]
|
||
for test in (
|
||
lambda r: base == r.base or base in r.joined,
|
||
lambda r: base == r.detail,
|
||
lambda r: base in r.full or base in r.detail,
|
||
):
|
||
got = [r for r in rows if test(r)]
|
||
if got:
|
||
return got
|
||
return []
|
||
|
||
|
||
def _kind_word(word: str, base: str) -> str | None:
|
||
"""괄호 낱말이 다른 기종(조합)이면 그 기종 글 — 「부착용집게」 · 「브레이커조합」 · 「자동덮개」."""
|
||
w = word.removesuffix("조합")
|
||
if len(w) < 2 or w == base:
|
||
return None
|
||
rows = table()[0]
|
||
if any(w in (r.base, r.detail) for r in rows):
|
||
return w
|
||
if any(r.base.startswith(base + w) for r in rows):
|
||
return base + w
|
||
return None
|
||
|
||
|
||
# ── 조합 기계 ─────────────────────────────────────────────────────────
|
||
def parts(name: Any, spec: Any = "") -> list[tuple[str, str]]:
|
||
"""기계 글 → [(이름, 규격)] — 「+」 · 괄호 속 다른 기종이면 둘 이상 · 규격 없는 쪽 = 앞 쪽 규격 ·
|
||
하나면 받은 그대로."""
|
||
text = f"{name or ''} {spec or ''}".strip()
|
||
known = table()[1]
|
||
out: list[tuple[str, str]] = []
|
||
for piece in [p for p in text.split("+") if p.strip()] or [text]:
|
||
main, base, extra = _clean(piece), split_name(piece)[0], []
|
||
for m in _PAREN.finditer(_clean(piece)):
|
||
bits = [b for b in re.split(r"[,;]", m.group(1)) if b.strip()]
|
||
words = [w for b in bits if not re.search(r"\d", b) for w in _words(b)]
|
||
kind = next((k for w in words if w not in known and (k := _kind_word(w, base))), None)
|
||
if kind:
|
||
main = main.replace(m.group(0), " ")
|
||
extra.append((kind, ",".join(b for b in bits if re.search(r"\d", b))))
|
||
bare = _TOKEN.sub(
|
||
" ", _PAREN.sub(lambda m: " " if re.search(r"\d", m.group(1)) else m.group(0), main)
|
||
)
|
||
own = " ".join(split_name(main)[2])
|
||
out += [(" ".join(bare.split()).strip(" ,"), own), *extra]
|
||
if len(out) < 2:
|
||
return [(str(name or ""), str(spec or ""))]
|
||
first = next((s for _, s in out if s), "")
|
||
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
|
||
|
||
|
||
def alias(name: Any, spec: Any) -> list[str]:
|
||
"""별칭 장부 기계 짝 — 이름 + 규격 열쇠(규격 속 할증 글은 뺌) · 없으면 []."""
|
||
return _alias_rows().get((norm(name), norm(_clean(spec))), [])
|
||
|
||
|
||
# ── 찾기 ──────────────────────────────────────────────────────────────
|
||
def _brief(r: Row) -> dict[str, Any]:
|
||
raw = r.raw
|
||
return {"키": r.key, "이름": raw.get("이름"), "규격": raw.get("규격") or "",
|
||
"원문번호": raw.get("원문번호")} # fmt: skip
|
||
|
||
|
||
def _query(rows: list[Row], base: str) -> str:
|
||
names = {str(r.raw.get("이름")) for r in rows}
|
||
if len(names) == 1:
|
||
return names.pop()
|
||
details = {str(r.raw.get("상세구분")) for r in rows}
|
||
return details.pop() if len(details) == 1 else base
|
||
|
||
|
||
def _ans(kind: str, rows: list[Row], query: str, why: str = "") -> dict[str, Any]:
|
||
key = rows[0].key if kind in (LINKED, ALIAS) else None
|
||
return {"판정": kind, "키": key, "후보": [_brief(r) for r in rows[:SHOW]], "찾기어": query,
|
||
"까닭": why, "수": len(rows)} # fmt: skip
|
||
|
||
|
||
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("이름")))
|
||
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
|
||
mine = tokens(" ".join(specs))
|
||
kinds = kind_rows(base)
|
||
known = table()[1]
|
||
rows = kinds
|
||
for w in (w for g in groups for w in g):
|
||
if w in known: # 기계표 괄호 낱말 = 꼭 맞아야 함
|
||
rows = [r for r in rows if w in r.full]
|
||
elif len(w) > 1 and (kept := [r for r in rows if w in r.said]): # 규격 글 낱말 = 좁힐 때만
|
||
rows = kept
|
||
if not mine: # 규격 수 없음 = 그 기종 전부(괄호 · 규격 낱말로 거른 것)
|
||
got = rows
|
||
else:
|
||
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("이름")))
|
||
if got:
|
||
return _ans(MANY, got, _query(got, base), f"후보 {len(got)}")
|
||
if not kinds:
|
||
return _ans(NONE, [], base, "기종 모름 — 별칭")
|
||
why = "우리 표에 없음(규격)" if rows else "우리 표에 없음(괄호 낱말)"
|
||
return _ans(
|
||
NONE, sorted(rows or kinds, key=lambda r: _gap(mine, r.spec)), _query(kinds, base), why
|
||
)
|
||
|
||
|
||
def _gap(mine: list, theirs: list) -> float:
|
||
"""가까운 규격 차례(보여주기만) — 첫 수끼리 비."""
|
||
if not mine or not theirs or mine[0][0] <= 0 or theirs[0][0] <= 0:
|
||
return math.inf
|
||
return abs(math.log(mine[0][0] / theirs[0][0]))
|
||
|
||
|
||
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]
|
||
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]["키"]:
|
||
s = str(out[0]["후보"][0]["규격"]) # 앞 줄 규격을 받은 쪽 = 이은 우리 규격(단위까지)
|
||
out.append({**find(n, s), "이름": n, "규격": s})
|
||
return out
|
||
|
||
|
||
def mark(got: dict[str, Any], name: Any, spec: Any) -> str | None:
|
||
"""못 이은 기계 표지 글(「참고 · 」 머리) — 이음 · 별칭 = None."""
|
||
said = f"「{' '.join(x for x in (str(name or ''), str(spec or '')) if x)}」"
|
||
if got["판정"] == MANY:
|
||
names = " · ".join(f"{c['이름']} {c['규격']}".strip() for c in got["후보"])
|
||
return f"{NOTE}기계 후보 {got['수']} — 고르기 {said}: {names}"
|
||
if got["판정"] == NONE:
|
||
return f"{NOTE}기계 못 찾음 — {said} {got['까닭']}"
|
||
return None
|
||
|
||
|
||
def same_kind(a: Any, b: Any) -> bool | None:
|
||
"""두 기계 이름이 같은 기종인지(연결 찾기) — 둘 다 기종을 찾으면 판정 · 아니면 None."""
|
||
found = []
|
||
for t in (a, b):
|
||
named = alias(t, "")
|
||
row = table()[2].get(named[0]) if len(named) == 1 else None
|
||
found.append(kind_rows(row.base if row else split_name(t)[0]))
|
||
if not all(found):
|
||
return None
|
||
return bool({r.base for r in found[0]} & {r.base for r in found[1]})
|
||
|
||
|
||
# ── 고른 것 → 별칭 장부(저장 때) ──────────────────────────────────────
|
||
def learn(doc: Any, who: Any) -> int:
|
||
"""가져온 식 문서 [저장] — 기계 줄에 사람이 고른 EQ(찾기 판정과 다름) = 지금 층 별칭 장부 줄
|
||
(갈래 기계 · 열쇠 「이름|규격|단위」) → M01 별칭 장부 후보로 모임(승인은 M01) · 쓴 줄 수."""
|
||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||
|
||
head = (doc or {}).get("가져온") if isinstance(doc, dict) else None
|
||
if not isinstance(head, dict) or not isinstance(head.get("기계"), list):
|
||
return 0
|
||
by_id = {dc.md.var_id(v): v for v in doc.get("변수") or [] if isinstance(v, dict)}
|
||
rows: dict[str, dict] = {}
|
||
for one in head["기계"]:
|
||
link = ((by_id.get(str((one or {}).get("기호"))) or {}).get("연결")) or {}
|
||
code = (link.get("입력") or {}).get("기계") if link.get("갈래") == "단가산출" else None
|
||
eq = by_code(code)
|
||
origin = one.get("원래") or {}
|
||
if not eq or eq == one.get("키") or not origin.get("이름"):
|
||
continue
|
||
if alias(origin["이름"], origin.get("규격")) == [eq]: # 이미 장부에 같은 짝
|
||
continue
|
||
plain = {"품명": str(origin["이름"]), "규격": str(origin.get("규격") or ""),
|
||
"단위": str(origin.get("단위") or "hr")} # fmt: skip
|
||
full = ledger.key({"이름": plain["품명"], "규격": plain["규격"], "단위": plain["단위"]})
|
||
pair = {"갈래": "기계", "키": eq, "입력값": {}, "배": 1}
|
||
rows[full] = {"원래": plain, "갈래": "짝", "짝": [pair]}
|
||
if rows:
|
||
ledger.put(store.root(), rows, who)
|
||
return len(rows)
|