feat(830-10): M01 재료 찾기가 지역 칸을 봄 — 로직 1,520 시험 계산 전후 다름 0
- master_material.label = 이름 + 지역 칸 · 검색어 지역 낱말 · 자재지역 입력 · 고르는 차례(master_pick)가 이름 글 대신 지역 칸과 맞대짐 - 로직 1,520: 같음 1,517 · 전부터 멈춤 3(GC001199 ~ 1201) — 지역 칸만 고쳤을 때 다르던 11(시멘트 전국 · 포항 모래 · 포항 레미콘)이 원래 줄로 - 계산 줄 품목 칸에 지역 · 상하차 · M01 고르기 창(가격 연결 · 로직 재료 후보) 글에 지역 · 상하차(모름 「-」 뺌) - M01 표 찾기(Store_Find) 낱말이 지역 칸에도 맞음 — 「레미콘 서울」 · 저장 뒤 찾기 기억 지움도 지역 칸 포함 - 새 줄 지역 기본 「-」 · test_m01_pick 합판 서울 기대를 지역 칸으로 - check_master 틀 · 본문 · 로직 · 조합 · 목차 0 · 단위 27 · 배 7(전과 같음) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -190,10 +190,16 @@ export interface PickItem {
|
||||
이름: string;
|
||||
규격: string;
|
||||
단위: string;
|
||||
지역?: string | null;
|
||||
상하차?: string | null;
|
||||
값: unknown;
|
||||
관급: boolean;
|
||||
}
|
||||
|
||||
/** 재료 줄 지역 · 상하차 글(830-10 · 요구 839) — 「-」(모름)은 뺌 · 둘 다 없으면 빈 글 */
|
||||
export const placeNote = (it: { 지역?: string | null; 상하차?: string | null }): string =>
|
||||
[it.지역, it.상하차].filter((v) => v && v !== "-").join(" · ");
|
||||
|
||||
export const fetchPick = (
|
||||
kind: PickKind,
|
||||
q: string,
|
||||
|
||||
@@ -487,12 +487,15 @@ def pick(kind: str, q: str, limit: int) -> dict:
|
||||
row.get("값") is None or row.get("구분") == "미확보" or row.get("상태") == "추정"
|
||||
):
|
||||
continue
|
||||
text = _squash(f"{row.get('이름')} {row.get('규격')} {' '.join(row.get('옛이름') or [])}")
|
||||
text = _squash(f"{row.get('이름')} {row.get('규격')} {row.get('지역') or ''} {' '.join(row.get('옛이름') or [])}") # fmt: skip
|
||||
if all(w in text or w == _squash(row.get("키")) for w in words):
|
||||
hits.append(
|
||||
{
|
||||
"ref": str(row.get("키")),
|
||||
**{k: row.get(k) for k in ("원문번호", "구분", "이름", "규격", "단위")},
|
||||
**{
|
||||
k: row.get(k)
|
||||
for k in ("원문번호", "구분", "이름", "규격", "단위", "지역", "상하차")
|
||||
}, # fmt: skip
|
||||
"값": _lowest(row),
|
||||
"관급": row.get(cm.mm.GOVT) is not None,
|
||||
}
|
||||
@@ -504,7 +507,7 @@ def _market_brief(row: dict) -> dict:
|
||||
"""후보 한 줄 — 값 열 다섯을 다 주고 `값` 에 시험 계산이 쓸 낮은 값(없으면 null)."""
|
||||
return {
|
||||
"ref": str(row.get("키")),
|
||||
**{k: row.get(k) for k in ("구분", "상세구분", "이름", "규격", "단위")},
|
||||
**{k: row.get(k) for k in ("구분", "상세구분", "이름", "규격", "단위", "지역", "상하차")},
|
||||
"값들": {s: row.get(s) for s in cm.mf.SLOTS},
|
||||
"값": _lowest(row),
|
||||
"관급": row.get(cm.mm.GOVT) is not None,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""찾기 맞춤 — 낱말(띄어 나눔)마다 키 · 원문번호 · 이름 · 옛이름 · 별칭 가운데 하나에 들면 맞음.
|
||||
"""찾기 맞춤 — 낱말(띄어 나눔)마다 키 · 원문번호 · 이름 · 옛이름 · 별칭 · 지역(재료 · 830-10) 가운데 하나에 들면 맞음.
|
||||
|
||||
띄어쓰기 · 대소문자 무시 · 별칭 = 설계자가 로직을 찾는 다른 이름(쉼표로 여럿 · 208-1).
|
||||
짝: `ui_template/ui_template_logic_find.ts` `matchLogic`(화면 쪽) ·
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
FIELDS = ("키", "원문번호", "이름", "옛이름", "별칭")
|
||||
FIELDS = ("키", "원문번호", "이름", "옛이름", "별칭", "지역")
|
||||
|
||||
|
||||
def squash(text) -> str:
|
||||
|
||||
@@ -49,9 +49,7 @@ def _index(files: dict, whole, moved: list[dict]):
|
||||
setattr(out, name, getattr(whole, name))
|
||||
return out
|
||||
found = getattr(whole, "_found", None) or {}
|
||||
texts = [
|
||||
(mm.plain(r.get("이름")).replace("×", "x"), mm.plain_spec(r.get("규격")), r) for r in moved
|
||||
]
|
||||
texts = [(mm.label(r).replace("×", "x"), mm.plain_spec(r.get("규격")), r) for r in moved]
|
||||
|
||||
def hit(memo_key) -> bool: # `master_pick._search` 의 거름과 같음 — 걸리면 답이 바뀔 수 있음
|
||||
term, kind, detail = memo_key[:3]
|
||||
|
||||
@@ -76,6 +76,9 @@ export interface ElementBrief {
|
||||
별칭?: string;
|
||||
/** 재료 후보 줄 — 값 열 다섯(지역·계약종별별 값) */
|
||||
값들?: Record<string, unknown>;
|
||||
/** 재료 후보 줄 — 지역 · 상하차 칸(830-10) */
|
||||
지역?: string | null;
|
||||
상하차?: string | null;
|
||||
}
|
||||
|
||||
export interface LogicOne {
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
type LogicOne,
|
||||
type LogicRow,
|
||||
} from "./M01_MasterData_UI_Logic_Api";
|
||||
import { placeNote } from "./M01_MasterData_Api_Fetch";
|
||||
import { formatNumber, type PickDone, type PickMode } from "./M01_MasterData_UI_Logic_Edit";
|
||||
import { tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
@@ -101,7 +102,11 @@ function itemRow(
|
||||
const text = el("div", {
|
||||
className: "m01-logic__stack",
|
||||
children: [
|
||||
el("strong", { text: `${item.이름 ?? ""} ${item.규격 ?? ""}`.trim() }),
|
||||
el("strong", {
|
||||
text: [`${item.이름 ?? ""} ${item.규격 ?? ""}`.trim(), placeNote(item)]
|
||||
.filter(Boolean)
|
||||
.join(" · "),
|
||||
}),
|
||||
el("span", {
|
||||
className: "ui-text ui-text--muted",
|
||||
text: [item.ref, note].filter(Boolean).join(" · "),
|
||||
@@ -170,7 +175,9 @@ const label = (it: Brief): string => {
|
||||
.filter(([, v]) => v !== null && v !== undefined)
|
||||
.map(([k, v]) => `${k} ${formatNumber(v)}`);
|
||||
const prices = cols.length ? cols : [formatNumber(it.값) || tx("Mat_NoPrice")];
|
||||
return [`${it.이름 ?? ""} ${it.규격 ?? ""}`.trim(), ...prices].join(" · ");
|
||||
return [`${it.이름 ?? ""} ${it.규격 ?? ""}`.trim(), placeNote(it), ...prices]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
};
|
||||
|
||||
const oldNames = (one: LogicOne, line: HoLine): string[] => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { createEmpty } from "@ui/ui_template_empty";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { fetchPick, type PickItem, type PickKind } from "./M01_MasterData_Api_Fetch";
|
||||
import { fetchPick, placeNote, type PickItem, type PickKind } from "./M01_MasterData_Api_Fetch";
|
||||
import { show } from "./M01_MasterData_UI_Cells";
|
||||
|
||||
/** 지역이 갈리는 재료 — 이름·규격만 적어 두고 지역은 계산 때 입력. */
|
||||
@@ -75,7 +75,8 @@ export function openPickModal(opt: PickOptions): void {
|
||||
list.replaceChildren(
|
||||
...(items.length
|
||||
? items.map((it) => {
|
||||
const tag = it.관급 ? ` · ${L("M01_Govt")}` : "";
|
||||
const tag =
|
||||
(it.관급 ? ` · ${L("M01_Govt")}` : "") + (placeNote(it) && ` · ${placeNote(it)}`);
|
||||
const b = el("button", {
|
||||
className: "m01-procure__item ui-list-row ui-sidebar-row",
|
||||
attrs: { type: "button", "data-size": "36" },
|
||||
|
||||
@@ -250,7 +250,7 @@ export function renderRows(
|
||||
const add = createButton({ label: L("M01_RowAdd"), variant: "ghost" });
|
||||
add.addEventListener("click", () => {
|
||||
const fresh = blank(d.rows[0] ?? FALLBACK);
|
||||
if (file === MARKET) fresh[LOAD] = LOADS[0];
|
||||
if (file === MARKET) Object.assign(fresh, { [LOAD]: LOADS[0], 지역: LOADS[0] }); // 모름 = -(830-10)
|
||||
const col = ["구분", "세부분류"].find((c) => c in fresh);
|
||||
addRow(file, d.version, sub && col ? { ...fresh, [col]: sub } : fresh);
|
||||
});
|
||||
|
||||
@@ -44,6 +44,12 @@ def plain(s) -> str:
|
||||
return re.sub(r"\s+", "", re.sub(r"[φΦ∅⌀]", "ø", s))
|
||||
|
||||
|
||||
def label(row: dict) -> str:
|
||||
"""맞대기 글 = 이름 + 지역 칸(요구 839 · 830-10) — 검색어 · 자재지역의 지역 낱말이 지역 칸과 맞대짐."""
|
||||
where = row.get("지역")
|
||||
return plain(row.get("이름")) + ("" if where in (None, "", "-") else plain(where))
|
||||
|
||||
|
||||
def plain_spec(s) -> str:
|
||||
return plain(s).replace("×", "x").replace("*", "x")
|
||||
|
||||
@@ -67,14 +73,14 @@ def _lowest(master: mf.Master, key: str) -> tuple[object, str | None]:
|
||||
|
||||
|
||||
def _market_index(master: mf.Master) -> list[tuple[str, str, str, str, str]]:
|
||||
"""(품명 뿌리, 이름, 규격, 단위, 키) — 「레미콘(관급)-경상북도(울진군)」 뿌리 = 레미콘."""
|
||||
"""(품명 뿌리, 이름 + 지역, 규격, 단위, 키) — 「레미콘(관급)」 뿌리 = 레미콘 · 지역 = 지역 칸(`label`)."""
|
||||
if not hasattr(master, "_market"):
|
||||
master._market = []
|
||||
for key, row in master.index.get("MT", {}).items():
|
||||
name = plain(row.get("이름"))
|
||||
root = re.sub(r"\(.*?\)", "", name.split(",")[0]).split("-")[0]
|
||||
master._market.append(
|
||||
(root, name, plain_spec(row.get("규격")), plain_unit(row.get("단위")), key)
|
||||
(root, label(row), plain_spec(row.get("규격")), plain_unit(row.get("단위")), key)
|
||||
)
|
||||
return master._market
|
||||
|
||||
@@ -99,7 +105,7 @@ def _tariff(master: mf.Master, label_ref, cond: dict, env: dict) -> list[str]:
|
||||
|
||||
|
||||
def _region(master: mf.Master, label_ref, cond: dict, env: dict, unit) -> list[str]:
|
||||
"""후보 조건{이름 · 규격 · 지역: 입력} — 뿌리가 이름으로 끝나고 이름에 지역 글자 · 단위 같음."""
|
||||
"""후보 조건{이름 · 규격 · 지역: 입력} — 뿌리가 이름으로 끝나고 이름 · 지역 칸에 지역 글자 · 단위 같음."""
|
||||
if cond.get("지역") == "입력" and REGION not in env:
|
||||
raise mf.FormulaError(f"「{master.label(label_ref)}」 입력 「{REGION}」 없음")
|
||||
region = plain(env.get(REGION, "")) if cond.get("지역") == "입력" else ""
|
||||
@@ -149,7 +155,7 @@ def candidates(
|
||||
) -> list[str]:
|
||||
"""고르기 조건 안 자재품목 키 — 구분 · 상세구분 · 이름 · 규격 낱말이 모두 든 줄 · 파일 차례대로.
|
||||
`unit` 을 주면 그 단위(호표 줄 단위)와 같은 줄만 — 단위가 다르면 다른 물건.
|
||||
입력 `자재지역` 이 있으면 이름에 그 지역이 든 줄만(그런 줄이 없으면 조건 안 전부).
|
||||
입력 `자재지역` 이 있으면 이름 · 지역 칸에 그 지역이 든 줄만(그런 줄이 없으면 조건 안 전부).
|
||||
조건에 `검색어` 가 있으면 기본 검색어 찾기(`master_pick`) ·
|
||||
빈 검색어 = 자재표에 없는 품목(후보 없음)."""
|
||||
if mp.empty_term(cond):
|
||||
@@ -169,7 +175,7 @@ def candidates(
|
||||
and (not need or plain_unit(row.get("단위")) == need)
|
||||
]
|
||||
region = plain((env or {}).get(REGION, ""))
|
||||
near = [k for k in hits if region in plain(master.get(k).get("이름"))] if region else []
|
||||
near = [k for k in hits if region in label(master.get(k))] if region else []
|
||||
return near or hits
|
||||
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ def _search(master, cond: dict, env: dict | None = None, unit: str | None = None
|
||||
|
||||
def rank(key):
|
||||
one = rows[key]
|
||||
name = mm.plain(one.get("이름"))
|
||||
name = mm.label(one) # 이름 + 지역 칸(830-10)
|
||||
return (
|
||||
not mm._offers(master, key),
|
||||
bool(want) and mm.plain_unit(one.get("단위")) != want,
|
||||
@@ -123,7 +123,7 @@ def pick(master, cond: dict, env: dict, unit: str | None = None) -> str:
|
||||
def priced(master, item: dict, ref, env: dict, index: int | None):
|
||||
"""재료 줄 단가 — (단가, 출처, 까닭, 단위경고). 고른 품목(`고름`)이 있으면 그것 · 검색어가 있으면 찾기 ·
|
||||
없으면 옛 길. 자재 단가는 줄 단위로 환산 · 성격이 다르면 그대로 두고 경고.
|
||||
넷째 = 줄에 덧붙일 칸 — `단위경고` · `품목`(잡힌 품목 키 · 이름 · 규격 · 단위 · 값 칸)."""
|
||||
넷째 = 줄에 덧붙일 칸 — `단위경고` · `품목`(잡힌 품목 키 · 이름 · 규격 · 단위 · 지역 · 상하차 · 값 칸)."""
|
||||
price, source, why, warn = _priced(master, item, ref, env, index)
|
||||
extra = {"단위경고": warn} if warn else {}
|
||||
key, _, slot = (source or "").partition(".")
|
||||
@@ -137,6 +137,7 @@ def priced(master, item: dict, ref, env: dict, index: int | None):
|
||||
"이름": row.get("이름"),
|
||||
"규격": row.get("규격"),
|
||||
"단위": row.get("단위"),
|
||||
**{c: row[c] for c in ("지역", "상하차") if c in row}, # 830-10
|
||||
"값칸": slot,
|
||||
}
|
||||
return price, source, why, extra
|
||||
|
||||
@@ -137,12 +137,14 @@ def test_재료_줄마다_품목_칸(client) -> None:
|
||||
i for i, x in enumerate(lines) if x.get("품목", {}).get("이름", "").startswith("합판(내수)")
|
||||
)
|
||||
first = lines[idx]["품목"]
|
||||
assert first["이름"] == "합판(내수), 서울" and first["값칸"]
|
||||
assert (first["이름"], first["지역"]) == ("합판(내수)", "서울") and first[
|
||||
"값칸"
|
||||
] # 830-10 지역 칸
|
||||
assert {"키", "이름", "규격", "단위", "값칸"} <= set(first)
|
||||
lines = client.post("/api/m01/calc", json={**body, "고름": {str(idx): "MT000019"}}).json()[
|
||||
"lines"
|
||||
]
|
||||
assert lines[idx]["품목"]["키"] == "MT000019" and "인천" in lines[idx]["품목"]["이름"]
|
||||
assert lines[idx]["품목"]["키"] == "MT000019" and "인천" in lines[idx]["품목"]["지역"]
|
||||
|
||||
|
||||
def test_목록_검색은_띄어쓰기를_무시(client) -> None:
|
||||
|
||||
Reference in New Issue
Block a user