_brief 응답에 구분·상세구분 추가(테스트 컨테이너 기계 후보 거름용) 표 상세화면 용도의 로직키를 누르면 로직 탭으로 전환 + 키 클립보드 복사(찾기용) 로직 목록을 직접 열지는 못함(로직 부분은 sub1 담당) — _화면_계약.md 한 줄 추가
280 lines
9.2 KiB
TypeScript
280 lines
9.2 KiB
TypeScript
/* =============================================================================
|
|
* M01_MasterData_UI_Tables.ts
|
|
* 표형 그룹(소요량·계수) — 구분·상세구분·찾기로 거른 표 목록(파일을 가로지름 · 쪽 나눔)
|
|
* → 표 하나(조건 칸 + 값 칸의 긴 줄) · 눌러 고침(노랑) · 줄 더하기·지우기
|
|
* ========================================================================== */
|
|
|
|
import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements";
|
|
import { t as L } from "@ui/ui_template_locale";
|
|
import {
|
|
fetchTable,
|
|
fetchTables,
|
|
type Row,
|
|
type TableHead,
|
|
type TableUsage,
|
|
} from "./M01_MasterData_Api_Fetch";
|
|
import { saveView, type Pick } from "./M01_MasterData_UI_Side";
|
|
import {
|
|
onDraftChange,
|
|
peek,
|
|
same,
|
|
setTable,
|
|
tableItems,
|
|
type TableItem,
|
|
} from "./M01_MasterData_Draft";
|
|
import {
|
|
buildCell,
|
|
buildPager,
|
|
coerce,
|
|
show,
|
|
toNumberish,
|
|
toRange,
|
|
} from "./M01_MasterData_UI_Cells";
|
|
|
|
const LIST_SIZE = 30;
|
|
const LIST_COLS = ["키", "원문번호", "구분", "상세구분", "이름", "기준", "출처", "비고"] as const;
|
|
/** 용도 대상 거름 드롭다운 후보 — PLAN 1-1 셋째 줄 그대로. */
|
|
const USAGE_TARGETS = ["인력", "기계", "재료", "할증", "환산", "작업량 조건"];
|
|
|
|
/** 돌려받은 함수 = 이 화면을 걷을 때 부를 해제. `openLogic` = 용도의 로직키 눌렀을 때 로직 탭으로. */
|
|
export function renderTables(
|
|
host: HTMLElement,
|
|
pick: Pick,
|
|
q: string,
|
|
openLogic: (key: string) => void,
|
|
): () => void {
|
|
let stop = (): void => {};
|
|
let usage = pick.view?.usage ?? "";
|
|
const list = (page = pick.view?.page ?? 1): void => {
|
|
stop();
|
|
stop = (): void => {};
|
|
void showList(
|
|
host,
|
|
pick,
|
|
q,
|
|
usage,
|
|
page,
|
|
(file, key) => {
|
|
stop = openTable(host, file, key, () => list(page), openLogic);
|
|
},
|
|
(next) => {
|
|
usage = next;
|
|
saveView(pick.group, { usage, page: 1 });
|
|
list(1);
|
|
},
|
|
);
|
|
};
|
|
list();
|
|
const off = onDraftChange(() => host.querySelector(".m01-master__tables") && markChanged(host));
|
|
return () => {
|
|
stop();
|
|
off();
|
|
};
|
|
}
|
|
|
|
/** 목록 위 「고침」 표시만 다시 — 목록을 다시 받지 않음. */
|
|
function markChanged(host: HTMLElement): void {
|
|
host.querySelectorAll<HTMLElement>(".m01-master__table-row").forEach((row) => {
|
|
const changed = peek(row.dataset.file!)?.tables ?? {};
|
|
row.classList.toggle("is-changed", row.dataset.key! in changed);
|
|
});
|
|
}
|
|
|
|
async function showList(
|
|
host: HTMLElement,
|
|
pick: Pick,
|
|
q: string,
|
|
usage: string,
|
|
page: number,
|
|
open: (file: string, key: string) => void,
|
|
onUsageChange: (usage: string) => void,
|
|
): Promise<void> {
|
|
let got: { total: number; tables: TableHead[] };
|
|
try {
|
|
got = await fetchTables(pick.group, pick.sub, pick.detail, q, page, LIST_SIZE, usage);
|
|
} catch (error) {
|
|
showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error");
|
|
return;
|
|
}
|
|
saveView(pick.group, { page, usage });
|
|
const rows = got.tables.map((t) => {
|
|
const tr = el("tr", {
|
|
className: "m01-master__table-row",
|
|
attrs: { "data-key": t.키, "data-file": t.file },
|
|
children: LIST_COLS.map((c) =>
|
|
el("td", { text: String(t[c] ?? ""), attrs: { title: String(t[c] ?? "") } }),
|
|
),
|
|
});
|
|
tr.classList.toggle("is-changed", t.키 in (peek(t.file)?.tables ?? {}));
|
|
tr.addEventListener("click", () => open(t.file, t.키));
|
|
return tr;
|
|
});
|
|
const usageField = createSelectField({
|
|
options: [
|
|
{ value: "", text: L("M01_All") },
|
|
...USAGE_TARGETS.map((v) => ({ value: v, text: v })),
|
|
],
|
|
value: usage,
|
|
compact: true,
|
|
label: L("M01_UsageTarget"),
|
|
});
|
|
usageField.select.addEventListener("change", () => onUsageChange(usageField.select.value));
|
|
host.replaceChildren(
|
|
el("div", { className: "m01-master__filter", children: [usageField.root] }),
|
|
buildPager(
|
|
got.total,
|
|
LIST_SIZE,
|
|
page,
|
|
(p) => void showList(host, pick, q, usage, p, open, onUsageChange),
|
|
),
|
|
el("div", {
|
|
className: "m01-master__grid-wrap m01-master__tables",
|
|
children: [
|
|
el("table", {
|
|
className: "m01-master__grid",
|
|
children: [
|
|
el("thead", {
|
|
children: [el("tr", { children: LIST_COLS.map((c) => el("th", { text: c })) })],
|
|
}),
|
|
el("tbody", { children: rows }),
|
|
],
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
}
|
|
|
|
/** 표 하나. 돌려받은 함수 = 해제. */
|
|
function openTable(
|
|
host: HTMLElement,
|
|
file: string,
|
|
key: string,
|
|
back: () => void,
|
|
openLogic: (key: string) => void,
|
|
): () => void {
|
|
let off = (): void => {};
|
|
void (async () => {
|
|
let version: string;
|
|
let table: Row & { 줄: Row[] };
|
|
try {
|
|
({ version, table } = await fetchTable(file, key));
|
|
} catch (error) {
|
|
showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error");
|
|
return;
|
|
}
|
|
const { 줄: orig, ...base } = table;
|
|
const conds = (table.조건 ?? {}) as Record<string, string>;
|
|
const values = (table.값칸 ?? {}) as Record<string, string>;
|
|
const cols = [...Object.keys(conds), ...Object.keys(values)];
|
|
for (const row of orig) for (const k of Object.keys(row)) if (!cols.includes(k)) cols.push(k);
|
|
const items = (): TableItem[] => tableItems(file, key) ?? orig.map((r, o) => ({ o, r }));
|
|
const commit = (next: TableItem[]): void => setTable(file, version, key, base, next, orig);
|
|
|
|
const parse = (col: string, text: string, prev: unknown): unknown => {
|
|
if (conds[col] === "범위") return toRange(text);
|
|
if (conds[col] === "고르기") return text;
|
|
return col in values || conds[col] === "수" ? toNumberish(text) : coerce(text, prev);
|
|
};
|
|
const headText = (c: string): string =>
|
|
c in conds ? `${c} (${conds[c]})` : c in values ? `${c} (${values[c]})` : c;
|
|
|
|
const paint = (): void => {
|
|
const now = items();
|
|
const body = el("tbody");
|
|
now.forEach((item, i) => {
|
|
const tr = el("tr");
|
|
tr.classList.toggle("is-new", item.o === null);
|
|
const del = el("button", {
|
|
className: "m01-master__row-btn",
|
|
text: L("M01_RowRemove"),
|
|
attrs: { type: "button" },
|
|
});
|
|
del.addEventListener("click", () => commit(now.filter((_, j) => j !== i)));
|
|
tr.append(el("td", { children: [del] }));
|
|
for (const c of cols) {
|
|
const was = item.o === null ? undefined : orig[item.o][c];
|
|
tr.append(
|
|
buildCell(show(item.r[c]), {
|
|
changed: item.o === null || !same(item.r[c], was),
|
|
onEdit: (text) => {
|
|
const value = parse(c, text, item.r[c]);
|
|
const r = { ...item.r };
|
|
if (value === null || value === "")
|
|
delete r[c]; // 원문 「-」 = 칸을 뺌
|
|
else r[c] = value;
|
|
commit(now.map((it, j) => (j === i ? { ...it, r } : it)));
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
body.append(tr);
|
|
});
|
|
const add = createButton({ label: L("M01_RowAdd"), variant: "ghost" });
|
|
add.addEventListener("click", () => commit([...now, { o: null, r: {} }]));
|
|
const backButton = createButton({ label: L("M01_BackToTables"), variant: "ghost" });
|
|
backButton.addEventListener("click", () => {
|
|
off();
|
|
back();
|
|
});
|
|
const notes = ((table.주 ?? []) as string[]).map((n) =>
|
|
el("p", { className: "m01-master__muted", text: n }),
|
|
);
|
|
const usage = table.용도 as TableUsage | undefined;
|
|
const logicLinks = (usage?.로직키 ?? []).map((k, i) => {
|
|
const btn = el("button", {
|
|
className: "m01-master__logic-key",
|
|
text: k,
|
|
attrs: { type: "button", title: L("M01_OpenLogic") },
|
|
});
|
|
btn.addEventListener("click", () => openLogic(k));
|
|
return i === 0 ? btn : el("span", { text: ", ", children: [btn] });
|
|
});
|
|
host.replaceChildren(
|
|
el("div", {
|
|
className: "m01-master__bar",
|
|
children: [
|
|
backButton,
|
|
el("strong", { text: String(table.이름 ?? key) }),
|
|
el("span", {
|
|
className: "m01-master__muted",
|
|
text: `${key} · ${table.기준 || "—"} · ${table.출처 || ""}`,
|
|
}),
|
|
...(usage
|
|
? [
|
|
el("span", {
|
|
className: "m01-master__muted",
|
|
text: `${L("M01_TableUsage")}: ${usage.공종} · ${usage.대상.join(", ")} · `,
|
|
children: logicLinks,
|
|
}),
|
|
]
|
|
: []),
|
|
add,
|
|
],
|
|
}),
|
|
el("div", {
|
|
className: "m01-master__grid-wrap",
|
|
children: [
|
|
el("table", {
|
|
className: "m01-master__grid",
|
|
children: [
|
|
el("thead", {
|
|
children: [
|
|
el("tr", {
|
|
children: [el("th"), ...cols.map((c) => el("th", { text: headText(c) }))],
|
|
}),
|
|
],
|
|
}),
|
|
body,
|
|
],
|
|
}),
|
|
],
|
|
}),
|
|
...notes,
|
|
);
|
|
};
|
|
off = onDraftChange(paint);
|
|
paint();
|
|
})();
|
|
return () => off();
|
|
}
|