feat(M02): 일위대가 조합 서고 — 좌측 항목 둘 · 상세 목록표(51-4)
- 좌측 컨테이너 = 층 단추 + 「단가산출 승격」 · 「일위대가 사전조합」 항목만 · 새로 · 본떠 · 지우기 단추 없앰 - 항목 누름 = 상세 칸 목록표(고름 · 키 · 이름 · 단위 · 기준 금액 · 고친 날) · 줄 누름 = 그 문서 상세 - 머리 [생성] · [복사](고른 줄) · [삭제](고른 줄 · 쓰는 곳 있으면 서버 까닭 알림) - 층 목록 서고 줄에 1 단위 기준 계(`기준금액`) 실음 · 시험 한 개 · 화면 시나리오 51-4 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Library_List.ts
|
||||
* 일위대가 조합 서고 목록표(51-4) — 좌측 항목(단가산출 승격 · 일위대가 사전조합)을 누르면 상세 칸에 뜸.
|
||||
* 표 = 고름 · 키 · 이름 · 단위 · 기준 금액(서버 1 단위 계) · 고친 날 · 줄 누름 = 그 문서 상세.
|
||||
* 머리 [생성] · [복사](고른 줄) · [삭제](고른 줄 · 쓰는 곳 있으면 서버 409 까닭 그대로).
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
createButton,
|
||||
createInputField,
|
||||
el,
|
||||
showConfirmDialog,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import {
|
||||
deleteLayerTemplate,
|
||||
deleteTemplate,
|
||||
getActiveOwner,
|
||||
listTemplates,
|
||||
newLibrary,
|
||||
readTemplate,
|
||||
saveTemplate,
|
||||
type Layer,
|
||||
type LibraryBody,
|
||||
type TemplateInfo,
|
||||
} from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
import type { PrecomboKind } from "./M02_MasterTemplete_UI_Side_Precombo";
|
||||
|
||||
/** 목록 줄 — 서버가 문서 `이름`(제목) · 단위 · 1 단위 기준 계를 실어 보냄 */
|
||||
type Row = TemplateInfo & { 제목?: string | null; 단위?: string | null; 기준금액?: number | null };
|
||||
|
||||
export interface LibraryListOptions {
|
||||
layer: Layer;
|
||||
kind: PrecomboKind;
|
||||
canEdit: boolean;
|
||||
/** 줄 누름 · 생성 · 복사 뒤 — 그 문서 상세 */
|
||||
open: (key: string, label: string) => void;
|
||||
}
|
||||
|
||||
const why = (error: unknown): string => (error instanceof Error ? error.message : "");
|
||||
const money = (v: number | null | undefined): string =>
|
||||
v == null ? "" : v.toLocaleString("ko-KR", { maximumFractionDigits: 2 });
|
||||
export const libraryLabel = (row: Row): string => (row.제목 ? `${row.이름} ${row.제목}` : row.이름);
|
||||
|
||||
/** `tools` = 상세 머리 단추 자리(페이지 slot) · 돌려준 `destroy` 는 안 씀(칸째 비움) */
|
||||
export function mountLibraryList(
|
||||
host: HTMLElement,
|
||||
tools: HTMLElement,
|
||||
opt: LibraryListOptions,
|
||||
): { getDoc: () => null; destroy: () => void } {
|
||||
let rows: Row[] = [];
|
||||
let picked: string | null = null;
|
||||
const body = el("tbody");
|
||||
const btnNew = createButton({ label: tc("Lib_Create"), variant: "filled" });
|
||||
const btnCopy = createButton({ label: tc("Lib_Copy"), variant: "ghost" });
|
||||
const btnDel = createButton({ label: tc("Lib_Delete"), variant: "danger" });
|
||||
const paintButtons = (): void => {
|
||||
btnCopy.disabled = btnDel.disabled = !rows.some((r) => r.이름 === picked);
|
||||
};
|
||||
|
||||
const draw = (): void => {
|
||||
body.replaceChildren(
|
||||
...(rows.length
|
||||
? rows.map((row) => {
|
||||
const radio = el("input", {
|
||||
attrs: { type: "radio", name: `m02-lib-${opt.kind}`, "aria-label": row.이름 },
|
||||
}) as HTMLInputElement;
|
||||
radio.checked = row.이름 === picked;
|
||||
const pick = el("td", { className: "m02-lib__pick", children: [radio] });
|
||||
pick.addEventListener("click", (event) => {
|
||||
event.stopPropagation(); // 고름 칸은 열지 않음
|
||||
picked = row.이름;
|
||||
radio.checked = true;
|
||||
paintButtons();
|
||||
});
|
||||
const tr = el("tr", {
|
||||
className: "m02-lib__row",
|
||||
attrs: { "data-lib-key": row.이름 },
|
||||
children: [
|
||||
pick,
|
||||
...[row.이름, row.제목 ?? "", row.단위 ?? "", money(row.기준금액), row.수정일].map(
|
||||
(v) => el("td", { text: v, attrs: { title: v } }),
|
||||
),
|
||||
],
|
||||
});
|
||||
tr.addEventListener("click", () => opt.open(row.이름, libraryLabel(row)));
|
||||
return tr;
|
||||
})
|
||||
: [
|
||||
el("tr", {
|
||||
children: [el("td", { text: L("M02_NoTemplates"), attrs: { colspan: "6" } })],
|
||||
}),
|
||||
]),
|
||||
);
|
||||
paintButtons();
|
||||
};
|
||||
|
||||
const load = async (): Promise<void> => {
|
||||
try {
|
||||
rows = (await listTemplates(opt.layer, null)).filter((r) => r.종류 === opt.kind) as Row[];
|
||||
} catch (error) {
|
||||
rows = [];
|
||||
showToast(L("M02_LoadFailed").replace("{value}", why(error)), "error");
|
||||
}
|
||||
draw();
|
||||
};
|
||||
|
||||
const nameDialog = (title: string, start: string, onOk: (name: string) => Promise<void>) => {
|
||||
const name = createInputField({ label: L("M02_Name"), placeholder: L("M02_Name") });
|
||||
name.input.value = start;
|
||||
openModal({
|
||||
title,
|
||||
closeLabel: L("M02_Close"),
|
||||
dialogClass: "m02-modal",
|
||||
mount: (box, close) => {
|
||||
const ok = createButton({ label: L("M02_Create"), variant: "filled" });
|
||||
ok.addEventListener("click", async () => {
|
||||
const text = name.input.value.trim();
|
||||
if (!text) return showToast(L("M02_NameNeeded"), "error");
|
||||
ok.disabled = true; // 연달아 눌러도 한 번만
|
||||
try {
|
||||
await onOk(text);
|
||||
close();
|
||||
} catch (error) {
|
||||
showToast(L("M02_ActionFailed").replace("{value}", why(error)), "error");
|
||||
} finally {
|
||||
ok.disabled = false;
|
||||
}
|
||||
});
|
||||
name.input.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter" && !event.isComposing) ok.click();
|
||||
});
|
||||
box.append(name.root, ok);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/** 새로 — 서버가 그 층 다음 키로 씀 → 그 문서 상세 */
|
||||
const made = (key: string, label: string): void => {
|
||||
showToast(L("M02_Created"), "success");
|
||||
opt.open(key, label);
|
||||
};
|
||||
const create = async (body: LibraryBody, label: string): Promise<void> =>
|
||||
made((await newLibrary(opt.layer, opt.kind, body)).이름, label);
|
||||
|
||||
/** 단가승격 = 단가산출 로직 고르기 → 이름 · 단위는 로직 것 · 사전조합 = 이름만 · 빈 묶음 */
|
||||
btnNew.addEventListener("click", () => {
|
||||
if (opt.kind === "promote")
|
||||
return openLogicPicker((x) => {
|
||||
const doc = {
|
||||
이름: x.이름,
|
||||
단위: x.결과단위 || null,
|
||||
담은로직: [{ 로직: x.키, 입력값: {} }],
|
||||
};
|
||||
void create({ 문서: doc }, x.이름).catch((error) =>
|
||||
showToast(L("M02_ActionFailed").replace("{value}", why(error)), "error"),
|
||||
);
|
||||
});
|
||||
nameDialog(L("M02_NewTitle"), "", (name) =>
|
||||
create({ 문서: { 이름: name, 단위: null, 담은로직: [] } }, name),
|
||||
);
|
||||
});
|
||||
|
||||
/** 복사 = 같은 층 새 키(서버 · 본뜬 = 원래 키) → 고른 이름으로 고쳐 씀 */
|
||||
btnCopy.addEventListener("click", () => {
|
||||
const from = rows.find((r) => r.이름 === picked);
|
||||
if (!from) return;
|
||||
nameDialog(L("M02_CopyTitle"), `${from.제목 ?? from.이름} (2)`, async (name) => {
|
||||
const got = await newLibrary(opt.layer, opt.kind, {
|
||||
본뜬: { 층: opt.layer, 주인: getActiveOwner(), 키: from.이름 },
|
||||
});
|
||||
const doc = await readTemplate(opt.layer, opt.kind, got.이름, null);
|
||||
await saveTemplate(opt.layer, opt.kind, got.이름, null, got.판, {
|
||||
...(doc.문서 as object),
|
||||
이름: name,
|
||||
});
|
||||
made(got.이름, name);
|
||||
});
|
||||
});
|
||||
|
||||
btnDel.addEventListener("click", async () => {
|
||||
await load(); // 판을 최신으로
|
||||
const info = rows.find((r) => r.이름 === picked);
|
||||
if (!info) return;
|
||||
const label = libraryLabel(info);
|
||||
if (!(await showConfirmDialog(L("M02_DeleteConfirm").replace("{value}", label)))) return;
|
||||
try {
|
||||
if (opt.layer === "system") await deleteTemplate(opt.kind, info.이름, info.판);
|
||||
else await deleteLayerTemplate(opt.layer, opt.kind, info.이름, info.판);
|
||||
showToast(L("M02_Deleted"), "success");
|
||||
picked = null;
|
||||
await load();
|
||||
} catch (error) {
|
||||
// 쓰는 곳이 있으면 서버가 막음 — 까닭(쓰는 곳 이름) 그대로
|
||||
showToast(why(error) || L("M02_ActionFailed").replace("{value}", ""), "error");
|
||||
}
|
||||
});
|
||||
|
||||
const heads = ["", "Lib_Key", "Lib_Name", "Lib_Unit", "Lib_Base", "Lib_Date"] as const;
|
||||
host.replaceChildren(
|
||||
el("div", {
|
||||
className: "ui-general-block__table-wrap ui-general-block__table-wrap--sticky-head",
|
||||
attrs: { "data-lib-list": opt.kind },
|
||||
children: [
|
||||
el("table", {
|
||||
className: "ui-general-block__table m02-lib__table",
|
||||
children: [
|
||||
el("thead", {
|
||||
children: [
|
||||
el("tr", { children: heads.map((h) => el("th", { text: h ? tc(h) : "" })) }),
|
||||
],
|
||||
}),
|
||||
body,
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
if (opt.canEdit)
|
||||
tools.replaceChildren(
|
||||
el("span", { className: "m02-main__tools", children: [btnNew, btnCopy, btnDel] }),
|
||||
);
|
||||
void load();
|
||||
return { getDoc: () => null, destroy: () => undefined };
|
||||
}
|
||||
Reference in New Issue
Block a user