feat(M01): 테스트 컨테이너 고르기 부품 공용화 — 구분 → 상세구분 → 후보 거름 · B·C 에도 재료 바꿔 보기
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_Test_Pick.ts
|
||||
* 재료·기계 바꿔 고르기 부품 — 방식 A·B·C 가 같이 씀 · 구분 → 상세구분 → 후보 순으로 좁힘
|
||||
* 바꾼 것은 시험 계산용 복사본에만 — 정본 로직은 그대로
|
||||
* ========================================================================== */
|
||||
|
||||
import { createSelectField, el } from "@ui/ui_template_elements";
|
||||
import {
|
||||
fetchMaterials,
|
||||
searchElements,
|
||||
searchPrice,
|
||||
type ElementBrief,
|
||||
type HoLine,
|
||||
type LogicOne,
|
||||
type LogicRow,
|
||||
} from "./M01_MasterData_UI_Logic_Api";
|
||||
import { formatNumber } from "./M01_MasterData_UI_Logic_Edit";
|
||||
import { tt } from "./M01_MasterData_UI_Test_Text";
|
||||
|
||||
export type Swaps = Map<number, ElementBrief>;
|
||||
|
||||
const PICKABLE = ["재료", "기계"];
|
||||
|
||||
/** 고를 수 있는 줄이 있는지 */
|
||||
export const hasPickable = (logic: LogicRow): boolean =>
|
||||
(logic.호표 ?? []).some((l) => PICKABLE.includes(l.종류));
|
||||
|
||||
/** 바꿔 본 줄이 있으면 고친 복사본 — 없으면 undefined(정본 그대로 셈) */
|
||||
export function swappedRow(logic: LogicRow, swaps: Swaps): LogicRow | undefined {
|
||||
if (!swaps.size) return undefined;
|
||||
return {
|
||||
...logic,
|
||||
호표: (logic.호표 ?? []).map((l, i) => {
|
||||
const s = swaps.get(i);
|
||||
return s ? { ...l, 요소: s.ref, 이름: s.이름 ?? l.이름, 규격: s.규격 ?? l.규격 } : l;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
type Brief = ElementBrief & { 구분?: string; 상세구분?: string };
|
||||
|
||||
const distinct = (items: Brief[], key: "구분" | "상세구분"): string[] => [
|
||||
...new Set(items.map((i) => i[key]).filter((v): v is string => !!v)),
|
||||
];
|
||||
|
||||
const label = (it: Brief): string =>
|
||||
[`${it.이름 ?? ""} ${it.규격 ?? ""}`.trim(), formatNumber(it.값)].filter(Boolean).join(" · ");
|
||||
|
||||
function pickRow(one: LogicOne, line: HoLine, i: number, swaps: Swaps, onChange: () => void) {
|
||||
const cond =
|
||||
typeof line.요소 === "object" ? (line.요소 as unknown as Record<string, unknown>) : null;
|
||||
const price = cond ? undefined : one.prices[line.요소]?.값;
|
||||
let all: Brief[] = [];
|
||||
let sub = "";
|
||||
let detail = String(cond?.["상세구분"] ?? "");
|
||||
|
||||
const subBox = createSelectField({
|
||||
options: [],
|
||||
value: "",
|
||||
compact: true,
|
||||
onChange: (v) => ((sub = v), (detail = ""), fill()),
|
||||
});
|
||||
const detailBox = createSelectField({
|
||||
options: [],
|
||||
value: "",
|
||||
compact: true,
|
||||
onChange: (v) => ((detail = v), fill()),
|
||||
});
|
||||
const itemBox = createSelectField({
|
||||
options: [{ value: "", text: tt("Mat_Default") }],
|
||||
value: "",
|
||||
onChange: (ref) => {
|
||||
const item = all.find((it) => it.ref === ref);
|
||||
if (item) swaps.set(i, item);
|
||||
else swaps.delete(i);
|
||||
onChange();
|
||||
},
|
||||
});
|
||||
const subRow = el("div", {
|
||||
className: "m01-test__filters",
|
||||
children: [subBox.root, detailBox.root],
|
||||
});
|
||||
|
||||
// 구분 → 상세구분 → 후보 — 앞 단을 고르면 뒷 단 목록이 좁혀짐(구분 칸이 없는 후보면 그 단은 숨김)
|
||||
const fill = (): void => {
|
||||
const subs = distinct(all, "구분");
|
||||
const inSub = all.filter((it) => !sub || it.구분 === sub);
|
||||
const details = distinct(inSub, "상세구분");
|
||||
const inDetail = inSub.filter((it) => !detail || it.상세구분 === detail);
|
||||
const all_ = { value: "", text: tt("Pick_All") };
|
||||
subBox.root.hidden = subs.length < 1;
|
||||
detailBox.root.hidden = details.length < 1;
|
||||
subBox.setOptions([all_, ...subs.map((v) => ({ value: v, text: v }))], sub);
|
||||
detailBox.setOptions([all_, ...details.map((v) => ({ value: v, text: v }))], detail);
|
||||
itemBox.setOptions(
|
||||
[
|
||||
{ value: "", text: tt("Mat_Default") },
|
||||
...inDetail.map((it) => ({ value: it.ref, text: label(it) })),
|
||||
],
|
||||
swaps.get(i)?.ref ?? "",
|
||||
);
|
||||
};
|
||||
|
||||
// 고르기 조건({구분, 상세구분, 규격}) 줄 = 조건 안 후보 · 옛 줄 = 이름으로 찾은 후보
|
||||
const found = cond
|
||||
? fetchMaterials({
|
||||
sub: String(cond["구분"] ?? ""),
|
||||
spec: String(cond["규격"] ?? ""),
|
||||
region: "울진",
|
||||
})
|
||||
: line.종류 === "재료"
|
||||
? searchPrice((line.이름 ?? "").split(/[\s(·]/)[0]) // 옛 줄 — 이름 첫 낱말로 자재품목(구분 붙음)
|
||||
: searchElements(line.종류, line.이름 ?? "");
|
||||
void found
|
||||
.then((r) => {
|
||||
all = r.items as Brief[];
|
||||
if (cond) sub = String(cond["구분"] ?? "");
|
||||
fill();
|
||||
})
|
||||
.catch(() => undefined);
|
||||
|
||||
const spec = line.규격 ? ` ${line.규격}` : "";
|
||||
const cost =
|
||||
price !== undefined && price !== null ? ` · ${tt("Mat_Price")} ${formatNumber(price)}` : "";
|
||||
return el("div", {
|
||||
className: "m01-test__pick",
|
||||
children: [
|
||||
el("strong", { text: `${line.종류} · ${line.이름 ?? String(line.요소)}${spec}` }),
|
||||
el("span", { className: "m01-test__muted", text: `${line.단위 ?? ""}${cost}` }),
|
||||
subRow,
|
||||
itemBox.root,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/** 재료·기계 줄마다 한 칸씩 — 한 번 만들어 두고 다시 그릴 때 같은 노드를 씀(고른 것이 안 날아감) */
|
||||
export function pickPanel(one: LogicOne, swaps: Swaps, onChange: () => void): HTMLElement {
|
||||
const rows = (one.logic.호표 ?? [])
|
||||
.map((line, i) => ({ line, i }))
|
||||
.filter((x) => PICKABLE.includes(x.line.종류))
|
||||
.map((x) => pickRow(one, x.line, x.i, swaps, onChange));
|
||||
return el("div", {
|
||||
className: "m01-test__picks",
|
||||
children: rows.length ? rows : [el("p", { text: tt("Mat_None") })],
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user