revert(Z01): 롤백된 마스터 DB·화면 작업이 dev 로 되들어온 것 걷어냄

- 사용자 「완전히 되돌려」 뒤 dev 를 거쳐 다시 들어온 1-2·1-3·1-4 코드 26 경로를 9629430a 모습으로(고친 17 되돌림 · 새로 생긴 9 지움)
- 대상: Z01_MasterData 17 · main.py · ui_template_locale_b3.ts · resources/tester 의 Z01 시험 7
- 품셈 md·도구는 안 건드림 · 이력 재작성 없음(되돌림 커밋)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0145cr1DMC5Ft8fouQSXtp69
This commit is contained in:
2026-09-19 01:36:21 +09:00
co-authored by Claude Opus 5
parent 6b417d3b79
commit f2fe4f4214
26 changed files with 1012 additions and 3598 deletions
+152 -285
View File
@@ -1,11 +1,10 @@
/* =============================================================================
* Z01_MasterData_UI_Rows.ts
* 우측 표 보기 — 고른 표(트리 표 · 기초단가)의 줄을 쪽 나누기·검색으로 그리고, 기초단가는 고침
* 우측 표 보기 — 고른 표(트리 표 · 기초단가)의 줄을 쪽 나누기·검색으로 그리고, 기초단가는 칸을 고침
*
* 한 길로 두 가지를 그림 — 트리 표는 읽기 전용, 기초단가는 `edit` 이 있어 고침.
* 칸을 누르면 표 위 「칸 설명」 줄에 고칠 수 있는지 · 못 고치는 까닭 · 산출근거가 섬
* 한 길로 두 가지를 그림 — 트리 표는 읽기 전용, 기초단가는 `save` 가 있어 editable 칸을 고침.
* 칸을 누르면 표 위 「칸 설명」 줄에 고칠 수 있는지 · 못 고치는 까닭 · 원래 값 · 산출근거가 섬
* (2026-09-15 브레인 — 계산으로 나오는 칸은 못 고치고 왜 못 고치는지 옆에 보일 것).
* ⚠ 자동저장 금지 — 고친 칸·추가 줄·삭제 줄은 초안(`BaseDraft`)에 쌓고 [저장] 한 번에 보냄(PLAN 1-0).
* ========================================================================== */
import {
@@ -13,22 +12,14 @@ import {
createInputField,
createSelectField,
el,
hideLoadingOverlay,
showConfirmDialog,
showLoadingOverlay,
showToast,
} from "@ui/ui_template_elements";
import { t as L } from "@ui/ui_template_locale";
import {
ApiError,
type BaseSave,
type MasterRows,
type RowsQuery,
} from "./Z01_MasterData_Api_Fetch";
import type { MasterRows, RowsQuery } from "./Z01_MasterData_Api_Fetch";
import {
boldSegments,
cellInfo,
cellText,
clampPage,
columnTitle,
filterValue,
@@ -41,29 +32,24 @@ import {
rowCells,
shownColumns,
valueWithUnit,
type CellInfo,
type MasterColumn,
type SortOrder,
} from "./Z01_MasterData_UI_Cells";
import { BaseDraft, type AddedRow } from "./Z01_MasterData_UI_Draft";
const PAGE_SIZE = 50;
const SEARCH_DELAY_MS = 300;
type Row = Record<string, unknown>;
type LocaleKey = Parameters<typeof L>[0];
export interface RowsSource {
title: string;
key: string;
/** false 면 검색 칸을 숨김 — 서버에 검색이 없는 표 */
/** false 면 검색 칸을 숨김 — 서버에 검색이 없는 표(고친 것 모아 보기) */
searchable?: boolean;
load: (query: RowsQuery) => Promise<MasterRows>;
/** 있으면 기초단가 — 초안에 쌓고 [저장] 한 번에 보냄 · [초기값으로]는 바로 저장 */
edit?: {
scope: string;
save: (body: BaseSave) => Promise<unknown>;
reset: (rowKeys: string[]) => Promise<unknown>;
};
/** 있으면 editable 칸을 고침 — 응답은 고친 뒤 그 줄 */
save?: (rowId: string, values: Row) => Promise<Row>;
}
export interface RowsView {
@@ -72,15 +58,36 @@ export interface RowsView {
}
/** 사전 문구의 {value} 자리에 값을 넣음. */
function fill(key: LocaleKey, value: string): string {
function fill(key: Parameters<typeof L>[0], value: string): string {
return L(key).replace("{value}", value);
}
/** 고른 칸 — 서버 줄은 row_key(`@id`) · 저장 전 새 줄은 tmp. */
interface Picked {
row: string;
added: boolean;
column: string;
/**
* 뜻이 바뀐 줄의 옛 이름·지금 이름 — 서버가 합쳐 한 줄로 줌(자재는 품명·규격·인도조건이 이어 붙어 옴).
* 화면은 쪼개거나 다시 합치지 않음. 안 오면 버그로 드러냄.
*/
function meaningNames(info: CellInfo): string {
if (!info.was || !info.now) return L("Z01_MasterData_State_MeaningMissing");
return `${fill("Z01_MasterData_State_Was", info.was)} / ${fill("Z01_MasterData_State_Now", info.now)}`;
}
/** 덮개 판정 한 줄 — 서버 state 그대로 · state 가 없으면 버그로 드러냄. */
function stateText(info: CellInfo, column: MasterColumn): string {
const original = valueWithUnit(info.original, column.unit);
switch (info.state) {
case "meaning_changed":
// 값이 틀린 것보다 나쁨 — 같은 번호에 딴 물건이 앉음. 옛 이름·지금 이름을 나란히.
// ⚠ 이름이 안 오면 물음표로 때우지 않고 버그로 드러냄 — 물음표는 「원래 그런가 보다」로 넘김.
return `${L("Z01_MasterData_State_MeaningChanged")} — ${meaningNames(info)} · ${fill("Z01_MasterData_Edit_OriginalWas", original)}`;
case "source_changed":
return `${fill("Z01_MasterData_State_SourceChanged", valueWithUnit(info.current, column.unit))} (${fill("Z01_MasterData_Edit_OriginalWas", original)})`;
case "orphan":
return L("Z01_MasterData_State_Orphan");
case "missing":
return L("Z01_MasterData_State_Missing");
default:
return fill("Z01_MasterData_State_Edited", original);
}
}
export function buildRowsView(): RowsView {
@@ -93,10 +100,7 @@ export function buildRowsView(): RowsView {
};
let showHidden = false;
let last: MasterRows | null = null;
let picked: Picked | null = null;
let draft: BaseDraft | null = null;
/** 409 를 받음 — 다른 곳에서 먼저 저장해 이 초안은 저장 못 함(조용히 버리지 않고 알림) */
let stale = false;
let selected: { index: number; key: string } | null = null;
let searchTimer = 0;
const fetchLatest = latestOnly((from: RowsSource, query: RowsQuery) => from.load(query));
@@ -149,26 +153,12 @@ export function buildRowsView(): RowsView {
className: "z01-master__pager",
children: [first, prev, pageInput, pageInfo, next, end],
});
// 기초단가 고치기 — [저장]·[고친 것 버리기]·[줄 추가]·[줄 삭제]·[초기값으로]
const action = (key: LocaleKey, onClick: () => void, variant: "filled" | "ghost" = "ghost") =>
createButton({ label: L(key), variant, onClick });
const saveButton = action("Z01_MasterData_Save", () => void save(), "filled");
const discardButton = action("Z01_MasterData_Discard", discard);
const addButton = action("Z01_MasterData_AddRow", addRow);
const deleteButton = action("Z01_MasterData_DeleteRow", toggleDelete);
const resetButton = action("Z01_MasterData_Reset", () => void reset());
const draftStatus = el("span", { className: "z01-master__draft-status" });
const actions = el("div", {
className: "z01-master__toolbar",
children: [saveButton, discardButton, addButton, deleteButton, resetButton, draftStatus],
});
const root = el("section", {
className: "z01-master__panel",
children: [
el("div", { children: [title, titleKey] }),
notice,
banner,
actions,
toolbar,
detail,
grid,
@@ -180,13 +170,6 @@ export function buildRowsView(): RowsView {
detail.hidden = true;
notice.hidden = true;
banner.hidden = true;
actions.hidden = true;
// 저장 안 한 고침이 조용히 사라지지 않게 — 나갈 때 물음(자동저장은 안 만듦, CLAUDE.md 5장).
window.addEventListener("beforeunload", (event) => {
if (!draft?.count || !root.isConnected) return;
event.preventDefault();
event.returnValue = L("Z01_MasterData_Unsaved");
});
function goTo(page: number): void {
const pages = pageCount(last?.total ?? 0, PAGE_SIZE);
@@ -221,65 +204,15 @@ export function buildRowsView(): RowsView {
if (!data) return;
grid.classList.remove("is-loading");
last = data;
const edit = source.edit;
if (edit && data.revision !== undefined) {
// 고친 것이 남은 초안은 쪽을 넘겨도 지킴 — 그새 판이 바뀌었으면 stale 로 알림(조용히 안 버림).
if (draft?.scope !== edit.scope || !draft.count) {
draft = new BaseDraft(edit.scope, data.revision);
draft.onChange = drawActions;
}
stale = draft.revision !== data.revision;
}
picked = null;
selected = null;
detail.hidden = true;
draw();
}
const rowKey = (row: Row, index: number): string =>
row["@id"] == null ? `#${index}` : String(row["@id"]);
const changedColumns = (row: Row): string[] =>
Array.isArray(row.changed_columns) ? (row.changed_columns as string[]) : [];
/** 고른 서버 줄 — 저장 전 새 줄·열쇠 없는 줄이면 없음. */
function pickedRow(): Row | undefined {
if (!picked || picked.added) return undefined;
const key = picked.row;
return last?.rows.find((row) => row["@id"] != null && String(row["@id"]) === key);
}
/** 새 줄에 적은 글자를 수로 읽을지 — 같은 열 다른 줄 값의 꼴을 따름(코드 칸이 수로 바뀌지 않게). */
const sample = (column: string): unknown =>
last?.rows.find((row) => row[column] != null)?.[column];
function drawActions(): void {
// 서버가 판(revision)을 준 표만 고침 — 안 주는 표(공종 축 등 DB 밖)는 단추를 안 세움.
actions.hidden = !draft;
const count = draft?.count ?? 0;
const row = pickedRow();
saveButton.disabled = count === 0 || stale;
discardButton.disabled = count === 0 && !stale;
addButton.disabled = !draft || stale;
deleteButton.disabled = !draft || !(picked?.added || row);
const undo = Boolean(row && draft?.deleted.has(String(row["@id"])));
deleteButton.querySelector(".ui-btn__label")!.textContent = L(
undo ? "Z01_MasterData_UndeleteRow" : "Z01_MasterData_DeleteRow",
);
// 초기값으로는 바로 저장 — 저장 안 한 초안이 있으면 판이 갈려 초안이 버려지니 먼저 저장·버리게 함.
resetButton.disabled = !row || count > 0 || !(changedColumns(row).length || row.is_added);
resetButton.title = count > 0 ? L("Z01_MasterData_ResetBlocked") : "";
draftStatus.classList.toggle("is-warning", stale);
draftStatus.textContent = stale
? L("Z01_MasterData_Draft_Stale")
: count && draft
? `${L("Z01_MasterData_Draft_Edits")} ${draft.edits.size} · ${L("Z01_MasterData_Draft_Added")} ${draft.added.length} · ${L("Z01_MasterData_Draft_Deleted")} ${draft.deleted.size} — ${L("Z01_MasterData_Draft_Pending")}`
: L("Z01_MasterData_Draft_None");
}
function headCell(meta: MasterRows, column: MasterColumn): HTMLElement {
const th = el("th", { children: [el("span", { text: columnTitle(column) })] });
const marks: [string, string][] = [];
if (source?.edit && meta.editable?.includes(column.key)) {
if (source?.save && meta.editable?.includes(column.key)) {
marks.push(["✎", L("Z01_MasterData_Edit_Can")]);
}
const reason = meta.locked?.[column.key];
@@ -353,11 +286,39 @@ export function buildRowsView(): RowsView {
const columns = shownColumns(meta.columns, showHidden);
const headRow = el("tr", { children: columns.map((column) => headCell(meta, column)) });
const body = el("tbody");
// 저장 전 새 줄은 어느 쪽에서나 맨 위 — 쪽을 넘겨도 안 사라짐.
for (const added of draft?.added ?? []) body.append(addedRow(meta, columns, added));
meta.rows.forEach((row, index) => body.append(serverRow(meta, columns, row, index)));
meta.rows.forEach((row, index) => {
const texts = rowCells(row, columns);
const tr = el("tr");
columns.forEach((column, i) => {
const info = cellInfo(meta, row, column.key);
const td = el("td", { text: texts[i], attrs: { title: texts[i] } });
if (info.state) {
// 원본이 바뀐 칸은 줄째 또렷이 — 흐리면 옛 값이 조용히 계속 쓰임(2026-09-15 브레인).
td.classList.add("is-overridden", `is-${info.state.replaceAll("_", "-")}`);
if (info.state !== "edited") {
// 뜻 바뀜은 값 어긋남보다 나쁨 — 표시부터 더 세게(2026-09-16 브레인).
td.textContent = `${info.state === "meaning_changed" ? "⛔" : "⚠"} ${texts[i]}`;
tr.classList.add(info.state === "meaning_changed" ? "has-danger" : "has-warning");
}
td.title = stateText(info, column);
}
const canEdit = Boolean(source?.save) && info.editable && row["@id"] != null;
if (canEdit) td.classList.add("is-editable");
if (selected?.index === index && selected.key === column.key)
td.classList.add("is-selected");
td.addEventListener("click", () => {
if (td.querySelector("input")) return;
grid.querySelector("td.is-selected")?.classList.remove("is-selected");
selectCell(index, column);
td.classList.add("is-selected");
if (canEdit) startEdit(td, index, column);
});
tr.append(td);
});
body.append(tr);
});
grid.replaceChildren(
body.children.length
meta.rows.length
? el("table", {
className: "z01-master__grid",
children: [el("thead", { children: [headRow] }), body],
@@ -370,100 +331,25 @@ export function buildRowsView(): RowsView {
pageInfo.textContent = `/ ${pages.toLocaleString("ko-KR")} ${L("Z01_MasterData_Page")} · ${meta.total.toLocaleString("ko-KR")} ${L("Z01_MasterData_Rows")}`;
first.disabled = prev.disabled = state.page <= 1;
next.disabled = end.disabled = state.page >= pages;
drawActions();
}
const isPicked = (row: string, added: boolean, column: string): boolean =>
picked?.row === row && picked.added === added && picked.column === column;
function serverRow(meta: MasterRows, columns: MasterColumn[], row: Row, index: number) {
const key = rowKey(row, index);
const texts = rowCells(row, columns);
const changed = changedColumns(row);
const deleted = draft?.deleted.has(key) ?? false;
const tr = el("tr");
if (row.is_added) tr.classList.add("is-added");
if (deleted) tr.classList.add("is-deleted");
columns.forEach((column, i) => {
const info = cellInfo(meta, row, column.key);
const stored = row[column.key];
const pending = draft?.edit(key, column.key);
const text = pending ? cellText(pending.value, column.unit) : texts[i];
const td = el("td", { text, attrs: { title: text } });
// 고친 칸 표시 — 저장 전(초안) · 저장했고 초기값과 다름(서버 changed_columns)
if (pending) td.classList.add("is-draft");
else if (changed.includes(column.key)) td.classList.add("is-changed");
const canEdit = Boolean(draft) && info.editable && row["@id"] != null && !deleted;
if (canEdit) td.classList.add("is-editable");
if (isPicked(key, false, column.key)) td.classList.add("is-selected");
td.addEventListener("click", () => {
if (td.querySelector("input")) return;
pick(td, { row: key, added: false, column: column.key });
if (!canEdit) return;
startEdit(td, pending ? pending.value : stored, stored ?? sample(column.key), (value) =>
draft?.setEdit(key, column.key, value, stored),
);
});
tr.append(td);
});
return tr;
}
function addedRow(meta: MasterRows, columns: MasterColumn[], added: AddedRow) {
const tr = el("tr", { className: "is-new", attrs: { title: L("Z01_MasterData_Row_New") } });
for (const column of columns) {
const value = added.data[column.key];
const td = el("td", { text: cellText(value, column.unit) });
// 새 줄은 식으로 나오는 칸만 빼고 다 채움 — 열쇠·이름도 사람이 넣어야 줄이 섬.
const canEdit = !meta.formula?.[column.key];
if (canEdit) td.classList.add("is-editable");
if (isPicked(added.tmp, true, column.key)) td.classList.add("is-selected");
td.addEventListener("click", () => {
if (td.querySelector("input")) return;
pick(td, { row: added.tmp, added: true, column: column.key });
if (!canEdit) return;
startEdit(
td,
value,
sample(column.key),
(next) => draft?.setAdded(added.tmp, column.key, next),
true,
);
});
tr.append(td);
}
return tr;
}
function pick(td: HTMLElement, next: Picked): void {
grid.querySelector("td.is-selected")?.classList.remove("is-selected");
td.classList.add("is-selected");
picked = next;
showDetail();
drawActions();
}
/**
* 칸 설명 줄 — 고칠 수 있나 · 못 고치는 까닭 · 저장 전 고침 · 초기값과 다름 · 식과 값 넣은 식.
* `failure` = 방금 고침이 안 들어간 까닭 — 토스트만 뜨고 사라지면 사람은 들어간 줄 앎(브레인 ③).
* 칸 설명 줄 — 고칠 수 있나 · 못 고치는 까닭 · 원래 값(되돌리기) · 식과 값 넣은 식.
* `failure` = 방금 저장이 안 된 까닭 — 토스트만 뜨고 사라지면 사람은 저장된 줄 앎(브레인 ③).
*/
function showDetail(failure?: string): void {
const at = picked;
const column = last?.columns.find((each) => each.key === at?.column);
if (!last || !at || !column) return;
const row = at.added
? (draft?.added.find((each) => each.tmp === at.row)?.data ?? {})
: (pickedRow() ?? last.rows[Number(at.row.slice(1))] ?? {});
function selectCell(index: number, column: MasterColumn, failure?: string): void {
if (!last) return;
selected = { index, key: column.key };
const row = last.rows[index];
const info = cellInfo(last, row, column.key);
const lines: HTMLElement[] = [el("strong", { text: columnTitle(column) })];
const line = (text: string, warn = false) => {
const span = el("span", { className: "z01-master__detail-line", text });
span.classList.toggle("is-warning", warn);
return span;
};
if (failure) lines.push(line(fill("Z01_MasterData_Edit_NotSaved", failure), true));
if (at.added) lines.push(line(L("Z01_MasterData_Row_New")));
else if (source?.edit) {
const line = (text: string) => el("span", { className: "z01-master__detail-line", text });
if (failure) {
const failed = line(fill("Z01_MasterData_Edit_NotSaved", failure));
failed.classList.add("is-warning");
lines.push(failed);
}
if (source?.save) {
lines.push(
line(
info.editable
@@ -471,14 +357,30 @@ export function buildRowsView(): RowsView {
: `${L("Z01_MasterData_Edit_Locked")} — ${info.reason ?? L("Z01_MasterData_Edit_NotListed")}`,
),
);
const pending = draft?.edit(at.row, column.key);
if (pending) {
const value = valueWithUnit(pending.value, column.unit);
lines.push(line(fill("Z01_MasterData_Cell_Draft", value)));
}
if (info.state) {
const stateLine = line(stateText(info, column));
if (info.state !== "edited") stateLine.classList.add("is-warning");
lines.push(stateLine);
// 단추 글자에 무엇으로 바뀌는지 값을 넣음 — 안 보이고 누르면 금액이 말없이 바뀜(브레인).
// 뜻이 바뀐 줄에서 「새 원본 값으로 받기」는 위험 — 글자를 가르고 한 번 더 물음(브레인 ③).
const target =
info.state === "meaning_changed"
? fill("Z01_MasterData_Edit_DropOnMeaning", valueWithUnit(info.current, column.unit))
: info.state === "source_changed"
? fill("Z01_MasterData_Edit_TakeSource", valueWithUnit(info.current, column.unit))
: info.state === "orphan"
? null
: fill("Z01_MasterData_Edit_RevertTo", valueWithUnit(info.original, column.unit));
if (target && source?.save) {
lines.push(
createButton({
label: target,
variant: info.state === "meaning_changed" ? "danger" : "ghost",
onClick: () => void drop(index, column, info),
}),
);
}
if (changedColumns(row).includes(column.key))
lines.push(line(L("Z01_MasterData_Cell_Changed")));
if (row.is_added) lines.push(line(L("Z01_MasterData_Row_Added")));
}
if (info.rule) lines.push(line(`${L("Z01_MasterData_Formula")} ${info.rule}`));
if (info.filled) lines.push(line(info.filled));
@@ -486,38 +388,31 @@ export function buildRowsView(): RowsView {
detail.hidden = false;
}
/**
* 칸 고치기 — Enter·칸 나감 = 초안에 넣음(서버엔 안 감) · Esc = 그대로.
* `typeOf` = 수로 읽을지 가르는 본보기 값 · `allowEmpty` = 비우면 칸을 뺌(새 줄).
*/
function startEdit(
td: HTMLElement,
current: unknown,
typeOf: unknown,
apply: (value: unknown) => void,
allowEmpty = false,
): void {
function startEdit(td: HTMLElement, index: number, column: MasterColumn): void {
if (!last) return;
const current = last.rows[index][column.key];
const input = el("input", { className: "ui-input z01-master__cell-input" });
input.value = current == null ? "" : String(current);
td.replaceChildren(input);
input.focus();
input.select();
let finished = false;
const finish = (keep: boolean): void => {
const finish = (save: boolean): void => {
if (finished) return;
finished = true;
const empty = allowEmpty && input.value.trim() === "";
const value = !keep ? current : empty ? null : parseCellInput(input.value, typeOf);
const value = save ? parseCellInput(input.value, current) : current;
if (value === undefined) {
const why = fill("Z01_MasterData_Edit_Invalid", input.value);
showToast(why, "error");
draw();
showDetail(why);
selectCell(index, column, why);
return;
}
if (keep) apply(value);
draw();
showDetail();
if (value === current) {
draw();
return;
}
void commit(index, column, value);
};
input.addEventListener("keydown", (event) => {
if (event.key === "Enter") finish(true);
@@ -526,87 +421,59 @@ export function buildRowsView(): RowsView {
input.addEventListener("blur", () => finish(true));
}
function addRow(): void {
draft?.addRow();
draw();
}
function toggleDelete(): void {
if (!draft || !picked) return;
if (picked.added) {
draft.removeAdded(picked.row);
picked = null;
detail.hidden = true;
} else if (pickedRow()) {
draft.toggleDelete(picked.row);
/** 고침 버리기 — 뜻이 바뀐 줄은 한 번 더 묻고(딴 물건에 값이 붙은 자리라 되돌리기와 뜻이 다름). */
async function drop(index: number, column: MasterColumn, info: CellInfo): Promise<void> {
if (info.state === "meaning_changed") {
const ok = await showConfirmDialog(
`${meaningNames(info)}\n${L("Z01_MasterData_Edit_MeaningConfirm")}`,
L("Z01_MasterData_Edit_DropConfirm"),
);
if (!ok) return;
}
draw();
await commit(index, column, null);
}
function discard(): void {
draft?.clear();
stale = false;
void load();
}
/** [저장] — 초안 한 벌을 보냄. 409 면 초안을 지키고 알림(다른 곳에서 먼저 저장함). */
async function save(): Promise<void> {
const edit = source?.edit;
const active = draft;
if (!edit || !active?.count) return;
showLoadingOverlay();
/** 저장 — null 은 덮개에서 그 칸을 빼 원래 값으로 되돌림. */
async function commit(index: number, column: MasterColumn, value: unknown): Promise<void> {
const from = source;
const table = last;
const rowId = table?.rows[index]["@id"];
if (!from?.save || !table || rowId == null) return;
try {
await edit.save(active.payload());
active.clear();
showToast(L("Z01_MasterData_Saved"), "success");
void load();
const updated = await from.save(String(rowId), { [column.key]: value });
if (from !== source || table !== last) return;
table.rows[index] = updated;
draw();
selectCell(index, column);
showToast(
L(value === null ? "Z01_MasterData_Edit_Reverted" : "Z01_MasterData_Edit_Saved"),
"success",
);
} catch (error) {
if (error instanceof ApiError && error.status === 409) stale = true;
showToast(error instanceof Error ? error.message : L("Z01_MasterData_Edit_Failed"), "error");
drawActions();
} finally {
hideLoadingOverlay();
const why = error instanceof Error ? error.message : L("Z01_MasterData_Edit_Failed");
showToast(why, "error");
if (from !== source || table !== last) return;
draw();
selectCell(index, column, why);
}
}
/** [초기값으로] — 고른 줄을 seed 로(추가한 줄은 지워짐) · 바로 저장이라 한 번 물음. */
async function reset(): Promise<void> {
const edit = source?.edit;
const row = pickedRow();
if (!edit || !row || draft?.count) return;
const ok = await showConfirmDialog(L("Z01_MasterData_ResetConfirm"), L("Z01_MasterData_Reset"));
if (!ok) return;
showLoadingOverlay();
try {
await edit.reset([String(row["@id"])]);
showToast(L("Z01_MasterData_ResetDone"), "success");
void load();
} catch (error) {
showToast(error instanceof Error ? error.message : L("Z01_MasterData_Edit_Failed"), "error");
} finally {
hideLoadingOverlay();
}
}
function show(next: RowsSource): void {
source = next;
function show(picked: RowsSource): void {
source = picked;
state.page = 1;
state.sort = null;
state.filters = {};
filterBox.replaceChildren();
title.textContent = next.title;
titleKey.textContent = next.key;
title.textContent = picked.title;
titleKey.textContent = picked.key;
notice.hidden = true;
banner.hidden = true;
search.root.hidden = next.searchable === false;
search.root.hidden = picked.searchable === false;
toolbar.hidden = false;
pager.hidden = false;
detail.hidden = true;
last = null;
picked = null;
draft = null;
stale = false;
actions.hidden = true;
selected = null;
grid.replaceChildren();
void load();
}