Merge branch 'sub_laptop_1' of C:/Aislo_wt/sub_laptop_1 into main_laptop_1

This commit is contained in:
2026-11-11 20:48:16 +09:00
6 changed files with 582 additions and 3 deletions
+256
View File
@@ -0,0 +1,256 @@
"""M02 불러오기 재료 표기 다름 찾기(PLAN 817-1 · 요구 773-5) — `M02_Hopyo_Row._m01_picks` 재료 길이 부름.
서버 단독 · 읽기만 · 별칭 · 정확 일치 · 바탕 이름 유일 짝(`M02_Hopyo_Material.sure`)이 못 찾은 줄만.
- `same(줄)` ㉮ 이음 = 이름이 같은 품목(표기만 다름) · 규격 수 묶음 같음 · 규격 낱말 안 부딪힘 · 그런 우리
품목 하나(지역 · 관급/사급 줄은 한 품목) → 그 자재 줄 · 아니면 None.
- `narrow(줄)` ㉯ 후보 좁힘 = 이름 같은 품목 묶음을 규격 낱말(어근 「초본류」 → 초본)을 품은 줄 · 수 묶음
닮은 줄 · 덧말 적은 줄 차례로(자동 이음 아님 · 1 순위 맨 위).
- `spec_first(후보, 줄)` = 후보 가운데 규격 수 묶음(둘 이상)이 줄과 같은 것 먼저.
이름 같음: 바탕(괄호 · 지역 · 관급/사급 꼬리 · 등급 「1급」 뗌 · 빈칸 · 점 「P.V.C」 · 소리 표기 `SPELL`) 같음 ·
괄호 차례 「패널(유로폼)」 = 「유로폼패널」(글자 묶음 같음) · 괄호 이름 「서비스볼트(고장력볼트)」 = 고장력볼트 ·
(약한 같음 — 수 묶음이 같을 때만) 끝 낱말 같음 「지수판」 ↔ 「PVC지수판」 · 앞 낱말 「구역화물자동차」 ↔ 구역화물 ·
규격에 든 이름 「안고초」 ↔ 산림종자 안고초(가져온 규격 빔).
수 묶음: 규격 + 이름 괄호의 수(차례 무관) · KS 번호 · 무게(kg · ㎏/포 · m/kg) · 제곱 꼬리(㎡ · ㎠) 뗌 ·
천 단위 쉼표 · 같음 > 낱마다 단위 배(1 · 10 · 100 · 1000 — 3.6×3.0×3.0 m = 30×30×3600 mm) > 한쪽이 다른 쪽에 듦
(이름 같을 때만 · 덧글 「2000 길이 · 20~30mm」).
규격 낱말 부딪힘 = 두 쪽 다 괄호 · 규격 한글 낱말이 있고 두 글자 묶음이 하나도 안 겹침(「연강용」 ↔ 「주철용」) —
상대 이름에 든 낱말은 뺌.
"""
from __future__ import annotations
import re
import unicodedata
from functools import lru_cache
from itertools import permutations
from typing import Any
from M02_MasterTemplete import M02_Hopyo_Match as hm
from M02_MasterTemplete import M02_Hopyo_Material as hmat
SPELL = (
(r"칠렌", "틸렌"),
(r"매쉬", "메쉬"),
(r"신나(?![가-힣])", "신너"),
(r"pipe", "파이프"),
) # 소리 · 영문 표기
NOISE = frozenset({"이내", "이하", "이상", "국산", "포함"})
SCALES = (1, 10, 100, 1000)
_STEM = re.compile(r"(?<=..)[류형용]$")
_KS = re.compile(r"ks\s*[a-z]?\s*-?\s*\d+")
_SQUARE = re.compile(r"(?:c|m)?m[23](?![\d.])")
_THOUSAND = re.compile(r"(?<=\d),(?=\d{3}(?!\d))")
_NUM = re.compile(r"\d+(?:\.\d+)?")
_PAREN = re.compile(r"\(([^()]*)\)")
def _low(text: Any) -> str:
t = unicodedata.normalize("NFKC", str(text or "")).lower().replace("m/m", "mm")
return hmat._APART.sub("", t)
def _tidy(text: str) -> str:
t = re.sub(r"(?<=[a-z])[.\s]+(?=[a-z])", "", text)
for a, b in SPELL:
t = re.sub(a, b, t)
t = re.sub(r"\d+급", "", t)
return re.sub(r"[^가-힣a-z0-9]", "", t)
def _bag(text: str) -> tuple[float, ...]:
t = _THOUSAND.sub("", _SQUARE.sub("", _KS.sub("", text)))
return tuple(sorted(float(x) for x in _NUM.findall(t)))
def _quals(text: str) -> frozenset[str]:
words = (w for w in re.findall(r"[가-힣]{2,}", text) if not hmat._region(w))
return frozenset(w for w in words if w not in hmat.SUPPLY and w not in NOISE)
def _item(name: Any, spec: Any) -> dict[str, Any]:
"""이름 · 규격 → 바탕 · 괄호 조각 · 수 묶음 · 규격 낱말 · 온 이름 글."""
low, spec = _low(name), _low(spec)
parens = _PAREN.findall(low)
pieces = tuple(p for p in (_tidy(x) for x in parens for x in x.split(",")) if p)
core = _tidy(hmat._bare(low))
return {
"core": core,
"pieces": pieces,
"chars": "".join(sorted(core + "".join(pieces))),
"bag": _bag(f"{spec} {' '.join(parens)}"),
"quals": _quals(f"{' '.join(pieces)} {spec}"),
"whole": core + "".join(pieces),
"spec": _tidy(spec),
}
@lru_cache(maxsize=2)
def _book(_got: int) -> dict[str, Any]:
rows, _ = hmat.pool()
out: list[dict[str, Any]] = []
by: dict[str, dict[str, list[int]]] = {
"core": {},
"chars": {},
"piece": {},
"tail": {},
"spec": {},
}
for at, r in enumerate(rows):
one = {**_item(r["one"]["이름"], r["one"]["규격"]), "row": r}
out.append(one)
c = one["core"]
by["core"].setdefault(c, []).append(at)
by["chars"].setdefault(one["chars"], []).append(at)
by["spec"].setdefault(one["spec"], []).append(at)
for p in one["pieces"]:
by["piece"].setdefault(p, []).append(at)
for i in range(1, len(c) - 1): # 끝 낱말(두 글자 이상)
by["tail"].setdefault(c[i:], []).append(at)
return {"줄": out, "찾기": by}
def book() -> dict[str, Any]:
return _book(id(hm.targets()))
def _related(me: dict[str, Any]) -> dict[int, bool]:
"""이름 같은 우리 줄 → {차례: 굳셈(True) · 약함(False)}."""
by, c = book()["찾기"], me["core"]
strong = [*by["core"].get(c, []), *by["chars"].get(me["chars"], []), *by["piece"].get(c, [])]
if (
c not in by["core"]
): # 괄호 이름(「서비스볼트(고장력볼트)」) — 바탕 이름이 우리 표에 없을 때만
strong += [i for p in me["pieces"] for i in by["core"].get(p, [])]
weak = (
[*by["tail"].get(c, [])] if len(c) >= 2 else []
) # 우리 이름 끝 = 가져온 이름(「PVC지수판」)
for k in range(
2, len(c) - 1
): # 우리 이름 = 가져온 이름 앞(「구역화물자동차」) · 뒤 덧말이 우리 낱말 아님
if c[k:] not in by["core"] and c[k:] not in by["tail"]:
weak += by["core"].get(c[:k], [])
out = dict.fromkeys(weak, False)
out.update(dict.fromkeys(strong, True))
return out
def _bigrams(words: frozenset[str]) -> set[str]:
return {w[i : i + 2] for w in words for i in range(len(w) - 1)}
def _qual_fit(me: dict[str, Any], x: dict[str, Any]) -> tuple[bool, bool, bool]:
"""(부딪힘, 겹침, 낱말 있음) — 상대 온 이름에 든 낱말은 뺌."""
a = frozenset(w for w in me["quals"] if _tidy(w) not in x["whole"])
b = frozenset(w for w in x["quals"] if _tidy(w) not in me["whole"])
share = bool(_bigrams(a) & _bigrams(b))
return bool(a and b and not share), share, bool(a or b)
def _near(x: float, y: float) -> bool:
return any(abs(x * s - y) <= 1e-6 * max(abs(y), 1) or abs(y * s - x) <= 1e-6 * max(abs(x), 1) for s in SCALES) # fmt: skip
def num_fit(a: tuple[float, ...], b: tuple[float, ...]) -> tuple[int, int]:
"""(갈래 3 같음 · 2 단위 배 · 1 한쪽이 듦 · 0 다름, 맞은 수 개수)."""
if a == b:
return 3, len(a)
if a and len(a) == len(b) and len(a) <= 6:
if any(all(_near(x, y) for x, y in zip(a, p)) for p in permutations(b)):
return 2, len(a)
small, big = sorted((list(a), list(b)), key=len)
if small and len(small) < len(big):
rest = list(big)
for x in small:
if x not in rest:
return 0, 0
rest.remove(x)
return 1, len(small)
return 0, 0
def _fold(x: dict[str, Any]) -> tuple:
r = x["row"]
return r["base"], r["key"][0], r["key"][2]
def _scored(line: dict[str, Any]) -> tuple[dict[str, Any], list[tuple[tuple, dict[str, Any]]]]:
me = _item(line.get("이름"), line.get("규격"))
rows = book()["줄"]
out = []
for at, strong in _related(me).items():
x = rows[at]
clash, share, words = _qual_fit(me, x)
if not clash:
out.append(
((strong, share, *num_fit(me["bag"], x["bag"]), words, x["core"] == me["core"]), x)
)
return me, out
def _pick(group: list[dict[str, Any]], line: dict[str, Any]) -> dict[str, Any]:
"""한 품목의 변형 줄들 → 하나(줄 관급/사급 › 줄 지역 › 서울 › 같은 단위 › 엑셀 차례 — `M02_Elem_Alias.one` 과 같은 차례)."""
supply, regions = hmat._wants(line)
unit = hm.mm.plain_unit(line.get("단위"))
def rank(r: dict[str, Any]) -> tuple:
return (
supply is not None and r["supply"] not in (None, supply),
bool(regions) and not hmat._hit(r, regions),
"서울" not in r["region"],
r["key"][1] != unit,
r["at"],
)
return min((x["row"] for x in group), key=rank)["one"]
def same(line: dict[str, Any]) -> dict[str, Any] | None:
"""㉮ 표기만 다름 — 우리 품목 하나면 그 자재 줄(`hm.targets` 꼴) · 아니면 None."""
me, scored = _scored(line)
if not me["spec"]: # 가져온 규격 빔 = 규격에 든 이름(「안고초」)만 · 그 밖은 ㉯
hits = [book()["줄"][i] for i in book()["찾기"]["spec"].get(me["whole"], [])]
hits = [x for x in hits if not x["bag"] and not me["bag"]]
return _pick(hits, line) if hits and len({_fold(x) for x in hits}) == 1 else None
ok = []
for (strong, share, tier, common, words, same_core), x in scored:
if not tier or (tier == 1 and not strong):
continue
if not me["bag"] and not x["bag"] and not share and words:
continue # 수 없는 규격 = 규격 낱말이 겹쳐야
ok.append(((tier, common, share, strong, same_core), x))
if not ok:
return None
best = max(k for k, _ in ok)
group = [x for k, x in ok if k == best]
return _pick(group, line) if len({_fold(x) for x in group}) == 1 else None
def _stems(me: dict[str, Any]) -> set[str]:
return {_STEM.sub("", w) for w in me["quals"]}
def narrow(line: dict[str, Any], limit: int = 5) -> list[dict[str, Any]]:
"""㉯ 이름 같은 품목 묶음(굳센 같음) → 규격 낱말 어근 품은 줄 › 수 묶음 닮음 › 덧말 적음 차례(품목마다 하나)."""
me, scored = _scored(line)
stems = _stems(me)
rows = [(k, x) for k, x in scored if k[0]]
rank = []
for (_strong, _share, tier, common, _words, same_core), x in rows:
text = x["whole"] + x["spec"]
hit = sum(s in text for s in stems)
rank.append(((-hit, -tier, -common, not same_core, len(x["spec"]), x["row"]["at"]), x))
seen, out = set(), []
for _k, x in sorted(rank, key=lambda t: t[0]):
if (f := _fold(x)) not in seen:
seen.add(f)
out.append(_pick([y for _, y in rank if _fold(y) == f], line))
return out[:limit]
def spec_first(cands: list[dict[str, Any]], line: dict[str, Any]) -> list[dict[str, Any]]:
"""후보 차례 — 규격 수 묶음(둘 이상)이 줄과 같은 것 먼저(「중하중그레이팅 395×995×52」)."""
mine = _item(line.get("이름"), line.get("규격"))["bag"]
if len(mine) < 2:
return cands
return sorted(cands, key=lambda c: _item(c.get("이름"), c.get("규격"))["bag"] != mine)
+18 -1
View File
@@ -103,7 +103,7 @@ def _m01_picks(kind: str, name: str, spec: str, unit: str, _whole: int) -> list[
clean, apart = hmat.clean({"이름": name, "규격": spec, "단위": unit})
sure, _ = hmat.sure(clean)
got = [r["one"] for r in sure[:1] if not r["one"].get("전국")]
got = got or hmat.candidates(clean, TOP)
got = got or _found(clean)
more = {"별도계상": True} if apart else {}
return [
pick("재료", x.get("자재"), x["이름"], x.get("규격"), x.get("단위"), **more)
@@ -113,6 +113,23 @@ def _m01_picks(kind: str, name: str, spec: str, unit: str, _whole: int) -> list[
return []
def _found(line: dict[str, Any]) -> list[dict[str, Any]]:
"""817-1 재료 — ㉮ 표기만 다름 하나 › 옛 후보 하나 › ㉯ 이름 같은 묶음 좁힘 + 규격 수 같은 후보 먼저 + 옛 후보."""
from M02_MasterTemplete import M02_Hopyo_MatSame as ms
if one := ms.same(line):
return [one]
cands = hmat.candidates(line, 4 * TOP)
if len({x.get("자재") for x in cands[:TOP]}) == 1:
return cands[:TOP]
out, seen = [], set()
for x in [*ms.narrow(line, TOP), *ms.spec_first(cands, line)[:TOP], *cands[:TOP]]:
if x.get("자재") not in seen:
seen.add(x.get("자재"))
out.append(x)
return out
def _alias(kind: str, name: str, spec: str) -> list[dict[str, Any]]:
"""M01 별칭 칸 이음(`M02_Elem_Alias`) — 인력 = 이름 · 재료 = 이름 + 규격 › 표기 정규화 정확 일치 ·
같은 품목 지역 줄은 하나."""
+16 -2
View File
@@ -9,6 +9,7 @@ from __future__ import annotations
NOTE = "참고 · " # `M02_San_Emit.NOTE` 와 같음(사람 확인 표지 머리)
OIL_BASE = ("전국평균", "일반용(갑)Ⅰ 저압") # 유가 · 전력 기본(요구 748 · 우리 기초 기본값)
QUOTE = "견적 — 그대로(이름 · 값) · [단가 갱신] 대상 아님" # 815-1 K5 · 요구 762-3
APART = "별도계상 · 품목은 프로젝트에서" # 817-1 — 일위대가 길(`M02_CheckApart`)과 같은 글 · 요소 잇기 없음
def m01(kind: str, src: dict) -> tuple[str | None, str | None]:
@@ -24,6 +25,10 @@ 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 == "재료" and apart(
line
): # 817-1 — 원가 합 밖(원본 금액 그대로) · 품목은 프로젝트에서 이음
return None, f"{NOTE}{APART}"
if (
kind == "기계"
): # 812-9 — 하나 = 이음 · 아니면 표지(후보 여럿 · 못 찾음 · 조용히 넘기지 않음)
@@ -68,6 +73,13 @@ def m01(kind: str, src: dict) -> tuple[str | None, str | None]:
return None, None
def apart(line: dict) -> bool:
"""줄 이름 · 규격에 「별도계상」(`M02_Hopyo_Material.clean`)."""
from M02_MasterTemplete import M02_Hopyo_Material as hmat
return hmat.clean(line)[1]
def quote(src: dict) -> tuple[str | None, str | None]:
"""견적 · 운임 · 수수료 꼴 줄(갈래 밖 — EST 견적 · STmate 고정 코드 · 815-1 K5) → 우리 자재에 이어지면
(코드 · 별칭 · 정확 일치 · 단가 줄 · 우리 값 있을 때) 그 키 + 갈래 재료(우리 값 · 엑셀 값은 검산) ·
@@ -143,11 +155,13 @@ def why(item: dict) -> str:
from M02_MasterTemplete import M02_Hopyo_Elem as elem
from M02_MasterTemplete import M02_Hopyo_Row as hrow
if apart({"이름": name, "규격": res.get("규격") or ""}):
return APART
if elem.oil_rows(name):
return "유가 후보 여럿 — 고름"
line = {"이름": name, "규격": res.get("규격") or "", "단위": res.get("단위") or ""}
if hrow.m01_picks("재료", line):
return "자재 후보 여럿 — 고름"
if picks := hrow.m01_picks("재료", line): # 817-1 ㉯ 1 순위를 글에(고르기 창은 이름 찾기)
return f"자재 후보 여럿 — 고름 · 1 순위 {picks[0]['이름']} {picks[0]['규격']}".strip()
return "운임표 없음" if any(w in name for w in FREIGHT) else "자재 없음(우리 자재표)"
if kind == "인력":
return "노임 없음"
Binary file not shown.
@@ -0,0 +1,100 @@
# 817-1 불러오기 재료 찾기 — 전후 전수 보고(요구 773-5 · 774 · 방향 장부 O 장)
- 대상 = 815 와 같은 자 — 원파일 EST 6 · STmate 7(2,220 블록) + 실무 엑셀 11(1,021 블록) · 재는 법 = 815-1 과 같음(블록마다 금액 · 요소 이음 · [우리 단가로])
- 전 = 817-1 앞 판 · 후 = 커밋 bb243af
- 자세한 표 = `_목록_817-1_재료찾기.xlsx`(과녁 100 줄 · 새로 이어진 줄 · 남은 못 이음 · 전후)
- 별칭 값 · M01 마스터 자료 · storage 층 사본 안 건드림 · 인력 · 기계 · 유가 길 그대로
## 요약
| 잣대 | 전 | 후 |
|---|---|---|
| 재료 이음(전체) | 88.0 %(1,792 / 2,036) | **93.5 %(1,904 / 2,036)** |
| 못 이은 재료 | 244 줄 · 100 종 | **132 줄 · 49 종** |
| 새로 이어진 줄 | — | 112 줄 · 51 종(목록 시트 「새로 이어진 줄」) |
| 끊긴 이음 | — | 0 |
| 이은 키가 바뀐 줄 | — | 28 줄 — 치즐 0.7㎥ 이 엉뚱한 「치즐(0.2㎥)」 에서 「치즐(0.7㎥)」 로(바로잡음) |
| 금액(엑셀 단가 그대로 셈) | 3,241 블록 100 % | 같음 100 %(셈 바뀐 블록 0) |
| 검산 틀린 줄 | 929 | 929(그대로) |
### 원천 셋
| 원천 | 전후 | 인력 | 기계 | 재료 | 유가 | 형제 | 모두 우리 값 | 우리 값 + 견적 | 이어짐 · 우리 값 빔 | 엑셀 단가 남음 | 0 원 줄 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| EST 원파일(1,849) | 전 | 100 | 97.2 | 99.4 | 100 | 100 | 1,622 | 6 | 100 | 75 | 46 |
| | 후 | 100 | 97.2 | **99.7** | 100 | 100 | 1,622 | 6 | 100 | 75 | 46 |
| STmate 원파일(371) | 전 | 100 | 95.2 | 40.2 | — | 100 | 278 | 13 | 1 | 79 | 0 |
| | 후 | 100 | 95.2 | **59.8** | — | 100 | **285** | 13 | 11 | **62** | 0 |
| 실무 엑셀(1,021) | 전 | 100 | 93.2 | 54.3 | 100 | 100 | 823 | 12 | 9 | 177 | 0 |
| | 후 | 100 | 93.2 | **78.2** | 100 | 100 | **854** | 12 | 27 | **128** | 0 |
- 「이어짐 · 우리 값 빔」 = 우리 자재 줄에 이었는데 그 줄 값 칸이 빈 블록(요구 776 — 흠 아님 · 자재표 보강 몫) · 늘어난 28 블록 = 모두 치즐 0.7㎥(값 칸 빔)
- 블록 옮김: 엑셀 단가 남음 → 모두 우리 값 43(STmate 7 · 엑셀 36) · 엑셀 단가 남음 → 이어짐 · 우리 값 빔 23 · 모두 우리 값 → 이어짐 · 우리 값 빔 5(치즐 0.2 → 0.7 바로잡음)
## 엔진 — 새 단 셋(`M02_Hopyo_MatSame` · 재료 길 = 별칭 › 정확 일치 › 바탕 이름 유일 짝 › ㉮ › ㉯)
- ㉮ 이음(자동) = 이름이 같은 품목 + 규격 수 묶음 같음 + 규격 낱말 안 부딪힘 + 그런 우리 품목 하나(지역 · 관급/사급 줄은 한 품목)
- 이름 같음: 빈칸 · 괄호 · 지역 · 관급/사급 꼬리 · 등급(1급) 뗌 · 점 · 빈칸 영문(P.V.C) · 소리 표기 넷(칠렌 → 틸렌 · 매쉬 → 메쉬 · 신나 → 신너 · pipe → 파이프) · 괄호 차례(유로폼(패널) = 유로폼패널) · 괄호 이름(서비스볼트(고장력볼트) → 고장력볼트 — 가져온 바탕 이름이 우리 표에 없을 때만) · 규격에 든 이름(안고초 → 산림종자 안고초)
- 약한 같음(수 묶음 같음 · 단위 배일 때만): 우리 이름 끝 = 가져온 이름(지수판 → PVC지수판 · 고무테이프 → 절연 면고무테이프 · 철물 직교자재 → 조임철물) · 우리 이름 = 가져온 이름 앞(구역화물자동차 → 구역화물 · 뒤 덧말이 우리 낱말이 아닐 때만)
- 수 묶음: 차례 무관 · KS 번호 · 제곱 꼬리 뗌 · 천 단위 쉼표 · 같음 › 낱마다 단위 배(3.6×3.0×3.0 = 30×30×3600) › 한쪽이 다른 쪽에 듦(측구수로관 300×300×2000×120 → 300×300×120 · 이름이 굳세게 같을 때만)
- 낱말 부딪힘 = 두 쪽 다 괄호 · 규격 한글 낱말이 있고 두 글자 묶음이 안 겹침(연강용 ↔ 주철용 · 표준 ↔ 단부구간 · 스티로폼 ↔ ABS코팅폼)
- ㉯ 후보 좁힘 = 이름 같은 묶음에서 규격 어근(초본류 → 초본) 품은 줄 › 수 묶음 닮음 › 바탕 같음 › 덧말 적은 줄 차례 + 옛 후보 가운데 규격 수 묶음(둘 이상) 같은 줄 먼저 · 자동 이음 아님
- 화면: 못 이은 재료 까닭 글에 1 순위(「자재 후보 여럿 — 고름 · 1 순위 종 자 초본위주형」) — [이음 고르기] 창은 이름 찾기 그대로
- 사전 = 소리 · 영문 넷 · 덧글 낱말 다섯(이내 · 이하 · 이상 · 국산 · 포함) 한 곳 · 자재마다 박은 예외 없음
## 과녁 100 줄(브레인 과녁을 우리 자재표로 다시 확인)
| 과녁 | 줄 | 같음 | 다름 |
|---|---|---|---|
| 이음 | 65 | 49 | 16 — 1 순위는 맞음 10 · 그 밖 6 |
| 후보 1 순위(종자) | 5 | 5 | 0 |
| 후보(사람이 고름) | 26 | 25 | 1 |
| 잇지 않음 | 4 | 4 | 0 |
과녁과 다른 줄(맞추지 않음 · 까닭):
| 행 | 가져온 줄 | 과녁 | 결과 | 까닭 |
|---|---|---|---|---|
| 3 · 7 | 각재 외송 · 각재 (외송) | 이음 | ㉯ 1 순위 각재(외송) | 규격 수 없음 · 빔 → 수 묶음 다름 |
| 4 | 요소 | 이음 | ㉯ 1 순위 요소 N:46% | 가져온 규격 빔 |
| 31 | 유압식드릴스리브 T38×190mm | 이음 | 후보 1 순위 유압드릴스리브 | 「유압식」 ↔ 「유압」 이름 다름 |
| 40 | 안전표지판 삼각,D=900 | 이음 | ㉯ 1 순위 교통안전표지판 900,삼각 | 같은 수 · 낱말 줄 둘(LED 교통안전표지판(태양광) 900,삼각) |
| 47 | 강관써포트 V4 2600-4000mm | 이음 | 후보 1 순위 강관동바리 | 이름 다름 |
| 55 | 아세틸렌 용접용 | 이음 | ㉯ 1 순위 아세틸렌 98%(용접용) | 규격 수 없음 ↔ 98% |
| 90 · 92 | 중하중그레이팅 395×995×52 · 595×995×67 | 이음 | 후보 1 순위 스틸그레이팅(중하중용) 같은 치수 | 이름 다름 — 같은 수 묶음 후보 먼저(전에는 다른 치수 OK그레이팅이 1 순위) |
| 98 | 공사안전PE드럼 500×800 | 이음 | 후보 1 순위 PE드럼(일체형) Φ500×800 | 가져온 이름 앞 덧말은 안 뗌(PVC용접봉 ↔ 용접봉 틀린 이음 막음) |
| 67 | 와이어매쉬 #6 100×100 | 이음 MT007130(관급) | 이음 MT007136(서울) | 같은 품목 변형 — 별칭 · 정확 일치 길과 같은 차례(서울 먼저) |
| 54 | 다이아몬드톱날 Ø14"×3.2t | 이음 블레이드 | 후보(1 순위 다름) | 이름 다름 — 별칭 몫 |
| 79 · 81 | 참싸리(박피) · 참싸리 박피 | 이음 산림종자 참싸리 | 못 찾음 | 박피 ↔ 풍화암,강마사토 덧말 부딪힘 — 별칭 몫 |
| 83 | 퇴비(천연부엽토) 20㎏/포 | 이음 천연부엽토 | 후보(1 순위 퇴비 1등급) | 「퇴비」가 우리 표에 따로 있어 괄호 이름 길 안 씀(보조강재(열연강판) → 열연강판 같은 이음 막음) |
| 95 | 삼각표지판 삼각,900mm | 이음 | 후보(1 순위 다름) | 이름 다름 — 별칭 몫 |
| 93 | 잔디 0.18×0.18×0.01 | 후보 | **이음 천연잔디(한국형) 0.18*0.18*0.01** | 우리 표에 수 묶음 같은 줄 하나 — 과녁은 잔디 0.03 줄만 봄 · 브레인 확인 필요 |
- 「가까운 줄 제안」 바로잡음: 신나 → 신너(조합페인트용) KSM-6060(2종) ㉮(전 = 신나무 1 순위) · 중하중그레이팅 → 같은 치수 1 순위
- 틀린 이음 막는 견본(시험): 용접봉(연강용) ↔ 주철용 · PVC용접봉 ↔ 용접봉 · 어닐링철선 #6 ↔ #8 · 잔디 0.01 ↔ 0.03 · 표지판지주 3.5M ↔ 3000 · 강관비계 6m ↔ 4.0m · 신나 ↔ 신나무 — 모두 안 이음
## 새로 이어진 줄(51 종 · 112 줄) — 틀린 이음 0
- 전부 목록 시트(가져온 이름 · 규격 → 우리 키 · 이름 · 규격 · 맞은 까닭) · 한 줄씩 읽음 — 과녁과 다른 이음은 93 잔디(위 표) · 67 와이어매쉬 변형뿐
- 과녁 밖에서 새로 이어진 것 = 0(새 이음 51 종 모두 과녁 100 줄 안)
## 별도계상
- 단가산출 불러오기 길(엑셀 산근 · 원파일): 전에는 표지 없이 「자재 후보 여럿」 못 이음 → 이제 요소 잇기 없음 + 표지 「참고 · 별도계상 · 품목은 프로젝트에서」(일위대가 호표 길 `M02_CheckApart` 와 같은 글) · 못 이음 띠 「파형강관(별도계상) (별도계상 · 품목은 프로젝트에서) · 이음 고르기」
- 원가 합 밖 = 그대로(줄 값 0 · 원본 금액 그대로) · 0 원 줄 수에 안 듦(엑셀 값 0 줄) · 대상 = 동부청영월 파형강관 · 이형철근 8 줄(엑셀 · STmate)
- 일위대가 호표 길 · 프로젝트로 복사(`M02_Doc_Copy` 합밖) 은 전부터 되어 있음
## 화면(8001 · 5174 · 일반 계정 개인 층 · 「sub1임시_동부청영월.xlsx」 · 「sub1임시_울진소광.xlsx」)
- 단산58 유로폼 설치 · 해체 — 「유로폼(패널) 600*1200」 줄 → MT000467 유로폼패널(저장된 SK 이음 키로 확인) · 이음 없는 자원 0
- 단산71 파형강관부설 — 「파형강관(별도계상) (별도계상 · 품목은 프로젝트에서)」 · 금액 15,898 그대로
- 단산1492 씨뿌리기(울진소광) — 「종자(초본류) … 1 순위 종 자 초본위주형」 · 「종자(목본류) … 1 순위 종 자 목본군락형」(ORCA 탭 사진 `tmp/q17/ui/orca_1492.png`)
- 밝음 · 어두움 · 1366×768 가로 넘침 0 · 콘솔 오류 0(로그인 전 세션 401 하나 뺌) · 만든 SK 셋 · 올린 HP 둘 지움
## 남은 것
| # | 내용 | 갈래 |
|---|---|---|
| 1 | 일위대가 호표 화면(일위대가표 시트) 줄 판정은 다른 길(`M02_Hopyo_Units`) — 안고초 · 참싸리 등 아직 분홍(못 이음) | 이번 고침 밖 |
| 2 | 남은 못 이은 재료 132 줄 · 49 종 = 이름 다름 · 규격 다름 · 우리 표에 없음 → 별칭 몫(목록 시트) | 사용자 별칭 |
| 3 | 93 잔디 0.01 → 천연잔디(한국형) 0.01 이음 맞는지 | 브레인 확인 |
@@ -0,0 +1,192 @@
"""817-1 불러오기 재료 찾기 — 표기만 다름 ㉮ 이음 · ㉯ 후보 좁힘 · 틀린 이음 막기 · 별도계상 표지(요구 773-5 · 774).
과녁 = 사용자 시트 「수동연결(자재)」 100 줄(브레인 과녁 `_과녁_773_자재표기.tsv` 를 우리 자재표로 다시 확인한 결과).
「후보」 줄 키 = 1 순위를 굳힌 줄만(종자 · 그레이팅 · 표기만 다름인데 수 묶음 · 이름이 안 맞아 ㉯ 로 둔 줄) · None = 이음만 안 됨.
"""
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path[:0] = [str(ROOT), str(ROOT / "resources" / "master_data" / "scripts")]
from M02_MasterTemplete import M02_Hopyo_MatSame as ms # noqa: E402
from M02_MasterTemplete import M02_Hopyo_Row as hrow # noqa: E402
from M02_MasterTemplete import M02_San_Link as sl # noqa: E402
TABLE = [
("접착제", "", "후보", None), # 1
("패널(유로폼)", "600*1200mm", "이음", "MT000467"), # 2
("각재", "외송", "후보", "MT000393"), # 3
("요소", "", "후보", "MT006763"), # 4
("내부코너패널(유로폼)", "(100x200)x1200mm", "이음", "MT000470"), # 5
("결속선(철선)", "#20 , 0.9M/M", "후보", None), # 6
("각재 (외송)", "", "후보", "MT000393"), # 7
("파형강관", "별도계상", "후보", None), # 8
("금강사", "", "없음", None), # 9
("고무테이프", "0.5mm×19mm×10m", "이음", "MT009202"), # 10
("낙석방지책", "H2500×W2000(표준)", "후보", None), # 11
("낙석방지책", "H2500×W2000(단부)", "후보", None), # 12
("종자", "초본류", "후보", "MT006646"), # 13
("종자", "목본류", "후보", "MT006648"), # 14
("어닐링철선(결속선)", "#20 0.9mm 200.4m/kg", "이음", "MT000209"), # 15
("내수합판1급", "12mmx1220x2440", "이음", "MT000018"), # 16
("어닐링철선(결속선)", "#20 0.9M/M", "이음", "MT000209"), # 17
("유로폼(패널)", "600*1200", "이음", "MT000467"), # 18
("내부코너패널", "(200+200)*1200", "이음", "MT000472"), # 19
("합판(내수)", "12×1220×2440", "이음", "MT000018"), # 20
("조임철물", "(직교자재)", "이음", "MT000499"), # 21
("에폭시수지", "YD-128", "후보", None), # 22
("종자", "생태복원형,초본류", "후보", "MT006646"), # 23
("종자", "생태복원형,목본류", "후보", "MT006648"), # 24
("내부코너패널(유로폼)", "(100+200)*1200mm", "이음", "MT000470"), # 25
("유로폼(패널)", "600×1200", "이음", "MT000467"), # 26
("콘테이너하우스", "창고용(3.0m×6.0m×2.6m)", "이음", "MT007111"), # 27
("전기뇌관(MS)", "3.5m×500개(하이데토)", "이음", "MT005677"), # 28
("BUTTON BIT", "Ø38×76㎜", "후보", None), # 29
("유압드릴샹크", "T38×CM470", "후보", None), # 30
("유압식드릴스리브", "T38×190mm", "후보", "MT005687"), # 31
("미송(판재)", "", "후보", None), # 32
("산림종자", "참싸리(박피)", "이음", "MT006664"), # 33
("유기질퇴비", "", "후보", None), # 34
("PVC지수판", "200x5T KSM3805", "이음", "MT006544"), # 35
("PVC용접봉", "Ø3.2 (국산)", "후보", None), # 36
("PVC파이프(일반용VG2)", "50㎜ × 4m", "이음", "MT002926"), # 37
("혼합골재", "영월", "후보", None), # 38
("이형철근", "별도계상", "후보", None), # 39
("안전표지판", "삼각,D=900", "후보", "MT003202"), # 40
("표지판지주", "D=76.3mm, 3.5M", "후보", None), # 41
("블레이드(커터)", 'd=320 400mm 14"xt3.2', "이음", "MT005749"), # 42
("떼", "", "없음", None), # 43
("강관비계", "φ48.6m/m×2.3mm", "이음", "MT000481"), # 44
("철물", "", "후보", None), # 45
("각재(말뚝)", "Φ60", "후보", None), # 46
("강관써포트", "V4 2600-4000mm 14.3kg", "후보", None), # 47
("안고초", "", "이음", "MT006667"), # 48
("비수리", "", "이음", "MT006665"), # 49
("내수합판1급", "12m/m 1220×2440", "이음", "MT000018"), # 50
("각재(외송)", "3.6×3.0×3.0", "이음", "MT000393"), # 51
("내수합판1급", "12mm 1220×2440", "이음", "MT000018"), # 52
("각재(거푸집용,외송)", "3.6×3.0×3.0", "이음", "MT000393"), # 53
("다이아몬드톱날", 'Ø14"×3.2t(콘크리트용)', "후보", None), # 54
("아세틸렌", "용접용", "후보", "MT003163"), # 55
("산소", "고압가스", "이음", "MT003166"), # 56
("신나", "KSM6060 2종(조합페인트류)", "이음", "MT003398"), # 57
("초류종자(국내산)", "참싸리,비수리,낭아초등", "후보", None), # 58
("종 자", "초본형", "후보", "MT006646"), # 59
("소나무우드칩(멀칭재)", "", "후보", None), # 60
("3단코어비트", '5"(125mm)', "후보", None), # 61
("P V C PIPE", "Φ50M/M,VG1", "이음", "MT002861"), # 62
("콘테이너(창 고)", "3×6×2.6", "이음", "MT007111"), # 63
("강관비계", "48.6mm×2.3mm×6m", "후보", None), # 64
("철물", "직교자재", "이음", "MT000499"), # 65
("문양거푸집(ABS코팅품)", "600×1200", "이음", "MT000453"), # 66
("와이어매쉬", "#6 100×100", "이음", "MT007136"), # 67
("지수판", "T5×200", "이음", "MT006544"), # 68
("용접봉(연강용)", "3.2m/m", "후보", None), # 69
("어닐링철선 (결속선)", "#6 4.0mm 10.1m/kg", "후보", None), # 70
("구역화물자동차(4.5Ton)", "80km이내", "이음", "MT023493"), # 71
("구역화물자동차(4.5Ton)", "60km이내", "이음", "MT023491"), # 72
("산소(6000ℓ)", "99%,120kg/㎠", "이음", "MT003165"), # 73
("아세칠렌", "AC 98%,용접용", "이음", "MT003163"), # 74
("서비스볼트(고장력볼트)", "M20×60", "이음", "MT002536"), # 75
("보조강재(열연강판)", "T=9-12", "후보", None), # 76
("구역화물자동차(8Ton)", "10km이내", "이음", "MT023654"), # 77
("구역화물자동차(8Ton)", "20km이내", "이음", "MT023655"), # 78
("참싸리(박피)", "", "없음", None), # 79
("부산물비료", "20kg/포", "후보", None), # 80
("참싸리", "박피", "없음", None), # 81
("복합비료", "21-17-17 20kg/포", "이음", "MT006490"), # 82
("퇴비(천연부엽토)", "20㎏/포", "후보", None), # 83
("문양거푸집(ABS코팅폼)", "600×1200×20~30mm", "이음", "MT000453"), # 84
("P.V.C PIPE", "VG1(50mm)", "이음", "MT002861"), # 85
("다웰바(DOWEL BAR)", "Ø32×1,000", "이음", "MT006540"), # 86
("P.V.C PIPE", "VG1(35mm)", "이음", "MT002859"), # 87
("휠라", "T=15mm,1000×1000mm", "없음", None), # 88
("측구수로관", "300×300×2000×120,앵글포함", "이음", "MT015883"), # 89
("중하중그레이팅", "395×995×52", "후보", "MT007025"), # 90
("측구수로관", "500×500×2000×150,앵글포함", "이음", "MT015887"), # 91
("중하중그레이팅", "595×995×67", "후보", "MT007032"), # 92
("잔디", "0.18×0.18×0.01", "이음", "MT006796"), # 93
("강관비계", "4000×48.6mm×2.3mm", "이음", "MT000481"), # 94
("삼각표지판", "삼각,900mm", "후보", None), # 95
("표지판지주", "D=76.3mm×3.0m", "이음", "MT003241"), # 96
("공사안내표지판", "300×1500", "후보", None), # 97
("공사안전PE드럼", "500×800", "후보", "MT003311"), # 98
("윙카호스", "10cm", "후보", None), # 99
("조합페인트", "KSM6020 2급,백색", "이음", "MT003372"), # 100
]
def _link(name, spec):
key, mark = sl.m01("재료", {"이름": name, "규격": spec, "단위": "", "갈래": "재료"})
return key, mark, hrow.m01_picks("재료", {"이름": name, "규격": spec, "단위": ""})
@pytest.mark.parametrize("name,spec,want,key", TABLE)
def test_target_rows(name, spec, want, key):
got, _mark, picks = _link(name, spec)
if want == "이음":
assert got == key
else:
assert got is None
assert bool(picks) == (want == "후보")
if key:
assert picks[0]["키"] == key
def test_counts():
linked = sum(1 for *_x, w, _k in TABLE if w == "이음")
assert linked == 51
@pytest.mark.parametrize(
"name,spec,bad",
[
("용접봉(연강용)", "3.2m/m", "MT003195"), # 연강용 ↔ 주철용
("PVC용접봉", "Ø3.2 (국산)", "MT003195"), # PVC 용접봉 ↔ 용접봉
("어닐링철선 (결속선)", "#6 4.0mm 10.1m/kg", "MT000199"), # #6 ↔ #8
("잔디", "0.18×0.18×0.01", "MT019017"), # 0.01 ↔ 0.03
("표지판지주", "D=76.3mm, 3.5M", "MT003241"), # 3.5M ↔ 3000
("신나", "KSM6060 2종(조합페인트류)", "MT018835"), # 신나 ↔ 신나무
("강관비계", "48.6mm×2.3mm×6m", "MT000481"), # 6m ↔ 4.0m
],
)
def test_no_wrong_link(name, spec, bad):
got, _mark, picks = _link(name, spec)
assert got != bad
if name == "신나":
assert got == "MT003398" # 신너(조합페인트용)
def test_seed_narrow():
assert [x["자재"] for x in ms.narrow({"이름": "종자", "규격": "초본류"})][:3] == [
"MT006646",
"MT006647",
"MT006649",
]
def test_spec_forms():
assert ms.num_fit(ms._bag("3.6×3.0×3.0"), ms._bag("30×30×3600mm"))[0] == 2
assert ms.num_fit(ms._bag("T5×200"), ms._bag("200×5T"))[0] == 3
assert ms.num_fit(ms._bag("Ø32×1,000"), ms._bag("Φ32*1000"))[0] == 3
assert ms._item("PVC지수판", "200x5T KSM3805")["bag"] == ms._item("PVC지수판", "200×5T")["bag"]
assert ms._item("유로폼(패널)", "")["chars"] == ms._item("유로폼패널", "")["chars"]
assert ms._item("P.V.C PIPE", "")["core"] == ms._item("PVC파이프(일반관),서울", "")["core"]
assert ms._item("아세칠렌", "")["core"] == "아세틸렌"
def test_apart_mark():
key, mark, _ = _link("파형강관", "별도계상")
assert key is None and mark == f"{sl.NOTE}{sl.APART}"
assert (
sl.why({"자원": {"갈래": "재료", "이름": "철근(별도계상)", "규격": "별도계상"}}) == sl.APART
)
def test_why_shows_first():
got = sl.why({"자원": {"갈래": "재료", "이름": "종자", "규격": "초본류"}})
assert got == "자재 후보 여럿 — 고름 · 1 순위 종 자 초본위주형"