From 8759bd79293ecfcf5b7207c509f7360d7e8c03d5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 24 Sep 2026 22:07:11 +0900 Subject: [PATCH] =?UTF-8?q?fix(M01):=20=EC=9E=A1=ED=9E=8C=20=ED=92=88?= =?UTF-8?q?=EB=AA=A9=20=EC=9D=B4=EB=A6=84=20=C2=B7=20=EB=AA=A8=EB=8B=AC=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EC=B9=B8=20=EA=B4=84=ED=98=B8=20=EC=95=9E?= =?UTF-8?q?=20=EB=82=B1=EB=A7=90=20=C2=B7=20=EC=83=81=EC=84=B8=20=EC=9E=AC?= =?UTF-8?q?=EB=A3=8C=20=EC=A4=84=20=EB=AA=A8=EB=8B=AC=20=EC=84=9C=EB=B2=84?= =?UTF-8?q?=20=ED=92=88=EB=AA=A9=20=C2=B7=20=EB=8B=A8=EC=9C=84=EA=B2=BD?= =?UTF-8?q?=EA=B3=A0=20=EB=94=B1=EC=A7=80=20=EC=83=81=EC=84=B8=20=EC=A4=84?= =?UTF-8?q?=EC=97=90=EB=8F=84=20(PLAN=208-5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT --- .../M01_MasterData_UI_LogicLab_Detail.ts | 11 ++++++ .../M01_MasterData_UI_LogicLab_Modal.ts | 34 ++++++++++++------- M01_MasterData/M01_MasterData_UI_Logic_Api.ts | 2 ++ .../M01_MasterData_UI_Logic_Calc.ts | 2 ++ .../M01_MasterData_UI_Logic_CalcPick.ts | 30 ++++++++++++---- 5 files changed, 59 insertions(+), 20 deletions(-) diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts index 7a030dd2..5842ec45 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts @@ -106,6 +106,8 @@ interface Entry { /** 서버 요소조각의 첫 요소 — 자리 표시 든 줄이 입력으로 풀린 키 · 글 */ unitRef?: string; unitText?: string; + /** 자재 단위가 줄 단위와 성격이 다름 — 「줄 m · 자재 Ton」 */ + warn?: string; } /** 재료 고르기 줄 — `요소` 가 글 대신 {구분·상세구분·규격·대표} 객체 */ @@ -237,6 +239,7 @@ function entries(ctx: DetailContext): Entry[] { qtyFrag: [{ text: item.수량 }], price: price === undefined || price === null ? "" : formatPrice(price), amount: line ? line.금액 : null, + warn: line?.단위경고, extra: false, open: (self) => openMaterialModal({ @@ -338,6 +341,14 @@ function lineRow(e: Entry): HTMLElement { ? [el("div", { className: "m01-logic__tag", text: `${e.share} ${tl("Share")}` })] : []), ...(e.spec ? [el("div", { className: "m01-logic__muted", text: e.spec })] : []), + ...(e.warn + ? [ + el("div", { + className: "m01-logic__tag m01lab__warn", + text: `${tx("Calc_UnitWarn")} · ${e.warn}`, + }), + ] + : []), ], }), cellOf(e.unit), diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts index 76c267e9..1d197668 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts @@ -10,7 +10,6 @@ import { createButton, el } from "@ui/ui_template_elements"; import { - fetchMaterials, searchElements, type ElementBrief, type NamedFormula, @@ -46,7 +45,18 @@ export interface PickCond { export const condText = (cond: PickCond): string => ["구분", "상세구분", "이름", "규격", "대표", "지역", "계약종별"] .map((k) => String(cond[k] ?? "")) - .join("|"); + .join("|") + (cond["검색어"] ? `|${cond["검색어"]}` : ""); + +/** 재료 모달 검색 칸 글 — 검색어 첫 낱말의 괄호 앞(「합판(내수) 12t」 → 「합판」) */ +export const headWord = (text: unknown): string => + String(text ?? "") + .trim() + .split(/\s+/)[0] + .replace(/[((].*$/, ""); + +/** 고르기 줄 후보 찾기 글 — 검색어 → 이름 → 상세구분 → 구분 차례 첫 낱말 */ +export const seedOf = (cond: PickCond): string => + [cond["검색어"], cond.이름, cond.상세구분, cond.구분].map(headWord).find(Boolean) ?? ""; export interface ModalTarget { title: string; @@ -227,19 +237,12 @@ const muted = (text: string): HTMLElement => el("p", { className: "m01-logic__mu /** 재료 고르기 줄 후보 목록 — 잡힌 품목(`pickedRef`)을 노란 줄로 · 후보가 100건을 넘으면 앞 100건만 옴 */ function candidateView( cond: PickCond, - unit: string, pickedRef: string, onPicked: (item: ElementBrief | null) => void, ): HTMLElement { const box = el("div", { className: "m01lab__data", attrs: { "data-candidates": "1" } }); box.append(muted("…")); - void fetchMaterials({ - sub: String(cond["구분"] ?? ""), - detail: String(cond["상세구분"] ?? ""), - spec: String(cond["규격"] ?? ""), - region: "", - unit, - }) + void searchElements("재료", seedOf(cond)) .then((got) => { onPicked(got.items.find((it) => it.ref === pickedRef) ?? null); const rows = got.items.map((it) => @@ -301,12 +304,17 @@ export function openMaterialModal(target: ModalTarget): void { const element = target.element ?? ""; const base = element.split(".")[0]; // 고르기 줄 = 서버가 조건 글을 열쇠로 준 줄(대표가 안 정해졌으면 없음) + const priced = cond ? target.prices[condText(cond)] : undefined; const known = cond - ? target.prices[condText(cond)] + ? target.unitRef && priced?.ref !== target.unitRef + ? undefined // 서버가 잡은 품목이 다르면 그 품목으로 + : priced : element ? (target.prices[element] ?? target.prices[base]) : undefined; - const priceRef = cond ? pickedOf(cond, target.values, known?.ref ?? "") : element; + const priceRef = cond + ? target.unitRef || pickedOf(cond, target.values, known?.ref ?? "") + : element; const logicKey = /^로직\((\w+)/.exec(element)?.[1] ?? ""; const sum = el("p", { className: "m01lab__sum" }); const setSum = (price: ElementBrief | null | undefined): void => { @@ -366,7 +374,7 @@ export function openMaterialModal(target: ModalTarget): void { rate, ...(cond ? [ - candidateView(cond, target.unit ?? "", priceRef, (item) => { + candidateView(cond, priceRef, (item) => { if (known || !priceRef) return; const show = (brief: ElementBrief | null): void => { rate.replaceChildren( diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Api.ts b/M01_MasterData/M01_MasterData_UI_Logic_Api.ts index 809d64f6..c61432a3 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_Api.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_Api.ts @@ -90,6 +90,8 @@ export interface CalcLine { 출처?: string; /** 자재 단위가 줄 단위와 성격이 달라 환산 못 하고 셈 — 「줄 m · 자재 Ton」 */ 단위경고?: string; + /** 재료 줄이 잡은 품목 — 키 · 이름 · 규격 · 단위 · 값 칸 */ + 품목?: { 키: string; 이름?: string; 규격?: string; 단위?: string; 값칸?: string }; } export type CalcAnswer = | { diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts b/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts index 0f1fdd8f..a8e22451 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_Calc.ts @@ -21,6 +21,7 @@ import { machineControl, materialRows, pickBody, + showItems, } from "./M01_MasterData_UI_Logic_CalcPick"; import { formatNumber } from "./M01_MasterData_UI_Logic_Edit"; import { formatMoney } from "./M01_MasterData_UI_Logic_Money"; @@ -183,6 +184,7 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void { const answer = await runCalc(body); if (ctx.onText) ctx.onText(await runText(body)); ctx.onLines(answer.ok ? (answer.lines ?? null) : null); + if (answer.ok && answer.lines) showItems(host, ctx.values, answer.lines); out.replaceChildren(...answerView(answer, draft, (ctx.row.결과단위 ?? "").startsWith("원"))); } catch (error) { showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error"); diff --git a/M01_MasterData/M01_MasterData_UI_Logic_CalcPick.ts b/M01_MasterData/M01_MasterData_UI_Logic_CalcPick.ts index b24842fe..ebf5111e 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_CalcPick.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_CalcPick.ts @@ -6,10 +6,10 @@ import { createButton, el } from "@ui/ui_template_elements"; import { fetchRows, type Row } from "./M01_MasterData_Api_Fetch"; -import type { ElementBrief, LogicInput, LogicRow } from "./M01_MasterData_UI_Logic_Api"; +import type { CalcLine, ElementBrief, LogicInput, LogicRow } from "./M01_MasterData_UI_Logic_Api"; import { openPickTable } from "./M01_MasterData_UI_Logic_PickTable"; import { tx } from "./M01_MasterData_UI_Logic_Text"; -import { condText, type PickCond } from "./M01_MasterData_UI_LogicLab_Modal"; +import { condText, headWord, type PickCond } from "./M01_MasterData_UI_LogicLab_Modal"; interface Chosen { ref: string; @@ -33,7 +33,6 @@ export function pickBody(values: Record): { 고름?: Record `${row["이름"] ?? ""} ${row["규격"] ?? ""}`.trim(); -const firstWord = (text: string): string => text.trim().split(/\s+/)[0] ?? ""; const MATERIAL_KEYS = /^MT\d{6}$/; const MACHINE_CODE = /^\d{4}-\d{4}$/; @@ -73,17 +72,18 @@ export function materialRows( ? (item.요소 as unknown as PickCond) : null; const brief = (cond ? prices[condText(cond)] : prices[item.요소]) ?? null; - const label = item.이름 ?? (brief?.이름 as string | undefined) ?? ""; + const named = item.이름 && !/^[A-Z]{2}\d{6}/.test(item.이름) ? item.이름 : ""; + const label = named || (brief?.이름 as string | undefined) || ""; const held = mine.get(i); - const nowRef = held?.ref ?? brief?.ref; const nowName = held?.label ?? (brief ? `${brief.이름 ?? ""} ${brief.규격 ?? ""}`.trim() : "") ?? ""; const seeds = [cond?.["검색어"], cond?.이름, cond?.상세구분, cond?.구분, label] - .map((w) => firstWord(String(w ?? ""))) + .map(headWord) .filter((w, k, all) => w && all.indexOf(w) === k); const current = el("span", { className: "m01-logic__muted", text: `${nowName || tx("Mat_NoPrice")} `, + attrs: { "data-mat-idx": String(i), "data-ref": brief?.ref ?? "" }, }); const button = pickButton(tx("Pick_Item"), () => openPickTable({ @@ -91,7 +91,7 @@ export function materialRows( title: `${tx("Pick_MatTitle")} · ${label}`, seed: seeds[0] ?? "", alt: seeds.slice(1), - isCurrent: (r) => String(r["키"]) === nowRef, + isCurrent: (r) => String(r["키"]) === (mine.get(i)?.ref ?? current.dataset.ref), onPick: (r) => { mine.set(i, { ref: String(r["키"]), label: nameOf(r) }); current.textContent = `${nameOf(r)} `; @@ -103,6 +103,22 @@ export function materialRows( }); } +/** 계산 답의 `품목` 을 잡힌 품목 글로 — 고른 재료가 있는 줄은 그대로 */ +export function showItems( + host: HTMLElement, + values: Record, + lines: CalcLine[], +): void { + const mine = picksOf(values); + host.querySelectorAll("[data-mat-idx]").forEach((span) => { + const i = Number(span.dataset.matIdx); + const item = lines[i]?.품목; + if (!item || mine.has(i)) return; + span.dataset.ref = item.키; + span.textContent = `${`${item.이름 ?? ""} ${item.규격 ?? ""}`.trim() || item.키} `; + }); +} + /** 기계 입력 칸 — 표 모달로 고르면 입력값(원문 분류번호 · 키 목록이면 키) */ const machines = new Map(); async function machineRow(code: string): Promise {