Files
Aislo/M01_MasterData/M01_MasterData_UI_Combo.ts
T
eomsangdonandClaude Sonnet 5 ca5c2649e3 feat(M01): 일위대가 조합 화면 뼈대 — 왼쪽 컨테이너 · 목록 · 상세 · 로직 더하기 · 미리 보기
- 왼쪽 패널 「일위대가 조합」 컨테이너(단가산출 로직 아래) · 구분/상세구분 거름 · 새 조합 · 지우기 · 저장
- 상세: 이름·구분·단위·비고 + 담은 로직 표(올리기/내리기/빼기) · 로직 줄을 누르면 단가산출 로직 상세로 감
- 로직 더하기 창: 구분 → 상세구분 → 찾기로 하나씩 고름
- 미리 보기: 담은 로직별 입력값으로 계산해 노무비·재료비·경비 합계(보기만)
- 서버 길(조합 목록·저장·지우기)은 아직 없어 Combo_Api 한 곳에 가정 이름으로 둠

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT
2026-09-21 23:27:47 +09:00

315 lines
9.9 KiB
TypeScript

/* =============================================================================
* M01_MasterData_UI_Combo.ts
* 일위대가 조합 화면 — 왼쪽 컨테이너(구분 · 상세구분 거름) / 오른쪽 목록 · 상세
* 상세 = 이름·구분·단위·비고 + 담은 로직 표(차례 · 키 · 이름 · 결과 단위 · 메모 · 순서) + 미리 보기
* 저장·지우기는 서버 조합 마스터(`일위대가조합.json` · 키 UA) — 길은 `Combo_Api` 한 곳
* ========================================================================== */
import {
createButton,
createInputField,
el,
showConfirmDialog,
showToast,
} from "@ui/ui_template_elements";
import { fetchLogics, type LogicSummary } from "./M01_MasterData_UI_Logic_Api";
import {
deleteCombo,
fetchCombo,
fetchCombos,
saveCombo,
type Combo,
type ComboSummary,
} from "./M01_MasterData_UI_Combo_Api";
import { openLogicPicker } from "./M01_MasterData_UI_Combo_Pick";
import { buildPreview } from "./M01_MasterData_UI_Combo_Preview";
import { tc } from "./M01_MasterData_UI_Combo_Text";
import type { SideHandle } from "./M01_MasterData_UI_Side";
import "./M01_MasterData_UI_Logic_Style.css";
import "./M01_MasterData_UI_LogicLab_Style.css";
const FILTER_ID = "조합|";
const blankCombo = (): Combo => ({
: "",
이름: "",
구분: "",
상세구분: "",
단위: "",
비고: "",
로직: [],
});
export interface ComboHandle {
/** 컨테이너를 펼칠 때 — 목록을 새로 받아 그림 */
show: () => Promise<void>;
}
export function mountM01Combo(
host: HTMLElement,
side: SideHandle,
onOpenLogic: (key: string) => void,
): ComboHandle {
let items: ComboSummary[] = [];
let logics = new Map<string, LogicSummary>();
let pick = { sub: "", detail: "" };
const filterHost = el("div");
side.comboHost.replaceChildren(filterHost);
const view = el("div", { className: "m01-logic__editor" });
host.replaceChildren(view);
const load = async (): Promise<void> => {
try {
const [combos, all] = await Promise.all([fetchCombos(), fetchLogics()]);
items = combos;
logics = new Map(all.map((x) => [x., x]));
} catch (error) {
showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error");
}
drawFilter();
drawList();
};
/* --- 왼쪽 거름 --- */
const drawFilter = (): void => {
const subs = [...new Set(items.map((x) => x.구분).filter(Boolean))].map((name) => ({
name,
book: null,
details: [...new Set(items.filter((x) => x.구분 === name).map((x) => x.상세구분))].filter(
Boolean,
),
}));
filterHost.replaceChildren(
side.filter({
id: FILTER_ID,
store: "m01.filter.조합",
subs,
total: items.length,
subLabel: tc("Sub"),
detailLabel: tc("Detail"),
onPick: (sub, detail) => {
pick = { sub, detail };
drawList();
},
}),
);
};
/* --- 목록 --- */
const toolbar = (...more: HTMLElement[]): HTMLElement =>
el("div", {
className: "m01-logic__section-head",
children: [
createButton({ label: tc("New"), variant: "filled", onClick: () => draw(blankCombo()) }),
...more,
],
});
const drawList = (): void => {
const shown = items.filter(
(x) => (!pick.sub || x.구분 === pick.sub) && (!pick.detail || x.상세구분 === pick.detail),
);
const heads = [
tc("Head_Key"),
tc("Head_Name"),
tc("Sub"),
tc("Detail"),
tc("Head_Unit"),
tc("Head_Count"),
];
const rows = shown.map((x) => {
const tr = el("tr", {
className: "m01-master__table-row",
attrs: { "data-combo-key": x. },
children: [x., x.이름, x.구분, x.상세구분, x.단위, String(x.로직수)].map((v) =>
el("td", { text: v }),
),
});
tr.addEventListener("click", () => void open(x.));
return tr;
});
view.replaceChildren(
toolbar(),
shown.length
? el("div", {
className: "m01-master__grid-wrap",
children: [
el("table", {
className: "m01-master__grid",
children: [
el("thead", {
children: [el("tr", { children: heads.map((t) => el("th", { text: t })) })],
}),
el("tbody", { children: rows }),
],
}),
],
})
: el("p", { className: "m01-logic__muted", text: tc("Empty") }),
);
};
const open = async (key: string): Promise<void> => {
try {
draw(await fetchCombo(key));
} catch (error) {
showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error");
}
};
/* --- 상세 --- */
const draw = (combo: Combo): void => {
const field = (label: string, name: "이름" | "구분" | "상세구분" | "단위" | "비고") => {
const f = createInputField({ type: "text", label });
f.input.value = combo[name];
f.input.setAttribute("data-combo-field", name);
f.input.addEventListener("input", () => (combo[name] = f.input.value));
return f.root;
};
const preview = buildPreview(() => combo.로직);
const table = el("div");
const drawTable = (): void => {
const move = (i: number, d: number): void => {
const j = i + d;
if (j < 0 || j >= combo.로직.length) return;
[combo.로직[i], combo.로직[j]] = [combo.로직[j], combo.로직[i]];
drawTable();
};
const trs = combo.로직.map((row, i) => {
const info = logics.get(row.);
const memo = createInputField({ type: "text" });
memo.input.value = row.메모;
memo.input.addEventListener("input", () => (row.메모 = memo.input.value));
const name = el("button", {
className: "ui-btn ui-btn--ghost",
text: info?.이름 ?? row.,
attrs: { type: "button", title: tc("Open") },
});
name.addEventListener("click", () => onOpenLogic(row.));
const btns = el("div", {
className: "m01-logic__chips",
children: [
createButton({ label: tc("Up"), variant: "ghost", onClick: () => move(i, -1) }),
createButton({ label: tc("Down"), variant: "ghost", onClick: () => move(i, 1) }),
createButton({
label: tc("Remove"),
variant: "ghost",
onClick: () => {
combo.로직.splice(i, 1);
drawTable();
},
}),
],
});
return el("tr", {
attrs: { "data-combo-logic": row. },
children: [
el("td", { text: String(i + 1) }),
el("td", { text: row.키 }),
el("td", { children: [name] }),
el("td", { text: info?.결과단위 ?? "" }),
el("td", { children: [memo.root] }),
el("td", { children: [btns] }),
],
});
});
const heads = [
tc("Col_Order"),
tc("Col_Key"),
tc("Col_Name"),
tc("Col_Unit"),
tc("Col_Memo"),
tc("Col_Move"),
];
table.replaceChildren(
el("table", {
className: "m01-master__grid",
children: [
el("thead", {
children: [el("tr", { children: heads.map((t) => el("th", { text: t })) })],
}),
el("tbody", { children: trs }),
],
}),
...(trs.length ? [] : [el("p", { className: "m01-logic__muted", text: tc("NoLogic") })]),
);
void preview.redraw();
};
const add = createButton({
label: tc("AddLogic"),
variant: "filled",
onClick: () =>
openLogicPicker((x) => {
if (!logics.has(x.)) logics.set(x., x);
combo.로직.push({ : x.키, 메모: "", 입력: {} });
drawTable();
}),
});
const save = createButton({
label: tc("Save"),
variant: "filled",
onClick: async () => {
if (!combo.이름.trim()) return showToast(tc("NeedName"), "error");
try {
const saved = await saveCombo(combo);
showToast(tc("Saved"), "success");
await load();
draw(saved);
} catch (error) {
showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error");
}
},
});
const remove = createButton({
label: tc("Delete"),
variant: "ghost",
onClick: async () => {
if (!combo. || !(await showConfirmDialog(tc("DeleteAsk")))) return;
try {
await deleteCombo(combo.);
showToast(tc("Deleted"), "success");
await load();
} catch (error) {
showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error");
}
},
});
const back = createButton({ label: "←", variant: "ghost", onClick: () => drawList() });
view.replaceChildren(
el("div", {
className: "m01-logic__section-head",
children: [back, el("h3", { text: combo.키 || tc("New") }), remove, save],
}),
el("section", {
className: "m01lab__box m01-logic__section",
children: [
el("div", {
className: "m01lab__info",
children: [
field(tc("Head_Name"), "이름"),
field(tc("Sub"), "구분"),
field(tc("Detail"), "상세구분"),
field(tc("Head_Unit"), "단위"),
field(tc("Head_Note"), "비고"),
],
}),
],
}),
el("section", {
className: "m01lab__box m01-logic__section",
children: [
el("div", {
className: "m01-logic__section-head",
children: [el("h3", { text: tc("Logics_Title") }), add],
}),
table,
],
}),
preview.root,
);
drawTable();
};
return { show: load };
}