- 상세 쪽 openMaterialModal 기준 — 위 물가자료 · 유통물가 · 물가정보 · 거래가격 가로 넷(빈 칸 「-」 · 시험 계산이 쓴 값 굵게) - 후보 = /materials(조건 · 단위 거름) · 조건 없으면 이름 찾기 · 찾기 칸(검색어로 먼저 거름 · 없으면 재료 전체 찾기) · 목록 값 열 뺌 - 줄 누르면 고름(시험 입력 · 저장 안 함) → 다시 셈 · 상세 표 재료 줄에서 골라도 시험 계산 자재 칸 따라감 - 기계 고르기 · M02 일위대가 [바꾸기]는 PickTable 그대로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
194 lines
7.7 KiB
TypeScript
194 lines
7.7 KiB
TypeScript
/* =============================================================================
|
|
* M01_MasterData_UI_Logic_CalcPick.ts
|
|
* 시험 계산의 재료 · 기계 고르기 — 재료 칸마다 잡힌 품목 이름(누르면 고르기) · 기계 입력은 단추 하나
|
|
* 재료 = 상세 표 줄과 같은 재료 모달(`openMaterialModal` · 711-2) · 기계 = 마스터 표 모달(`PickTable`)
|
|
* 고른 재료는 `고름`(호표 줄 차례 → 키)에 실어 다시 계산 · 저장 안 함
|
|
* ========================================================================== */
|
|
|
|
import { createButton, el } from "@ui/ui_template_elements";
|
|
import { fetchRows, type Row } from "./M01_MasterData_Api_Fetch";
|
|
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, openMaterialModal, type PickCond } from "./M01_MasterData_UI_LogicLab_Modal";
|
|
|
|
interface Chosen {
|
|
ref: string;
|
|
label: string;
|
|
}
|
|
|
|
/** 로직마다 넣은 값(`values`)에 딸린 고른 재료 — 다시 그려도 남고 서버에는 계산 요청에만 실림 */
|
|
const chosen = new WeakMap<Record<string, string>, Map<number, Chosen>>();
|
|
const picksOf = (values: Record<string, string>): Map<number, Chosen> => {
|
|
let mine = chosen.get(values);
|
|
if (!mine) chosen.set(values, (mine = new Map()));
|
|
return mine;
|
|
};
|
|
|
|
/** 재료 고름(시험 입력 · 저장 안 함) — 시험 계산 칸(`host`)의 자재 글을 바꾸고 다시 셈 · 상세 표 줄 모달이 씀 */
|
|
export function pickMaterial(
|
|
host: HTMLElement,
|
|
values: Record<string, string>,
|
|
i: number,
|
|
it: Chosen,
|
|
): void {
|
|
picksOf(values).set(i, it);
|
|
const link = host.querySelector<HTMLElement>(`[data-mat-idx="${i}"]`);
|
|
if (link) link.textContent = it.label;
|
|
host.querySelector<HTMLButtonElement>("[data-calc-run]")?.click();
|
|
}
|
|
|
|
/** 계산 요청 몸의 `고름` — 고른 것이 없으면 안 실음 */
|
|
export function pickBody(values: Record<string, string>): { 고름?: Record<string, string> } {
|
|
const mine = picksOf(values);
|
|
return mine.size
|
|
? { 고름: Object.fromEntries([...mine].map(([i, c]) => [String(i), c.ref])) }
|
|
: {};
|
|
}
|
|
|
|
const nameOf = (row: Row): string => `${row["이름"] ?? ""} ${row["규격"] ?? ""}`.trim();
|
|
|
|
const MATERIAL_KEYS = /^MT\d{6}$/;
|
|
const MACHINE_CODE = /^\d{4}-\d{4}$/;
|
|
const MACHINE_KEY = /^EQ\d{6}$/;
|
|
|
|
/** 재료 키 목록 입력 — 표 모달로 바뀌어 걷음 */
|
|
export const isMaterialKeyInput = (spec: LogicInput): boolean =>
|
|
!!spec.고르기?.length && spec.고르기.every((o) => MATERIAL_KEYS.test(String(o)));
|
|
|
|
const isMachineInput = (spec: LogicInput): boolean =>
|
|
!!spec.고르기?.length &&
|
|
spec.고르기.every((o) => MACHINE_CODE.test(String(o)) || MACHINE_KEY.test(String(o)));
|
|
|
|
const pickButton = (label: string, onClick: () => void): HTMLButtonElement =>
|
|
createButton({ label, variant: "ghost", onClick });
|
|
|
|
/** 자재 칸 — 이름 + 잡힌 품목(누르면 고르기 모달) · 긴 이름은 두 칸 */
|
|
const line = (label: string, current: HTMLElement): HTMLElement =>
|
|
el("div", {
|
|
className: `m01-logic__field${label.length > 10 ? " m01-logic__field--wide" : ""}`,
|
|
children: [el("span", { text: label }), current],
|
|
});
|
|
|
|
/** 재료 줄마다 한 칸 — 지금 잡힌 품목 + 품목 고르기 */
|
|
export function materialRows(
|
|
row: LogicRow,
|
|
prices: Record<string, ElementBrief | null>,
|
|
values: Record<string, string>,
|
|
again: () => void,
|
|
): HTMLElement[] {
|
|
const mine = picksOf(values);
|
|
return (row.호표 ?? [])
|
|
.map((item, i) => ({ item, i }))
|
|
.filter((x) => x.item.종류 === "재료")
|
|
.map(({ item, i }) => {
|
|
const cond =
|
|
typeof item.요소 === "object" && item.요소 !== null
|
|
? (item.요소 as unknown as PickCond)
|
|
: null;
|
|
const brief = (cond ? prices[condText(cond)] : prices[item.요소]) ?? null;
|
|
const named = item.이름 && !/^[A-Z]{2}\d{6}/.test(item.이름) ? item.이름 : "";
|
|
const label = named || (brief?.이름 as string | undefined) || "";
|
|
const held = mine.get(i);
|
|
const nowName =
|
|
held?.label ?? (brief ? `${brief.이름 ?? ""} ${brief.규격 ?? ""}`.trim() : "") ?? "";
|
|
// 잡힌 품목 이름 = 링크 단추 — 누르면 고르기 모달([품목 고르기] 단추 걷음)
|
|
const current = createButton({
|
|
label: nowName || tx("Mat_NoPrice"),
|
|
variant: "link",
|
|
onClick: () => pick(),
|
|
});
|
|
current.dataset.matIdx = String(i);
|
|
current.dataset.ref = brief?.ref ?? "";
|
|
current.title = tx("Pick_Item");
|
|
const title = `${label}${item.규격 ? ` ${item.규격}` : ""}`;
|
|
const pick = (): void =>
|
|
openMaterialModal({
|
|
title,
|
|
expr: item.수량,
|
|
...(cond ? { cond } : { element: item.요소 }),
|
|
unit: item.단위,
|
|
kind: "재료",
|
|
prices,
|
|
middles: row.중간 ?? [],
|
|
values,
|
|
unitRef: mine.get(i)?.ref || current.dataset.ref || undefined,
|
|
usedSlot: current.dataset.slot || undefined,
|
|
onPick: (it) => {
|
|
mine.set(i, it);
|
|
current.textContent = it.label;
|
|
again();
|
|
},
|
|
});
|
|
return line(title, current);
|
|
});
|
|
}
|
|
|
|
/** 계산 답의 `품목` 을 잡힌 품목 글로 — 고른 재료가 있는 줄은 그대로 */
|
|
export function showItems(
|
|
host: HTMLElement,
|
|
values: Record<string, string>,
|
|
lines: CalcLine[],
|
|
): void {
|
|
const mine = picksOf(values);
|
|
host.querySelectorAll<HTMLElement>("[data-mat-idx]").forEach((span) => {
|
|
const i = Number(span.dataset.matIdx);
|
|
const item = lines[i]?.품목;
|
|
span.dataset.slot = item?.값칸 ?? ""; // 시험 계산이 쓴 값 칸 — 모달 위 가로 칸 굵게
|
|
if (!item || mine.has(i)) return;
|
|
span.dataset.ref = item.키;
|
|
span.textContent = `${item.이름 ?? ""} ${item.규격 ?? ""}`.trim() || item.키;
|
|
});
|
|
}
|
|
|
|
/** 기계 입력 칸 — 표 모달로 고르면 입력값(원문 분류번호 · 키 목록이면 키) */
|
|
const machines = new Map<string, Row | null>();
|
|
async function machineRow(code: string): Promise<Row | null> {
|
|
if (!machines.has(code)) {
|
|
let found: Row | null = null;
|
|
try {
|
|
const got = await fetchRows("기계.json", 1, 5, code);
|
|
found = got.rows.find((r) => r["원문번호"] === code || r["키"] === code) ?? null;
|
|
} catch {
|
|
found = null;
|
|
}
|
|
machines.set(code, found);
|
|
}
|
|
return machines.get(code) ?? null;
|
|
}
|
|
|
|
export function machineControl(
|
|
spec: LogicInput,
|
|
values: Record<string, string>,
|
|
again: () => void,
|
|
): HTMLElement | null {
|
|
if (!isMachineInput(spec)) return null;
|
|
const current = el("span", { className: "ui-text ui-text--muted" });
|
|
const paint = (): void => {
|
|
const code = values[spec.이름] ?? "";
|
|
const known = machines.get(code);
|
|
current.textContent = `${code ? `${code}${known ? ` · ${nameOf(known)}` : ""}` : tx("Mat_NoPrice")} `;
|
|
if (code && !machines.has(code)) void machineRow(code).then(paint);
|
|
};
|
|
paint();
|
|
const button = pickButton(tx("Pick_Machine"), () => {
|
|
const code = values[spec.이름] ?? "";
|
|
const known = machines.get(code);
|
|
openPickTable({
|
|
kind: "기계",
|
|
title: `${tx("Pick_Machine")} · ${spec.이름}`,
|
|
seed: String(known?.["이름"] ?? spec.이름),
|
|
isCurrent: (r) => !!code && (r["원문번호"] === code || r["키"] === code),
|
|
onPick: (r) => {
|
|
const useKey = MACHINE_KEY.test(String(spec.고르기?.[0]));
|
|
const value = String(useKey ? r["키"] : r["원문번호"]);
|
|
machines.set(value, r);
|
|
values[spec.이름] = value;
|
|
paint();
|
|
again();
|
|
},
|
|
});
|
|
});
|
|
return el("div", { children: [current, button] });
|
|
}
|