feat(812-9): 가져온 기계 잇기 다시 — 기계표 전체 · 표기 정규화 · 괄호 낱말 · 범위 규격 · 별칭 먼저 · 조합 기계 둘 · 조회어 기종만
- 새 `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>
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
* 고르면 줄이 생김(닫으면 안 생김) · 있는 줄 = 자원 이름 단추
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
import { createButton, createIconButton } from "@ui/ui_template_button";
|
||||
import { createRowMenu } from "@ui/ui_template_context_menu";
|
||||
import { eqText } from "@ui/ui_template_equation";
|
||||
@@ -97,6 +98,36 @@ const KIND_KEY: Record<MoneyKind, LsKey> = {
|
||||
율: "K_율",
|
||||
};
|
||||
|
||||
/** 가져온 기계 찾기 판정(서버 기계 찾기 · 문서 `가져온.기계`) — 그 단가 기호 칸 */
|
||||
interface FoundMach {
|
||||
기호?: string;
|
||||
판정?: string;
|
||||
찾기어?: string;
|
||||
조합?: boolean;
|
||||
후보?: { 이름?: string; 규격?: string }[];
|
||||
}
|
||||
|
||||
function machineFound(d: FormulaDoc, sym: string): FoundMach | undefined {
|
||||
const head = (d as { 가져온?: { 기계?: FoundMach[] } }).가져온;
|
||||
return head?.기계?.find((x) => x.기호 === sym);
|
||||
}
|
||||
|
||||
/** 못 이은 가져온 기계 줄 표지(고르면 사라짐 — 연결이 단가산출로 바뀜) · 조합 기계 다음 줄 */
|
||||
function machineMark(f: FoundMach | undefined, link: { 갈래?: string }): HTMLElement | null {
|
||||
if (!f) return null;
|
||||
const picked = link.갈래 === "단가산출";
|
||||
const key = f.판정 === "후보 여럿" ? "Mach_Many" : f.판정 === "못 찾음" ? "Mach_None" : null;
|
||||
const badge =
|
||||
key && !picked
|
||||
? createBadge(lt(key), "warning", { size: 18 })
|
||||
: f.조합
|
||||
? createBadge(lt("Mach_Combo"), "neutral", { size: 18 })
|
||||
: null;
|
||||
if (badge && key && f.후보?.length)
|
||||
badge.title = f.후보.map((c) => `${c.이름 ?? ""} ${c.규격 ?? ""}`.trim()).join(" · ");
|
||||
return badge;
|
||||
}
|
||||
|
||||
/** 금액 줄 몸 — 한 줄(804-7 · 요구 687-7): [종류][이름 단추 | 이름 칸 · 단가 식][수량][단위][비목] · 좁으면 접힘 */
|
||||
function moneyBody(h: EditHooks, i: number): HTMLElement[] {
|
||||
const d = h.doc;
|
||||
@@ -122,6 +153,8 @@ function moneyBody(h: EditHooks, i: number): HTMLElement[] {
|
||||
? (h.subDoc(link.키)?.이름 ?? h.name(link.키))
|
||||
: h.name(link.키)
|
||||
: "";
|
||||
// 가져온 기계 줄(812-9) — 찾기 판정 · 조회어 = 기종 이름만(규격 뺌 · 이은 우리 기계 › 정규화 이름)
|
||||
const found = kind === "기계" ? machineFound(d, own) : undefined;
|
||||
const pick = createButton({
|
||||
label: (size && !name.includes(size) ? `${name} ${size}` : name) || "—",
|
||||
variant: "ghost",
|
||||
@@ -133,13 +166,15 @@ function moneyBody(h: EditHooks, i: number): HTMLElement[] {
|
||||
kind,
|
||||
(p) => h.change((doc) => repick(doc, i, own, kind, p)),
|
||||
undefined,
|
||||
String(mat?.검색어 ?? line.이름 ?? ""),
|
||||
found?.찾기어 ?? String(mat?.검색어 ?? line.이름 ?? ""),
|
||||
),
|
||||
});
|
||||
pick.classList.add("lstk-res");
|
||||
pick.title = `${lt("Repick")}${res ? ` — ${res}` : ""}`;
|
||||
pick.dataset.repick = String(i);
|
||||
head.push(pick);
|
||||
const mark = machineMark(found, link);
|
||||
if (mark) head.push(mark);
|
||||
} else
|
||||
head.push(
|
||||
field(lt("Name"), line.이름, (t) => put((l) => (l.이름 = t)), "m"),
|
||||
|
||||
@@ -87,6 +87,9 @@ const TEXT = {
|
||||
],
|
||||
Direct_Hint: ["표 값을 손으로 고침 — 표 연결 끊김", "Table value typed by hand — table link off"],
|
||||
Tbl_Missing: ["표를 못 읽음", "Table not found"],
|
||||
Mach_Many: ["기계 후보 여럿 — 고르기", "Several machines — pick one"],
|
||||
Mach_None: ["기계 못 찾음 — 고르기", "Machine not found — pick one"],
|
||||
Mach_Combo: ["조합 기계 — 경비는 첫 줄", "Combined machine — cost on first line"],
|
||||
Tbl_Hint: ["값 칸을 누르면 그 값이 들어감", "Click a value cell to use it"],
|
||||
Tbl_Bound: [
|
||||
"이 값의 줄은 위 옵션이 고름(다른 줄은 옵션에서 바꿈 · 여기선 칸만)",
|
||||
|
||||
@@ -239,6 +239,21 @@ def _link_of(item: dict) -> tuple[dict | None, str]:
|
||||
return link, ""
|
||||
|
||||
|
||||
MACHINES = "_기계" # 문서 셈 동안 잠깐 — `derive` 가 머리 `가져온.기계` 로 옮김
|
||||
|
||||
|
||||
def _machine_entry(item: dict, sym: str) -> dict:
|
||||
"""기계 줄 찾기 판정 한 칸 — {기호, 원래 {이름, 규격, 단위}, 판정, 키, 후보, 찾기어, 까닭, 조합?}."""
|
||||
res = item["자원"]
|
||||
got = res["찾기"]
|
||||
one = {
|
||||
"기호": sym,
|
||||
"원래": {"이름": got["이름"], "규격": got["규격"], "단위": item.get("단위")},
|
||||
}
|
||||
one |= {k: got.get(k) for k in ("판정", "키", "후보", "찾기어", "까닭")}
|
||||
return one | {k: True for k in ("조합", "조합첫") if res.get(k)}
|
||||
|
||||
|
||||
def _excel(item: dict) -> Any:
|
||||
if item.get("값묶음"):
|
||||
return {k: Decimal(str(item["값묶음"].get(k) or 0)) for k in imp.COSTS}
|
||||
@@ -246,12 +261,16 @@ def _excel(item: dict) -> Any:
|
||||
|
||||
|
||||
def _attach(doc: dict, row: dict, ids: list[str], src: dict[str, dict]) -> None:
|
||||
"""금액 줄 단가 숫자(엑셀 값) → 이은 연결 + 엑셀 값 기억(SK 고침으로 바뀐 값 = 숫자 그대로)."""
|
||||
"""금액 줄 단가 숫자(엑셀 값) → 이은 연결 + 엑셀 값 기억(SK 고침으로 바뀐 값 = 숫자 그대로) ·
|
||||
기계 찾기 판정(812-9) = 문서 `가져온.기계`(단가 기호마다 · 화면 고르기 · 저장 때 별칭)."""
|
||||
by_id = {dc.md.var_id(v): v for v in doc.get("변수") or []}
|
||||
found: list[dict] = doc.setdefault(MACHINES, [])
|
||||
for at, line in enumerate((doc.get("금액") or [])[: len(row["호표"])]):
|
||||
item = src.get(ids[at]) if at < len(ids) else None
|
||||
price = str(line.get("단가") or "")
|
||||
var = by_id.get(price.removesuffix(".계"))
|
||||
if item is not None and var is not None and (item.get("자원") or {}).get("찾기"):
|
||||
found.append(_machine_entry(item, dc.md.var_id(var)))
|
||||
if item is None or var is None or (var.get("연결") or {}).get("갈래") != "숫자":
|
||||
continue
|
||||
excel = _excel(item)
|
||||
@@ -322,8 +341,13 @@ def derive(sk: dict, ctx: tuple | None = None, whole: Any = None) -> dict:
|
||||
if not (doc.get("금액") or doc.get("결과") or row["호표"] or row.get("덧줄")):
|
||||
_empty(doc) # 빈 산근 = 엑셀 0 원(줄 없음 합) — 「—」 아님
|
||||
marks.append(EMPTY)
|
||||
found = doc.pop(MACHINES, None)
|
||||
head |= {"기계": found} if found else {} # 812-9 기계 찾기 판정(줄 쌓기 고르기 · 저장 때 별칭)
|
||||
doc["가져온"] = head | {"표지": marks} | ({"알림": notes} if notes else {})
|
||||
shape.shape(doc, meta) # 809-1 · 요구 704 — 지은 이름 → 엑셀 글 머리 · EST 기계 세 줄 = 한 줄
|
||||
if found: # 세 줄을 한 줄로 합쳐 사라진 기호의 기계 판정은 뺌
|
||||
alive = {dc.md.var_id(v) for v in doc.get("변수") or []}
|
||||
doc["가져온"]["기계"] = [x for x in found if x["기호"] in alive]
|
||||
return {**doc, "키": str(sk.get("키") or key), "이름": str(sk.get("이름") or doc.get("이름"))}
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from decimal import Decimal, InvalidOperation
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
|
||||
md = dc.md
|
||||
PRICED = ("노임", "재료", "기계") # 시간당 · 단위 단가 = 값(참고)
|
||||
@@ -130,15 +131,9 @@ def txt(v: Any) -> str:
|
||||
return str(v)
|
||||
|
||||
|
||||
_ALIAS = (("로울러", "롤러"), ("굴삭", "굴착"), ("브레이카", "브레이커"), ("로우더", "로더"))
|
||||
_ALIAS += (("물탱크", "살수차"), ("아스팔트페이버", "아스팔트피니셔")) # 같은 기계 다른 이름
|
||||
|
||||
|
||||
def _plain(text: str) -> str:
|
||||
t = re.sub(r"\s", "", text)
|
||||
for old, new in _ALIAS:
|
||||
t = t.replace(old, new)
|
||||
return t
|
||||
"""표기 정규화(소리 표기 · 빈칸) — 기계 찾기와 같은 규칙(812-9 `M02_Machine_Find.norm`)."""
|
||||
return mfind.norm(text)
|
||||
|
||||
|
||||
def _head(text: str) -> str:
|
||||
@@ -148,6 +143,11 @@ def _head(text: str) -> str:
|
||||
|
||||
|
||||
def _same_machine(a: str, b: str) -> bool:
|
||||
"""같은 기종 — 기계표 · 별칭 장부로 둘 다 기종을 찾으면 그 판정(812-9 · 박힌 짝 없음) ·
|
||||
못 찾으면 이름 머리 품음."""
|
||||
got = mfind.same_kind(a, b)
|
||||
if got is not None:
|
||||
return got
|
||||
x, y = _head(a), _head(b)
|
||||
return bool(x and y) and (x in y or y in x)
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ class Block:
|
||||
self.got = {"확정": 0, "표지": 0, "추정": 0}
|
||||
self.memo: list[int] = [] # 글 줄 행(812-3 차림 · 분수 그림 위아래 줄 뺌)
|
||||
self.frac: set[int] = set()
|
||||
self.noted: set[tuple] = set() # 기계 표지 단 자원(812-9 · 블록에 한 번)
|
||||
|
||||
# ── 작은 도구 ──────────────────────────────────────────────────
|
||||
def mark(self, ln: dict, why: str) -> None:
|
||||
@@ -313,6 +314,9 @@ class Block:
|
||||
prices = {ln["비목"]: (ln.get("연결단가") or [0])[0] for ln in rows}
|
||||
r = self.res(rows[0], None, None)
|
||||
r |= {"단가": prices}
|
||||
parts = em.machine_parts(r) if self.keys else [] # 812-9 기계 찾기 · 조합 = 줄 둘
|
||||
r |= {"찾기": parts[0]} if parts else {}
|
||||
r |= {"조합첫": True} if len(parts) > 1 else {}
|
||||
head = next(
|
||||
(b for b in reversed(self.lines[: self.lines.index(rows[0])])
|
||||
if (b.get("자원") or {}).get("코드") == r.get("코드") and not b.get("비목")),
|
||||
@@ -321,7 +325,8 @@ class Block:
|
||||
title = head.get("품명") or r.get("원이름") or r.get("이름") or "줄"
|
||||
name = self.o.names.item(title)
|
||||
one = {"이름": name, "규격": r.get("규격") or "", "단위": r.get("단위") or "", "수량": qty}
|
||||
one |= {"종류": "로직", "값묶음": em.bundle(r), "이음": self.logic_link(r), "자원": r}
|
||||
link = em.hourly_text(parts[0]["키"]) if parts else None
|
||||
one |= {"종류": "로직", "값묶음": em.bundle(r), "이음": link, "자원": r}
|
||||
one |= {"자리": place} if place else {}
|
||||
value = total["결과"][-1] if total and total.get("결과") else None
|
||||
for ln in rows: # 노 · 재 · 경 글 = 펼침(맞댈 값은 한 줄 계 — 소계 글 · 없으면 첫 줄)
|
||||
@@ -329,37 +334,38 @@ class Block:
|
||||
at = total or rows[0]
|
||||
value = value if value is not None else sum(ln["결과"][-1] for ln in rows)
|
||||
self.o.add_line(one, True, at["행"], at.get("보임") or "", value)
|
||||
call = self.parts(one, parts, at, rows[0])
|
||||
if total:
|
||||
self.register(total, f"줄.'{name}'")
|
||||
self.register(total, call)
|
||||
self.got["확정"] += 3
|
||||
|
||||
def parts(self, one: dict, parts: list[dict], at: dict, ln: dict) -> str:
|
||||
"""조합 기계 다음 줄(값 0 · 제 이음) · 기계 표지(블록에 한 번) → 이 기계 부름 글(줄 합)."""
|
||||
calls = [f"줄.'{one['이름']}'"]
|
||||
for p in parts[1:]:
|
||||
extra = em.part_line(one, p, self.o.names)
|
||||
self.o.add_line(extra, True, at["행"], at.get("보임") or "", None)
|
||||
calls.append(f"줄.'{extra['이름']}'")
|
||||
seen = (one["자원"].get("이름"), one["자원"].get("규격"))
|
||||
if seen not in self.noted:
|
||||
self.noted.add(seen)
|
||||
for m in em.part_marks(one["자원"], parts):
|
||||
self.mark(ln, m.removeprefix(NOTE))
|
||||
return calls[0] if len(calls) == 1 else f"({' + '.join(calls)})"
|
||||
|
||||
def machine_line(self, ln: dict, got: tuple | None) -> None:
|
||||
qty, place = got if got else ("1", self.frozen(ln, "수량 식 못 읽음")[1])
|
||||
price = (ln.get("연결단가") or [ln["결과"][-1]])[0]
|
||||
r = self.res(ln, ln["비목"], price if got else ln["결과"][-1])
|
||||
r["갈래"] = "기계" if r["갈래"] == "기계" else r["갈래"]
|
||||
name = self.o.names.item(f"{ln.get('품명') or r.get('이름') or '줄'} {COMP[ln['비목']]}")
|
||||
link, _ = em._m01(r["갈래"], r) if self.keys else (None, None)
|
||||
one = em.resource(name, r.get("단위"), qty, r, link)
|
||||
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)
|
||||
one |= {"자리": place} if place else {}
|
||||
self.o.add_line(one, True, ln["행"], ln.get("보임") or "", ln["결과"][-1])
|
||||
self.got["확정"] += bool(got)
|
||||
self.register(ln, f"줄.'{name}'")
|
||||
|
||||
def logic_link(self, r: dict) -> str | None:
|
||||
"""기계 = 시간당 기계경비 로직 부름 글(797-1 짝 고름) · 그 밖 = 없음(산근 = 형제 SK 참조)."""
|
||||
if not self.keys or r.get("갈래") != "기계":
|
||||
return None
|
||||
key, _ = em._m01("기계", r)
|
||||
if not key:
|
||||
return None
|
||||
from M02_MasterTemplete import M02_Doc_Import as dim
|
||||
|
||||
got = dim.hourly_link(key, "노무비")
|
||||
if not got:
|
||||
return None
|
||||
args = "".join(f", {k}={v}" for k, v in got["입력"].items())
|
||||
return f"로직({got['키']}{args})"
|
||||
self.register(ln, self.parts(one, parts, ln, ln))
|
||||
|
||||
def subtotal(
|
||||
self, ln: dict, kids: list[str], part: str | None = None, glyph: bool = True
|
||||
|
||||
@@ -166,6 +166,12 @@ def _items(doc: dict, texts: list[dict], rows: list[int], stm: bool) -> list[dic
|
||||
items: list[dict] = []
|
||||
cur: dict | None = None
|
||||
cut = False # STmate 계 줄 뒤 — 다음 첫 글 줄 = 제목
|
||||
money = doc.get("금액") or []
|
||||
mach = {
|
||||
str(x.get("기호")): x
|
||||
for x in (doc.get("가져온") or {}).get("기계") or []
|
||||
if isinstance(x, dict) and (x.get("조합") or x.get("조합첫"))
|
||||
} # 조합 기계(812-9 · 요구 738-3) = 기계마다 항목 하나 · 제목 = 그 기계
|
||||
|
||||
def new(title: str = "", sub: bool = False) -> dict:
|
||||
it = {"제목": title, "가나다": sub, "줄": [], "합계": False}
|
||||
@@ -186,6 +192,11 @@ def _items(doc: dict, texts: list[dict], rows: list[int], stm: bool) -> list[dic
|
||||
cur = new(title, sub)
|
||||
cut = False
|
||||
else:
|
||||
one = mach.get(price_sym(money[val]) or "") if kind == "금액" else None
|
||||
if one and one.get("조합"):
|
||||
cur = new(_mach_title(one), bool(cur and cur["가나다"]))
|
||||
elif one and cur is not None and "+" in cur["제목"]:
|
||||
cur["제목"] = _mach_title(one)
|
||||
cur = cur if cur is not None else new()
|
||||
if kind == "공식" and cur["줄"] and cur["줄"][-1][0] == "공식":
|
||||
cur["줄"][-1][1].extend(x for x in val if x not in cur["줄"][-1][1])
|
||||
@@ -200,6 +211,16 @@ def _items(doc: dict, texts: list[dict], rows: list[int], stm: bool) -> list[dic
|
||||
return keep
|
||||
|
||||
|
||||
def _mach_title(one: dict) -> str:
|
||||
"""조합 기계 항목 제목 — 이은 우리 기계 이름 · 규격 › 가져온 이름 · 규격."""
|
||||
first = (one.get("후보") or [{}])[0] if one.get("키") else {}
|
||||
said = first or {
|
||||
"이름": (one.get("원래") or {}).get("이름"),
|
||||
"규격": (one.get("원래") or {}).get("규격"),
|
||||
}
|
||||
return " ".join(str(said.get(k) or "") for k in ("이름", "규격")).strip()
|
||||
|
||||
|
||||
def _save_item(it: dict, money: list[dict]) -> dict:
|
||||
out: dict[str, Any] = {}
|
||||
if it["제목"]:
|
||||
@@ -269,6 +290,12 @@ def price_sym(line: dict) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def machine_syms(doc: dict) -> set[str]:
|
||||
"""가져온 기계 줄 단가 기호(812-9 `가져온.기계`) — 못 이어 숫자 연결이어도 그 줄 기계(이름 단추 · 고르기)."""
|
||||
got = (doc.get("가져온") or {}).get("기계") if isinstance(doc.get("가져온"), dict) else None
|
||||
return {str(x.get("기호")) for x in got or [] if isinstance(x, dict)}
|
||||
|
||||
|
||||
def owned_of(doc: dict) -> dict[int, str]:
|
||||
count: dict[str, int] = {}
|
||||
for ns in (
|
||||
@@ -280,11 +307,17 @@ def owned_of(doc: dict) -> dict[int, str]:
|
||||
count[n] = count.get(n, 0) + 1
|
||||
vars_ = {md.var_id(v): v for v in doc.get("변수") or []}
|
||||
lines = {str(x.get("기호") or "") for x in doc.get("식") or []}
|
||||
mach = machine_syms(doc)
|
||||
out = {}
|
||||
for i, line in enumerate(doc.get("금액") or []):
|
||||
s = price_sym(line)
|
||||
kind = ((vars_.get(s) or {}).get("연결") or {}).get("갈래") if s else None
|
||||
if s and kind in ("요소", "단가산출") and count.get(s) == 1 and s not in lines:
|
||||
if (
|
||||
s
|
||||
and (kind in ("요소", "단가산출") or s in mach)
|
||||
and count.get(s) == 1
|
||||
and s not in lines
|
||||
):
|
||||
out[i] = s
|
||||
return out
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
POST /api/m02/layers/{층}/sangeun/{키}/doc/preview 미리 셈(층 쓰기) {문서?, 끊음?, 할일?}
|
||||
→ {…, 전, 후, 바뀐자원?, 버림?}
|
||||
POST /api/m02/layers/{층}/sangeun/{키}/doc/save 저장(층 쓰기) {문서?, 끊음?, 할일?, 판} → 읽기 꼴
|
||||
· 가져온 기계 줄에 고른 EQ = 이 층 별칭 장부(812-9)
|
||||
POST /api/m02/layers/{층}/sangeun/{키}/match {할일 찾기|바꾸기|되돌리기, 판?}(층 쓰기 · 810-4)
|
||||
→ {연결후보?, 판}(바꾸기 = 확정만 · 아니면 409)
|
||||
⚠ 등록은 `main.py`(로그인만 · 층 권한은 길 안에서).
|
||||
@@ -18,6 +19,7 @@ from pydantic import BaseModel
|
||||
|
||||
from common_util.common_util_auth import verify_session
|
||||
from M02_MasterTemplete import M02_Doc_Sk_Path as path
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
from M02_MasterTemplete.M02_MasterTemplete_Router_Items import Layer, Session, _call, _root
|
||||
|
||||
router = APIRouter(prefix="/api/m02", tags=["M02 MasterTemplete Sangeun Doc"])
|
||||
@@ -84,4 +86,8 @@ def save_doc(
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
return _call(folder, path.save, key, body.model_dump(exclude_none=True))
|
||||
got = _call(folder, path.save, key, body.model_dump(exclude_none=True))
|
||||
if body.문서 is not None: # 812-9 — 가져온 기계 줄에 고른 EQ = 이 층 별칭 장부 줄
|
||||
who = {"id": session.get("user_id"), "이름": session.get("name")}
|
||||
_call(folder, mfind.learn, body.문서, who)
|
||||
return got
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
고르면 지금 잇기 길 `{노임|기계|자재|유가: 키}`).
|
||||
자재 = `M02_Hopyo_Material.candidates` 점수(이름 글자 묶음 · 규격) · 단가 = M01 자재 값(엑셀 줄 단위)
|
||||
노임 = LB 이름 글자 묶음 닮음 · 단가 = LB 값
|
||||
기계 = EQ 갈래 겹침 · 규격 수(같음 1 · 겹침 0.5) · 이름 낱말 덤 · 괄호 낱말 어긋남 깎음 ·
|
||||
단가 없음(기계경비는 로직 셈) · `machine` = 하나로 가름(778-1)
|
||||
기계 = 기계 찾기(812-9 `M02_Machine_Find` — 기계표 전체 · 표기 정규화 · 괄호 낱말 · 규격) 후보 ·
|
||||
단가 없음(기계경비는 로직 셈) · `machine` = 하나로 정할 때만
|
||||
유가(785-3) = MO 이름에 글 이름이 듦(경유 ⊂ 자동차용경유) · 전국 먼저 · 단가 = MO 값
|
||||
차례 = 점수(0.1 단위로 접음) › 엑셀 단가에 가까운 것(엑셀 단가 = 블록 단위 줄 단가 · 모르면 점수만).
|
||||
서버 단독 · 읽기만.
|
||||
@@ -21,18 +21,11 @@ from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
from M02_MasterTemplete import M02_Hopyo_Material as hmat
|
||||
from M02_MasterTemplete import M02_Hopyo_UnitLine as unit_line
|
||||
from M02_MasterTemplete.M02_Hopyo_Words import mtypes, nfkc, specnums
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
from M02_MasterTemplete.M02_Hopyo_Words import nfkc
|
||||
|
||||
TOP = 3
|
||||
POOL = 5
|
||||
# 괄호 낱말(778-1) — 글에 있으면 EQ 이름에도 있어야 함 · 부속 = 글에 없으면 본 장비 아님
|
||||
FORMS = (
|
||||
("타이어", ("타이어",)),
|
||||
("무한", ("무한궤도",)),
|
||||
("습지", ("습지",)),
|
||||
("트럭탑재", ("트럭탑재",)),
|
||||
)
|
||||
PARTS = ("자동덮개", "부착용", "콤팩터", "습지")
|
||||
|
||||
|
||||
def prices(block: dict[str, Any] | None) -> dict[str, float]:
|
||||
@@ -125,57 +118,24 @@ def oils(name: str) -> list[dict[str, Any]]:
|
||||
]
|
||||
|
||||
|
||||
def _form(text: str, name: Any) -> tuple[bool, bool]:
|
||||
"""괄호 낱말(778-1) — (글 형식 낱말이 EQ 이름에 다 있음, EQ 이름에 글에 없는 부속 있음)."""
|
||||
t, n = nfkc(text), nfkc(name)
|
||||
ok = all(any(w in n for w in ws) for k, ws in FORMS if k in t)
|
||||
return ok, any(w in n and w not in t for w in PARTS)
|
||||
def _eq(one: dict[str, Any]) -> dict[str, Any]:
|
||||
raw = m01.loaded()[1].numbers.get("EQ", {}).get(str(one.get("원문번호")), {})
|
||||
unit = (raw.get("단위") or {}).get("내용시간") if isinstance(raw.get("단위"), dict) else ""
|
||||
return {"키": one["키"], "갈래": "기계", "이름": one["이름"], "규격": one.get("규격") or "",
|
||||
"단위": unit or "시간", "단가": None} # fmt: skip
|
||||
|
||||
|
||||
def narrow(text: str, rows: list[tuple[str, Any]]) -> list[str]:
|
||||
"""EQ `[(키, 이름)]` → 괄호 낱말로 가른 키 — 형식 다름 뺌 · 부속은 남는 것 있을 때만 뺌."""
|
||||
keep = [(c, _form(text, n)[1]) for c, n in rows if _form(text, n)[0]]
|
||||
return [c for c, part in keep if not part] or [c for c, _ in keep]
|
||||
def machine(text: str, *_: Any) -> dict | None:
|
||||
"""기계 글(이름 규격) → EQ 한 줄 — 기계 찾기(812-9 `M02_Machine_Find`)가 하나로 정할 때만."""
|
||||
got = mfind.find(text)
|
||||
if got["판정"] not in (mfind.LINKED, mfind.ALIAS):
|
||||
return None
|
||||
return m01.loaded()[1].numbers.get("EQ", {}).get(str(got["후보"][0]["원문번호"]))
|
||||
|
||||
|
||||
def machine(text: str, kinds: set | None = None, nums: set | None = None) -> dict | None:
|
||||
"""기계 글 → EQ 한 줄 — 갈래 겹침 + 규격 수 같음 · 괄호 낱말로 하나일 때만."""
|
||||
kinds = set(kinds or ()) or mtypes(text)
|
||||
nums = set(nums or ()) or set(specnums(text))
|
||||
rows = {
|
||||
str(r.get("키")): r
|
||||
for r in m01.loaded()[1].numbers.get("EQ", {}).values()
|
||||
if nums
|
||||
and kinds & mtypes(f"{r.get('상세구분')} {r.get('이름')}")
|
||||
and nums == set(specnums(r.get("규격")))
|
||||
}
|
||||
got = narrow(text, [(k, r.get("이름")) for k, r in rows.items()])
|
||||
return rows[got[0]] if len(got) == 1 else None
|
||||
|
||||
|
||||
def machines(text: str, kinds: set | None = None, nums: set | None = None) -> list[dict]:
|
||||
"""기계 글(이름 규격) → EQ 후보 — 갈래 · 규격 수 없으면 글에서 읽음 · 괄호 낱말 어긋남 깎음."""
|
||||
whole = m01.loaded()[1]
|
||||
kinds = set(kinds or ()) or mtypes(text)
|
||||
nums = set(nums or ()) or set(specnums(text))
|
||||
mine = hm.words(text)
|
||||
rows = []
|
||||
for row in whole.numbers.get("EQ", {}).values():
|
||||
types = mtypes(f"{row.get('상세구분')} {row.get('이름')}")
|
||||
if not kinds & types:
|
||||
continue
|
||||
spec = set(specnums(row.get("규격")))
|
||||
score = len(kinds & types) / len(kinds | types)
|
||||
score += 1.0 if nums and nums == spec else 0.5 if nums & spec else 0.0
|
||||
score += 0.3 * len(mine & hm.words(row.get("이름")))
|
||||
ok, part = _form(text, row.get("이름"))
|
||||
score -= (0.0 if ok else 1.0) + (0.5 if part else 0.0)
|
||||
one = {"키": str(row.get("키")), "갈래": "기계", "이름": row.get("이름")}
|
||||
unit = (row.get("단위") or {}).get("내용시간") if isinstance(row.get("단위"), dict) else ""
|
||||
rows.append(
|
||||
(score, {**one, "규격": row.get("규격") or "", "단위": unit or "시간", "단가": None})
|
||||
)
|
||||
return _order(rows, None)
|
||||
def machines(text: str, *_: Any) -> list[dict]:
|
||||
"""기계 글 → EQ 후보(보여주기만 · 812-9) — 이음 · 후보 여럿 = 그것 · 못 찾음 = 같은 기종(가까운 규격 먼저)."""
|
||||
return [_eq(one) for one in mfind.find(text)["후보"][:TOP]]
|
||||
|
||||
|
||||
def lost(one: dict[str, Any], want: float | None = None) -> list[dict[str, Any]]:
|
||||
|
||||
@@ -20,13 +20,12 @@ from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
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
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Labor_Fixed as fixed
|
||||
from M02_MasterTemplete.M02_Hopyo_Words import mtypes, specnums
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
|
||||
COSTS = ("노무비", "재료비", "경비")
|
||||
_BASE = re.compile(r"(?<!주)(노무비|재료비|경비|인건비)\s*의")
|
||||
@@ -93,13 +92,9 @@ class Linker:
|
||||
pair = (str(row.get("키")), _d(row.get("값")))
|
||||
self.labor.setdefault(str(row.get("이름") or ""), []).append(pair)
|
||||
self.machines = [
|
||||
(str(row.get("키")), mtypes(f"{row.get('상세구분')} {row.get('이름')}"),
|
||||
set(specnums(row.get("규격"))), mm_plain(row.get("이름")), mm_plain(row.get("규격")))
|
||||
(str(row.get("키")), mm_plain(row.get("이름")), mm_plain(row.get("규격")))
|
||||
for row in self.whole.numbers.get("EQ", {}).values()
|
||||
] # fmt: skip
|
||||
self.eq_names = {
|
||||
str(r.get("키")): r.get("이름") for r in self.whole.numbers.get("EQ", {}).values()
|
||||
}
|
||||
]
|
||||
self.near = 0 # 못 이은 이음 후보 수
|
||||
|
||||
def link(self, line: dict[str, Any], price: dict[str, Decimal]) -> dict[str, str | None]:
|
||||
@@ -117,14 +112,12 @@ class Linker:
|
||||
return out
|
||||
|
||||
def _eqs(self, line: dict[str, Any]) -> list[str]:
|
||||
"""기계 줄 → EQ 후보 — 이름 · 규격 글 같음 › 갈래 + 규격 수 · 괄호 낱말."""
|
||||
"""기계 줄 → EQ 후보 — 이름 · 규격 글 같음 › 기계 찾기(812-9 `M02_Machine_Find`)."""
|
||||
name, spec = mm_plain(line.get("이름")), mm_plain(line.get("규격"))
|
||||
same = [c for c, _, _, n, s in self.machines if n == name and s == spec]
|
||||
same = [c for c, n, s in self.machines if n == name and s == spec]
|
||||
if len(same) != 1:
|
||||
text = f"{line.get('이름') or ''} {line.get('규격') or ''}"
|
||||
kinds, nums = mtypes(text), set(specnums(text))
|
||||
same = [c for c, t, s, _, _ in self.machines if kinds & t and nums and nums == s]
|
||||
same = elem.narrow(text, [(c, self.eq_names.get(c)) for c in same])
|
||||
got = mfind.find(line.get("이름"), line.get("규격"))
|
||||
same = [] if got["판정"] == mfind.NONE else [c["키"] for c in got["후보"]]
|
||||
return same
|
||||
|
||||
def candidates(self, line: dict[str, Any]) -> list[str]:
|
||||
|
||||
@@ -20,6 +20,7 @@ from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
from M02_MasterTemplete import M02_Hopyo_Material as hmat
|
||||
from M02_MasterTemplete import M02_Hopyo_ReadEst as est
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
|
||||
mm, mp = hm.mm, hm.mp
|
||||
COSTS = ("노무비", "재료비", "경비")
|
||||
@@ -65,6 +66,11 @@ def _rows(table: str) -> dict[str, dict[str, Any]]:
|
||||
|
||||
def m01_picks(kind: str, line: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""갈래 하나 → M01 고른 것 후보(LB · MT · MO · EQ · FX) — 유일 짝이면 하나 · 같은 글 한 번."""
|
||||
if kind == "기계": # 812-9 기계 찾기 — 별칭 장부가 바뀌므로 lru 밖(찾기가 기계표를 기억)
|
||||
got = mfind.find(line.get("이름"), line.get("규격"))
|
||||
rows = [] if got["판정"] == mfind.NONE else got["후보"]
|
||||
unit = str(line.get("단위") or "") or "hr"
|
||||
return [pick("기계", x["키"], x["이름"], x.get("규격"), unit) for x in rows]
|
||||
text = tuple(str(line.get(k) or "") for k in ("이름", "규격", "단위"))
|
||||
return copy.deepcopy(_m01_picks(kind, *text, id(m01.loaded()[1])))
|
||||
|
||||
@@ -80,13 +86,6 @@ def _m01_picks(kind: str, name: str, spec: str, unit: str, _whole: int) -> list[
|
||||
]
|
||||
got = elem.labor(str(name or ""))
|
||||
return [pick("인력", x["키"], x["이름"], "", "인", 단가=x["단가"]) for x in got]
|
||||
if kind == "기계":
|
||||
text = f"{name} {spec}"
|
||||
one = elem.machine(text)
|
||||
rows = [one] if one else []
|
||||
got = [{"키": r["키"], "이름": r["이름"], "규격": r.get("규격")} for r in rows]
|
||||
got = got or elem.machines(text)
|
||||
return [pick("기계", x["키"], x["이름"], x.get("규격"), unit or "hr") for x in got]
|
||||
if kind == "유가":
|
||||
return [
|
||||
pick("유가", x["키"], x["이름"], x["규격"], x["단위"], 단가=x["단가"])
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
"""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)
|
||||
@@ -101,6 +101,9 @@ export interface HopyoBuiltAll {
|
||||
export interface LedgerPair {
|
||||
로직?: string;
|
||||
자재?: string;
|
||||
/** 고른 것 꼴(785-3 · 812-9 기계) — 갈래 + 키 */
|
||||
갈래?: string;
|
||||
키?: string;
|
||||
입력값: Record<string, unknown>;
|
||||
배: number;
|
||||
몫: number | null;
|
||||
|
||||
@@ -106,7 +106,7 @@ export function mergeRows(rows: LedgerRow[], edits: Edits, scope: HopyoScope): L
|
||||
/** 짝 글 — 「GC000198 × 1 · 몫 0.1 · 소량감률=50」 */
|
||||
export const pairText = (p: LedgerPair): string =>
|
||||
[
|
||||
`${p.로직 ?? p.자재 ?? ""} × ${p.배}`,
|
||||
`${p.로직 ?? p.자재 ?? [p.갈래, p.키].filter(Boolean).join(" ")} × ${p.배}`,
|
||||
p.몫 != null ? `몫 ${p.몫}` : "",
|
||||
Object.entries(p.입력값 ?? {})
|
||||
.map(([k, v]) => `${k}=${formatNumber(v)}`)
|
||||
|
||||
@@ -177,7 +177,7 @@ def reprice(logic: dict, where: dict, price: dict[str, float]) -> int:
|
||||
|
||||
def _m01(kind: str, src: dict) -> tuple[str | None, str | None]:
|
||||
"""1차 줄 판정 유일 짝 M01 키 → (키, 표지) — 없음 = (None, None) · 여럿 = (None, 표지 · 유가 ·
|
||||
환율만 — 재료 · 인력 · 기계는 지금처럼 조용히 이음 없음). 재료 줄 이름이 M01 유가전력(MO)이면
|
||||
환율만 — 재료 · 인력은 조용히 이음 없음) · 기계 = 기계 찾기(812-9) 못 이으면 늘 표지. 재료 줄 이름이 M01 유가전력(MO)이면
|
||||
유가 · 화폐 이름 그대로(달러 · USD …)면 환율(FX)로 판정(797-1 · 「유로폼」 같은 글 속 낱말은 아님)."""
|
||||
if kind not in ("인력", "재료", "기계"):
|
||||
return None, None
|
||||
@@ -185,6 +185,13 @@ def _m01(kind: str, src: dict) -> tuple[str | None, str | None]:
|
||||
from M02_MasterTemplete import M02_Hopyo_Row as hrow
|
||||
|
||||
line = {k: src.get(k) or "" for k in ("이름", "규격", "단위")}
|
||||
if (
|
||||
kind == "기계"
|
||||
): # 812-9 — 하나 = 이음 · 아니면 표지(후보 여럿 · 못 찾음 · 조용히 넘기지 않음)
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
|
||||
got = mfind.find(line["이름"], line["규격"])
|
||||
return got["키"], mfind.mark(got, line["이름"], line["규격"])
|
||||
if kind == "재료":
|
||||
if elem.oil_rows(line["이름"]):
|
||||
kind = "유가"
|
||||
@@ -211,6 +218,60 @@ def _m01(kind: str, src: dict) -> tuple[str | None, str | None]:
|
||||
return None, None
|
||||
|
||||
|
||||
def hourly_text(key: str | None) -> str | None:
|
||||
"""기계 EQ → 시간당 기계경비 로직 부름 글(797-1 짝 고름) · 키 · 자료 없음 = None."""
|
||||
if not key:
|
||||
return None
|
||||
from M02_MasterTemplete import M02_Doc_Import as dim
|
||||
|
||||
got = dim.hourly_link(key, "노무비")
|
||||
if not got:
|
||||
return None
|
||||
return f"로직({got['키']}{''.join(f', {k}={v}' for k, v in got['입력'].items())})"
|
||||
|
||||
|
||||
# ── 기계 부분(812-9 · 조합 기계 = 줄 둘) ─────────────────────────────────
|
||||
def machine_parts(src: dict) -> list[dict]:
|
||||
"""기계 자원 → 부분 찾기(`M02_Machine_Find.find_all` · 조합 = 둘 이상) · 기계 아님 = []."""
|
||||
if src.get("갈래") != "기계":
|
||||
return []
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
|
||||
return mfind.find_all(src.get("이름"), src.get("규격"))
|
||||
|
||||
|
||||
def part_line(main: dict, part: dict, names: "Names") -> dict:
|
||||
"""조합 기계 다음 줄 — 같은 수량 식 · 값 0(파일 경비 = 한 덩이 = 첫 줄) · 제 이음."""
|
||||
res = {**main["자원"], "이름": part["이름"], "규격": part["규격"], "찾기": part, "조합": True}
|
||||
res.pop("조합첫", None)
|
||||
res["단가"] = dict.fromkeys(res.get("단가") or COMP, 0)
|
||||
title = part["후보"][0]["이름"] if part["키"] else part["이름"]
|
||||
one = {"이름": names.item(str(title)), "규격": part["규격"], "단위": main.get("단위") or ""}
|
||||
one |= {"수량": main["수량"], "자원": res}
|
||||
if main.get("종류") == "로직":
|
||||
one |= {"종류": "로직", "값묶음": bundle(res), "이음": hourly_text(part["키"])}
|
||||
else:
|
||||
one |= {"종류": main["종류"], "비목": main["비목"], "단가": Decimal(0), "이음": part["키"]}
|
||||
return one | ({"자리": main["자리"]} if main.get("자리") else {})
|
||||
|
||||
|
||||
def part_marks(src: dict, parts: list[dict]) -> list[str]:
|
||||
"""부분 표지 — 못 이은 부분(후보 여럿 · 못 찾음) · 조합 기계(경비 한 덩이 = 첫 줄 · 다음 줄 0)."""
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
|
||||
out = [m for p in parts if (m := mfind.mark(p, p["이름"], p["규격"]))]
|
||||
if len(parts) > 1:
|
||||
said = " ".join(x for x in (str(src.get("이름") or ""), str(src.get("규격") or "")) if x)
|
||||
names = " + ".join(
|
||||
f"{p['후보'][0]['이름']} {p['후보'][0]['규격']}" if p["키"] else p["이름"]
|
||||
for p in parts
|
||||
)
|
||||
out.append(
|
||||
f"{NOTE}조합 기계 — 경비는 한 덩이 「{said}」 = {names} · 엑셀 값은 첫 줄 · 다음 줄 0"
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
# ── 내보낼 로직 틀 ─────────────────────────────────────────────────────
|
||||
class Out:
|
||||
def __init__(self, name: str, unit: str, form: str, code: Any) -> None:
|
||||
@@ -290,6 +351,7 @@ class _Stm:
|
||||
self.open: list[tuple[int, list[str]]] = [] # 위 단계가 아직 안 담은 단계(엔진 `_cover`)
|
||||
self.out: dict[str, int] = {} # 제외 단계 줄 → 그 계 줄까지 줄 수(합에서 빠지는 때)
|
||||
self.fix: list[tuple[dict, int]] = [] # 누계 % 덧줄 · 그때 줄 수
|
||||
self.noted: set[tuple] = set() # 기계 표지 단 자원(812-9 · 블록에 한 번)
|
||||
|
||||
def brackets(self, node: tuple, src: dict, kept: dict) -> None:
|
||||
"""끼움 `[Q=0.208]` → 중간(왼쪽부터 · 원문 차례)."""
|
||||
@@ -395,11 +457,24 @@ class _Stm:
|
||||
if not one_ and len(sole) == 1: # 비목 꼬리 없어도 값 든 비목 하나면 단가 줄(셈 같음)
|
||||
one_, to = sole[0], None
|
||||
res |= {"비목": one_, "옮김": to} if one_ else {}
|
||||
link, many = _m01(res["갈래"], res) if self.keys else (None, None)
|
||||
parts = machine_parts(res) if self.keys else [] # 812-9 기계 찾기 · 조합 = 줄 둘
|
||||
if parts:
|
||||
res["찾기"], link, many = parts[0], parts[0]["키"], None
|
||||
res |= {"조합첫": True} if len(parts) > 1 else {}
|
||||
else:
|
||||
link, many = _m01(res["갈래"], res) if self.keys else (None, None)
|
||||
one = resource(o.names.item(name), pr.get("단위"), cf, res, link)
|
||||
if parts and one["종류"] == "로직":
|
||||
one["이음"] = hourly_text(link)
|
||||
self.line(one, node, True, src)
|
||||
if many:
|
||||
o.glyph(src["행"], src["글"], mark=many)
|
||||
bare = {k: v for k, v in node.items() if k != "번지"}
|
||||
for p in parts[1:]:
|
||||
self.line(part_line(one, p, o.names), bare, True, src)
|
||||
seen = (res.get("이름"), res.get("규격"))
|
||||
marks = [] if seen in self.noted else part_marks(res, parts)
|
||||
self.noted.add(seen)
|
||||
for m in [*marks, *([many] if many else [])]:
|
||||
o.glyph(src["행"], src["글"], mark=m)
|
||||
|
||||
def rate(self, node: dict, src: dict, cf: str) -> None:
|
||||
rate, o = node["율"], self.o
|
||||
|
||||
@@ -99,6 +99,12 @@ export function priceSym(line: MoneyLine): string | null {
|
||||
}
|
||||
|
||||
/** 금액 줄 하나만 단가로 쓰는 연결 기호(요소 · 단가산출) — {줄 차례: 기호} */
|
||||
/** 가져온 기계 줄 단가 기호(812-9 `가져온.기계`) — 못 이어 숫자 연결이어도 그 줄 기계(이름 단추 · 고르기) */
|
||||
export function machineSyms(doc: FormulaDoc): Set<string> {
|
||||
const got = (doc as { 가져온?: { 기계?: { 기호?: string }[] } }).가져온?.기계;
|
||||
return new Set((Array.isArray(got) ? got : []).map((x) => String(x?.기호 ?? "")));
|
||||
}
|
||||
|
||||
export function ownedOf(doc: FormulaDoc): Map<number, string> {
|
||||
const count = new Map<string, number>();
|
||||
const bump = (ns: string[]): void => ns.forEach((n) => count.set(n, (count.get(n) ?? 0) + 1));
|
||||
@@ -107,12 +113,13 @@ export function ownedOf(doc: FormulaDoc): Map<number, string> {
|
||||
if (doc.결과) bump([doc.결과]);
|
||||
const vars = new Map((doc.변수 ?? []).map((v) => [varId(v), v]));
|
||||
const lines = new Set((doc.식 ?? []).map((l) => String(l.기호 ?? "")));
|
||||
const mach = machineSyms(doc);
|
||||
const out = new Map<number, string>();
|
||||
(doc.금액 ?? []).forEach((l, i) => {
|
||||
const s = priceSym(l);
|
||||
const kind = s ? vars.get(s)?.연결?.갈래 : undefined;
|
||||
if (s && (kind === "요소" || kind === "단가산출") && count.get(s) === 1 && !lines.has(s))
|
||||
out.set(i, s);
|
||||
const mine = kind === "요소" || kind === "단가산출" || (!!s && mach.has(s));
|
||||
if (s && mine && count.get(s) === 1 && !lines.has(s)) out.set(i, s);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
@@ -137,6 +144,7 @@ export function kindOf(
|
||||
const lb = link.키 ? String(link.키).startsWith("LB") : line.비목 === "노무비"; // 고르기 전 = 비목으로
|
||||
return lb ? "인력" : "재료";
|
||||
}
|
||||
if (s && machineSyms(doc).has(s)) return "기계"; // 못 이은 가져온 기계 줄(812-9)
|
||||
return "율";
|
||||
}
|
||||
|
||||
|
||||
@@ -138,6 +138,11 @@ export function repick(doc: FormulaDoc, i: number, id: string, kind: MoneyKind,
|
||||
const now = linkOf(kind, p);
|
||||
if (now.갈래 === "요소" && was.지역) now.지역 = was.지역; // 지역 옵션 이음 그대로
|
||||
if (now.갈래 === "단가산출" && was.키 === now.키) now.입력 = { ...was.입력, ...now.입력 }; // 같은 로직 = 다른 넘길 입력 그대로
|
||||
// 가져온 기계 줄(812-9) — 엑셀 값 기억 그대로(엑셀 단가 기준 금액 안 바뀜 · 못 이은 기계 = 숫자 값 · 묶음)
|
||||
const kept = (was as { 엑셀?: unknown }).엑셀 ?? (was.갈래 === "숫자" ? was.값 : undefined);
|
||||
if (kind === "기계" && kept !== undefined) (now as { 엑셀?: unknown }).엑셀 = kept;
|
||||
const cost = (line as { 비목?: string }).비목 ?? "";
|
||||
if (kind === "기계" && ["노무비", "재료비", "경비"].includes(cost)) now.칸 = cost; // 비목 한 칸 줄 = 그 칸
|
||||
v.연결 = now;
|
||||
v.이름 = p.이름;
|
||||
line.이름 = p.이름;
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"줄": {
|
||||
"아스팔트페이버(피니셔)||시간": {
|
||||
"원래": {
|
||||
"품명": "아스팔트페이버(피니셔)",
|
||||
"규격": "",
|
||||
"단위": "hr"
|
||||
},
|
||||
"갈래": "짝",
|
||||
"짝": [
|
||||
{
|
||||
"갈래": "기계",
|
||||
"키": "EQ000193",
|
||||
"입력값": {},
|
||||
"배": 1,
|
||||
"몫": null
|
||||
}
|
||||
],
|
||||
"엑셀조항": [],
|
||||
"누가": {
|
||||
"id": "요구735-5·736",
|
||||
"이름": "사용자 승인(시스템 관리자)"
|
||||
},
|
||||
"때": "2026-11-09T11:13:18Z",
|
||||
"처음파일": null
|
||||
},
|
||||
"디젤엔진||시간": {
|
||||
"원래": {
|
||||
"품명": "디젤 엔진",
|
||||
"규격": "",
|
||||
"단위": "hr"
|
||||
},
|
||||
"갈래": "짝",
|
||||
"짝": [
|
||||
{
|
||||
"갈래": "기계",
|
||||
"키": "EQ000500",
|
||||
"입력값": {},
|
||||
"배": 1,
|
||||
"몫": null
|
||||
}
|
||||
],
|
||||
"엑셀조항": [],
|
||||
"누가": {
|
||||
"id": "요구735-5·736",
|
||||
"이름": "사용자 승인(시스템 관리자)"
|
||||
},
|
||||
"때": "2026-11-09T11:13:18Z",
|
||||
"처음파일": null
|
||||
},
|
||||
"워터제트||시간": {
|
||||
"원래": {
|
||||
"품명": "워터제트",
|
||||
"규격": "",
|
||||
"단위": "hr"
|
||||
},
|
||||
"갈래": "짝",
|
||||
"짝": [
|
||||
{
|
||||
"갈래": "기계",
|
||||
"키": "EQ000400",
|
||||
"입력값": {},
|
||||
"배": 1,
|
||||
"몫": null
|
||||
}
|
||||
],
|
||||
"엑셀조항": [],
|
||||
"누가": {
|
||||
"id": "요구735-5·736",
|
||||
"이름": "사용자 승인(시스템 관리자)"
|
||||
},
|
||||
"때": "2026-11-09T11:13:18Z",
|
||||
"처음파일": null
|
||||
},
|
||||
"버킷준설기||시간": {
|
||||
"원래": {
|
||||
"품명": "버킷준설기",
|
||||
"규격": "",
|
||||
"단위": "hr"
|
||||
},
|
||||
"갈래": "짝",
|
||||
"짝": [
|
||||
{
|
||||
"갈래": "기계",
|
||||
"키": "EQ000425",
|
||||
"입력값": {},
|
||||
"배": 1,
|
||||
"몫": null
|
||||
}
|
||||
],
|
||||
"엑셀조항": [],
|
||||
"누가": {
|
||||
"id": "요구735-5·736",
|
||||
"이름": "사용자 승인(시스템 관리자)"
|
||||
},
|
||||
"때": "2026-11-09T11:13:18Z",
|
||||
"처음파일": null
|
||||
},
|
||||
"바이브레이트|봉상후렉시블|시간": {
|
||||
"원래": {
|
||||
"품명": "바이브레이트",
|
||||
"규격": "봉상후렉시블",
|
||||
"단위": "hr"
|
||||
},
|
||||
"갈래": "짝",
|
||||
"짝": [
|
||||
{
|
||||
"갈래": "기계",
|
||||
"키": "EQ000248",
|
||||
"입력값": {},
|
||||
"배": 1,
|
||||
"몫": null
|
||||
}
|
||||
],
|
||||
"엑셀조항": [],
|
||||
"누가": {
|
||||
"id": "요구735-5·736",
|
||||
"이름": "사용자 승인(시스템 관리자)"
|
||||
},
|
||||
"때": "2026-11-09T11:13:18Z",
|
||||
"처음파일": null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,8 +134,10 @@ def test_sample_stone_wall_reference_and_direct_q() -> None:
|
||||
and got[1][2][-1] == "뒷채움돌기초잡석채집운반"
|
||||
)
|
||||
assert got[1][2][0].split()[0] == "=Q" # 단산 참조 Q 직접 = 그 항목 공식 묶음
|
||||
assert titles[3].startswith("전석쌓기 기계") and got[3][2] == ["=Q_3", "굴삭기(무한궤도)"]
|
||||
assert got[4] == ("전석쌓기 인부", False, ["석공", "보통인부"])
|
||||
# 조합 기계 「굴삭기 0.6㎥+부착용집게」 = 기계마다 항목 하나 · 제목 = 그 기계(812-9 · 요구 738-3)
|
||||
assert titles[3] == "굴착기(무한궤도) 0.6㎥" and got[3][2] == ["=Q_3", "굴삭기(무한궤도)"]
|
||||
assert got[4] == ("부착용 집게 0.6∼0.8", False, ["부착용 집게"])
|
||||
assert got[5] == ("전석쌓기 인부", False, ["석공", "보통인부"])
|
||||
assert "합" not in "".join(titles) # 합계 절 = 화면 자동 합계(두 번 안 보임)
|
||||
|
||||
|
||||
@@ -155,8 +157,10 @@ def test_sample_ganada_sub_items() -> None:
|
||||
_, doc = _doc("EST", "거창", 2)
|
||||
got = _show(doc)
|
||||
assert got[0] == ("기 계(100%)", False, [])
|
||||
assert got[1][:2] == ("굴삭기(무한궤도) (0.7+브레이카)", True)
|
||||
assert got[2] == ("치즐(0.7㎥용)", True, ["치즐(0.7㎥)"]) # 「다. 소 계」 절 = 뺌
|
||||
# 「가. 굴삭기(무한궤도)(0.7+브레이카)」 = 조합 기계 둘 = 가나다 항목 둘(812-9 · 요구 738-3)
|
||||
assert got[1][:2] == ("굴착기(무한궤도) 0.7㎥", True)
|
||||
assert got[2] == ("대형 브레이커 0.7㎥", True, ["대형 브레이커"])
|
||||
assert got[3] == ("치즐(0.7㎥용)", True, ["치즐(0.7㎥)"]) # 「다. 소 계」 절 = 뺌
|
||||
|
||||
|
||||
# ── (b) 전수 ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
|
||||
"""812-9 지킴 — 가져온 기계 잇기(`M02_Machine_Find` · 요구 718 · 719).
|
||||
|
||||
(a) 기계표 전체에서 찾음 — 낱말표 밖 기종(로우더 · 공기 압축기 · 램 머 · 라인마커 …) · 후보 여럿이던 것
|
||||
(물탱크 · 크레인(무한궤도) · 머캐덤 · 콘크리트 펌프차 …) · 지어 맞추기 없음(1.0Ton = 우리 표에 없음)
|
||||
(b) 표기 정규화 · 괄호 낱말(무한궤도 · 타이어 · 일반용 · 표에 없는 낱말 발파 = 안 봄) · 범위 · 복수 규격
|
||||
(c) 조합 기계 = 둘(대형브레이카 +굴착기 · 0.7+브레이카 · +부착용집게 · +진동콤팩터 · 덤프트럭(자동덮개))
|
||||
(d) 별칭 먼저 — 지금 층 장부 기계 짝 = 그 EQ · 이름만 열쇠 = 그 기종으로 · 후보 여럿 표지 ·
|
||||
[저장] 때 고른 EQ = 지금 층 장부 줄(`learn`)
|
||||
(e) 금액 전후 같음 — 조합 줄 둘(첫 줄 엑셀 값 · 다음 줄 0) · 원파일 저장 금액 그대로
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HERE = Path(__file__).resolve().parent
|
||||
for p in (ROOT, HERE):
|
||||
if str(p) not in sys.path:
|
||||
sys.path.insert(0, str(p))
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger # noqa: E402
|
||||
from M02_MasterTemplete import M02_Machine_Find as mf # noqa: E402
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store # noqa: E402
|
||||
from resources.tester.test_m02_labor_ref import world # noqa: E402, F401 — 같은 임시 층
|
||||
|
||||
|
||||
def one(name: str, spec: str = "") -> tuple[str, str, str]:
|
||||
got = mf.find(name, spec)
|
||||
first = got["후보"][0] if got["후보"] else {}
|
||||
return got["판정"], str(first.get("이름") or ""), str(first.get("규격") or "")
|
||||
|
||||
|
||||
LINKED = [ # 낱말 모름 · 후보 여럿이던 예(812-8) → 하나
|
||||
("로우더(타이어)", "0.57㎥", "로더(타이어)", "0.57㎥"),
|
||||
("로우더(타이어)", "1.72㎥", "로더(타이어)", "1.72㎥"),
|
||||
("공기 압축기", "21.0㎥/min", "공기압축기(이동식)", "21.0㎥/min"),
|
||||
("공기 압축기", "7.1㎥/min", "공기압축기(이동식)", "7.1㎥/min"),
|
||||
("라인마커", "10Km/hr", "라인 마커", "10km/hr"),
|
||||
("램 머", "80kg", "래 머", "80㎏"),
|
||||
("진동파일해머", "60kW", "진동파일 해머(전동식)", "60㎾"),
|
||||
("진동파일해머", "45kW", "진동파일 해머(전동식)", "45㎾"),
|
||||
("진동파일해머", "40kW", "진동파일 해머(전동식)", "40㎾"),
|
||||
("노면파쇄기", "2.0m", "노면파쇄기", "2.0m"),
|
||||
("아스팔트 디스트리뷰터", "3800ℓ", "아스팔트 디스트리뷰터", "3,800ℓ"),
|
||||
("고소작업차", "3Ton", "고소작업차", "3ton"),
|
||||
("고소작업차", "5Ton", "고소작업차", "5ton"),
|
||||
("취부기(녹생토)", "18.65kW", "취부기(녹생토 암절개면 보호식재용)", "18.65㎾"),
|
||||
("아스팔트 스프레이어", "400ℓ", "아스팔트 스프레이어", "400ℓ"),
|
||||
("커 터", "320~400mm", "커터(콘크리트 및 아스팔트용)", "320~400"),
|
||||
("경운기", "1000kg", "경운기", "1,000㎏"),
|
||||
("건설용펌프(자흡식)", "50mm", "건설용펌프(자흡식)", "50(1.49×10)"),
|
||||
("강연선인장기", "120Ton", "강연선인장기", "120ton"),
|
||||
("디젤 엔진", "11.19kW", "엔진(디젤)", "기종 디젤 엔진 · 규격 11.19㎾"),
|
||||
("물탱크", "5500ℓ", "물탱크(살수차)", "5,500ℓ"),
|
||||
("물탱크", "16000ℓ", "물탱크(살수차)", "16,000ℓ"),
|
||||
("콘크리트 펌프차", "32m,80~95㎥/hr", "콘크리트 펌프차", "32 [80∼95]"),
|
||||
("머캐덤 로울러(자주식)", "10~12Ton", "머캐덤 롤러(자주식)", "10∼12"),
|
||||
("타이어 로울러(자주식)", "8~15Ton", "타이어 롤러(자주식)", "8∼15"),
|
||||
("크레인(무한궤도)", "40Ton", "크레인(무한궤도)", "40 (1.53)"),
|
||||
("크레인(무한궤도)", "35Ton", "크레인(무한궤도)", "35 (1.33)"),
|
||||
("크로울러 드릴(공기식)", "17㎥/min", "크롤러드릴(공기식)", "17 (120㎜)"),
|
||||
("모터 그레이더", "3.6m(일반용)", "모터그레이더(일반용)", "3.6m"),
|
||||
("콘크리트 믹서", "0.3㎥", "콘크리트 믹서", "0.30㎥"),
|
||||
("진동 로울러(자주식)", "12Ton", "진동롤러(자주식)", "12ton"),
|
||||
("굴삭기(무한궤도)", "0.7㎥", "굴착기(무한궤도)", "0.7㎥"),
|
||||
("굴착기(타이어형)", "0.6㎥", "굴착기(타이어)", "0.6㎥"),
|
||||
("굴착기(무한궤도)", "0.4㎥(발파)", "굴착기(무한궤도)", "0.4㎥"),
|
||||
("불도우저(무한궤도)", "32Ton", "불도저(무한궤도)", "32ton"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name,spec,our,size", LINKED)
|
||||
def test_linked(name: str, spec: str, our: str, size: str) -> None:
|
||||
kind, *got = one(name, spec)
|
||||
assert kind in (mf.LINKED, mf.ALIAS) and got == [our, size] # 승인 별칭(디젤 엔진) = 별칭
|
||||
|
||||
|
||||
MASTER = [ # 마스터 층 승인 별칭(요구 735-5 · 736) — 이름 별칭 + 규격은 수로 맞댐
|
||||
("아스팔트페이버(피니셔)", "3.0m", "EQ000193"),
|
||||
("아스팔트페이버(피니셔)", "1.7m", "EQ000192"),
|
||||
("디젤 엔진", "11.19kW", "EQ000500"),
|
||||
("디젤 엔진", "26.11kW", "EQ000503"),
|
||||
("디젤 엔진", "52.22kW", "EQ000504"),
|
||||
("워터제트", "96kW", "EQ000400"),
|
||||
("버킷준설기", "7.46kW", "EQ000425"),
|
||||
("바이브레이트", "봉상후렉시블", "EQ000248"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name,spec,eq", MASTER)
|
||||
def test_master_alias(name: str, spec: str, eq: str) -> None:
|
||||
got = mf.find(name, spec)
|
||||
assert got["키"] == eq and got["판정"] in (mf.LINKED, mf.ALIAS)
|
||||
|
||||
|
||||
def test_no_guess() -> None:
|
||||
"""우리 표에 없는 규격 = 못 찾음(가까운 규격으로 안 이음) · 기종 모름 = 별칭 몫 · 여럿 = 여럿."""
|
||||
got = mf.find("덤프트럭", "1.0Ton")
|
||||
assert got["판정"] == mf.NONE and got["까닭"] == "우리 표에 없음(규격)" and got["키"] is None
|
||||
assert got["후보"][0]["규격"] == "2.5ton" # 보여주기만 — 가까운 규격 먼저
|
||||
for name, spec in (("에어 호스", "1.91cm"), ("모노펌프", "5-20㎥/hr")): # 승인 전 = 별칭 몫
|
||||
got = mf.find(name, spec)
|
||||
assert (got["판정"], got["까닭"]) == (mf.NONE, "기종 모름 — 별칭")
|
||||
got = mf.find("크레인(트럭)", "10Ton") # 「트럭」 = 기계표 괄호에 없는 낱말 → 안 봄
|
||||
assert got["판정"] == mf.MANY and got["수"] == 2
|
||||
assert mf.mark(got, "크레인(트럭)", "10Ton").startswith("참고 · 기계 후보 2 — 고르기")
|
||||
assert "기계 못 찾음" in mf.mark(mf.find("덤프트럭", "1.0Ton"), "덤프트럭", "1.0Ton")
|
||||
assert mf.mark(mf.find("물탱크", "5500ℓ"), "물탱크", "5500ℓ") is None
|
||||
|
||||
|
||||
def test_norm_paren_spec() -> None:
|
||||
assert mf.norm("로우더") == mf.norm("로더") and mf.norm("로울러") == mf.norm("롤러")
|
||||
assert mf.norm("브레이카") == mf.norm("브레이커") and mf.norm("굴삭기") == mf.norm("굴착기")
|
||||
assert mf.norm("불도우저") == mf.norm("불도저") and mf.norm("램 머") == mf.norm("래 머")
|
||||
assert mf.tokens("10~12Ton") == [(10.0, 12.0, "ton")]
|
||||
assert mf.tokens("32m,80~95㎥/hr") == [(32.0, 32.0, "m"), (80.0, 95.0, "m3/hr")]
|
||||
assert mf.tokens("5,500ℓ") == [(5500.0, 5500.0, "l")]
|
||||
assert mf.spec_fit(mf.tokens("0.7㎥"), mf.tokens("0.6∼0.8")) == "안"
|
||||
assert one("굴착기(타이어)", "0.6㎥")[1] == "굴착기(타이어)" # 괄호 낱말이 가름
|
||||
assert one("굴착기(무한궤도)", "0.6㎥")[1] == "굴착기(무한궤도)"
|
||||
|
||||
|
||||
COMBO = [ # 조합 기계 = 둘 · 규격 없는 쪽 = 앞 규격
|
||||
("굴삭기(무한궤도)", "0.7+브레이카", [("굴착기(무한궤도)", "0.7㎥"), ("대형 브레이커", "0.7㎥")]),
|
||||
("대형브레이카", "+굴착기(0.7㎥)", [("대형 브레이커", "0.7㎥"), ("굴착기(무한궤도)", "0.7㎥")]),
|
||||
("대형브레이카", "+굴착기(0.4㎥)", [("대형 브레이커", "0.4㎥"), ("굴착기(무한궤도)", "0.4㎥")]),
|
||||
("굴삭기+진동콤팩터", "0.7㎥", [("굴착기(무한궤도)", "0.7㎥"),
|
||||
("유압식 진동콤팩터(굴착기 부착용)", "0.7㎥")]),
|
||||
("굴삭기(무한궤도)", "0.2㎥+부착용집게", [("굴착기(무한궤도)", "0.2㎥"), ("부착용 집게", "0.2㎥")]),
|
||||
("굴삭기(무한궤도)", "0.6㎥+부착용집게", [("굴착기(무한궤도)", "0.6㎥"), ("부착용 집게", "0.6∼0.8")]),
|
||||
("로우더(타이어)+소형노면파쇄기", "0.95㎥", [("로더(타이어)", "0.95㎥"), ("소형노면파쇄기", "0.95㎥")]),
|
||||
("덤프트럭(자동덮개)", "15Ton", [("덤프트럭", "15ton"), ("덤프트럭 자동덮개시설", "15톤용")]),
|
||||
("굴삭기(무한궤도)", "0.7㎥,(브레이커조합)", [("굴착기(무한궤도)", "0.7㎥"), ("대형 브레이커", "0.7㎥")]),
|
||||
] # fmt: skip
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name,spec,want", COMBO)
|
||||
def test_combo_two(name: str, spec: str, want: list) -> None:
|
||||
got = mf.find_all(name, spec)
|
||||
assert [g["판정"] for g in got] == [mf.LINKED, mf.LINKED]
|
||||
assert [(g["후보"][0]["이름"], g["후보"][0]["규격"]) for g in got] == want
|
||||
|
||||
|
||||
def test_combo_many_part() -> None:
|
||||
"""대형브레이카 +굴착기(1.0㎥) — 굴착기 무한궤도 · 타이어 둘 다 1.0 = 그 쪽만 후보 여럿(사용자 고름)."""
|
||||
got = mf.find_all("대형브레이카", "+굴착기(1.0㎥)")
|
||||
assert [g["판정"] for g in got] == [mf.LINKED, mf.MANY]
|
||||
assert mf.find_all("덤프트럭", "15Ton") == [{**mf.find("덤프트럭", "15Ton"), "이름": "덤프트럭", "규격": "15Ton"}] # fmt: skip
|
||||
|
||||
|
||||
def _pair(eq: str, name: str, spec: str, unit: str = "hr") -> dict:
|
||||
full = ledger.key({"이름": name, "규격": spec, "단위": unit})
|
||||
row = {"원래": {"품명": name, "규격": spec, "단위": unit}, "갈래": "짝"}
|
||||
return {full: row | {"짝": [{"갈래": "기계", "키": eq, "입력값": {}, "배": 1}]}}
|
||||
|
||||
|
||||
def test_alias_first_and_learn(world) -> None: # noqa: F811
|
||||
me = world["personal"]
|
||||
me.mkdir(parents=True, exist_ok=True)
|
||||
with store.at(me):
|
||||
assert mf.find("아스팔트페이버(피니셔)", "3.0m")["판정"] == mf.NONE
|
||||
ledger.write(me, _pair("EQ000193", "아스팔트페이버(피니셔)", ""), {"id": "sub3임시"})
|
||||
got = mf.find("아스팔트페이버(피니셔)", "3.0m") # 이름만 열쇠 = 그 기종 → 규격으로 하나
|
||||
assert (got["판정"], got["후보"][0]["이름"], got["후보"][0]["규격"]) == (
|
||||
mf.ALIAS, "아스팔트 피니셔", "3m") # fmt: skip
|
||||
ledger.write(me, _pair("EQ000129", "크레인(트럭)", "15Ton"), {"id": "sub3임시"})
|
||||
got = mf.find("크레인(트럭)", "15Ton") # 이름 + 규격 열쇠 = 그 EQ(찾기보다 먼저)
|
||||
assert (got["판정"], got["키"]) == (mf.ALIAS, "EQ000129")
|
||||
assert (
|
||||
mf.find("크레인(트럭)", "15Ton(할증125%)")["키"] == "EQ000129"
|
||||
) # 규격 속 할증 글 = 안 봄
|
||||
assert mf.same_kind("아스팔트페이버(피니셔)", "아스팔트 피니셔") is True
|
||||
doc = {
|
||||
"변수": [{"열쇠": "P_크", "기호": "P", "아래": "크", "연결": {
|
||||
"갈래": "단가산출", "키": "GC000270", "입력": {"기계": "'2104-0010'"}}}],
|
||||
"가져온": {"기계": [{"기호": "P_크", "판정": mf.MANY, "키": None,
|
||||
"원래": {"이름": "크레인(트럭)", "규격": "10Ton", "단위": "hr"}}]},
|
||||
} # fmt: skip
|
||||
assert mf.learn(doc, {"id": "sub3임시"}) == 1
|
||||
got = mf.find("크레인(트럭)", "10Ton")
|
||||
assert got["판정"] == mf.ALIAS and got["후보"][0]["원문번호"] == "2104-0010"
|
||||
assert mf.learn(doc, {"id": "sub3임시"}) == 0 # 이미 같은 EQ = 안 씀
|
||||
assert mf.find("크레인(트럭)", "10Ton")["판정"] == mf.MANY # 다른 층(마스터) = 안 봄
|
||||
|
||||
|
||||
def test_combo_money_same() -> None:
|
||||
"""원파일 거창 측구터파기 연암 — 「굴삭기(무한궤도) 0.7+브레이카」 = 줄 둘 · 금액 = 원파일 저장 그대로."""
|
||||
from test_810_2_native_doc import doc_and_money, native
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_FromNative as fn
|
||||
|
||||
saved = native("EST", "거창")
|
||||
blk = fn.numbers(saved)[2]
|
||||
sk, doc, money = doc_and_money(fn.emit(saved, 2))
|
||||
names = [(m["이름"], m.get("규격")) for m in doc["금액"]]
|
||||
assert ("굴삭기(무한궤도)", "0.7+브레이카") in names and ("대형 브레이커", "0.7㎥") in names
|
||||
assert float(money["계"]) == pytest.approx(float(blk["저장"]["T"]), abs=1)
|
||||
found = {x["원래"]["이름"]: x for x in doc["가져온"]["기계"]}
|
||||
assert found["브레이카"]["조합"] is True and found["브레이카"]["찾기어"] == "대형 브레이커"
|
||||
assert any("조합 기계 — 경비는 한 덩이" in m for m in doc["가져온"]["표지"])
|
||||
from M02_MasterTemplete import M02_Doc_NativeLayout as lay
|
||||
|
||||
from test_812_3_native_layout import _show
|
||||
|
||||
groups = [(g[0], g[2]) for g in _show(doc)] # 조합 기계 = 기계마다 항목(요구 738-3)
|
||||
assert ("굴착기(무한궤도) 0.7㎥", ["=Q", "굴삭기(무한궤도)"]) in groups
|
||||
assert ("대형 브레이커 0.7㎥", ["대형 브레이커"]) in groups
|
||||
owned = lay.owned_of(doc) # 못 이은 기계 줄(시간당 자료 없음 = 숫자 묶음)도 기계 = 이름 단추
|
||||
at = next(i for i, m in enumerate(doc["금액"]) if m["이름"] == "대형 브레이커")
|
||||
assert owned.get(at) == found["브레이카"]["기호"]
|
||||
Reference in New Issue
Block a user