feat(830-10): 불러오기 재료 찾기를 지역 · 상하차 칸에 맞춤 — 가져온 줄 글은 빌더 규칙으로 판독
- 새 M02_Mat_Place: 가져온 이름 · 규격 → (뺀 이름, 뺀 규격, 지역, 상하차)(material_merge 판독 한 벌) · 고르는 차례 rank - 고르는 차례 = 상하차 맞음(글 없으면 -) › 값 있는 줄 › 줄 지역 › 전국 · 지역 모름 › 서울 › 경기 › 그 밖(값 가운데에 가까운 줄) - Elem_Alias(_same · one · region_of) · Hopyo_Match(목록에 지역 · 상하차 · 값 · 이름 같음 = 뺀 이름 + 지역 · 상하차 같음) · Hopyo_Material(_places · 변형 지역 = 지역 칸 · _prefer) · MatSame._pick · MatFind 후보 글 = 지역 칸 - 다시 고르기 · 찾기 창 · 줄 풍선말 후보 글에 지역 · 상하차(M01 placeNote 한 벌) - 시험: 814_1 · m02_hopyo_match 기대를 지역 칸 꼴로 · 새 지킴 test_830_10_material_region 21 - 불러오기 지킴 812_1 · 814_1 · 815_1 · 817_1 · 827_2 · 827_3 · 828_1 · 828_2 · 778_5 ×2 · 793_2 ×2 · 797_1 ×2 · m02_hopyo_match 통과 · typecheck 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import type { FormulaDoc } from "../common_util/common_util_formula_doc";
|
||||
import type { Picked } from "../common_util/common_util_line_stack_pick";
|
||||
import { call, type Layer } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch";
|
||||
import { at } from "../M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api";
|
||||
import { placeNote } from "../M01_MasterData/M01_MasterData_Api_Fetch";
|
||||
import type { MachineCtx } from "./line_stack_machine";
|
||||
|
||||
interface Cand {
|
||||
@@ -21,6 +22,8 @@ interface Cand {
|
||||
이름?: string;
|
||||
규격?: string;
|
||||
단위?: string;
|
||||
지역?: string;
|
||||
상하차?: string;
|
||||
}
|
||||
|
||||
export interface MatEntry {
|
||||
@@ -47,8 +50,8 @@ const LINKED = "이어짐";
|
||||
export const MAT_PICKED = "aislo:mat-picked";
|
||||
const enc = encodeURIComponent;
|
||||
const headOf = (d: FormulaDoc): Head | undefined => (d as { 가져온?: Head }).가져온;
|
||||
const text = (c: { 이름?: string; 규격?: string }): string =>
|
||||
`${c.이름 ?? ""} ${c.규격 ?? ""}`.trim();
|
||||
const text = (c: { 이름?: string; 규격?: string; 지역?: string; 상하차?: string }): string =>
|
||||
[`${c.이름 ?? ""} ${c.규격 ?? ""}`.trim(), placeNote(c)].filter(Boolean).join(" · ");
|
||||
|
||||
/** 그 단가 기호 줄의 재료 판정 칸 */
|
||||
export const matEntry = (d: FormulaDoc, sym: string | undefined): MatEntry | undefined =>
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
기계 찾기(`M02_Machine_Find.alias`) · 일위대가 호표 줄(`M02_Hopyo_Units`)
|
||||
- `est_code(코드)` = EST 원파일 자재 자원 코드(M…) ↔ 우리 자재 줄 `출처` 칸 「자재품목 M…」 같은 줄 ·
|
||||
공사 직접 번호(M0000000…)는 빼고 이름 길
|
||||
- `exact(이름, 규격)` = 지역 꼬리 뗀 이름 · 규격이 `master_alias.norm` 으로 같은 자재 줄(가까운 규격 아님)
|
||||
- `one(줄, 키들)` = 같은 품목(지역 꼬리 뗀 이름 · 규격 같음)의 지역 줄 여럿 → 줄 지역 › 서울 › 같은 단위 ›
|
||||
키 차례 하나 · 다른 품목이 섞이면 None(후보 여럿)
|
||||
- `exact(이름, 규격)` = 지역 · 인도 조건 글 뗀 이름 · 규격(`M02_Mat_Place.line`)이 `master_alias.norm` 으로 같은
|
||||
자재 줄(가까운 규격 아님)
|
||||
- `one(줄, 키들)` = 같은 품목(지역 · 인도 조건 글 뗀 이름 · 규격 같음)의 지역 · 상하차 줄 여럿 → 상하차 › 줄 지역 ›
|
||||
전국 › 서울 › 경기 › 같은 단위 › 그 밖 값 가운데 › 키 차례 하나(`M02_Mat_Place.rank` · 830-10) · 다른 품목이
|
||||
섞이면 None(후보 여럿)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -18,6 +20,7 @@ from functools import lru_cache
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Mat_Place as place
|
||||
|
||||
import master_alias as am # noqa: E402 — M01 store 가 scripts 경로를 넣음
|
||||
|
||||
@@ -29,21 +32,19 @@ PRICES = (
|
||||
"거래가격",
|
||||
"일반가격",
|
||||
) # 자재 사급 값 칸 다섯(조달청 뺌)
|
||||
BASE_REGION = "서울"
|
||||
_CODE = re.compile(r"\bM\d{10}\b")
|
||||
LOCAL = "M0000000" # 공사 직접 번호(파일마다 다른 품목 · 거창 치즐 = 우리 표 Air Hammer) = 코드로 안 이음
|
||||
_REGION = re.compile(r"\s*,\s*([가-힣]{2,4})\s*$") # 자재 이름 지역 꼬리 「, 서울」
|
||||
|
||||
|
||||
def region_of(name: Any) -> tuple[str, str]:
|
||||
"""자재 이름 → (지역 꼬리 뗀 이름, 지역 | 빈 글)."""
|
||||
text = str(name or "")
|
||||
m = _REGION.search(text)
|
||||
return (text[: m.start()], m.group(1)) if m else (text, "")
|
||||
"""가져온 자재 이름 → (지역 · 인도 조건 글 뗀 이름, 지역 | 빈 글) — 우리 줄 지역은 지역 칸."""
|
||||
got = place.line(name, "")
|
||||
return got["이름"], "" if got["지역"] == place.NONE else got["지역"]
|
||||
|
||||
|
||||
def _same(name: Any, spec: Any) -> tuple[str, str]:
|
||||
return am.norm(region_of(name)[0]), am.norm(spec)
|
||||
got = place.line(name, spec)
|
||||
return am.norm(got["이름"]), am.norm(got["규격"])
|
||||
|
||||
|
||||
@lru_cache(maxsize=2)
|
||||
@@ -96,15 +97,17 @@ def exact(name: Any, spec: Any) -> list[str]:
|
||||
|
||||
|
||||
def one(line: dict[str, Any], found: list[str]) -> str | None:
|
||||
"""같은 품목 지역 줄 여럿 → 하나(줄 지역 › 서울 › 같은 단위 › 키) · 다른 품목 섞임 = None."""
|
||||
"""같은 품목 지역 · 상하차 줄 여럿 → 하나(`place.rank` 상하차 › 지역 차례 › 같은 단위 › 값 가운데 › 키) ·
|
||||
다른 품목 섞임 = None."""
|
||||
rows = {k: row(MARKET, k) for k in found}
|
||||
if not rows or len({_same(r.get("이름"), r.get("규격")) for r in rows.values()}) != 1:
|
||||
return None
|
||||
mine, unit = region_of(line.get("이름"))[1], am.norm(line.get("단위"))
|
||||
want, unit = place.line(line.get("이름"), line.get("규격")), am.norm(line.get("단위"))
|
||||
mid = place.middle(list(rows.values()))
|
||||
|
||||
def rank(k: str) -> tuple:
|
||||
where = region_of(rows[k].get("이름"))[1]
|
||||
return (where != mine if mine else True, where != BASE_REGION, am.norm(rows[k].get("단위")) != unit, k) # fmt: skip
|
||||
*head, gap = place.rank(rows[k], want["지역"], want["상하차"], mid)
|
||||
return (*head, am.norm(rows[k].get("단위")) != unit, gap, k)
|
||||
|
||||
return min(rows, key=rank)
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
from M02_MasterTemplete import M02_Hopyo_Material as hmat
|
||||
from M02_MasterTemplete import M02_Mat_Place as place
|
||||
|
||||
SPELL = (
|
||||
(r"칠렌", "틸렌"),
|
||||
@@ -189,16 +190,20 @@ def _scored(line: dict[str, Any]) -> tuple[dict[str, Any], list[tuple[tuple, dic
|
||||
|
||||
|
||||
def _pick(group: list[dict[str, Any]], line: dict[str, Any]) -> dict[str, Any]:
|
||||
"""한 품목의 변형 줄들 → 하나(줄 관급/사급 › 줄 지역 › 서울 › 같은 단위 › 엑셀 차례 — `M02_Elem_Alias.one` 과 같은 차례)."""
|
||||
"""한 품목의 변형 줄들 → 하나(줄 관급/사급 › 상하차 › 지역 차례 › 같은 단위 › 값 가운데 › 엑셀 차례 —
|
||||
`M02_Elem_Alias.one` 과 같은 차례 · `M02_Mat_Place.rank` · 830-10)."""
|
||||
supply, regions = hmat._wants(line)
|
||||
unit = hm.mm.plain_unit(line.get("단위"))
|
||||
load = place.line(line.get("이름"), line.get("규격"))["상하차"]
|
||||
mid = place.middle([x["row"]["one"] for x in group])
|
||||
|
||||
def rank(r: dict[str, Any]) -> tuple:
|
||||
*head, gap = place.rank(r["one"], regions, load, mid)
|
||||
return (
|
||||
supply is not None and r["supply"] not in (None, supply),
|
||||
bool(regions) and not hmat._hit(r, regions),
|
||||
"서울" not in r["region"],
|
||||
*head,
|
||||
r["key"][1] != unit,
|
||||
gap,
|
||||
r["at"],
|
||||
)
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
||||
from M02_MasterTemplete import M02_Mat_Place as place
|
||||
|
||||
mf = m01.cm.mf
|
||||
mp = mf.mp
|
||||
@@ -233,6 +234,9 @@ def _targets(whole_id: int) -> dict[str, Any]:
|
||||
"이름": name,
|
||||
"규격": str(row.get("규격") or ""),
|
||||
"단위": str(row.get("단위") or ""),
|
||||
"지역": str(row.get("지역") or place.NONE), # 830-10 지역 · 상하차 칸
|
||||
"상하차": str(row.get("상하차") or place.NONE),
|
||||
"값": place.value(row),
|
||||
"plain": mm.plain(name),
|
||||
"spec": mm.plain_spec(row.get("규격")),
|
||||
"base": (base := re.sub(r"-(사급|관급)$", "", name.strip())),
|
||||
@@ -268,7 +272,7 @@ def _seeds() -> dict[str, set[str]]:
|
||||
|
||||
|
||||
def _public(one: dict[str, Any]) -> dict[str, Any]:
|
||||
keep = ("로직", "자재", "전국", "이름", "규격", "단위")
|
||||
keep = ("로직", "자재", "전국", "이름", "규격", "단위", "지역", "상하차")
|
||||
return {k: one[k] for k in keep if k in one}
|
||||
|
||||
|
||||
@@ -276,7 +280,16 @@ def _exact(line: dict[str, Any], pool: str, got: dict[str, Any]) -> list[dict[st
|
||||
"""② 이름 같음 · 씨앗 — 겹치면 여럿(부르는 쪽이 후보로 돌림)."""
|
||||
name, spec = mm.plain(line.get("이름")), mm.plain_spec(line.get("규격"))
|
||||
same = got["이름"][pool].get(name) or []
|
||||
if pool == "자재":
|
||||
if (
|
||||
pool == "자재"
|
||||
): # 830-10 — 가져온 이름의 지역 · 인도 조건 글 = 우리 지역 · 상하차 칸(옛 온 이름 같음과 같은 뜻)
|
||||
want = place.line(line.get("이름"), line.get("규격"))
|
||||
same = [
|
||||
m
|
||||
for m in got["이름"]["자재"].get(mm.plain(want["이름"])) or []
|
||||
if (m["지역"], m["상하차"]) == (want["지역"], want["상하차"])
|
||||
]
|
||||
spec = mm.plain_spec(want["규격"])
|
||||
return [m for m in same if m["spec"] == spec] or (same if len(same) == 1 else [])
|
||||
if same:
|
||||
return same
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
맞음 = ① 이름 · 규격 같음 하나(`Match._exact`) ② 바탕 이름 · 단위 · 규격 맞음 1 이 한 품목뿐
|
||||
(지역 · 관급/사급만 다른 변형 = 한 품목 · 같은 단위 먼저 · 줄 글의 관급/사급 먼저 · 없으면
|
||||
별도계상 줄 = 관급 · 재료비 줄 = 사급).
|
||||
지역(361-1) = 줄 글에 지역 낱말 → 그 지역 변형(꼬리표 「지역 — 줄 글」) · 없으면 지역 없는
|
||||
지역(361-1 · 830-10) = 우리 줄은 지역 칸 · 줄 글에 지역 낱말 → 그 지역 변형(꼬리표 「지역 — 줄 글」) · 없으면 지역 없는
|
||||
변형((전국) 품목) · 그것도 없으면(레미콘) 「전국」 = 키 · 단가 없는 자리표(`national` ·
|
||||
바탕 이름(전국)-관급/사급 · 규격 · 단위 · 꼬리표 「전국 — 지역은 프로젝트에서」) · 자동 고름 없음 ·
|
||||
지역 변형은 고르기 창에서 사용자가.
|
||||
@@ -25,6 +25,7 @@ from functools import lru_cache
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
from M02_MasterTemplete import M02_Mat_Place as place
|
||||
|
||||
mm = hm.mm
|
||||
APART = "별도계상"
|
||||
@@ -55,8 +56,8 @@ def _nfkc(text: Any) -> str:
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _places() -> frozenset[str]:
|
||||
"""곳 낱말 — M01 레미콘 이름의 지역 · 시군 낱말(자료에서 · 「홍천군」 · 「미원면」)."""
|
||||
names = (one["이름"] for one in hm.targets()["자재"] if one["이름"].startswith("레미콘("))
|
||||
"""곳 낱말 — M01 레미콘 줄 지역 칸의 지역 · 시군 낱말(자료에서 · 「홍천군」 · 「미원면」)."""
|
||||
names = (one["지역"] for one in hm.targets()["자재"] if one["이름"].startswith("레미콘"))
|
||||
words = {w for n in names for w in re.findall(r"[가-힣]{2,}", _nfkc(n))}
|
||||
return frozenset(words - {"레미콘", "조합공통품목", *SUPPLY})
|
||||
|
||||
@@ -120,7 +121,6 @@ def _pool(got_id: int) -> tuple[list[dict[str, Any]], dict[str, list[dict[str, A
|
||||
rows, index = [], {}
|
||||
for at, one in enumerate(hm.targets()["자재"]):
|
||||
name, spec = _nfkc(one["이름"]), _nfkc(one["규격"])
|
||||
words = set(re.findall(r"[가-힣]+", name))
|
||||
row = {
|
||||
"one": one,
|
||||
"at": at,
|
||||
@@ -128,7 +128,7 @@ def _pool(got_id: int) -> tuple[list[dict[str, Any]], dict[str, list[dict[str, A
|
||||
"nums": _nums(spec),
|
||||
"ranges": _ranges(spec),
|
||||
"spec": mm.plain_spec(spec),
|
||||
"region": sorted(w for w in words if _region(w) and w not in NO_REGION),
|
||||
"region": sorted(place.words(one["지역"])), # 830-10 지역 칸
|
||||
"supply": next((w for w in SUPPLY if w in name), None),
|
||||
"key": (
|
||||
tuple(sorted(_nums(spec))),
|
||||
@@ -210,18 +210,15 @@ def _hit(r: dict[str, Any], regions: list[str]) -> bool:
|
||||
|
||||
|
||||
def _prefer(rows: list[dict[str, Any]], line: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""변형 차례 — 줄 글의 관급/사급 · 지역 먼저 · 지역 없는 것 · 엑셀(M01) 차례."""
|
||||
"""변형 차례 — 줄 글의 관급/사급 › 상하차 › 지역(줄 지역 › 전국 · 지역 없음 › 서울 › 경기 › 값 가운데) ›
|
||||
엑셀(M01) 차례(`M02_Mat_Place.rank` · 830-10)."""
|
||||
supply, regions = _wants(line)
|
||||
load = place.line(line.get("이름"), line.get("규격"))["상하차"]
|
||||
mid = place.middle([r["one"] for r in rows])
|
||||
|
||||
def rank(r: dict[str, Any]) -> tuple:
|
||||
hit = _hit(r, regions)
|
||||
return (
|
||||
supply is not None and r["supply"] not in (None, supply),
|
||||
bool(regions) and not hit,
|
||||
bool(r["region"]),
|
||||
bool(r["region"]) or not any(w in r["one"]["이름"] for w in NO_REGION), # (전국) 먼저
|
||||
r["at"],
|
||||
)
|
||||
*head, gap = place.rank(r["one"], regions, load, mid)
|
||||
return (supply is not None and r["supply"] not in (None, supply), *head, gap, r["at"])
|
||||
|
||||
return sorted(rows, key=rank)
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import { createTabs } from "@ui/ui_template_tabs";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
|
||||
import type { LogicSummary } from "../M01_MasterData/M01_MasterData_UI_Logic_Api";
|
||||
import { placeNote } from "../M01_MasterData/M01_MasterData_Api_Fetch";
|
||||
import { call, type Layer } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { fetchLibLogics, fetchStandard, type StandardRow } from "./M02_MasterTemplete_Labor_Api";
|
||||
import type { LaborFixed, LaborRef } from "./M02_MasterTemplete_Labor_Calc";
|
||||
@@ -156,6 +157,9 @@ interface PickItem {
|
||||
이름: string;
|
||||
규격?: string | null;
|
||||
단위?: string | null;
|
||||
/** 재료 지역 · 상하차 칸(830-10) */
|
||||
지역?: string | null;
|
||||
상하차?: string | null;
|
||||
값: unknown;
|
||||
}
|
||||
/** 표 한 줄 + 누름 */
|
||||
@@ -278,7 +282,7 @@ export function openFindPicker(opt: FindPickOptions): void {
|
||||
규격: e.규격,
|
||||
단위: e.단위,
|
||||
금액: e.값,
|
||||
구분: join(kind, m.구분, put ? "" : L("M02_FindElementLater")),
|
||||
구분: join(kind, m.구분, placeNote(m), put ? "" : L("M02_FindElementLater")),
|
||||
출처: `M01 ${m.ref}`,
|
||||
dim: !put,
|
||||
go: !put
|
||||
|
||||
@@ -20,12 +20,16 @@ import { MAT_PICKED } from "../A00_Common/line_stack_material";
|
||||
import { openStackPick } from "../A00_Common/line_stack_pick";
|
||||
import { call } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { at, type HopyoScope } from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import { placeNote } from "../M01_MasterData/M01_MasterData_Api_Fetch";
|
||||
|
||||
export interface MatCand {
|
||||
키: string;
|
||||
이름: string;
|
||||
규격: string;
|
||||
단위: string;
|
||||
/** 지역 · 상하차 칸(830-10) — 같은 이름 · 규격 줄을 가름 */
|
||||
지역?: string;
|
||||
상하차?: string;
|
||||
단서?: string;
|
||||
}
|
||||
|
||||
@@ -60,7 +64,8 @@ const LINKED = "이어짐";
|
||||
const TONE: Record<string, BadgeTone> = { [LINKED]: "success", "우리 표에 없음": "neutral" };
|
||||
const enc = encodeURIComponent;
|
||||
const why = (e: unknown): string => (e instanceof Error ? e.message : String(e));
|
||||
const label = (c: { 이름: string; 규격: string }): string => `${c.이름} ${c.규격}`.trim();
|
||||
const label = (c: { 이름: string; 규격: string; 지역?: string; 상하차?: string }): string =>
|
||||
[`${c.이름} ${c.규격}`.trim(), placeNote(c)].filter(Boolean).join(" · ");
|
||||
|
||||
export function createMatPick(opt: { scope: HopyoScope; key: string; target: HopyoScope }) {
|
||||
const to = `?to=${opt.target.layer}${opt.target.owner ? `&to_owner=${enc(opt.target.owner)}` : ""}`;
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
"""불러오기 재료 지역 · 상하차 한 벌(PLAN 830-10 · 요구 839) — 서버 단독 · 읽기만.
|
||||
|
||||
- `line(이름, 규격)` = 가져온 줄 글(경쟁사 이름 · 규격 — 지역 · 인도 조건 글이 그대로 옴)을 M01 재료 표 빌더와
|
||||
같은 규칙(`material_merge.split_load` · `split_region`)으로 읽음 → {이름, 규격, 지역, 상하차} · 글 없음 = -.
|
||||
- 우리 줄 = M01 재료 표 지역 · 상하차 칸(이름 글로 읽지 않음).
|
||||
- `rank(우리 줄, 바라는 것, 가운데 값)` = 같은 품목 줄 여럿에서 고르는 차례 —
|
||||
상하차가 줄 글과 같음(글 없으면 -) › 값 있는 줄(M01 검색어 고르기와 같음) › 지역: 줄 지역 › 전국(지역 모름 - 포함) › 서울 › 경기 ›
|
||||
그 밖(값이 가운데 값에 가까운 줄) — 조달청 지역 규칙과 같은 차례(요구 830).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from statistics import median
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01 # noqa: F401 — scripts 경로를 넣음
|
||||
|
||||
import material_merge as mmerge # noqa: E402
|
||||
|
||||
NONE = mmerge.NONE
|
||||
NATION = ("전국", "전지역", NONE)
|
||||
PRICES = ("물가자료", "유통물가", "물가정보", "거래가격", "일반가격") # 사급 값 칸(조달청 뺌)
|
||||
|
||||
|
||||
def line(name: Any, spec: Any) -> dict[str, str]:
|
||||
"""가져온 줄 → {이름, 규격(지역 · 인도 조건 글 뺌), 지역, 상하차}."""
|
||||
n, s, load, _ = mmerge.split_load({"이름": str(name or ""), "규격": str(spec or "")})
|
||||
n, s, where = mmerge.split_region(n, s)
|
||||
return {"이름": n, "규격": s, "지역": where, "상하차": load}
|
||||
|
||||
|
||||
def words(where: Any) -> list[str]:
|
||||
"""지역 글 → 한글 낱말(전국류 뺌) — 「경상북도(칠곡군)」 → 경상북도 · 칠곡군."""
|
||||
return [w for w in re.findall(r"[가-힣]{2,}", str(where or "")) if w not in NATION]
|
||||
|
||||
|
||||
def hit(where: Any, want: Any) -> bool:
|
||||
"""우리 지역 글이 줄 지역 글과 맞음 — 같은 글 · 낱말 앞 두 글자가 하나라도 같음(「경북」 · 「경북제일」)."""
|
||||
if not want or want == NONE:
|
||||
return False
|
||||
if str(where) == str(want):
|
||||
return True
|
||||
mine = words(want) if isinstance(want, str) else list(want)
|
||||
return any(a[:2] == b[:2] for a in mine for b in words(where))
|
||||
|
||||
|
||||
def value(row: dict) -> Any:
|
||||
"""사급 값 칸 다섯 가운데 낮은 값(줄에 「값」 이 있으면 그것) — 없음 = None."""
|
||||
if "값" in row:
|
||||
return row["값"]
|
||||
got = [row[c] for c in PRICES if row.get(c) is not None]
|
||||
return min(got) if got else None
|
||||
|
||||
|
||||
def middle(rows: list[dict]) -> Any:
|
||||
got = [v for v in (value(r) for r in rows) if v is not None]
|
||||
return median(got) if got else None
|
||||
|
||||
|
||||
def rank(row: dict, want_where: Any, want_load: str, mid: Any) -> tuple:
|
||||
"""고르는 차례 열쇠(작을수록 먼저) — (상하차 다름, 값 없음, 지역 차례, 가운데 값과의 거리)."""
|
||||
where = str(row.get("지역") or NONE)
|
||||
if hit(where, want_where):
|
||||
tier = 0
|
||||
elif where in NATION or not words(where):
|
||||
tier = 1
|
||||
else:
|
||||
tier = 2 if where.startswith("서울") else 3 if where.startswith("경기") else 4
|
||||
v = value(row)
|
||||
gap = (
|
||||
0.0
|
||||
if tier < 4 or mid is None
|
||||
else float("inf")
|
||||
if v is None
|
||||
else abs(float(v) - float(mid))
|
||||
)
|
||||
return ((row.get("상하차") or NONE) != (want_load or NONE), v is None, tier, gap)
|
||||
@@ -352,5 +352,11 @@ def _cands(line: dict, narrowed: list[dict], used: list) -> list[tuple]:
|
||||
|
||||
|
||||
def _pub(one: dict, why: str) -> tuple:
|
||||
from M02_MasterTemplete import M02_Elem_Alias as ea
|
||||
|
||||
got = {"키": one.get("자재"), **{k: str(one.get(k) or "") for k in ("이름", "규격", "단위")}}
|
||||
row = ea.row(ea.MARKET, got["키"]) if got["키"] else {}
|
||||
got |= {
|
||||
k: str(one.get(k) or row.get(k) or "") for k in ("지역", "상하차")
|
||||
} # 830-10 다시 고르기 창
|
||||
return tuple((got | ({"단서": why} if why else {})).items())
|
||||
|
||||
@@ -166,7 +166,9 @@ def test_oil_power_material_links() -> None:
|
||||
assert got[dim.OPTION]["연결"]["값"] == "일반용(갑)Ⅰ 저압"
|
||||
ply, _ = link.m01("재료", {"이름": "합판(내수)", "규격": "12t×1220×2440mm", "단위": "㎡"})
|
||||
nail, _ = link.m01("재료", {"이름": "철못", "규격": "N 25", "단위": "kg"})
|
||||
assert whole.get(ply)["이름"] == "합판(내수), 서울" and whole.get(nail)["규격"] == "N 25"
|
||||
assert (whole.get(ply)["이름"], whole.get(ply)["지역"]) == ("합판(내수)", "서울") and whole.get(
|
||||
nail
|
||||
)["규격"] == "N 25"
|
||||
assert link.m01("재료", {"이름": "접착제", "규격": "", "단위": "kg"}) == (None, None)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
"""830-10 지킴 — 재료 표 지역 칸 분리 · 상하차 재분석 · M01 로직이 같은 줄 · 불러오기가 지역 칸으로 고름(요구 839).
|
||||
|
||||
- 지역 칸 = 단위 바로 뒤 · 상하차 앞 · 빈 칸 없음(모름 = -) · 이름 · 규격에 지역 글 0 · 인도 조건 글 0
|
||||
- 꼴 일곱 판독(빌더 = `material_merge.split_region`) · 지역 아닌 업체 · 산지 · 수종 글은 그대로
|
||||
- 상하차: (상차포함) = 상차 · (상차제외) = - · 같은 이름 · 규격 둘이 상하차 칸으로 갈림
|
||||
- 줄 · 키 = 24,027 · 지역 칸만 다른 줄 겹침 없음(이름 · 규격 · 단위 · 지역 · 상하차 같은 줄 = 830-9 때 겹침뿐)
|
||||
- M01 로직: 검색어 지역 낱말 · 자재지역 입력이 지역 칸과 맞대져 고치기 전 줄(시멘트 전국 · 포항 모래 · 포항 레미콘)
|
||||
- 불러오기: 가져온 이름의 지역 · 인도 조건 글 판독 · 줄 지역 › 전국 › 서울 › 경기 › 그 밖 · 상하차 맞는 줄
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
for p in (ROOT, ROOT / "resources/master_data/scripts"):
|
||||
if str(p) not in sys.path:
|
||||
sys.path.insert(0, str(p))
|
||||
|
||||
import material_merge as mm # noqa: E402
|
||||
|
||||
MASTER = ROOT / "resources/master_data"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def rows():
|
||||
return json.loads((MASTER / "재료_자재품목.json").read_text(encoding="utf-8"))["줄"]
|
||||
|
||||
|
||||
def test_지역_칸_자리_빈칸_없음(rows):
|
||||
cols = list(mm.COLUMNS)
|
||||
assert cols[cols.index("단위") + 1 : cols.index("단위") + 3] == ["지역", "상하차"]
|
||||
assert len(rows) == 24027 and all(str(r["지역"]).strip() for r in rows)
|
||||
got = Counter(r["지역"] != "-" for r in rows)
|
||||
assert got[True] == 6604 and got[False] == 17423
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name,spec,want",
|
||||
[
|
||||
("합판(내수), 서울", "12t", ("합판(내수)", "12t", "서울")), # F1
|
||||
("레미콘(서울)-사급", "25-18-120", ("레미콘-사급", "25-18-120", "서울")), # F2
|
||||
("레미콘(관급)-경상북도(칠곡군)", "", ("레미콘(관급)", "", "경상북도(칠곡군)")), # F3
|
||||
("이형철근(SD400,제주)-관급", "HD=10mm", ("이형철근(SD400)-관급", "HD=10mm", "제주")), # F4
|
||||
("H 형강(서울)", "SS275", ("H 형강", "SS275", "서울")), # F5
|
||||
("모래(건조상태·전국)", "", ("모래(건조상태)", "", "전국")),
|
||||
("도로용혼합골재(충남지역,관급)", "40mm", ("도로용혼합골재(관급)", "40mm", "충남지역")),
|
||||
("STEEL PIPE", "42C (서울)", ("STEEL PIPE", "42C", "서울")), # F6
|
||||
("콘크리트근가", "0.7M(제주지역)", ("콘크리트근가", "0.7M", "제주지역")),
|
||||
("운반비", "포항", ("운반비", "", "포항")), # F7
|
||||
("도로용혼합골재(관급)", "농산개발(홍성), 75mm", None), # 업체 · 지역 아님
|
||||
("조경석(관급)", "R5이상~15미만,보령웅천석(보령개발)", None), # 산지
|
||||
("레미콘(관급)-거양", "25-21-120", None), # 제조사
|
||||
("묘목, 잣나무", "1-0", None), # 수종
|
||||
("모래(건조상태·지역별)", "", None),
|
||||
],
|
||||
)
|
||||
def test_꼴_일곱_판독(name, spec, want):
|
||||
assert mm.split_region(name, spec) == (want or (name, spec, "-"))
|
||||
|
||||
|
||||
def test_이름_규격에_지역_인도_조건_글_0(rows):
|
||||
assert not [r for r in rows if mm.split_region(r["이름"], r["규격"])[2] != "-"]
|
||||
assert not [r for r in rows if mm.split_load(r)[3]]
|
||||
assert {r["지역"] for r in rows if r["이름"] == "레미콘-사급"} >= {"서울", "포항"}
|
||||
cut = [r for r in rows if mm.CUT in str(r["비고"] or "")]
|
||||
assert len(cut) == 30 and all(r["지역"].count("(") > r["지역"].count(")") for r in cut)
|
||||
|
||||
|
||||
def test_상하차_재분석(rows):
|
||||
assert mm.split_load({"이름": "폐기물운반비(상차포함)", "규격": "30km이하"})[:3] == (
|
||||
"폐기물운반비",
|
||||
"30km이하",
|
||||
"상차",
|
||||
)
|
||||
assert mm.split_load({"이름": "폐기물운반비(상차제외)", "규격": ""})[2:] == ("-", "상차제외")
|
||||
waste = [r for r in rows if r["이름"] == "폐기물운반비"]
|
||||
assert Counter(r["상하차"] for r in waste) == {"상차": 10, "-": 10}
|
||||
assert len({(r["규격"], r["상하차"]) for r in waste}) == 20 # 상하차 칸으로 갈림
|
||||
assert Counter(r["상하차"] for r in rows) == {"-": 23633, "상차": 135, "하차": 259}
|
||||
|
||||
|
||||
def test_지역만_다른_줄_겹침_없음(rows):
|
||||
keys = Counter((r["이름"], r["규격"], r["단위"], r["지역"], r["상하차"]) for r in rows)
|
||||
dup = {k: n for k, n in keys.items() if n > 1}
|
||||
assert (
|
||||
all(k[3] == "-" for k in dup) and sum(dup.values()) == 866
|
||||
) # EST 에서 온 겹침(830-9 와 같음)
|
||||
|
||||
|
||||
def test_로직이_같은_줄():
|
||||
import check_master as cm
|
||||
import master_formula as mf
|
||||
|
||||
files = cm.load()
|
||||
whole = mf.Master(files)
|
||||
rows = {str(r["키"]): r for d in files.values() if d.get("그룹") == "로직" for r in d["줄"]}
|
||||
|
||||
def picked(key: str, name: str) -> str:
|
||||
got = mf.run(whole, key, cm.시험입력(rows[key]))
|
||||
return next(x["품목"]["키"] for x in got["줄"] if x.get("이름") == name)
|
||||
|
||||
assert picked("GC000628", "시멘트") == "MT004176" # 검색어 「시멘트 전국」
|
||||
assert picked("GC000677", "모래") == "MT003834" # 자재지역 입력(포항)
|
||||
assert picked("GF000174", "기초콘크리트(레미콘) 무근") == "MT004876" # 포항 레미콘
|
||||
|
||||
|
||||
def test_불러오기가_지역_칸으로_고름():
|
||||
from M02_MasterTemplete import M02_Elem_Alias as ea
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
from M02_MasterTemplete import M02_Mat_Place as place
|
||||
|
||||
assert place.line("모 래, 서울, 도착도", "") == {
|
||||
"이름": "모 래",
|
||||
"규격": "",
|
||||
"지역": "서울",
|
||||
"상하차": "하차",
|
||||
}
|
||||
pick = lambda name, spec: ea.row(
|
||||
ea.MARKET, ea.one({"이름": name, "규격": spec}, ea.exact(name, spec))
|
||||
) # noqa: E731, E501
|
||||
assert pick("모르타르, 부산", "1:3")["지역"] == "부산" # 줄 지역
|
||||
assert pick("모르타르", "1:3")["지역"] == "서울" # 전국 없음 → 서울
|
||||
assert pick("레미콘(포항)-사급", "25-18-120")["지역"] == "포항"
|
||||
got = hm._exact({"이름": "레미콘(서울)-사급", "규격": "25-18-120"}, "자재", hm.targets())
|
||||
assert [(x["이름"], x["지역"]) for x in got] == [("레미콘-사급", "서울")] # 옛 온 이름 같음
|
||||
row = lambda where, v=1: {"지역": where, "상하차": "-", "물가자료": v} # noqa: E731
|
||||
order = sorted(
|
||||
[
|
||||
row("부산", 300),
|
||||
row("경기"),
|
||||
row("서울"),
|
||||
row("전국"),
|
||||
row("대구", 120),
|
||||
row("울산", 100),
|
||||
],
|
||||
key=lambda r: place.rank(r, "", "-", 120),
|
||||
)
|
||||
assert [r["지역"] for r in order] == ["전국", "서울", "경기", "대구", "울산", "부산"]
|
||||
assert place.rank(row("부산"), "부산", "-", None)[2] == 0
|
||||
assert place.rank({**row("서울"), "상하차": "하차"}, "", "-", None)[0] # 상하차 다름 = 뒤
|
||||
@@ -137,10 +137,13 @@ def test_전국_지역_줄글_전국품목():
|
||||
assert hmat.national(paid)["이름"] == "레미콘(전국)-사급" # 재료비 줄 = 사급 꼬리
|
||||
named = {**REMICON, "비고": "강원도 강릉시"} # 줄 글에 지역 = 그 지역(원문)
|
||||
rows, tags = hmat.sure(hmat.clean(named)[0])
|
||||
assert rows[0]["one"]["이름"] == "레미콘(관급)-강원도(강릉시)" and tags == [hmat.REGION_TAG]
|
||||
assert (rows[0]["one"]["이름"], rows[0]["one"]["지역"]) == (
|
||||
"레미콘(관급)",
|
||||
"강원도(강릉시)",
|
||||
) and tags == [hmat.REGION_TAG]
|
||||
assert hmat.national(named) is None
|
||||
rows, tags = hmat.sure(hmat.clean(H_BEAM)[0]) # (전국) 품목 있음 = 그것
|
||||
assert rows[0]["one"]["자재"] == "MT001694" and tags == []
|
||||
assert hmat.national(H_BEAM) is None
|
||||
seoul = hmat.sure(hmat.clean({**H_BEAM, "비고": "서울"})[0])[0][0]["one"]
|
||||
assert seoul["이름"] == "H 형강(서울)"
|
||||
assert (seoul["이름"], seoul["지역"]) == ("H 형강", "서울") # 830-10 지역 칸
|
||||
|
||||
Reference in New Issue
Block a user