From bb243af270304c933d1cb1220640adf169770a59 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 11 Nov 2026 20:24:56 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(817-1):=20=EB=B6=88=EB=9F=AC=EC=98=A4?= =?UTF-8?q?=EA=B8=B0=20=EC=9E=AC=EB=A3=8C=20=EC=B0=BE=EA=B8=B0=20=E2=80=94?= =?UTF-8?q?=20=ED=91=9C=EA=B8=B0=EB=A7=8C=20=EB=8B=A4=EB=A6=84=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=20=EC=9D=B4=EC=9D=8C=20=C2=B7=20=EA=B0=99=EC=9D=80=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=ED=9B=84=EB=B3=B4=20=EC=A2=81=ED=9E=98=20?= =?UTF-8?q?=C2=B7=20=EB=B3=84=EB=8F=84=EA=B3=84=EC=83=81=20=ED=91=9C?= =?UTF-8?q?=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 M02_Hopyo_MatSame — ㉮ 이름 같은 품목(빈칸 · 괄호 차례 · 등급 · 점 · 소리 표기 넷) + 규격 수 묶음 같음(차례 · 단위 배 · 한쪽 듦) + 규격 낱말 안 부딪힘 + 우리 품목 하나 → 이음 · ㉯ 이름 같은 묶음 = 규격 어근 품은 줄 · 덧말 적은 줄 1 순위 · 규격 수 같은 후보 먼저 - M02_Hopyo_Row 재료 길 = 바탕 이름 유일 짝 뒤 ㉮ › 옛 후보 하나 › ㉯ + 옛 후보 - M02_San_Link — 별도계상 줄 = 요소 잇기 없음 + 표지 「별도계상 · 품목은 프로젝트에서」 · 못 이은 재료 까닭에 1 순위 이름 · 규격 - 전수(원파일 2,220 · 엑셀 1,021) 재료 이음 88.0 → 93.5 % · 새 이음 112 줄 51 종 · 끊긴 이음 0 · 금액 그대로 · 치즐 0.7 이 엉뚱한 0.2 줄에서 0.7 줄로 옮김 - 시험 test_817_1_material_find(과녁 100 줄 · 틀린 이음 막기 · 별도계상 표지) Co-Authored-By: Claude Opus 5.5 --- M02_MasterTemplete/M02_Hopyo_MatSame.py | 256 +++++++++++++++++++ M02_MasterTemplete/M02_Hopyo_Row.py | 19 +- M02_MasterTemplete/M02_San_Link.py | 18 +- resources/tester/test_817_1_material_find.py | 192 ++++++++++++++ 4 files changed, 482 insertions(+), 3 deletions(-) create mode 100644 M02_MasterTemplete/M02_Hopyo_MatSame.py create mode 100644 resources/tester/test_817_1_material_find.py diff --git a/M02_MasterTemplete/M02_Hopyo_MatSame.py b/M02_MasterTemplete/M02_Hopyo_MatSame.py new file mode 100644 index 000000000..32fb4fac3 --- /dev/null +++ b/M02_MasterTemplete/M02_Hopyo_MatSame.py @@ -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) diff --git a/M02_MasterTemplete/M02_Hopyo_Row.py b/M02_MasterTemplete/M02_Hopyo_Row.py index f139d8e1f..9f4f24a20 100644 --- a/M02_MasterTemplete/M02_Hopyo_Row.py +++ b/M02_MasterTemplete/M02_Hopyo_Row.py @@ -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`) — 인력 = 이름 · 재료 = 이름 + 규격 › 표기 정규화 정확 일치 · 같은 품목 지역 줄은 하나.""" diff --git a/M02_MasterTemplete/M02_San_Link.py b/M02_MasterTemplete/M02_San_Link.py index 43731d4d8..986f4e0b2 100644 --- a/M02_MasterTemplete/M02_San_Link.py +++ b/M02_MasterTemplete/M02_San_Link.py @@ -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 "노임 없음" diff --git a/resources/tester/test_817_1_material_find.py b/resources/tester/test_817_1_material_find.py new file mode 100644 index 000000000..768b8bc2d --- /dev/null +++ b/resources/tester/test_817_1_material_find.py @@ -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 순위 종 자 초본위주형" From 44593e82fc41105893818df748380bdd9ddb5965 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 11 Nov 2026 20:46:17 +0900 Subject: [PATCH 2/2] =?UTF-8?q?knowledge(817-1):=20=EB=B6=88=EB=9F=AC?= =?UTF-8?q?=EC=98=A4=EA=B8=B0=20=EC=9E=AC=EB=A3=8C=20=EC=B0=BE=EA=B8=B0=20?= =?UTF-8?q?=EC=A0=84=ED=9B=84=20=EC=A0=84=EC=88=98=20=EB=B3=B4=EA=B3=A0=20?= =?UTF-8?q?=E2=80=94=20=EC=9E=AC=EB=A3=8C=2088.0=20=E2=86=92=2093.5=20%=20?= =?UTF-8?q?=C2=B7=20=EC=83=88=20=EC=9D=B4=EC=9D=8C=20112=20=EC=A4=84=20?= =?UTF-8?q?=C2=B7=20=EB=81=8A=EA=B9=80=200=20=C2=B7=20=EA=B3=BC=EB=85=81?= =?UTF-8?q?=20100=20=EC=A4=84=20=EB=8C=80=EC=A1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/raw/요구_1104/_목록_817-1_재료찾기.xlsx | Bin 0 -> 24185 bytes docs/raw/요구_1104/_보고_817-1_재료찾기.md | 100 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 docs/raw/요구_1104/_목록_817-1_재료찾기.xlsx create mode 100644 docs/raw/요구_1104/_보고_817-1_재료찾기.md diff --git a/docs/raw/요구_1104/_목록_817-1_재료찾기.xlsx b/docs/raw/요구_1104/_목록_817-1_재료찾기.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..930b466f3d42b111407dd6df2411565eee92a5ac GIT binary patch literal 24185 zcmZ^JQ;;MJv~5q@wr$(CZQC}dZQI7QZQDI<+qU)koO3_keG%^?vmz@pqIRv^SZn2$ zmjVJt1^@s62Pi4F(h=3{gAVw)s{1+Me-2|iBY8(VdnY;rdwW`U8*7;u8K{1Gc;Ro! z(@k5I8+-=*g~FXNFL5^3IAbB#;16%5zSwo{E&8Z-lyUVzA~%+h!BSs@F)@HfKD(b^5}e!iZ%Xm*?H5FRqui(=9^ z3ybiw?nm}JoDgfH99lfN{hA__gLj5*H9bDC@%XOwFqY9-tO&UGX3dJs)Kb{=iK@o? zM9Y6H*=+u1x_cXUM`cFz{nWbuf51uC&LV;b1pvq*0{}q!0mt2%&e`0=#^k>}g`Y@)xlE90 z&*TQCK7F4S5j#F%_qe~JH9QFkOS*H{U!_}L>S&?c+EHU9Mi8LKD6P5ag)ElVZPK&= z7-07qLqZ(b2#5riaz~z6(%#X+q$657Yh2${4YIJ#M;9~J_tB*`=l`p$plPRv-*||} z^nx;>9Boqg3rS6Yv@NXh?$vteV{MDqWag9;htc1b9!nkHZ`!W(KvpwsSGj+%*V2qn z)oNsRPW*Skm`@T&yTnuBJPJ4?3>Rm%`n0R})8J)p)cBe+vkm6X{;fNE^Tb@FJvQ); zLPt)2_Jeg#kpM(vp~YY0m8Z4p_>%HyCAs-$pY8KZ^DM|{ExCCsx6S#O@ZqYz?l9fA za-jp&;lmYwozE>u$v&&CicV3@1x5GIJrPlw4*BQt0o3hXmnP?cQgkWaT#_(XaXK~y zv~Sd}IKmOWhKGQcEaos$m_;44<3L~m*al~%<-KbJnj?1l6XvwzmS!lL&CwEGmALUUp>8Bz`!Z9ORZJF3Si`+jmk!+`bcO8HAGA zav?c#Vw(wi&pbWF7n3l~T)#q|%W|9HsixHG%*-6*L^%?5Y(A;`JjsdK5-tDfZ6I7u z5+xSN9i?eMKwxi@fMG=91J4dgvy{gMEP`N2$q#vH9IiyzLGo$o1|Qvy4tuV!%ivMV zSC?1|dVv?k+{5Hm(?JH}Z|0ao!_)RiC)}n!^)R}bZgbA}xvf1hH&S zl^!LNmk(>&$XmI!4o++Goq4HeUb*#-8>gU{gjyV>&YF|Dn@HIuAL=IUWpO8;_&>huksuI97q%eA3ix zj#p-9-;etgY74KqQ*cJY?g|L5@AX(vZw5 z#>}z9%p7%dlI_7rMmU$ z-+kJTiNPIm9qT>l2vz$&ZSKF>CUS{OXL$@E{BjlnBud5xMMYygkiSZZ14a5Zl{6-N z0~lajg8e(~4XW!7x{U&5x{8;>PlP%!L{@i>1Q9u+78-;A&2j(wI?>WCYWa=Y-zG?z z(RdMUTqwMchsjZBMVaKjdW{q+czsWKsspo|R9|Yq(>9xi?0a2zfu|9TNA$iheFwK8 z)G5=mwO^$2TYU-lm2sQHew}j*NcRYdTByjk&nF_{H$sJBcYBT8D@hhtqt~rJe&)y0 zdePJ74z%||g=Voa6#}0Wm^WSms8N*G1G8Q%vSC$8UDfa!1GL9)^?T@iQ}*ppkqk_x z#mGvIXh=JOMgBv+^#vI7Cm3ezco1EN^k&w=16fG!jjwB%+Wx_q-OI?HU;lf$dd70? zuaN)%T%-R2K>u%mxY;>cIhmW7I6Kk(`|B_cqLK#UO?iok^qpZ_e?|l3%4XJ?Q z9Dc!-u#D0mdnsi&!D9S347bY zS9*Kir% zD&8Q%zk7K6Of0RB_Uvdo`QB$wAg+U+z81ia{oEX$mYH(98Jhf5T@ek>KQ3P9OKE4k z%XLypz1|kQ?$~Z;_vg{RKl({L>bBCmsK1^9*tafDzjAxnSs1DM@<%=3p@;5Sc2pZ^f$(s0~8>At+G2tHR(a#s0W z{>sOxDe3CKq_Kg&W0O4Cb`gOJx|&Lma<(IDX9E+oz1P%IJa|6U8s*nmZN+iH6T>=R z&`Cra%k;8mGlos(TGDW?{93->eS$S{b zN>?F4L6Ud2qq}e|s1a8pRH*Sv?S`~d2p?^o;&#qVum2tIlo^g)`$3-7vlzC-CM^4= zca>eF9Xcr6@R~u3+c7_{{GA0WW_N@go)Pse?Yjr4Jwd|VX0L5YZFodbdm+_ zinQhpc&_siYT}i{#MGVQECYijDVN(~H!>Nr*%{X6wCKX{*!nc@q~ga zn&=BwVC?5N2E|tR8ZG*;8S(;WVNCp$KD!XocvQ>B z!6=cwVj>;2Hi*>RNtj-n-yGx99|Al0JTH#orR+vCUMNlkJFDl;I;yfy??Dz?@cMXC zp)+11eYdwUyN_^!X0A-hgHh-iD*9MUH|>5QuWlr&XXIhQ$a!;Y zGJXPJ;jv61j6sjq<9rd>cxa^OZgU2UX8$5N0P_8}mKRqi=XTdC^w8I=X<6s;a#F!o zq}|?&_rtNfg$GZkRClh~dw8O3sy^mY&BI`>g)-z7bndo9gwjBBa|!3pp*rn1M3L2L zvv(LA*D-kmA<7l{AFIsU^et(55vF7vzT?Kv9^#m;4sux~?n`m#cr}tu!z1%HgBuO^ zMn$GwqL_VAum(AX)~MX^+lQnB;4=T4_3x#EtM9+{65gl!YoiULM>N4{&POUc7&twJ z+u$v(Z@Ci;l77J@;^OPr&84Nma-T2Dq#Ti>K`P?(QZgr#gyG(0FQW7KWAKP79!~-c ze97_6G&oXcf`$TtZurB%x?Ye&M}=?v$I&=H*1^bU%D$k z)Si(ir^iKY@lP-VDNKL1!_t)-2)Kg5oEGv5lOd|~Z{Fm~@FiSy_%Kid#P)KtyqZwS96UY6v50G*tt*zZ@#Y5i)ujyv3)3GQeSC$FsLv0B@J}&J z6V1dq2?10(qZ%>jLZxE+k9#Lbk4?ZmYf~xM>#j|})nYa$&>8D=O~c}E7-2YKvNd(O1)cBPJHq&@IkDbmwhO$9xlSmYEOhsVN(kLSAe7i;Yv zE%rmH%~h9SyykHG&XQ*^Hm~B-FVHiCrLKl%?i_1Bo@yG=@hoM$q3P)yto$Y_!(R8&)`Jixt4z)Az ze5Tu?Pt8D26zI-3F)Cdq&zV058O6$1bZAW$i)mV z&i>7;ig@0~l9=nqQ84>`!>0IfAJZrilo(h8>`F0v@Wj&*a*&t9A-`aSVJ1d}e#&?-UklOp?NH=llve5 z!PY)psynn`StLjDw>g5mEKS|kd5XBT{{*ufLod~lmuS~p6te^)cu_mf&xdH4AR=C^ zRiugqE3jX*(WB}4Le*LC!!A;oFg*;idiXpsucECE_p6F;1xEWg_x+im5u z#i)>^`^Fx3bp;YMyI&Ifc-)H;K%zDr>Ba04gbs)ftN<+szSm%CrbEBGTV_D}5{BQz zW2NPm${^;L=BTGeiFgWm=LQnapWl|32QJNKms{Sl7YR^rF(xCL8gAfG*dZ4j!(!W@ zAK4+T%LW@M!(u0J4#?G&V_x!7>MSYtm=FieidHk?#k@s*217|WD(Xk+kW~B1#rS>Y zCkXhC7#AALG4GThICmUFt2}`Zz?z~|q~deq@%e~k=A*dpm||>_(X1tC?y3)!}Iy(9^s>LB^XXWt_MbAWW zuQB1jdaPo2tJB!+Ey_Nqu(b;$pJNQrIO|26#-YElP3|5&RIreJEuy=YoT?$>9$&wu z!Ab5p(zbtWWZXO}(BmPezGxBrn-obG!}xXwip(p&M=z}iCu+tn5=d)io*)wn#*muL ziWD;9&AdR1__6L8EBw^2E+<(?xAVQMh*?7~dwM!wEvIZ*y*3|1Iwb}{I#)00;?Qc6 zqZ#aO-df=#i$JVLin;7tFDm;p5Xfm6S_S`wgJQO*E}eKjq7cy;uS0VlSu2_91P}yUE8J`r47YjKMhT3L+n+PL`MJXEN ze3@i`lC6O|zR4dpuo~HBsp#V%$6A1K5T8~^ND)wNg(K1j3OpmQeuXbH@jOxFHY0VP zp0uy!&S4^`YFmQAyE3@*5B{Nz7HSVj(X}0GdIZ~?EsO#v)LK0`L`l$T{z51en4txf z4b0@U6ny3I=2{}iWu~qLf~N0&?W;WVBlQ$4ks3g%kmvztGBi-GMGTM=@eN6lIa5`tCi zfgl0#EeOZFGn5fD#b`}U#jI0=0{F2sQ}o6xe2lbCBp?K+qkg#vYTikIGkUmXjTC_^IzF>a{LsU z1Jr@OQk8<%ea;g z9;su%0=sp!!V<+?t#s%%%|*+TZd#r`WI&Qb_GF>;MaYt8H;F!cIJjTNV>4CqU?5&y zUiu0E>Tg{1%iN-0x-1KY3tn$U9p&a=&uK4cEti@ZgEnvp5)E#2Z7SV8qItES?_bXb^(epFmdUcQIM8GLnHq(B+gFzxydL#} zIObIOE~=&inmJ1v(3P0j8ocdaf^KW#>zFRt21+S8OrbXQcR@q`oY+X%!N|mazTU{F z0w7|Q9nl;vrkSN3VKxx%SbsI~aXV)WxPFAwFV4yc63n-x()u+Z#+jO^W*rJ-fP_z7 zNw(IJhi_*;iau<^z1<&Zg)*%4z84lW%u!2NF>i9-u2ynLyXeqF2HMuTloTHKCSPhE ztjUs>Gol3<`qGvm{!@I)l8hFX#K;gnO{9pBAykhN+5(D=&X!P3%n$XDnPmc)k7n&7 z#pGk%Y#i;Nu}Zw~A!67g(W=x@F*7+dQLrY`)x9EEeOh1T!K(t8str$AoKr~N2aAWscXyU#?upz6QdC*%Jdg1^_ zvX)^SN-v8%p7fzbj_^{h`PfbYRSeyOlMv0WAKl7Evhu@MjN`-=-kA!-)wMI`J^&&| zq*k-Q{2J+$VIBY`28{T4P_t8FpL8v6Y-M8o=nLIe79s50%R@;<^vCF|r*G}3V!8VF zlTNu+2I79L46q`}x-@@mKz6RJNGsOH zik`NszE~hIIty-JPTT3&OL6yImC9dDwIst3S63tMu99|KC4wvRpz2qh!o(gp3NPjp zx0nZZsBR3DNb+#IWkuV+l@I8RBGrwgEcryN%)sMLkyMXFZeli0bd;Y2<3APoYFe`6ssbr-4KWOc&S&I`WP2thRi7f!`ftuXEzCnHAvkh(ET5_FaS;nK-c^o$hM z@&O3t7J+J;vD=E&~}t5aimvu0<;QEasKHe`WSh=6(~u8c#3r0ACO^1n(#f(Fv5t_#>6 z0WeH4rm52TF>mAt5D`+?RsRl}ora1ng?(tX)3WozvVbDJs=*rFiYIO5^}>GMoRIV>>N0xyBdkI(R$jY{Eq3}?a=0LTA|7A+oxsqbF)_y zY2eScuD7kyS8T%kue>`GFP> zr1|r&Keer}O~YI{Dp@t2qgkx~Jb!v#cZ@J>X@Ucdc3Pss@;#|@17u*qz1n`<`;57;=B_nyn z?q+iqoHA2kU;Fo>4>D@9j$bR`SC|L$y&@B$~6NV@7pwPj~IiqG3=AY9_bNA_0mv=ceJ=)dy65W`64tURML^t9j zLYTNLb5y6d8Vn+9bthkvt@VuR`eN-ZycWbv2@W-haiV0=Ul-gK&iGA;$B>q1`ZAp^ zfI8vb$>BtHQY*&6n?QLf>n8ym*Pb0*K<-j<#V}irfW2haoAPa_esKg?6q;azn z1KebA7&zkhs3}7`1RHDdxi;GxvAW#0L9xdLXmau@^@36m7&AfgwI-hd@h|fGFWxa7 zEkR@4`ZC)^E|WR7moDd5_E4D8v+jU&iOtJMogx z<4aM_E_V*FB8z%seWV9eZR}pO?s#CyQ94q)xrx1eWHPH++#gQxxgJ_xYFP7IzW0|6 zMGEm3p`or$j>e8Bn-1kv<|Y9Y>P@_NfeEe5p~rDjym@8g)=&|zt116AB__>|8*uBh z+M6(0t#;lAD)ERE*}UAu5KCFyH;%aS!)BVlz1<*2wIKJ48A#U>0-o!8ojQ1rKZ=S7 z1X<;-8S_l(Ur&g#6!lX47Z&+?JRl^7RBAsjnZJ*0Mm~qf%5hosOklse%U0hW_pYU* zzb7ymGJvzk|d`#eY4iLfyt~FJXhR14)R)Xn(HBTQ*=R zi}|nPo%xi1k%g6-DlNy0Z`thx({&hypx^8LS1#%)L+~7}ZTbo1Qg!Tlo^hOaCh^n{ zpOMZFS^tmnjZ<9JP=`K06=1==Ny#-4Z$G#uAxXaG1W^uO7E@14sAnaX=*Z>_alkKZ zWGDVXMB14Gn?*aALoEAz-*OR)WV2ZCAK{tae}uqJf)-67ZF?sDfvgqNp9(5Tc$~5z zPtXvrCTtMbyBoj#e#+}&5}xIS$t+Cji5PjWCN|Hw2Q&G^VZaX0YWGRIr-AcjjW9+F zNoZv23eCZ%ylnX=oeyjYQ{KHiCi|RIX z_gJk-!A5>xo;A5+wSi4j5ICFJ(CJS2uRnQMMKz4dG3a6RXlA644?DECQ(Y@`(qc$~ zBwOlQyHN5!EFBiy@=zw_PungZCPEHUi!5A?@?z)vM|3>_S7{2BJw)4F4^wfX=njzV3t5!RTtuD$;o4K!$)kF8WC=}a} zoyAOntM8Gy;~ftTuKU2-1Gvsb@8Jo7bw(XsN$5*Z>}I#+1N`{jZJU4}oeoR%A8JM* zv@GdDjv|&&pEoT(a-%HmapcV)igt^jKfD1_ODD)UQ;@2iM3pKG()_c8;R^YkEz(w!}ED7M`=z>h+vpE zD7X;jbj`W>$37zg#Rq3V+)d_lr~94R;Ha|ZQUxj4YYSMBepU=QrdnCk>_L{F89#4H z{$C3(OEQ|97kA&W0P|?)^`I7y$dMf_%ry13&EamJlr@>{cN{>FYwMZX&ew@*?Ebzd z*fjV3*d2}aD_U}B(vIQUi>r<2U^F{(K7=JNH#TOu`j4I3hnw(@h1UBJMFp+5NT4l; zMuSMq4nbHu&uh<24Tz!@w7Pf6J{ST~mjv|d#f$~h3tePTrW!@F6JHp{Ea$VR{KPn9 zImG`j4+%&Z+zVEhS2rL)U0~Fu9g4FJAlPL7aJomI3*NBs)$yhC+ZSlHTZaJgaH6cKVHUGAL+!PJS7**mGN;_x(EIXK^T_=(esh4s(BESg_|wzQ}rJ*oTX zckuSh@0Gb?@j02eqRbFqTU?3uI5nGV;q5`X?Sq8prtH4V)*n}LZL%!gsqpKZQ-fmb zP@mX3)+5b=>cYTA30zlbs9$PF4xFyfVe>@(|en{z^OT` znqQr+fTFW=z|jUXqSL+kbnx%ul0t?J1DvT_&K1&q^APi~tog*Y!W&L%u!0rvIHE1*)K#bw^ z1DnqzMC@QJN4~XRpr_+4&-BVu!`GQnCl^m9?Ok=*%;>1KadtXABOOeBCpXbOelTZi zgScFOvfizYC})C^0XyWzxdYXbevMlaXO{Zpv*#ow$HMt-*q`0Ejbmlq{)A5E%G*2Y zu|O1RkJwm&{QREkjy*rjbv6fJ2-Ycff32%Sb>7!qLSz+;il6rls{hRvWhO3#42`+n z$5RCX#fv^f&x2x#=+sg%Tw!v*I4C)+S-10_7Q}15tQ&F6&7&z~gFc1#{X)XbC>E!m z7!}Vkyw`dVXo(#`>0*(FUni%`E1&6GaOXj|8Aj~seoUdG;fAOOx3Sq@LEpj+{*Tqu zb(NK}IZ3O13L8QbS)UM$&HUtg^q#F_)6=PEr}p9Mq7bQig!w|U-?!WV22?1Z6J>u7 zb&J=WD%YBnYRa#hBjr(RPoNzs;FrkS)is^=^BZb$n0)pF=i2H8BdLHjYs=&1nu zqurht<&*!jBsrEJQB9J_aypUDi7Eq%Uev|mQ<Zb#V^WjXcW6ojP zJTs|Db{$$FS;y2fK{)k&c#&1=n=z{u|DDA4yjC?cp)$#pGohTre~SSoqySPbEO@t933yUo{r8yPZyKU!{h0YKvrRn zVc_##c*ME@lMh#{{pS(5wV4=)m?c_e<(DP6EsCf9M?LN}dW=M6nnX=P97eS9e17P^ z5ZTFu)I14YY&Un9&yPceAIZ$5oxoZiiG-8jX4{Dh4^zys5xn15CvN_ix^*;DbbxPPskT-)yYkAN07JjDH>2W6vOUj;57~+Z^gQ=gS>aT@CEKguIaP1 zo}Kv4SGix1bP;4ejfR+ZhXh_p3G^*eR{XDfJVz{()`*dTR`mqB`u3M z)~I~DRS&r4W%Rgx_jUikWye6qw%x)mb0b--s(P*J+*&q1n63|QuX}>W@|%m_By{^C zfB%?;>g=xe)p1~xbhn_hl6k2)rs|NFJKO!KQu?ou%8%On8w$Da+hzE;&T6!O$lgp$ z5bK*o?eVciA>5emp&6KEAMZuzC{2tOuNbz&KVc_2Ws)vp#0V*n_!;eu@^T>q!rXu; zc({@uStCN8^KrRzuij*0o82P8o|by#<{roK0{c2-d(Qn8pTl12QnLcD$%EG_8kr_;J587w-bgp^4=B_K}o`F-JmY@6x z5_utt(`@&`z?hMMt4#tCgmM>5r31OhzB;eYcxXDGlzk0bzC#0XGQJvqw!_Hn5T9(N z%1UyFgMD)>-^z=5^8ZGJN~Htl9@va1NUyNpQ!9|iar~GFIV6_IO8>oqNzyuEAzV-+ z{~p7}iiwxZFV5wc;*){=#M<3@DkD=CmM7#U36+d_LkKxV>o_`I3>fvl$GKEXcq%{mj z9FPAPWp7Y31&c%GMqf8}mKS`dWa9I34MKde%s!xJ$=VYPWj4|6(|Mc?65IPUxj-4U zI>R>?t}Y=WZo6W?GUT=Ag8vg@Vxt$p3tu<#rmVhvlG`MYZKgl{u>-04wA7#=_c(($ zNPc~05MF#kcDD(q8|)`5-xEC%XWp^*f@B!)0V1LC&&HlkE_l5{WP7NF zq#nb3QtPGp4AOluao#*qPKDB3 z^$i_pws^6&G~m9ouTm6%Zj*0*2XkaWXc==FSK}62%xUrSD-^?O*r4(JNMmx&(a507 zSvM~Gx9|r?B+RrT3G#|>K~SXi|CU=gUxHXQyLx(lSH!dKjJ0lDoE4x})36!0{S9Qy z2vqwF<%{257~W4SyKfr4`kmV5GYI4D{yn-~YMaLm$NOT{Uw!UYh;~<9+5(O{Aldbb z%MmjclF(yk@6~!uXPGda%h9lZ^?95BvJP<$T5}|Gx5%yf=-Lx!Vy-YwT06&1(Bb14 zGKLNNbmOe+h`XqSJJma9io1{d!4p4zkyGC&7tg*WwqkA>(rPPY@ND6n{U+Fx99gZu z{zi>v1^QVgCCgD)E*TrU_<^oXvDBPE$u5~gw!+=xn|3mcKYQS&(F!qr=I*dRnjBkF zVybm)Hj~TyIQ9xxN;;$Wb<C+9EN1B`&tsget!NWJu|K0~vBD7lGfB^ugRtNb1?SnD? zvsK)reJzKjhWe={T1hEWzmT)tOvMi=d!wq;sE}BkYhjOw9M!PHx=KeSxIqn5L;^!g z3^i&^K}DUNxU_!Ej7KBH5Mxu zl-I93_m`PW13fqE3JVjw$IZdiJnu!s!qSn`&p|Z(vp^m__x0S~x7^*TR_@yCIa-da zMu#qVTO1Z<1G^`sCyDO!wFDrnWlS|Id`!^S)3u(xYVx24_9=UwZ+ul09Kz4Un$gdy z`O-?H{^~IW3E9=eLvB24ubLf7*_%`1^eUHXOv>peu zm9Pgq7PlU)hnItgBYEUDu-QYWj6CqvuKamhGu4`;Nb!cv|9%+M^i5fZaCe zjqdyrZ6*v{gE!!N^pS0%yhn#u==Y2p0G~rwHV;YKHfb%Ip#i~tyt=(O*T_KEri`DF z?4KUpJ5)bUyc}f8<|1ZIMNE9f+J^-yx{5E)#dS@%Zk{~aeR@Ey3|CdOKPu$%<1F%w z``y@cP5uX|m*aVHg{dqu4Oz&=;QfuV-2x$sfd=-iloTItQ=B9~{fkykUq|(Om!B7Q z)D$kybuV&N*QtR`8G9XaT~%#N<9@yhAHhw}exbAD))pp&i^tDhxyK)DRJtAnDYoQs z=x@TeT>O6tyY_p^-VVz({dV7;al>jx zF9a2Q&CDZG;H6P+1y8PER$lOTfRU!Bn5)@ z5NXOP-@iQz-D6$RPqR{-VR1!;|K{CMt7|TuH`P$|!K<=Cf-oRr8cH`(Ti%Rw+GuKs zH3}*SRWb9(SOn8Y->YF6Pd`wp<4+b7j|d9EMg~AvApz$0SEg7}q;kd=2`OYquw`Hu zKS(jW<%elkZOQp8GRp+E%esX(I$pNkSE$5o?tCH@@= zbM&yv3A8IqP{l}-&)Bt~G5}r$nbMPrLbyORa1va=dM;G|aS}cvE{0oaf3wnckWek+ z(}-%-Nrq((ycqrDkaLm6B1(v!n%Qi4w{t`jA{P}7Rp6ymK6U#W<~@ZObFlX65+{z6 zAn)vY>ram+D$wtqHZh4T8$o|ra$BNQ#)AjsNDw9>1Mkt|9)S>Ka86Vn2;^6E&a=_j zP3!^jdajsZ-gd5~^(mP9GSkjBf#;DCd{N1eojOiq@0%RsNNO0I$DO(}h%no!(IQdO zQiN-RzbqnVs8Rel9QZcyH^}3VJdP~RCt#f#R_XwE_v#w=xvgJ5E}-Ykf+{62f$&G*a>d|-A!whyA|BU@B#6TAvwiox05s-j( z&l&(0%EoIH@vjJ&h!1St8tEDB{cG^7qjg&j<>>0sSpD|B1v^elg9%B`V2+t#IEJZ< zfOLJCke*i=TA8!kxIYQaWikt+jxAfH2u?ozZLgBWcM{p*KyL_PB1{|NpJB&990`~A z+^+2>QSB(O<9DrLp+HVDa5e*6F^Wk98?{lkH*fri773Op1OM$RHiyuZ>#-#H*Ge0H zy+b}e+@SZVs~#Wf`V&O4xRAV!tLTsav_&rma)D}I>fJ7dYZ*%WUP@ruFetR2N~1z- z`P3%%NOnk6vkq!)tgj9#fe49^?rULLGnJjaTFyPjU~(yCI28QC?YP?j4VVI%;~`OC zpZ*h?vo?B={w922V*h$&$e6_78g#E|aNY|m)c|?O6y$8FJ_maIp?+^R@)eGsliVZT z@`sn!11t$J3sOuLThmQ&(#+n${szd@WLl9oG>ph@c$7Y2Lcqr{fE*)O{>H{04e#Uf zM8t7>mT3GR{oWLI*&KpeZU5^e)2vtkpazQ&{t!>Nf(MHt4P$n6``4a$)5l}m zo7$smUgT&+zEmf2+2gGJPB8S8DUJ}OyVd z#lNwZgBp;cYxy0j;yWv@@X(9(fzFymN~>fBqOgJC?fZr2j1hpUrXdF4_x6hrXzAo_ z($VoyAqOdGhDrib()9eLB&6UMGR331^afC{Zesf4>qp><=-EgmnOz7k%6pg5C;Y*y zSij=Qnx4!=MS~zRAG&w zs~`jyOt?0hg~p8?7#&hTG@*m?WF!6IN2x&tC&Qxy<8=)BQQsGHdft?7+(n|E3`d{3 zeW)?57A03wBj+F3!w8F;us}GnXRZ@9SF=>3Yz)#+H_wI(ge`-A2e{bVb?C)Qi}&Rn zJ1GW;90fsOy&6G<5OQ?t!;+!z%ZT@#DAL1{DAEIalcfa$2UZVCK%k~0k=n_Y8R9N} zsrd>^1=Xfcf_)lZbve+yp0lYa79*D`f@KD1qwTuWvw{S%c#w6a!bya;J5U(X0B6K2 z8T5B|D?UxIe7h`GBs?Ji=S)zhKY)Est-e8cj5MboH5zvGCPZ#z6 zDwZ$^bcFMdk;iW`M9*^h{T%_f;ylrzsn$p+)gO=vGWtp$ZAyeLW@bhd$waC@Y0rw- zgiE48Ettd$pC)U*Yh4t$3Jxe|$y!fn*B=1&Ig$i5#Ngh>-T^plJuV_ERR)E{khhe$Msf4z= zXK51SW2D8S+i&pM-MWQK zgy?~SYTba=^6A&QM}PLp=i|B?JATt9cgigE3wBxBqs6>O^j!k=SfMvhhA$i!27@wU zrvRMsr!PFajxbN3J-i;@%z7%4GLld)$hnE*0w<0ct&w}M0|ACu6UWNXdhE>e`QGKp zAA^w)WO0`|Dutk|!lPPmTWQrRYpUNjFP**zgb*w(^uypsW07MKx%cRKsMJh_w-c6e zlN|16LAVnWqIKF{Ux zSTxkr1Ha=3+NhEVc%UbDhT411Sl!F^+86XbktZYZbI`xwW6&jsuEK+T@>s z+CBjdXW&Y(|0uL7C8|a^G3#&qon)cb{Bso^#ecDmBSD-sFj@x(=#@(D$YkwqHT3>| zEFKQexoMyp|)^b~-+=;>K`*?5oN0~ZEPj@C(?!fGT zE-Y7%234wx?XIn}N(>q96Yqa3LjD2FqG?0`09ft+uOejnM-fhG+t^`=C45oAfI!J} zWMo}=nCN4$OF&p(a9XWII6W)WS!mUgdytTBb-+PMlsG#Qk1$e)G|F(?4W-Zs4bOQx zkXL$xWpy8a4IE*W9`k8GLUS+`wPzH9cgy@%CoU~-FU>j2sV=QJez+X_lR3|J_e%Kc zWZ-S+nRhK(=YF1O?fNzz?QP)9$>c%B_e-r>Mixah^2X)N)Aemh+4rHmMj3sEQKq3j zc(yLQo2m43n^roTgXh)7!P}b9&mcnDPlr58_H$j|*Zb~?AMeuY+uq^gExO=a=xOp) zt8n=~^=^pH)*sM&D#W!q2IUTE-Y5~tn9|QC?>hICEH^!)W-n43cVasy#)ovzwmT!M zlcGDTgY$1`xa{G`y;?;7F!{$TDPIyE9$HBrm7k@?Wuzub#_f#FeeNsl?=K$~&TPC4 z>~F^l%iCf8+%lucRsoLgfYKaG4rE`UTG^(5^`c6Tls+n^oEz*qimfmDXd-cBKN^Q(4yw1aDR zIY}EhCq5<|QE@94t#DQ}5cE4gw$1y7R(Z#a<<=(_f z`|F`QZKKU!&>`b$T@il={c#N-?iPs6?0`c*|G2uEWNz7>zZp5J^s3p|plG8j-?fXy z`Y@q@=L>MZ2yM-xhL5X`IDR0TZxO9I0DIdJ;Lgr{Bi%k03pLTA=f|r-oK?GfB@21% zK|FU~e`*lE0Z! ze#QHIr!vqw@W*in9cG901j5&6bw}e~cNR|!E)zIis(bO(79Gi7rNJna;i%%QE(x#U z1QF>LOJ*eXy0qGMLqMxqCQ@n*Y5j6v=?%Yw?#$hFJALnaGMszYV-$?xv|S^)9Zvjn zDc~!qL{#z~&u%q+^HB7)lXlzHij>SpM<+K?Q2dhP{sZ?OU?t)nCMvAEWKVp{J=9*gH1vkDoF5bctJ3%kZ2M5(c_}? zGTUNv08WOWN!p;=l8t3QqRuZpZ=3WqMtAxyIlAb$qjk=j9<;JBySSh*jI&$Pn$B=v zFM=GSOS>Cn8oFV5Xz0=2#7*N+B&U2NrY#$E-(ZyNo*RVyP=cNtTOn^gNvl zK`vf~QMjF=NbCKdI?g(*$@cBzv@`>$As_-HCNYo(snI1hI!AYx0#f4WM!LH}O1c}7 zRyqZwq(zCB$LDxn-yiSu+m3C={`h{k-Pd`a=XG5>@6SY%H8A=CiS=_a_3Wm{0%f0q zQAMA;+ef!tsX+R~1lX=VH9Z<|?&l)x%>~E8_Xy>3uC|0;H__tJsGE7H&>yjvOcZ68 z6p1lsp{WG!_Ezd?hX`pw7Qpzg_b)>s1uqBhrhC)4V@eE5kQyvsNWS&Sm zXvfdYQRoZ89p}R{ckNCKfAB01vVIZcmnBi>bEt~fXHY1Jxgl_1J9xdC#XTP!l)t}S zraR}Oe{sMPn;66vTfl~O*bFwb>9D2L`|NDUR?yPRdU596j@^$y&t<|3CQw&LHxCu$ z_OOT&(e`&#C}rH`vncSOO9|(i%tufMa_`diyqric+F$`vMpR~8rwg5`9!Fhw$pNK??e4on(^P3F zmy%tzJ$;;$u0rU>1GBn5!1C{kC&c+pZEmXS(bZptEMA zJldp1NYwZ16#je3H$=O}aUaA<_MO8I~m#MA}RK z$lyfRTg!#l{W9IhL1QQ^>l+=)eD+CjxomYE>@x#h{l;fsh&fK(p2HI@esY+!kd!w& z68)2;f=xcEfP?q#p@mj7MafjLACv~KW23Lg2nFdn2k!dLxpG>Vq?pjpFDG#!ICXwBoQ&&WM&PLUmH`xk+jX zU-Uw1HoG>TbZ%FQ_>triYbp?Gr~{=`t8xP0q{nZYs=s-~IV2a^99km)JOC@n6+$M~ zZfIE^vZk+;$Vv?1%=R2Vx+7d<5z}+RUtAoSziwQ%Noq^DC|x@Cu3DBC&e2KnYkZm4 z2hLMIE_}I?z?&*q8BIZ@PIxv5FaeXtRVzD_!&s&Ujnu~#F!J%qnLmsfVJw9Uvh*(7 zHMvxxxv6;3an72r+lx4n9f$?-?s7L6$a_WIRe<>$u4=lCPNRbj3_MozXsdpXZPt$3 z_n16hW^M1^mRN@VU<*E$sQO%)_(UpIgi0nCOKq=idc+l`0LpFI;K(n#7En=O-iBC{ zM@p_O$>~!B_d95{atMh+J>OI(wd_Yu7PuzC z3x}JCH4;OG-muku2@s=mV|j#r27(7xXM$S@AR$N;a zOx4Ycpb`okSCGTv3+#Bk0vHfF|FN7QoywOIGua!5X@sXY5#!}+q~LN zA88p7#%MCi{QZZm#|I8x%RG5V@TPZoU0DQ$xTA;9xH*50?y5?_!U?T~v2v2^U!k?7 z+C7JQYpt6+YgwBel||N!m4YXe^Sux%`c@kKv*GR3*rVXMYGHFA-Su1K#OfQkX{}?D zKy7(Fd*X_;>uHpj04`E$O=4q1%g8LgD(j+AqnHldEWA4urcOl>!bDG1UZ(Ss?Q5j- zpik?jaG4j5!{P_G9cH4DF5?6WA3z*XL*eKG=HQM)iGmDrw%J)FBH0hBkENkkUfk%& zW$^Z$nW4$F{T258qBN&WXvOmFTMVrITnbv@ zD;00{dWw_Y`Sj}IP)-V8V>q{Q0-o@XtK;?SoX&oGRRbKrFO( zC}UD6o@YVW1D~CQ;&ZK%6KLOpFZKB*JaK6C(N#E|rSMh8UeZs0!<@cRoGIKU+|S9K z8tmdMYg{PTcal0cjyQBESQrJi-D1ph3c{J?WL%->I-Dtz_!M)$=p{{y+#SgyRfzak zcSzaGPdrk4Ueyfn&UjuvH>}M6apUiCnAM(crd4ki`^m7!971MpZ1yE*m}M9qBkP=K zHWJmI2i#u|i^MKNP>fdx8G5`k=zSck>J2e_&n}tlwP7_E@KIgtT{vkB^sNRzC_qWl zr{i0vayj+Yqxz`!In3TTc7OFosXRnvCL%0xNL7JDvO0y082_n4v_X}4q6YnTBKExL z8%bwd4t*~JT68Th;e#Zrput*vr-o{lb0z|6q9xC-_FcpcJB;OH@(_o5#x|r>ZY+4wH-0r{5}Wvj2jnq z_8d=@#jh+u$Q0%*@zm=w8xTSWE+3b1ldZa}6gYB#v88B@J9$qE*%!{pSRF#=gqfxa{Z|kAqcaKtU zA09=mzDpc4IJ>1MBCXl;AQr`%4jzRYuWeZ4| zwAcfY`?au>fiUHOn2XhNYI-!_2Q<+YyFl~{7F*azKh+=`DJRUwjG?)_`N8Pbk9Zdb&d3uH&dzP%~7GftBFNroh#bR72#D$~%>-Lzf(>Z@m z+BwuIkup^*e;E-#T`49fnoPyv z-04p|2tUl-vOTpus`xbhVt2wQZqf}sR(`VgFiJ%pVA zy(2;PL3hJw-y$UaW3>hnj$GSK4hWP%Ie#d*-9TrMU;(V=CCFBX+EHat-d)C=5QB`( z?~?NKfqSvLD|-ogSm;mDhzs0Z>K17t1yfhW!gboUPSLotEdnFXfV*a@%P^Ou)Wm$o zNfn%zN_$%&uN8I#`jrU*Mbq?7i-CSJYfus#V;jizPj+6Lp7c0u{jZF`qj`r+|8X>v z`NW^=hk~`QlQT)A+XHO60}GRod00jpVgx*fFreqgz1vZ?+Wj?VmeO{`7@87ra?6QK zJ}C%}`8K`$`C7pfsHAC@MqG~!oE9~TkR-;~W^K=^l00PDH>lKwMUNG$BBp%d$X0eu zk%W#A51uvRZRqPY>dAW$`9AdY6R>xPz4(iP0i}k-Wlk0=&$l(#-j5ZUc2^=Z z-+_cX##z%`c7ChWY~J9H4%ak>>c$vX&29isSkMD&4y=|CECNacSb2Dbn%vxh%uyX` zTkG!0h=@b&VMBWLj+EB*G7$5LMnR&JH_7c8VBx}5DzRK)0!1|<_9Z%9|iYnd;I9Q9SRWFungmXrtC%dvU{(Po`*nX z*xWlMEH%ae)`f}(M8zuuE+Vqy?7u>a+TLX+M6^ojv@rg<+MYVA4zc3QXvavO=Hy|4 z6$V@=JunQ7Yq)9vz2??tQ6jyH;yupQJW&%zsxVAVV+l5tum-fDT#ek`qvQm?%XTlWT@z`(_TCj?@eSzmU2hp1W4M8Z{xB#As_wmKv8x}&zDf6DM%j|O~q z;>zLD5PtkCu4s|G_vjQ~eEa|rxmW~L*a?yh7UOd_ai=)Qwxm$?h=!Ux-YK!puHIFo zNZ0V)7yE35{;T>>aXU~so% z!#3ulj7QDs8ssDKkVm$Kb*WSvUrsvTe_f(aE!pQYRf*aR6WkfjJv}QW4b=vHA*p@U zyO`j3!gy#Pyz2NA&i27GBaPCf@fFSX^S61NH zs`k&b>{#ztL*_8qHi6CINwSL^ux$doB?XxQx^bZu zr&hO?G(|lZbqho3Bf63Gnf&!$Qut%)W|d?)`Povn58C*;)1NiaVbj*Nnvq0$c;~}n zCk4x)Hd3o8PJ**pUptiS5&93`HY?X`>IH7+>!BJK%ChOdH<~qdgI37)KXn`hG@E1U zJqxM;($0p1^q%3&Dj0dsM`?Igh>iODjIvC_0p$fLkhwRiB+ncu5I&6=Vr79YV=QV= z($$<&fOQFfD0kjYP${{@jmN;NzVhL|Et zwW&5LV=pT)7x7p57-y??S(3@sg{uOZgH!>b=>?<8aDO>5`mA_c?`YLc?aBSQ;F=Ig zIaq*O;gDEq{q4tkG%G-BxD`NtFKs#mKZW5b1&^XsS3$zo zc%S(w7cFNw<&+KQS&8ZX(L5u{wDOLR(s^6SR`9DOdR1=$)9^WZTWMT&LnQr-o=&dY z3pYl@x>4HW$Rfck;~Br+w_*S?#xFQUYcRd7*Z;W}PhLx@X^Mh`)PaqJbl0Ty7w*x; z)yvk*<*($ks4*Xz3ISY`Y!EQXE2|WVQ-=~4tLm25{`#c(Pys$jW<3JlnVmHe^xrer zs{5uU=AX~f-I`t0+2qZiVBw$8EQcqIHf^RMM5jbEF_;y-pBi-!u>(bp+)zjfWl z%iTw!XP%huhQ`PZk$kbAV)B^4A2f&>(%A5)-*KO`;!f6}CtuJ3YafeCN?L*%~Un!XA$R^$Cv@HCMf2?k!m%j4k zp3`9!v*v$pDG6mD(;-efeEx6^%l%l|;3H{GV`h)FdSk2RCx%aZUB zIDN2TrLyu|Yy1eZhm4I?N5|o_pFSF52GyV-M3Hi;KXEB*tYlZ;^f%3T>LW`$(_4N! z!)Fum9_5ft{nw+2H&t>TPeAX?GRV;9mYUi!r6SI&m{#8MNjyWtr@+a%w%Q`YFfz7X zO2%`2N7j$Q>SZi39!n-b0GNQT#Dh4IELm2vQUjET>HT{GRG?a3_`kx<5!5H>MJy_q z!Nkc+IUN+Rj)*<7mx$vQ7D7x>`UoNECwx^nfD{b+0C6vB;k?Q=i=9oucS^U~4fo-3 z4br=z9d}_!dKZ#7cdv%dX0|TuY=1qc$I9EIaNvqo5}6q)6!)NTlzpy(B5)c*wsMnu zVoZ8Y&m%r}&{z`6-)3a6RuXV!xl_8#=iBx{di1_%aF-+1*f1MqIOS-8x#dM!v$>)_ zoqYzdd_4gTuy)(eG$^=S$(~}z>^8a7nt5OopCSLK=uV-NgX(;7@_QzKJGwB2J(ZGP zS+;jo$o;)wxZdPa(`bzEU`2u`TZ;-vnOkxx9UQ@~MdU`DJ@LBtK+g zRuYTBq3ZG;W>uGUE;aLxKTCjm_xSs~`-_qXcW|)z>&%OYReZ4kxg*eqZa!?~`eG&q zmuw`ilPp0(UOP}^Fbxu+ogc1G`=LdT|LQABm?(|Evco1udy(udbYDW3Rzv5^bWCu) z|1i8&Ncm-Z;gs+tTO*e~)%eI<%Of;~kD`zhl}#FbHkb}%SG<+1h^v=e1GYd$WA_RF zju(S!L7Kn?{2Dw{duyDmhbpgtVT4|(5|Ewuboik~Y6{w@j5gw0(co-|t*fJjsa?nE7g$#x>=p!5b$#vaQC7&)cdV&JYN?3t&i(@UB7R9ha*YnNuE41*R%I^(O|=@8X+! zceU#82}c^1@Yf0Al!Gp$d_Ics+d+NRST1>Q2A8!jasD%h$`0Y!cioL)_1#!f{5_U` z$L;?e(cdvV8pn?x2mwe|!Y`2&w8ROKVH9&rWK~95J@DaYDqZ`5GRTRld1Bc$H(I{8 zBOw+!j2c#@FV#bqw}+#q%*UJwJnTNqE6Y;; zCd8B-Os?TwsN|B7Dz$}#M^kj=0-1ZKnHPIom#h_M@>jTv(-CA?j=q-X82|apC*#^T znC_z38U1fqurAcW-qpAcE;{hAIU=5YnocVk4LJ z#(lbK_95{p;Qh|2LHtJ|t)ZfI8L|_`qD?8a=#@;>R?b@ZItN8HE_>zeBa};_CO!@9uE_f5i5E^?k(k-&!OjKjf{>O!y?O$DfHF)d7&lG=re%|@~OWshHN57+xBO&45-2r!r KRZaJ=cmD%3oV?Ni literal 0 HcmV?d00001 diff --git a/docs/raw/요구_1104/_보고_817-1_재료찾기.md b/docs/raw/요구_1104/_보고_817-1_재료찾기.md new file mode 100644 index 000000000..4b6870c69 --- /dev/null +++ b/docs/raw/요구_1104/_보고_817-1_재료찾기.md @@ -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 이음 맞는지 | 브레인 확인 |