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:
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user