refactor(M01): 표 · 모달 · 띠 알림 · 배지 · 알약 · 보조 글 · 빈 상태를 부품으로 (PLAN 553-5)

- 표 둘 = 표 부품(m01-master__grid = 일반표 · m01-logic__grid = 데이터표) · 페이지 CSS 는 폭 · 머리 고정 · 칸 상태만
- 손 모달 다섯(조달 찾기 · 요소 고르기 · 표 바꾸기 · 복사 시트 · 표 고르기) = 모달 부품 openModal
- 알림 띠 · 막힘 까닭 = 띠 알림 · 태그 = 배지 · 알약 · × · 삭제 · 로직 키 = 단추 부품 · muted 세 벌 = 보조 글 · 빈 줄 = 빈 상태
- M02 가 빌린 M01 클래스(표 · muted · 고르기 창)도 같은 부품으로 · 죽은 CSS 걷음 · 지킴 시험 test_553_5_m01_m02

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 22:57:33 +09:00
co-authored by Claude Opus 5.5
parent 6d8b38b518
commit 022eff69f5
34 changed files with 367 additions and 559 deletions
@@ -31,6 +31,9 @@ import { fetchTables, type TableHead } from "./M01_MasterData_Api_Fetch";
import { tx } from "./M01_MasterData_UI_Logic_Text";
import { tl } from "./M01_MasterData_UI_LogicLab_Text";
import "./M01_MasterData_UI_LogicLab_Style.css";
import { createBadge } from "@ui/ui_template_badge";
import { createText } from "@ui/ui_template_text";
import { openModal } from "@ui/ui_template_modal";
const OWNERS = ["현장", "공용"] as const;
const TABLE_GROUPS = ["소요량", "계수"] as const;
@@ -78,29 +81,16 @@ function swapKey(raw: string, newKey: string): string {
/** 표 바꾸기 모달 — 소요량·계수 두 그룹을 함께 찾음 */
function openTablePick(onPick: (table: TableHead) => void): void {
const close = (): void => backdrop.remove();
const search = createInputField({ type: "search", placeholder: tl("Copy_PickSearch") });
const list = el("div", { className: "m01-logic__pick-list" });
const dialog = el("div", {
className: "m01-logic__pick ui-popup",
attrs: { role: "dialog", "aria-label": tl("Copy_PickTitle") },
children: [
el("div", {
className: "m01lab__modal-top",
children: [
el("h3", { text: tl("Copy_PickTitle") }),
createButton({ label: tl("Modal_Close"), variant: "ghost", onClick: close }),
],
}),
search.root,
list,
],
// 모달 부품 — 제목줄 [닫기] · 바깥 누르기 · Esc · 위 고정 · 찾기 칸에 커서
const { close } = openModal({
title: tl("Copy_PickTitle"),
closeLabel: tl("Modal_Close"),
dialogClass: "m01-logic__pick",
backdropClass: "m01-logic__backdrop",
mount: (body) => body.append(search.root, list),
});
const backdrop = el("div", { className: "m01-logic__backdrop", children: [dialog] });
backdrop.addEventListener("click", (ev) => ev.target === backdrop && close());
backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && close());
document.body.append(backdrop);
search.input.focus();
let timer: number | undefined;
const run = async (): Promise<void> => {
@@ -113,11 +103,11 @@ function openTablePick(onPick: (table: TableHead) => void): void {
...(hits.length
? hits.map((t) => {
const b = el("button", {
className: "m01-logic__pick-row m01-logic__pick-row--button",
attrs: { type: "button" },
className: "m01-logic__pick-row ui-list-row ui-sidebar-row",
attrs: { type: "button", "data-size": "32" },
children: [
el("span", { text: `${t.이름 || t.키} · ${t.원문번호 || ""}` }),
el("span", { className: "m01-logic__muted", text: t.키 }),
el("span", { className: "ui-text ui-text--muted", text: t.키 }),
],
});
b.addEventListener("click", () => {
@@ -126,7 +116,7 @@ function openTablePick(onPick: (table: TableHead) => void): void {
});
return b;
})
: [el("p", { className: "m01-logic__muted", text: tl("Copy_PickNone") })]),
: [el("p", { className: "ui-text ui-text--muted", text: tl("Copy_PickNone") })]),
);
};
search.input.addEventListener("input", () => {
@@ -148,7 +138,7 @@ function aliasRow(a: FormulaAlias, onSwap: (raw: string) => void, sub: HTMLEleme
className: "m01-logic__section-head",
children: [
el("strong", { text: a.이름 }),
el("span", { className: "m01-logic__muted", text: about }),
el("span", { className: "ui-text ui-text--muted", text: about }),
],
}),
el("pre", { className: "m01lab__expr", text: a.원문 }),
@@ -172,7 +162,7 @@ function logicAliasControls(
): { root: HTMLElement } {
let forked = false;
const original = a.원문;
const status = el("span", { className: "m01-logic__muted" });
const status = el("span", { className: "ui-text ui-text--muted" });
const refBtn = createButton({
label: tl("Copy_Alias_Ref"),
variant: "ghost",
@@ -224,8 +214,10 @@ function lineBox(row: LogicRow, line: FormulaLine, onChange: () => void): HTMLEl
className: "m01-logic__section-head",
children: [
el("strong", { text: line.이름 || line.자리 }),
el("span", { className: "m01-logic__tag", text: tag }),
...(line.단위 ? [el("span", { className: "m01-logic__muted", text: line.단위 })] : []),
createBadge(tag, "accent"),
...(line.단위
? [el("span", { className: "ui-text ui-text--muted", text: line.단위 })]
: []),
],
}),
box,
@@ -287,9 +279,7 @@ export async function openCopyScreen(opt: CopyOptions): Promise<void> {
onLines: () => undefined,
});
} catch (error) {
calcHost.replaceChildren(
el("p", { className: "m01-logic__bad", text: error instanceof Error ? error.message : "" }),
);
calcHost.replaceChildren(createText(error instanceof Error ? error.message : "", "error"));
}
};
@@ -316,7 +306,7 @@ export async function openCopyScreen(opt: CopyOptions): Promise<void> {
drawAliases();
refreshCalc();
const close = (): void => backdrop.remove();
let close = (): void => {};
const onSave = async (): Promise<void> => {
showLoadingOverlay();
try {
@@ -337,55 +327,48 @@ export async function openCopyScreen(opt: CopyOptions): Promise<void> {
}
};
const sheet = el("div", {
className: "m01-logic__pick m01copy__sheet",
attrs: { role: "dialog", "aria-label": tl("Copy_Title") },
children: [
el("div", {
className: "m01lab__modal-top",
children: [
el("h3", { text: `${tl("Copy_Title")} — ${row.이름}(${opt.key})` }),
createButton({ label: tl("Copy_Cancel"), variant: "ghost", onClick: close }),
],
}),
el("div", {
className: "m01copy__head",
children: [nameField.root, ownerField.root],
}),
el("section", {
className: "m01-logic__section",
children: [el("h3", { text: tl("Copy_Aliases") }), aliasHost],
}),
el("section", {
className: "m01-logic__section",
children: [
el("div", {
className: "m01-logic__section-head",
children: [
el("h3", { text: tl("Copy_Lines") }),
createButton({
label: tl("Copy_AddExtra"),
variant: "ghost",
onClick: () => addExtra(row, refreshCalc, drawLines),
}),
],
}),
linesHost,
],
}),
el("section", { className: "m01-logic__section", children: [calcHost] }),
el("div", {
className: "m01-logic__pick-bar",
children: [
el("span", {}),
el("span", {}),
createButton({ label: tl("Copy_Save"), onClick: () => void onSave() }),
],
}),
],
});
const backdrop = el("div", { className: "m01-logic__backdrop", children: [sheet] });
backdrop.addEventListener("click", (ev) => ev.target === backdrop && close());
backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && close());
document.body.append(backdrop);
// 모달 부품 — 제목줄 [취소] · 바깥 누르기 · Esc · 위 고정 · 폭만 복사 시트 곁값
({ close } = openModal({
title: `${tl("Copy_Title")} — ${row.이름}(${opt.key})`,
closeLabel: tl("Copy_Cancel"),
dialogClass: "m01-logic__pick m01copy__sheet",
backdropClass: "m01-logic__backdrop",
mount: (body) =>
body.append(
el("div", {
className: "m01copy__head",
children: [nameField.root, ownerField.root],
}),
el("section", {
className: "m01-logic__section",
children: [el("h3", { text: tl("Copy_Aliases") }), aliasHost],
}),
el("section", {
className: "m01-logic__section",
children: [
el("div", {
className: "m01-logic__section-head",
children: [
el("h3", { text: tl("Copy_Lines") }),
createButton({
label: tl("Copy_AddExtra"),
variant: "ghost",
onClick: () => addExtra(row, refreshCalc, drawLines),
}),
],
}),
linesHost,
],
}),
el("section", { className: "m01-logic__section", children: [calcHost] }),
el("div", {
className: "m01-logic__pick-bar",
children: [
el("span", {}),
el("span", {}),
createButton({ label: tl("Copy_Save"), onClick: () => void onSave() }),
],
}),
),
}));
}