feat(M01): 로직 개선 시험 「식으로 새로 만들기」 화면 — 식 적기 · 분석 · 변수 정하기 · 막힌 자리 짚기 · 시험 계산 · 저장 (PLAN 3-3)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PAdA5ThqmtVSsbzjusk1cJ
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_LogicLab_New_Find.ts
|
||||
* 새 로직 만들기의 고르기 창 둘
|
||||
* ① 마스터 요소 — 종류(인력·재료·기계) → 구분 → 상세구분 → 이름 찾기 → 한 줄 고르기
|
||||
* ② 표 찾기 — 표(소요량·계수)와 값 칸을 고른 뒤 표의 조건 칸마다 식을 적음
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
createButton,
|
||||
createInputField,
|
||||
createSelectField,
|
||||
el,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import {
|
||||
fetchFiles,
|
||||
fetchRows,
|
||||
fetchSubsAll,
|
||||
type FileInfo,
|
||||
type SubInfo,
|
||||
} from "./M01_MasterData_Api_Fetch";
|
||||
import type { DraftFind } from "./M01_MasterData_UI_Logic_Api";
|
||||
import { formatNumber } from "./M01_MasterData_UI_Logic_Edit";
|
||||
import { loadTable } from "./M01_MasterData_UI_Logic_Note";
|
||||
import { openPicker } from "./M01_MasterData_UI_Logic_Pick";
|
||||
import { tn } from "./M01_MasterData_UI_LogicLab_New_Text";
|
||||
|
||||
export interface PickedElement {
|
||||
ref: string;
|
||||
/** 이름 + 규격 */
|
||||
name: string;
|
||||
/** 수량에 쓰는 단위(「원/일」 → 「일」) */
|
||||
unit: string;
|
||||
group: string;
|
||||
}
|
||||
|
||||
const GROUPS = ["인력", "재료", "기계"];
|
||||
const SHOWN = 50;
|
||||
|
||||
/** 창 뼈대 — 제목 · 닫기 · 몸통 */
|
||||
function shell(title: string, body: HTMLElement[]): { close: () => void } {
|
||||
const close = (): void => backdrop.remove();
|
||||
const dialog = el("div", {
|
||||
className: "m01-logic__pick",
|
||||
attrs: { role: "dialog", "aria-label": title },
|
||||
children: [
|
||||
el("div", {
|
||||
className: "m01-logic__section-head",
|
||||
children: [
|
||||
el("h3", { text: title }),
|
||||
createButton({ label: tn("Close"), variant: "ghost", onClick: close }),
|
||||
],
|
||||
}),
|
||||
...body,
|
||||
],
|
||||
});
|
||||
const backdrop = el("div", { className: "m01-logic__backdrop", children: [dialog] });
|
||||
backdrop.addEventListener("click", (ev) => ev.target === backdrop && close());
|
||||
backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && close());
|
||||
document.body.append(backdrop);
|
||||
return { close };
|
||||
}
|
||||
|
||||
/** 그룹의 파일 — 재료는 자재품목(구분·상세구분이 있는 것) */
|
||||
async function fileOf(group: string): Promise<FileInfo | undefined> {
|
||||
const files = await fetchFiles(group);
|
||||
return group === "재료" ? (files.find((f) => f.file.includes("자재품목")) ?? files[0]) : files[0];
|
||||
}
|
||||
|
||||
const options = (values: string[]): { value: string; text: string }[] => [
|
||||
{ value: "", text: tn("All") },
|
||||
...values.map((v) => ({ value: v, text: v })),
|
||||
];
|
||||
|
||||
export function openElementPick(done: (picked: PickedElement) => void): void {
|
||||
let file: FileInfo | undefined;
|
||||
let subs: SubInfo[] = [];
|
||||
const groupBox = createSelectField({
|
||||
options: GROUPS.map((g) => ({ value: g, text: g })),
|
||||
value: GROUPS[0],
|
||||
compact: true,
|
||||
label: tn("Group"),
|
||||
});
|
||||
const subBox = createSelectField({
|
||||
options: options([]),
|
||||
value: "",
|
||||
compact: true,
|
||||
label: tn("Sub"),
|
||||
});
|
||||
const detailBox = createSelectField({
|
||||
options: options([]),
|
||||
value: "",
|
||||
compact: true,
|
||||
label: tn("Detail"),
|
||||
});
|
||||
const searchField = createInputField({ type: "search", placeholder: tn("Search") });
|
||||
const count = el("span", { className: "m01-logic__muted" });
|
||||
const list = el("div", { className: "m01-logic__pick-list" });
|
||||
const win = shell(tn("K_master"), [
|
||||
el("div", {
|
||||
className: "m01-logic__pick-bar",
|
||||
children: [groupBox.root, subBox.root, detailBox.root, searchField.root, count],
|
||||
}),
|
||||
list,
|
||||
]);
|
||||
|
||||
let seq = 0;
|
||||
const load = async (): Promise<void> => {
|
||||
if (!file) return;
|
||||
const mine = ++seq;
|
||||
try {
|
||||
const got = await fetchRows(
|
||||
file.file,
|
||||
1,
|
||||
SHOWN,
|
||||
searchField.input.value.trim(),
|
||||
false,
|
||||
subBox.select.value,
|
||||
detailBox.select.value,
|
||||
);
|
||||
if (mine !== seq) return;
|
||||
count.textContent = tn("Count", { n: got.total, m: Math.min(got.total, SHOWN) });
|
||||
list.replaceChildren(
|
||||
...got.rows.map((row) => {
|
||||
const name = `${row["이름"] ?? ""} ${row["규격"] ?? ""}`.trim();
|
||||
const unitText = String(row["단위"] ?? "");
|
||||
const unit = groupBox.select.value === "인력" ? "인" : (unitText.split("/").pop() ?? "");
|
||||
const button = el("button", {
|
||||
className: "m01-logic__pick-row m01-logic__pick-row--button",
|
||||
attrs: { type: "button", "data-ref": String(row["키"]) },
|
||||
children: [
|
||||
el("div", {
|
||||
className: "m01-logic__stack",
|
||||
children: [
|
||||
el("strong", { text: name }),
|
||||
el("span", { className: "m01-logic__muted", text: String(row["키"]) }),
|
||||
],
|
||||
}),
|
||||
el("span", {
|
||||
className: "m01-logic__money",
|
||||
text: `${formatNumber(row["값"])} ${unitText}`.trim(),
|
||||
}),
|
||||
],
|
||||
});
|
||||
button.addEventListener("click", () => {
|
||||
win.close();
|
||||
done({ ref: String(row["키"]), name, unit, group: groupBox.select.value });
|
||||
});
|
||||
return button;
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : tn("Failed"), "error");
|
||||
}
|
||||
};
|
||||
|
||||
const changeGroup = async (): Promise<void> => {
|
||||
file = await fileOf(groupBox.select.value);
|
||||
subs = file ? (await fetchSubsAll({ file: file.file })).subs : [];
|
||||
subBox.setOptions(options(subs.map((s) => s.name)), "");
|
||||
detailBox.setOptions(options([]), "");
|
||||
detailBox.select.disabled = true;
|
||||
await load();
|
||||
};
|
||||
groupBox.select.addEventListener("change", () => void changeGroup());
|
||||
subBox.select.addEventListener("change", () => {
|
||||
const details = subs.find((s) => s.name === subBox.select.value)?.details ?? [];
|
||||
detailBox.setOptions(options(details), "");
|
||||
detailBox.select.disabled = details.length === 0;
|
||||
void load();
|
||||
});
|
||||
detailBox.select.addEventListener("change", () => void load());
|
||||
let timer = 0;
|
||||
searchField.input.addEventListener("input", () => {
|
||||
window.clearTimeout(timer);
|
||||
timer = window.setTimeout(() => void load(), 250);
|
||||
});
|
||||
void changeGroup();
|
||||
}
|
||||
|
||||
/** 표 찾기 — 표·값 칸을 고른 뒤 조건 칸마다 식을 받음 · 표의 조건 칸이 없으면 바로 끝 */
|
||||
export function openTablePick(done: (find: DraftFind, label: string) => void): void {
|
||||
openPicker("table", "소요량", (item, column) => {
|
||||
void (async () => {
|
||||
const table = await loadTable(item.ref);
|
||||
const conds = Object.keys(table?.조건 ?? {});
|
||||
const label = `${item.이름 ?? item.ref} · ${item.ref} → ${column ?? ""}`;
|
||||
const find = { 표: item.ref, 값칸: column ?? "", 조건: {} as Record<string, string> };
|
||||
if (!conds.length) {
|
||||
done(find, label);
|
||||
return;
|
||||
}
|
||||
const inputs = conds.map((c) => {
|
||||
const box = createInputField({ type: "text", value: c });
|
||||
box.input.setAttribute("data-cond", c);
|
||||
return { c, box };
|
||||
});
|
||||
const win = shell(tn("TableTitle"), [
|
||||
el("p", { className: "m01-logic__muted", text: label }),
|
||||
el("p", { text: tn("Cond") }),
|
||||
...inputs.map(({ c, box }) =>
|
||||
el("label", {
|
||||
className: "m01-logic__field",
|
||||
children: [el("span", { text: c }), box.root],
|
||||
}),
|
||||
),
|
||||
createButton({
|
||||
label: tn("Ok"),
|
||||
onClick: () => {
|
||||
for (const { c, box } of inputs) find.조건[c] = box.input.value.trim() || c;
|
||||
win.close();
|
||||
done(find, label);
|
||||
},
|
||||
}),
|
||||
]);
|
||||
})();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user