fix(M01): 일위대가 조합 화면 점검 흠 고침 — 찾기 · 떠나기 확인 · 본뜬키 칸 · 표 폭 · 글

- 왼쪽 컨테이너에 이름 · 키 찾기 칸 · 거름 · 찾기를 만지면 다른 화면이 떠 있어도 조합 화면으로 · 걸러서 없으면 「찾은 조합 없음」
- 저장 안 한 고침이 있으면 [←] 에서 물음 · 다른 컨테이너 갔다 와도 고침 판정이 안 풀림
- 본뜬키를 다른 칸과 같은 읽기 전용 칸으로 · 표 · 미리 보기가 칸 폭을 채움 · 금액 오른쪽 맞춤 · 긴 이름 풍선
- 로직 고르기 창 열면 찾기 칸에 글쇠 · Enter 로 찾기 · 이름 칸 60자까지 · 서버 검사 글 머리(파일 · 새 키) 뗌 · 지우기 확인에 키 · 이름
- 흐린 안내 글을 보조 글색으로(밝은 테마 대비 3.1 → 4.5 넘김) · 계산 · 저장 규칙은 그대로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoyUwuTFiDJfvCfHpvKKzp
This commit is contained in:
2026-09-28 22:03:46 +09:00
co-authored by Claude Opus 5.5
parent 527e68db85
commit ae47bcd921
6 changed files with 80 additions and 21 deletions
@@ -0,0 +1,14 @@
/* 일위대가 조합 화면 — 표는 칸 폭을 채움 · 미리 보기 금액은 오른쪽 맞춤 ·
흐린 안내 글은 보조 글색(뮤트 글색은 밝은 테마에서 대비 3.1 로 모자람) */
.m01-combo .m01-master__grid {
width: 100%;
}
.m01-combo [data-combo="preview"] th:not(:first-child),
.m01-combo [data-combo="preview"] td:not(:first-child) {
text-align: right;
}
.m01-combo .m01-logic__muted {
color: var(--color-text-secondary);
}
+46 -18
View File
@@ -28,6 +28,7 @@ import { tc } from "./M01_MasterData_UI_Combo_Text";
import type { SideHandle } from "./M01_MasterData_UI_Side";
import "./M01_MasterData_UI_Logic_Style.css";
import "./M01_MasterData_UI_LogicLab_Style.css";
import "./M01_MasterData_UI_Combo.css";
const FILTER_ID = "조합|";
const blankCombo = (): Combo => ({
@@ -47,7 +48,9 @@ const blankCombo = (): Combo => ({
function reason(error: unknown): string {
if (error instanceof ApiError && typeof error.detail === "object" && error.detail) {
const d = error.detail as { errors?: string[]; stale?: string[] };
if (d.errors) return d.errors.join(" / ");
// 서버 검사 글 머리(「파일.json · 키 」)는 떼고 보임 — 새로 만들 때 키는 아직 없는 번호라 헷갈림
if (d.errors)
return d.errors.map((x) => x.replace(/^\S+\.json · (U[AM]\d{6} )?/, "")).join(" / ");
if (d.stale) return tc("Stale");
}
return error instanceof Error ? error.message : tc("Load_Failed");
@@ -62,6 +65,8 @@ export function mountM01Combo(
host: HTMLElement,
side: SideHandle,
onOpenLogic: (key: string) => void,
/** 왼쪽 거름 · 찾기를 만지면 — 다른 화면이 떠 있으면 이 화면으로 */
activate: () => void = () => {},
): ComboHandle {
let items: ComboSummary[] = [];
let logics = new Map<string, LogicSummary>();
@@ -69,11 +74,19 @@ export function mountM01Combo(
/** 화면을 바꾼 횟수 — 늦게 온 답은 이 값이 달라졌으면 버림 */
let nav = 0;
/** 열어 둔 상세 — 다른 화면에 갔다 와도 그대로 돌아오게 기억 */
let current: { combo: Combo; version: string } | null = null;
/** `saved` = 연 때의 모양 — 다시 그려도 「저장 안 한 고침」 판정이 안 풀리게 */
let current: { combo: Combo; version: string; saved: string } | null = null;
let logicsReady: Promise<void> = Promise.resolve();
let q = "";
const filterHost = el("div");
side.comboHost.replaceChildren(filterHost);
const view = el("div", { className: "m01-logic__editor" });
const find = createInputField({ type: "search", placeholder: tc("Search") });
find.input.addEventListener("input", () => {
q = find.input.value.trim().toLowerCase();
activate();
if (!current) drawList();
});
side.comboHost.replaceChildren(find.root, filterHost);
const view = el("div", { className: "m01-logic__editor m01-combo" });
host.replaceChildren(view);
/** 목록만 받음 — 로직 이름표(1300여 개)는 따로 받아 목록을 붙잡지 않음 */
@@ -106,7 +119,7 @@ export function mountM01Combo(
await load(false);
if (!current) drawList();
await logicsReady;
if (current) draw(current.combo, current.version);
if (current) draw(current.combo, current.version, current.saved);
};
/* --- 왼쪽 거름 --- */
@@ -130,6 +143,7 @@ export function mountM01Combo(
detailLabel: tc("Detail"),
onPick: (sub, detail) => {
pick = { sub, detail };
activate();
drawList();
},
}),
@@ -154,7 +168,10 @@ export function mountM01Combo(
nav++;
current = null;
const shown = items.filter(
(x) => (!pick.sub || x.구분 === pick.sub) && (!pick.detail || x.상세구분 === pick.detail),
(x) =>
(!pick.sub || x.구분 === pick.sub) &&
(!pick.detail || x.상세구분 === pick.detail) &&
(!q || `${x.키} ${x.이름}`.toLowerCase().includes(q)),
);
const heads = [
tc("Head_Key"),
@@ -177,7 +194,7 @@ export function mountM01Combo(
x.단위 ?? "",
String(x.count),
x.본뜬키 ?? tc("Origin_Self"),
].map((v) => el("td", { text: v })),
].map((v) => el("td", { text: v, attrs: { title: v } })),
});
tr.addEventListener("click", () => void open(x.키));
return tr;
@@ -199,7 +216,7 @@ export function mountM01Combo(
}),
],
})
: el("p", { className: "m01-logic__muted", text: tc("Empty") }),
: el("p", { className: "m01-logic__muted", text: tc(items.length ? "NoMatch" : "Empty") }),
);
};
@@ -216,14 +233,14 @@ export function mountM01Combo(
};
/* --- 상세 --- */
const draw = (combo: Combo, version: string): void => {
const draw = (combo: Combo, version: string, saved = JSON.stringify(combo)): void => {
nav++;
current = { combo, version };
const saved = JSON.stringify(combo);
current = { combo, version, saved };
const field = (label: string, name: "이름" | "구분" | "상세구분" | "단위" | "비고") => {
const f = createInputField({ type: "text", label });
f.input.value = combo[name] ?? "";
f.input.setAttribute("data-combo-field", name);
if (name === "이름") f.input.maxLength = 60; // 서버 한도(`MAX_이름`)와 같음
f.input.addEventListener("input", () => {
const v = f.input.value;
if (name === "이름") combo.이름 = v;
@@ -350,7 +367,11 @@ export function mountM01Combo(
label: tc("Delete"),
variant: "ghost",
onClick: async () => {
if (!combo.키 || !(await showConfirmDialog(tc("DeleteAsk")))) return;
if (
!combo.키 ||
!(await showConfirmDialog(`${combo.키} ${combo.이름} — ${tc("DeleteAsk")}`))
)
return;
try {
await deleteCombo(combo.키, version);
showToast(tc("Deleted"), "success");
@@ -360,7 +381,18 @@ export function mountM01Combo(
}
},
});
const back = createButton({ label: "←", variant: "ghost", onClick: () => drawList() });
const back = createButton({
label: "←",
variant: "ghost",
onClick: async () => {
if (JSON.stringify(combo) !== saved && !(await showConfirmDialog(tc("BackAsk")))) return;
drawList();
},
});
const origin = createInputField({ type: "text", label: tc("Head_Origin") });
origin.input.value = combo.본뜬키 ?? tc("Origin_Self");
origin.input.readOnly = true;
origin.input.setAttribute("data-combo-origin", combo.본뜬키 ?? "");
view.replaceChildren(
el("div", {
className: "m01-logic__section-head",
@@ -377,11 +409,7 @@ export function mountM01Combo(
field(tc("Detail"), "상세구분"),
field(tc("Head_Unit"), "단위"),
field(tc("Head_Note"), "비고"),
el("p", {
className: "m01-logic__muted",
text: `${tc("Head_Origin")}: ${combo.본뜬키 ?? tc("Origin_Self")}`,
attrs: { "data-combo-origin": combo.본뜬키 ?? "" },
}),
origin.root,
],
}),
],
@@ -18,6 +18,7 @@ import {
type SubBrief,
} from "./M01_MasterData_UI_Logic_Api";
import { tc } from "./M01_MasterData_UI_Combo_Text";
import "./M01_MasterData_UI_Combo.css";
const opts = (values: string[]): { value: string; text: string }[] =>
[L("M01_All"), ...values].map((v, i) => ({ value: i ? v : "", text: v }));
@@ -76,7 +77,7 @@ export function openLogicPicker(done: (logic: LogicSummary) => void): void {
});
const dialog = el("div", {
className: "m01-logic__pick",
className: "m01-logic__pick m01-combo",
attrs: { role: "dialog", "aria-label": tc("Pick_Title") },
children: [
el("div", {
@@ -103,6 +104,8 @@ export function openLogicPicker(done: (logic: LogicSummary) => void): void {
backdrop.addEventListener("click", (ev) => ev.target === backdrop && close());
backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && close());
document.body.append(backdrop);
searchField.input.addEventListener("keydown", (ev) => ev.key === "Enter" && find());
searchField.input.focus();
void Promise.all([fetchLogicSubs(), fetchLogics()])
.then(([s, l]) => {
@@ -72,7 +72,10 @@ export function buildPreview(combo: () => Combo): PreviewHandle {
const rowOf = (cells: string[], note = "", bold = false): HTMLElement =>
el("tr", {
className: bold ? "m01lab__subtotal" : "",
children: cells.map((t, i) => el("td", { text: i === 0 && note ? `${t} — ${note}` : t })),
children: cells.map((t, i) => {
const text = i === 0 && note ? `${t} — ${note}` : t;
return el("td", { text, attrs: { title: text } });
}),
});
const run = async (): Promise<void> => {
@@ -23,6 +23,12 @@ const TEXT = {
"저장 안 한 고침이 있음 — 로직 상세로 가면 이 화면에서는 그대로 두고 옮겨 감. 갈까요?",
"Unsaved edits — go to the logic anyway?",
],
BackAsk: [
"저장 안 한 고침이 있음 — 버리고 목록으로 갈까요?",
"Unsaved edits — discard and go back to the list?",
],
Search: ["이름 · 키로 찾기", "Search by name or key"],
NoMatch: ["찾은 조합 없음 — 거름 · 찾기 글을 바꿔 봄", "No match — change the filter or search"],
NeedName: ["이름을 적어야 저장됨", "A name is required"],
Load_Failed: ["불러오지 못함", "Load failed"],
Head_Key: ["키", "Key"],
+6 -1
View File
@@ -221,7 +221,12 @@ function buildPage(): HTMLElement {
return;
}
void import("./M01_MasterData_UI_Combo").then((m) => {
comboHandle = m.mountM01Combo(comboHost, side, (key) => openLogicTab(key));
comboHandle = m.mountM01Combo(
comboHost,
side,
(key) => openLogicTab(key),
() => comboHost.hidden && openComboTab(),
);
void comboHandle.show();
});
};