- B01 옛 「마스터 데이터」(Z01) 단추 제거 · 「마스터 요소」 이름을 「마스터 데이터」 로
- 재료 › 시중물가 줄마다 「조달 찾기」 — 나라장터자재 + 시중물가 조달 줄을 이름·규격으로 찾아 「나라장터:<열쇠>」 연결 · 노랑 표시 · 연결 끊기
- GET /api/m01/procurement 추가 · 값 묶음(조달{…}) 확정 전이라 연결은 조달›연결 칸에 임시로 담음
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT
179 lines
5.5 KiB
TypeScript
179 lines
5.5 KiB
TypeScript
/* =============================================================================
|
|
* M01_MasterData_UI_Rows.ts
|
|
* 요소 파일(인력·재료·기계·환율·요율) 표 — 한 줄 = 요소 · 이름 찾기 · 쪽 나눔 · 눌러 고침(노랑)
|
|
* ========================================================================== */
|
|
|
|
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
|
import { t as L } from "@ui/ui_template_locale";
|
|
import { openProcureModal } from "./M01_MasterData_UI_Procure";
|
|
import { fetchRows, type Row, type RowsPage } from "./M01_MasterData_Api_Fetch";
|
|
import {
|
|
addRow,
|
|
editRow,
|
|
onDraftChange,
|
|
peek,
|
|
removeAdd,
|
|
same,
|
|
setAdd,
|
|
toggleDelete,
|
|
} from "./M01_MasterData_Draft";
|
|
import {
|
|
buildCell,
|
|
buildPager,
|
|
coerce,
|
|
flatten,
|
|
isScalar,
|
|
SEP,
|
|
show,
|
|
withCell,
|
|
} from "./M01_MasterData_UI_Cells";
|
|
|
|
const SIZE = 50;
|
|
const MARKET = "재료_시중물가.json";
|
|
const LINK = `조달${SEP}연결`;
|
|
const FALLBACK: Row = { 열쇠: "", 이름: "", 규격: "", 단위: "", 값: null, 출처: "" };
|
|
|
|
/** 새 줄 바탕 — 본 줄과 같은 칸 · 글 칸은 "" · 수·값 칸은 null. */
|
|
function blank(sample: Row): Row {
|
|
const out: Row = {};
|
|
for (const [k, v] of Object.entries(sample)) {
|
|
if (v && typeof v === "object" && !Array.isArray(v)) out[k] = blank(v as Row);
|
|
else out[k] = typeof v === "string" && k !== "값" ? "" : null;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** 돌려받은 함수 = 이 화면을 걷을 때 부를 해제. */
|
|
export function renderRows(host: HTMLElement, file: string, q: string): () => void {
|
|
let page = 1;
|
|
let data: RowsPage | null = null;
|
|
|
|
const paint = (): void => {
|
|
if (!data) return;
|
|
const d = data;
|
|
const draft = peek(file);
|
|
const adds = draft?.adds ?? [];
|
|
const cols: string[] = [];
|
|
for (const row of [...adds, ...d.rows]) {
|
|
for (const k of Object.keys(flatten(row))) if (k !== LINK && !cols.includes(k)) cols.push(k);
|
|
}
|
|
const market = file === MARKET;
|
|
const head = el("tr", {
|
|
children: [
|
|
el("th"),
|
|
...cols.map((c) => el("th", { text: c })),
|
|
...(market ? [el("th", { text: L("M01_ProcureCol") })] : []),
|
|
],
|
|
});
|
|
const body = el("tbody");
|
|
|
|
adds.forEach((row, i) => {
|
|
const flat = flatten(row);
|
|
const tr = el("tr", { className: "is-new" });
|
|
tr.append(actionCell(L("M01_RowRemove"), () => removeAdd(file, i)));
|
|
for (const c of cols) {
|
|
tr.append(
|
|
buildCell(show(flat[c]), {
|
|
changed: true,
|
|
onEdit: (text) => setAdd(file, i, withCell(row, c, coerce(text, flat[c]))),
|
|
}),
|
|
);
|
|
}
|
|
body.append(tr);
|
|
});
|
|
|
|
for (const row of d.rows) {
|
|
const key = String(row["열쇠"]);
|
|
const cur = draft?.edits[key] ?? row;
|
|
const deleted = draft?.deletes.includes(key) ?? false;
|
|
const orig = flatten(row);
|
|
const now = flatten(cur);
|
|
const tr = el("tr");
|
|
tr.classList.toggle("is-deleted", deleted);
|
|
tr.append(
|
|
actionCell(deleted ? L("M01_RowRestore") : L("M01_RowDelete"), () =>
|
|
toggleDelete(file, d.version, key),
|
|
),
|
|
);
|
|
for (const c of cols) {
|
|
const editable = !deleted && isScalar(orig[c]);
|
|
tr.append(
|
|
buildCell(show(now[c]), {
|
|
changed: !same(now[c], orig[c]),
|
|
onEdit: editable
|
|
? (text) =>
|
|
editRow(file, d.version, key, row, withCell(cur, c, coerce(text, orig[c])))
|
|
: undefined,
|
|
}),
|
|
);
|
|
}
|
|
if (market) {
|
|
const link = String(now[LINK] ?? "");
|
|
const pick = (ref: string | null): void =>
|
|
editRow(file, d.version, key, row, withCell(cur, LINK, ref));
|
|
const btn = el("button", {
|
|
className: "m01-master__row-btn",
|
|
text: L("M01_ProcureFind"),
|
|
attrs: { type: "button" },
|
|
});
|
|
btn.addEventListener("click", () => openProcureModal(link, pick));
|
|
const td = buildCell(link, { changed: link !== String(orig[LINK] ?? "") });
|
|
td.append(btn);
|
|
tr.append(td);
|
|
}
|
|
body.append(tr);
|
|
}
|
|
|
|
const add = createButton({ label: L("M01_RowAdd"), variant: "ghost" });
|
|
add.addEventListener("click", () => addRow(file, d.version, blank(d.rows[0] ?? FALLBACK)));
|
|
const empty =
|
|
d.rows.length || adds.length
|
|
? []
|
|
: [el("p", { className: "m01-master__empty", text: L("M01_NoRows") })];
|
|
host.replaceChildren(
|
|
el("div", {
|
|
className: "m01-master__bar",
|
|
children: [add, buildPager(d.total, SIZE, d.page, go)],
|
|
}),
|
|
el("div", {
|
|
className: "m01-master__grid-wrap",
|
|
children: [
|
|
el("table", {
|
|
className: "m01-master__grid",
|
|
children: [el("thead", { children: [head] }), body],
|
|
}),
|
|
],
|
|
}),
|
|
...empty,
|
|
);
|
|
};
|
|
|
|
const go = (next: number): void => {
|
|
page = next;
|
|
void load();
|
|
};
|
|
|
|
const load = async (): Promise<void> => {
|
|
try {
|
|
data = await fetchRows(file, page, SIZE, q);
|
|
paint();
|
|
} catch (error) {
|
|
showToast(error instanceof Error ? error.message : L("M01_LoadFailed"), "error");
|
|
}
|
|
};
|
|
|
|
const off = onDraftChange(paint);
|
|
void load();
|
|
return off;
|
|
}
|
|
|
|
function actionCell(label: string, onClick: () => void): HTMLTableCellElement {
|
|
const button = el("button", {
|
|
className: "m01-master__row-btn",
|
|
text: label,
|
|
attrs: { type: "button" },
|
|
});
|
|
button.addEventListener("click", onClick);
|
|
return el("td", { children: [button] });
|
|
}
|