Merge commit '022eff69f' into main_laptop_1

This commit is contained in:
2026-10-26 23:06:07 +09:00
34 changed files with 367 additions and 559 deletions
+1 -1
View File
@@ -107,7 +107,7 @@ export function buildPager(
prev.addEventListener("click", () => goto(page - 1)); prev.addEventListener("click", () => goto(page - 1));
next.addEventListener("click", () => goto(page + 1)); next.addEventListener("click", () => goto(page + 1));
return el("div", { return el("div", {
className: "m01-master__pager", className: "ui-toolbar m01-master__pager",
children: [ children: [
el("span", { el("span", {
text: `${page} / ${last} ${L("M01_Page")} · ${total.toLocaleString("ko-KR")} ${L("M01_Rows")}`, text: `${page} / ${last} ${L("M01_Page")} · ${total.toLocaleString("ko-KR")} ${L("M01_Rows")}`,
+9 -7
View File
@@ -56,6 +56,7 @@ import { buildHeadBar } from "./M01_MasterData_UI_Head";
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch"; import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
import type { CfLogic } from "../M02_MasterTemplete/DataFlow/M02_DF_Model"; import type { CfLogic } from "../M02_MasterTemplete/DataFlow/M02_DF_Model";
import "./M01_MasterData_UI_Logic_Style.css"; import "./M01_MasterData_UI_Logic_Style.css";
import { createEmpty } from "@ui/ui_template_empty";
/** 저장 안 한 로직 하나 — origKey null = 새 로직 · row null = 지움 */ /** 저장 안 한 로직 하나 — origKey null = 새 로직 · row null = 지움 */
interface Draft { interface Draft {
@@ -129,12 +130,15 @@ export async function mountM01LogicLab(
const editor = el("div", { className: "m01-logic__editor" }); const editor = el("div", { className: "m01-logic__editor" });
// 로직 상세 — 왼쪽 지금 내용 · 오른쪽 접는 패널 둘(흐름도 · 원문 · 하나만 열림 · 폭 공유) // 로직 상세 — 왼쪽 지금 내용 · 오른쪽 접는 패널 둘(흐름도 · 원문 · 하나만 열림 · 폭 공유)
const panes = buildLogicSide(editor); const panes = buildLogicSide(editor);
const errors = el("div", { className: "m01-logic__reasons", attrs: { hidden: "" } }); const errors = el("div", {
className: "ui-banner ui-banner--danger m01-logic__reasons",
attrs: { hidden: "" },
});
const calc = el("div", { className: "m01lab__calc" }); const calc = el("div", { className: "m01lab__calc" });
const filterHost = el("div"); const filterHost = el("div");
const waiting = el("p", { className: "m01-logic__muted", text: tx("Loading") }); const waiting = el("p", { className: "ui-text ui-text--muted", text: tx("Loading") });
let loaded = false; // 첫 목록이 오기 전에는 「0 줄」 빈 표를 보이지 않음 let loaded = false; // 첫 목록이 오기 전에는 「0 줄」 빈 표를 보이지 않음
const pending = el("span", { className: "m01-logic__muted" }); const pending = el("span", { className: "ui-text ui-text--muted" });
const list = buildList( const list = buildList(
(item) => void open(item.id, item.sub, item.key), (item) => void open(item.id, item.sub, item.key),
(item, alias) => void onAlias(item, alias), (item, alias) => void onAlias(item, alias),
@@ -235,9 +239,7 @@ export async function mountM01LogicLab(
back.hidden = listing; back.hidden = listing;
if (!opened) return; if (!opened) return;
if (!opened.row) { if (!opened.row) {
editor.replaceChildren( editor.replaceChildren(createEmpty({ text: `● ${tx("List_Deleted")}`, small: true }));
el("p", { className: "m01-logic__empty", text: `● ${tx("List_Deleted")}` }),
);
return; return;
} }
const current = opened; const current = opened;
@@ -321,7 +323,7 @@ export async function mountM01LogicLab(
made.setExamples(o.values); // 띄우는 사이에 온 자동값까지 made.setExamples(o.values); // 띄우는 사이에 온 자동값까지
} catch (e) { } catch (e) {
panes.flowSplit.right.replaceChildren( panes.flowSplit.right.replaceChildren(
el("p", { className: "m01-logic__muted", text: tf("Failed", String(e)) }), el("p", { className: "ui-text ui-text--muted", text: tf("Failed", String(e)) }),
); );
} }
}; };
@@ -53,7 +53,7 @@ export function askBasicInfo(units: string[]): Promise<BasicInfo | null> {
compact: true, compact: true,
}); });
const source = createInputField({ label: tb("Source"), value: "자체", compact: true }); const source = createInputField({ label: tb("Source"), value: "자체", compact: true });
const why = el("p", { className: "m01-logic__muted", attrs: { role: "alert" } }); const why = el("p", { className: "ui-text ui-text--muted", attrs: { role: "alert" } });
const modal = openModal({ const modal = openModal({
title: tb("Title"), title: tb("Title"),
closeLabel: tb("Close"), closeLabel: tb("Close"),
@@ -84,7 +84,7 @@ export function askBasicInfo(units: string[]): Promise<BasicInfo | null> {
source.root, source.root,
why, why,
el("div", { el("div", {
className: "m01-logic__bar", className: "ui-toolbar m01-logic__bar",
children: [ children: [
createButton({ label: tb("Ok"), size: "sm", onClick: ok }), createButton({ label: tb("Ok"), size: "sm", onClick: ok }),
createButton({ createButton({
@@ -31,6 +31,9 @@ import { fetchTables, type TableHead } from "./M01_MasterData_Api_Fetch";
import { tx } from "./M01_MasterData_UI_Logic_Text"; import { tx } from "./M01_MasterData_UI_Logic_Text";
import { tl } from "./M01_MasterData_UI_LogicLab_Text"; import { tl } from "./M01_MasterData_UI_LogicLab_Text";
import "./M01_MasterData_UI_LogicLab_Style.css"; 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 OWNERS = ["현장", "공용"] as const;
const TABLE_GROUPS = ["소요량", "계수"] 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 { function openTablePick(onPick: (table: TableHead) => void): void {
const close = (): void => backdrop.remove();
const search = createInputField({ type: "search", placeholder: tl("Copy_PickSearch") }); const search = createInputField({ type: "search", placeholder: tl("Copy_PickSearch") });
const list = el("div", { className: "m01-logic__pick-list" }); const list = el("div", { className: "m01-logic__pick-list" });
const dialog = el("div", { // 모달 부품 — 제목줄 [닫기] · 바깥 누르기 · Esc · 위 고정 · 찾기 칸에 커서
className: "m01-logic__pick ui-popup", const { close } = openModal({
attrs: { role: "dialog", "aria-label": tl("Copy_PickTitle") }, title: tl("Copy_PickTitle"),
children: [ closeLabel: tl("Modal_Close"),
el("div", { dialogClass: "m01-logic__pick",
className: "m01lab__modal-top", backdropClass: "m01-logic__backdrop",
children: [ mount: (body) => body.append(search.root, list),
el("h3", { text: tl("Copy_PickTitle") }),
createButton({ label: tl("Modal_Close"), variant: "ghost", onClick: close }),
],
}),
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; let timer: number | undefined;
const run = async (): Promise<void> => { const run = async (): Promise<void> => {
@@ -113,11 +103,11 @@ function openTablePick(onPick: (table: TableHead) => void): void {
...(hits.length ...(hits.length
? hits.map((t) => { ? hits.map((t) => {
const b = el("button", { const b = el("button", {
className: "m01-logic__pick-row m01-logic__pick-row--button", className: "m01-logic__pick-row ui-list-row ui-sidebar-row",
attrs: { type: "button" }, attrs: { type: "button", "data-size": "32" },
children: [ children: [
el("span", { text: `${t.이름 || t.키} · ${t.원문번호 || ""}` }), 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", () => { b.addEventListener("click", () => {
@@ -126,7 +116,7 @@ function openTablePick(onPick: (table: TableHead) => void): void {
}); });
return b; 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", () => { search.input.addEventListener("input", () => {
@@ -148,7 +138,7 @@ function aliasRow(a: FormulaAlias, onSwap: (raw: string) => void, sub: HTMLEleme
className: "m01-logic__section-head", className: "m01-logic__section-head",
children: [ children: [
el("strong", { text: a.이름 }), 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.원문 }), el("pre", { className: "m01lab__expr", text: a.원문 }),
@@ -172,7 +162,7 @@ function logicAliasControls(
): { root: HTMLElement } { ): { root: HTMLElement } {
let forked = false; let forked = false;
const original = a.원문; const original = a.원문;
const status = el("span", { className: "m01-logic__muted" }); const status = el("span", { className: "ui-text ui-text--muted" });
const refBtn = createButton({ const refBtn = createButton({
label: tl("Copy_Alias_Ref"), label: tl("Copy_Alias_Ref"),
variant: "ghost", variant: "ghost",
@@ -224,8 +214,10 @@ function lineBox(row: LogicRow, line: FormulaLine, onChange: () => void): HTMLEl
className: "m01-logic__section-head", className: "m01-logic__section-head",
children: [ children: [
el("strong", { text: line.이름 || line.자리 }), el("strong", { text: line.이름 || line.자리 }),
el("span", { className: "m01-logic__tag", text: tag }), createBadge(tag, "accent"),
...(line.단위 ? [el("span", { className: "m01-logic__muted", text: line.단위 })] : []), ...(line.단위
? [el("span", { className: "ui-text ui-text--muted", text: line.단위 })]
: []),
], ],
}), }),
box, box,
@@ -287,9 +279,7 @@ export async function openCopyScreen(opt: CopyOptions): Promise<void> {
onLines: () => undefined, onLines: () => undefined,
}); });
} catch (error) { } catch (error) {
calcHost.replaceChildren( calcHost.replaceChildren(createText(error instanceof Error ? error.message : "", "error"));
el("p", { className: "m01-logic__bad", text: error instanceof Error ? error.message : "" }),
);
} }
}; };
@@ -316,7 +306,7 @@ export async function openCopyScreen(opt: CopyOptions): Promise<void> {
drawAliases(); drawAliases();
refreshCalc(); refreshCalc();
const close = (): void => backdrop.remove(); let close = (): void => {};
const onSave = async (): Promise<void> => { const onSave = async (): Promise<void> => {
showLoadingOverlay(); showLoadingOverlay();
try { try {
@@ -337,55 +327,48 @@ export async function openCopyScreen(opt: CopyOptions): Promise<void> {
} }
}; };
const sheet = el("div", { // 모달 부품 — 제목줄 [취소] · 바깥 누르기 · Esc · 위 고정 · 폭만 복사 시트 곁값
className: "m01-logic__pick m01copy__sheet", ({ close } = openModal({
attrs: { role: "dialog", "aria-label": tl("Copy_Title") }, title: `${tl("Copy_Title")} — ${row.이름}(${opt.key})`,
children: [ closeLabel: tl("Copy_Cancel"),
el("div", { dialogClass: "m01-logic__pick m01copy__sheet",
className: "m01lab__modal-top", backdropClass: "m01-logic__backdrop",
children: [ mount: (body) =>
el("h3", { text: `${tl("Copy_Title")} — ${row.이름}(${opt.key})` }), body.append(
createButton({ label: tl("Copy_Cancel"), variant: "ghost", onClick: close }), el("div", {
], className: "m01copy__head",
}), children: [nameField.root, ownerField.root],
el("div", { }),
className: "m01copy__head", el("section", {
children: [nameField.root, ownerField.root], className: "m01-logic__section",
}), children: [el("h3", { text: tl("Copy_Aliases") }), aliasHost],
el("section", { }),
className: "m01-logic__section", el("section", {
children: [el("h3", { text: tl("Copy_Aliases") }), aliasHost], className: "m01-logic__section",
}), children: [
el("section", { el("div", {
className: "m01-logic__section", className: "m01-logic__section-head",
children: [ children: [
el("div", { el("h3", { text: tl("Copy_Lines") }),
className: "m01-logic__section-head", createButton({
children: [ label: tl("Copy_AddExtra"),
el("h3", { text: tl("Copy_Lines") }), variant: "ghost",
createButton({ onClick: () => addExtra(row, refreshCalc, drawLines),
label: tl("Copy_AddExtra"), }),
variant: "ghost", ],
onClick: () => addExtra(row, refreshCalc, drawLines), }),
}), linesHost,
], ],
}), }),
linesHost, el("section", { className: "m01-logic__section", children: [calcHost] }),
], el("div", {
}), className: "m01-logic__pick-bar",
el("section", { className: "m01-logic__section", children: [calcHost] }), children: [
el("div", { el("span", {}),
className: "m01-logic__pick-bar", el("span", {}),
children: [ createButton({ label: tl("Copy_Save"), onClick: () => void onSave() }),
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);
} }
@@ -35,6 +35,7 @@ import { fragmentRow, type Fragment } from "./M01_MasterData_UI_LogicLab_Pill";
import { tl, type LabTextKey } from "./M01_MasterData_UI_LogicLab_Text"; import { tl, type LabTextKey } from "./M01_MasterData_UI_LogicLab_Text";
import "./M01_MasterData_UI_LogicLab_Style.css"; import "./M01_MasterData_UI_LogicLab_Style.css";
import "./M01_MasterData_UI_LogicLab_Pill.css"; import "./M01_MasterData_UI_LogicLab_Pill.css";
import { createBadge } from "@ui/ui_template_badge";
/** 서버 조각(`/text` 줄의 `조각`) → 수량 칸 조각 — 요소·표·로직은 알약(글 = 조각 글) · 나머지는 글자 */ /** 서버 조각(`/text` 줄의 `조각`) → 수량 칸 조각 — 요소·표·로직은 알약(글 = 조각 글) · 나머지는 글자 */
function toFragments(pieces: TextPiece[], ctx: DetailContext): Fragment[] { function toFragments(pieces: TextPiece[], ctx: DetailContext): Fragment[] {
@@ -131,7 +132,7 @@ const section = (title: string, body: HTMLElement[], hint = ""): HTMLElement =>
className: "m01-logic__section-head", className: "m01-logic__section-head",
children: [ children: [
el("h3", { text: title }), el("h3", { text: title }),
...(hint ? [el("span", { className: "m01-logic__muted", text: hint })] : []), ...(hint ? [el("span", { className: "ui-text ui-text--muted", text: hint })] : []),
], ],
}), }),
...body, ...body,
@@ -151,14 +152,14 @@ function infoBox(ctx: DetailContext): HTMLElement {
}); });
return el("label", { return el("label", {
className: "m01lab__cell", className: "m01lab__cell",
children: [el("span", { className: "m01-logic__muted", text: label }), box], children: [el("span", { className: "ui-text ui-text--muted", text: label }), box],
}); });
}; };
const cell = (label: string, value: string): HTMLElement => const cell = (label: string, value: string): HTMLElement =>
el("div", { el("div", {
className: "m01lab__cell", className: "m01lab__cell",
children: [ children: [
el("span", { className: "m01-logic__muted", text: label }), el("span", { className: "ui-text ui-text--muted", text: label }),
el("span", { text: value || "—" }), el("span", { text: value || "—" }),
], ],
}); });
@@ -172,7 +173,7 @@ function infoBox(ctx: DetailContext): HTMLElement {
const reasons = ctx.reasons.length const reasons = ctx.reasons.length
? [ ? [
el("div", { el("div", {
className: "m01-logic__reasons", className: "ui-banner ui-banner--danger m01-logic__reasons",
children: [ children: [
el("strong", { text: tx("Head_Blocked") }), el("strong", { text: tx("Head_Blocked") }),
...ctx.reasons.map((r) => el("div", { text: safeReason(r) })), ...ctx.reasons.map((r) => el("div", { text: safeReason(r) })),
@@ -182,7 +183,7 @@ function infoBox(ctx: DetailContext): HTMLElement {
: []; : [];
return section(tl("Info_Title"), [ return section(tl("Info_Title"), [
el("p", { el("p", {
className: "m01-logic__muted", className: "ui-text ui-text--muted",
text: tl(own ? "Info_Own" : ctx.admin ? "Info_Base" : "Info_ReadOnly"), text: tl(own ? "Info_Own" : ctx.admin ? "Info_Base" : "Info_ReadOnly"),
}), }),
...reasons, ...reasons,
@@ -198,7 +199,10 @@ function infoBox(ctx: DetailContext): HTMLElement {
cell(tx("Head_Owner"), row.소유 ?? ""), cell(tx("Head_Owner"), row.소유 ?? ""),
el("label", { el("label", {
className: "m01lab__cell m01lab__cell--wide", className: "m01lab__cell m01lab__cell--wide",
children: [el("span", { className: "m01-logic__muted", text: tx("Head_Note") }), note], children: [
el("span", { className: "ui-text ui-text--muted", text: tx("Head_Note") }),
note,
],
}), }),
], ],
}), }),
@@ -342,27 +346,15 @@ function lineRow(e: Entry): HTMLElement {
el("td", { el("td", {
children: [ children: [
el("div", { text: e.name }), el("div", { text: e.name }),
...(e.share ...(e.share ? [createBadge(`${e.share} ${tl("Share")}`, "accent")] : []),
? [el("div", { className: "m01-logic__tag", text: `${e.share} ${tl("Share")}` })] ...(e.spec ? [el("div", { className: "ui-text ui-text--muted", text: e.spec })] : []),
: []), ...(e.warn ? [createBadge(`${tx("Calc_UnitWarn")} · ${e.warn}`, "warning")] : []),
...(e.spec ? [el("div", { className: "m01-logic__muted", text: e.spec })] : []),
...(e.warn
? [
el("div", {
className: "m01-logic__tag m01lab__warn",
text: `${tx("Calc_UnitWarn")} · ${e.warn}`,
}),
]
: []),
], ],
}), }),
cellOf(e.unit), cellOf(e.unit),
el("td", { el("td", {
className: "m01lab__qty-cell", className: "m01lab__qty-cell",
children: [ children: [createBadge(e.qtyTag, "accent"), fragmentRow(e.qtyFrag)],
el("span", { className: "m01-logic__tag", text: e.qtyTag }),
fragmentRow(e.qtyFrag),
],
}), }),
cellOf(e.price, "m01-logic__money"), cellOf(e.price, "m01-logic__money"),
cellOf(e.amount === null ? "" : formatMoney(e.amount), "m01-logic__money"), cellOf(e.amount === null ? "" : formatMoney(e.amount), "m01-logic__money"),
@@ -379,7 +371,7 @@ function groupTable(group: Group, mine: Entry[], calculated: boolean): HTMLEleme
children: [ children: [
el("td", { el("td", {
attrs: { colspan: "5" }, attrs: { colspan: "5" },
className: "m01-logic__muted", className: "ui-text ui-text--muted",
text: tl("Empty_Group"), text: tl("Empty_Group"),
}), }),
], ],
@@ -405,7 +397,7 @@ function groupTable(group: Group, mine: Entry[], calculated: boolean): HTMLEleme
className: "m01-logic__scroll", className: "m01-logic__scroll",
children: [ children: [
el("table", { el("table", {
className: "m01-logic__grid m01lab__ho", className: "ui-table ui-table--data m01-logic__grid m01lab__ho",
children: [ children: [
el("thead", { el("thead", {
children: [el("tr", { children: headers.map((h) => el("th", { text: h })) })], children: [el("tr", { children: headers.map((h) => el("th", { text: h })) })],
@@ -447,7 +439,7 @@ function hoBox(ctx: DetailContext): HTMLElement {
const money = !("결과" in ctx.row) && (ctx.row.결과단위 ?? "").startsWith("원"); const money = !("결과" in ctx.row) && (ctx.row.결과단위 ?? "").startsWith("원");
if (!money) { if (!money) {
return section(tl("Ho_Title"), [ return section(tl("Ho_Title"), [
el("p", { className: "m01-logic__muted", text: tl("Not_Money") }), el("p", { className: "ui-text ui-text--muted", text: tl("Not_Money") }),
el("pre", { className: "m01lab__expr", text: ctx.row.결과?.식 ?? "" }), el("pre", { className: "m01lab__expr", text: ctx.row.결과?.식 ?? "" }),
]); ]);
} }
@@ -22,9 +22,3 @@
color: var(--color-text-body); color: var(--color-text-body);
font-weight: 600; font-weight: 600;
} }
.m01flow__warn {
margin: var(--spacing-4) 0 0;
color: var(--color-danger);
font-size: var(--text-body-sm);
}
@@ -34,6 +34,7 @@ import {
type FlowGroupKey, type FlowGroupKey,
} from "./M01_MasterData_UI_LogicLab_FlowRows"; } from "./M01_MasterData_UI_LogicLab_FlowRows";
import "./M01_MasterData_UI_LogicLab_FlowView.css"; import "./M01_MasterData_UI_LogicLab_FlowView.css";
import { createText } from "@ui/ui_template_text";
const TEXT = { const TEXT = {
Detail: ["단가산출 상세", "Unit-price detail"], Detail: ["단가산출 상세", "Unit-price detail"],
@@ -91,7 +92,7 @@ const groupTable = (g: FlowGroup): HTMLElement => {
className: "m01-logic__scroll", className: "m01-logic__scroll",
children: [ children: [
el("table", { el("table", {
className: "m01-logic__grid", className: "ui-table ui-table--data m01-logic__grid",
children: [ children: [
el("thead", { el("thead", {
children: [ children: [
@@ -159,7 +160,7 @@ export function buildFlowView(
ev = opts.result ?? evalLogic(lg, values, { logics: opts.logics }); ev = opts.result ?? evalLogic(lg, values, { logics: opts.logics });
} catch (e) { } catch (e) {
host.replaceChildren( host.replaceChildren(
section(tf("Detail"), [el("p", { className: "m01-logic__muted", text: String(e) })]), section(tf("Detail"), [el("p", { className: "ui-text ui-text--muted", text: String(e) })]),
); );
return; return;
} }
@@ -168,17 +169,17 @@ export function buildFlowView(
const detail = section(tf("Detail"), [ const detail = section(tf("Detail"), [
...(rows.groups.length ...(rows.groups.length
? rows.groups.map(groupTable) ? rows.groups.map(groupTable)
: [el("p", { className: "m01-logic__muted", text: tf("None") })]), : [el("p", { className: "ui-text ui-text--muted", text: tf("None") })]),
el("p", { el("p", {
className: "m01lab__total", className: "m01lab__total",
attrs: { "data-sum": String(rows.total) }, attrs: { "data-sum": String(rows.total) },
text: `${tf("Total")} ${rows.totalText}`, text: `${tf("Total")} ${rows.totalText}`,
}), }),
el("p", { el("p", {
className: "m01-logic__muted", className: "ui-text ui-text--muted",
text: `${tf("TotalNote")}: ${roundText(totalRound)}`, text: `${tf("TotalNote")}: ${roundText(totalRound)}`,
}), }),
...rows.errors.slice(0, 3).map((t) => el("p", { className: "m01flow__warn", text: t })), ...rows.errors.slice(0, 3).map((t) => createText(t, "error", 14)),
]); ]);
/** 함수 상자마다 식 두 줄(sub2 식 보기 · 받는 알약 값을 칸에 넣고 셈) — 못 그리면 이름 = 값만 */ /** 함수 상자마다 식 두 줄(sub2 식 보기 · 받는 알약 값을 칸에 넣고 셈) — 못 그리면 이름 = 값만 */
const linesOf = (id: string): FormulaLine[] | null => { const linesOf = (id: string): FormulaLine[] | null => {
@@ -193,7 +194,7 @@ export function buildFlowView(
const bodies = rows.funcs.map((f) => funcBlock(f, linesOf(f.box))); const bodies = rows.funcs.map((f) => funcBlock(f, linesOf(f.box)));
const text = section( const text = section(
tf("Formula"), tf("Formula"),
bodies.length ? bodies : [el("p", { className: "m01-logic__muted", text: tf("NoFunc") })], bodies.length ? bodies : [el("p", { className: "ui-text ui-text--muted", text: tf("NoFunc") })],
); );
host.replaceChildren(detail, text); host.replaceChildren(detail, text);
} }
@@ -19,7 +19,7 @@ export interface FormulaContext {
text: PreviewTextAnswer | null; text: PreviewTextAnswer | null;
} }
const muted = (text: string): HTMLElement => el("p", { className: "m01-logic__muted", text }); const muted = (text: string): HTMLElement => el("p", { className: "ui-text ui-text--muted", text });
export function buildFormula(host: HTMLElement, ctx: FormulaContext): void { export function buildFormula(host: HTMLElement, ctx: FormulaContext): void {
const text = ctx.text; const text = ctx.text;
@@ -30,7 +30,7 @@ export function buildFormula(host: HTMLElement, ctx: FormulaContext): void {
if (!text.ok) { if (!text.ok) {
host.replaceChildren( host.replaceChildren(
el("div", { el("div", {
className: "m01-logic__reasons", className: "ui-banner ui-banner--danger m01-logic__reasons",
children: [ children: [
el("strong", { text: tx("Calc_Stopped") }), el("strong", { text: tx("Calc_Stopped") }),
el("div", { text: safeReason(text.reason) }), el("div", { text: safeReason(text.reason) }),
@@ -65,10 +65,15 @@ export function buildFormula(host: HTMLElement, ctx: FormulaContext): void {
? [ ? [
el("div", { text: `${line.이름} : ${line.글}` }), el("div", { text: `${line.이름} : ${line.글}` }),
...(line.까닭 ...(line.까닭
? [el("div", { className: "m01-logic__muted", text: safeReason(line.까닭) })] ? [
el("div", {
className: "ui-text ui-text--muted",
text: safeReason(line.까닭),
}),
]
: []), : []),
] ]
: [el("div", { className: "m01-logic__muted", text: tl("Formula_Slot") })], : [el("div", { className: "ui-text ui-text--muted", text: tl("Formula_Slot") })],
}), }),
], ],
}); });
@@ -21,6 +21,7 @@ import { explain, loadTable, type TableRow } from "./M01_MasterData_UI_Logic_Not
import { jumpToMaster, type MasterJump } from "./M01_MasterData_UI_Side"; import { jumpToMaster, type MasterJump } from "./M01_MasterData_UI_Side";
import { fragmentRow, type Fragment } from "./M01_MasterData_UI_LogicLab_Pill"; import { fragmentRow, type Fragment } from "./M01_MasterData_UI_LogicLab_Pill";
import { tl } from "./M01_MasterData_UI_LogicLab_Text"; import { tl } from "./M01_MasterData_UI_LogicLab_Text";
import { createText } from "@ui/ui_template_text";
/** 로직 알약 → 그 로직 열기 — 로직 화면이 자기 열기를 등록 · 열려 있는 모달은 다 닫음 */ /** 로직 알약 → 그 로직 열기 — 로직 화면이 자기 열기를 등록 · 열려 있는 모달은 다 닫음 */
let logicOpener: (key: string) => void = () => {}; let logicOpener: (key: string) => void = () => {};
@@ -138,11 +139,11 @@ function tableView(table: TableRow, target: TableTarget): HTMLElement {
children: [ children: [
el("strong", { text: `${table.원문번호 ?? ""} ${table.이름 ?? table.키} · ${table.키}` }), el("strong", { text: `${table.원문번호 ?? ""} ${table.이름 ?? table.키} · ${table.키}` }),
el("p", { el("p", {
className: "m01-logic__muted", className: "ui-text ui-text--muted",
text: [`${tl("Modal_Source")} ${table.출처 ?? ""}`, use].filter(Boolean).join(" · "), text: [`${tl("Modal_Source")} ${table.출처 ?? ""}`, use].filter(Boolean).join(" · "),
}), }),
el("p", { el("p", {
className: "m01-logic__muted", className: "ui-text ui-text--muted",
text: hit ? tl("Modal_Hit") : target.col ? tl("Modal_ColOnly") : tl("Modal_NoHit"), text: hit ? tl("Modal_Hit") : target.col ? tl("Modal_ColOnly") : tl("Modal_NoHit"),
}), }),
goButton({ kind: "table", key: table.키 }), goButton({ kind: "table", key: table.키 }),
@@ -150,7 +151,7 @@ function tableView(table: TableRow, target: TableTarget): HTMLElement {
className: "m01-logic__scroll", className: "m01-logic__scroll",
children: [ children: [
el("table", { el("table", {
className: "m01-logic__grid m01lab__table", className: "ui-table ui-table--data m01-logic__grid m01lab__table",
children: [ children: [
el("thead", { el("thead", {
children: [el("tr", { children: cols.map((c) => el("th", { text: c })) })], children: [el("tr", { children: cols.map((c) => el("th", { text: c })) })],
@@ -217,7 +218,7 @@ function openDialog(title: string, mount: (body: HTMLElement) => void): void {
}); });
} }
const muted = (text: string): HTMLElement => el("p", { className: "m01-logic__muted", text }); const muted = (text: string): HTMLElement => el("p", { className: "ui-text ui-text--muted", text });
/** 재료 고르기 줄 후보 목록 — 잡힌 품목(`pickedRef`)을 노란 줄로 · 후보가 100건을 넘으면 앞 100건만 옴 */ /** 재료 고르기 줄 후보 목록 — 잡힌 품목(`pickedRef`)을 노란 줄로 · 후보가 100건을 넘으면 앞 100건만 옴 */
function candidateView( function candidateView(
@@ -249,7 +250,7 @@ function candidateView(
className: "m01-logic__scroll", className: "m01-logic__scroll",
children: [ children: [
el("table", { el("table", {
className: "m01-logic__grid m01lab__table", className: "ui-table ui-table--data m01-logic__grid m01lab__table",
children: [ children: [
el("thead", { el("thead", {
children: [ children: [
@@ -421,12 +422,10 @@ export function openNoteModal(title: string, text: string): void {
/** 수량 칸 표 알약을 눌렀을 때 — 그 표만 보임(걸린 줄 강조) */ /** 수량 칸 표 알약을 눌렀을 때 — 그 표만 보임(걸린 줄 강조) */
export function openTableModal(target: TableTarget): void { export function openTableModal(target: TableTarget): void {
openDialog(target.title, (body) => { openDialog(target.title, (body) => {
body.replaceChildren(el("p", { className: "m01-logic__muted", text: "…" })); body.replaceChildren(el("p", { className: "ui-text ui-text--muted", text: "…" }));
void loadTable(target.table).then((table) => { void loadTable(target.table).then((table) => {
body.replaceChildren( body.replaceChildren(
table table ? tableView(table, target) : createText(`${target.table} ?`, "error"),
? tableView(table, target)
: el("p", { className: "m01-logic__bad", text: `${target.table} ?` }),
); );
}); });
}); });
@@ -1,39 +1,9 @@
/* M01 로직 개선 시험 — 줄 모달의 알약 단추 */ /* 알약 = 단추 부품(보조 · 24) · 여기는 칸 폭 넘침만 */
.m01lab__pills {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
min-width: 0;
}
.m01lab__pill-row {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-8);
}
.m01lab__pill { .m01lab__pill {
padding: var(--spacing-4) var(--spacing-12);
border: 1px solid var(--color-border);
border-radius: 999px;
background: var(--color-bg);
color: var(--color-text);
font: inherit;
cursor: pointer;
max-width: 100%; max-width: 100%;
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.m01lab__pill.is-open {
background: var(--color-accent);
border-color: var(--color-accent);
color: var(--color-bg);
}
.m01lab__pill-stage {
min-width: 0;
}
/* 계산 안 된 만약() 가지 조각 — 흐리게 */ /* 계산 안 된 만약() 가지 조각 — 흐리게 */
.m01lab__qty-dim { .m01lab__qty-dim {
opacity: 0.45; opacity: 0.45;
@@ -6,6 +6,7 @@
* ========================================================================== */ * ========================================================================== */
import { el } from "@ui/ui_template_elements"; import { el } from "@ui/ui_template_elements";
import { createButton } from "@ui/ui_template_button";
export interface Pill { export interface Pill {
/** 알약 글 — 보기 「표 QF000421」 */ /** 알약 글 — 보기 「표 QF000421」 */
@@ -16,11 +17,10 @@ export interface Pill {
/** 알약 하나 — 글자 조각 사이에 끼워 쓰는 단추 */ /** 알약 하나 — 글자 조각 사이에 끼워 쓰는 단추 */
export function pillButton(p: Pill): HTMLButtonElement { export function pillButton(p: Pill): HTMLButtonElement {
const b = el("button", { // 알약 = 단추 부품(보조 · 24) — data-pill 은 시험 · 촬영이 잡음
className: "m01lab__pill", const b = createButton({ label: p.label, variant: "ghost", size: 24 });
text: p.label, b.classList.add("m01lab__pill");
attrs: { type: "button", "data-pill": p.label }, b.dataset.pill = p.label;
});
b.addEventListener("click", (ev) => { b.addEventListener("click", (ev) => {
ev.stopPropagation(); // 줄 전체 누르기(행 모달)와 안 겹치게 ev.stopPropagation(); // 줄 전체 누르기(행 모달)와 안 겹치게
p.onClick(); p.onClick();
@@ -51,7 +51,7 @@ function rawNode(b: Block): HTMLElement {
className: "m01-logic__scroll", className: "m01-logic__scroll",
children: [ children: [
el("table", { el("table", {
className: "m01-logic__grid m01lab-src__table", className: "ui-table ui-table--data m01-logic__grid m01lab-src__table",
children: [ children: [
el("tbody", { el("tbody", {
children: b.table.map((r) => children: b.table.map((r) =>
@@ -104,11 +104,11 @@ export function createSourceView(opts: SourceViewOptions = {}): SourceView {
download.hidden = !book; download.hidden = !book;
download.href = book ? `${API_BASE_URL}${opts.pdf}?${new URLSearchParams({ book })}` : ""; download.href = book ? `${API_BASE_URL}${opts.pdf}?${new URLSearchParams({ book })}` : "";
} }
body.replaceChildren(el("p", { className: "m01-logic__muted", text: ts("Loading") })); body.replaceChildren(el("p", { className: "ui-text ui-text--muted", text: ts("Loading") }));
const none = (): void => const none = (): void =>
body.replaceChildren( body.replaceChildren(
el("p", { className: "m01lab-src__none", text: ts("None") }), el("p", { className: "m01lab-src__none", text: ts("None") }),
el("p", { className: "m01-logic__muted", text: `${ts("From")} · ${src || "—"}` }), el("p", { className: "ui-text ui-text--muted", text: `${ts("From")} · ${src || "—"}` }),
); );
if (!src.trim()) return none(); if (!src.trim()) return none();
void fetch( void fetch(
@@ -125,7 +125,7 @@ export function createSourceView(opts: SourceViewOptions = {}): SourceView {
body.replaceChildren( body.replaceChildren(
...found.flatMap((f) => [ ...found.flatMap((f) => [
el("p", { el("p", {
className: "m01-logic__muted m01lab-src__file", className: "ui-text ui-text--muted m01lab-src__file",
text: [ text: [
f.file, f.file,
f.matched === f.number ? "" : ts("Near", f.number), f.matched === f.number ? "" : ts("Near", f.number),
@@ -116,15 +116,7 @@
overflow: auto; overflow: auto;
} }
.m01lab__modal-top { .m01lab__modal-body {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-8);
}
.m01lab__modal-body,
.m01lab__tables {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--spacing-8); gap: var(--spacing-8);
@@ -157,7 +149,7 @@
} }
/* 복사해서 만들기(3-4) — 별칭 꼴 식 시트 */ /* 복사해서 만들기(3-4) — 별칭 꼴 식 시트 */
.m01copy__sheet { .ui-modal.m01copy__sheet {
width: min(1040px, 94vw); width: min(1040px, 94vw);
--modal-max: 90vh; --modal-max: 90vh;
} }
@@ -177,7 +169,7 @@
} }
/* 시험 계산 — 재료 · 기계 고르기 표 모달 */ /* 시험 계산 — 재료 · 기계 고르기 표 모달 */
.m01-logic__pick--table { .ui-modal.m01-logic__pick--table {
width: min(1040px, 94vw); width: min(1040px, 94vw);
} }
@@ -193,7 +185,3 @@
.m01lab__pickrow.is-picked { .m01lab__pickrow.is-picked {
font-weight: 600; font-weight: 600;
} }
.m01lab__warn {
color: var(--color-warning, #b45309);
}
+8 -10
View File
@@ -27,6 +27,7 @@ import {
import { formatNumber } from "./M01_MasterData_UI_Logic_Edit"; import { formatNumber } from "./M01_MasterData_UI_Logic_Edit";
import { formatMoney } from "./M01_MasterData_UI_Logic_Money"; import { formatMoney } from "./M01_MasterData_UI_Logic_Money";
import { safeReason, tx } from "./M01_MasterData_UI_Logic_Text"; import { safeReason, tx } from "./M01_MasterData_UI_Logic_Text";
import { createBadge } from "@ui/ui_template_badge";
/** 고르기 값이 마스터 키(자재·기계·직종)면 이름으로 바꿔 보임 — 값 칸은 키 그대로 보냄 */ /** 고르기 값이 마스터 키(자재·기계·직종)면 이름으로 바꿔 보임 — 값 칸은 키 그대로 보냄 */
const KEY_RE = /^[A-Z]{2}\d{6}$/; const KEY_RE = /^[A-Z]{2}\d{6}$/;
@@ -197,7 +198,7 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
el("h3", { text: tx("Calc_Title") }), el("h3", { text: tx("Calc_Title") }),
...(fields.length || materials.length ...(fields.length || materials.length
? [el("div", { className: "m01-logic__head", children: [...fields, ...materials] })] // 요소 표가 쓰는 auto-fill 그리드 재사용 — 2열 ? [el("div", { className: "m01-logic__head", children: [...fields, ...materials] })] // 요소 표가 쓰는 auto-fill 그리드 재사용 — 2열
: [el("p", { className: "m01-logic__muted", text: tx("Calc_NoInputs") })]), : [el("p", { className: "ui-text ui-text--muted", text: tx("Calc_NoInputs") })]),
runButton, runButton,
out, out,
); );
@@ -205,12 +206,14 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
/** `money` = 결과가 돈(원) — 그때만 결과 값도 정수 · 수량·비율 결과는 소수 그대로 */ /** `money` = 결과가 돈(원) — 그때만 결과 값도 정수 · 수량·비율 결과는 소수 그대로 */
export function answerView(answer: CalcAnswer, draft: boolean, money: boolean): HTMLElement[] { export function answerView(answer: CalcAnswer, draft: boolean, money: boolean): HTMLElement[] {
const note = draft ? [el("p", { className: "m01-logic__muted", text: tx("Calc_Draft") })] : []; const note = draft
? [el("p", { className: "ui-text ui-text--muted", text: tx("Calc_Draft") })]
: [];
if (!answer.ok) { if (!answer.ok) {
return [ return [
...note, ...note,
el("div", { el("div", {
className: "m01-logic__reasons", className: "ui-banner ui-banner--danger m01-logic__reasons",
children: [ children: [
el("strong", { text: tx("Calc_Stopped") }), el("strong", { text: tx("Calc_Stopped") }),
el("div", { text: safeReason(answer.reason) }), el("div", { text: safeReason(answer.reason) }),
@@ -231,12 +234,7 @@ export function answerView(answer: CalcAnswer, draft: boolean, money: boolean):
: pctView(line).name, : pctView(line).name,
}), }),
...(line.단위경고 ...(line.단위경고
? [ ? [createBadge(`${tx("Calc_UnitWarn")} · ${line.단위경고}`, "warning")]
el("span", {
className: "m01-logic__tag m01lab__warn",
text: `${tx("Calc_UnitWarn")} · ${line.단위경고}`,
}),
]
: []), : []),
], ],
}), }),
@@ -247,7 +245,7 @@ export function answerView(answer: CalcAnswer, draft: boolean, money: boolean):
); );
parts.push( parts.push(
el("table", { el("table", {
className: "m01-logic__grid", className: "ui-table ui-table--data m01-logic__grid",
children: [ children: [
el("thead", { el("thead", {
children: [ children: [
@@ -81,7 +81,7 @@ export function materialRows(
.map(headWord) .map(headWord)
.filter((w, k, all) => w && all.indexOf(w) === k); .filter((w, k, all) => w && all.indexOf(w) === k);
const current = el("span", { const current = el("span", {
className: "m01-logic__muted", className: "ui-text ui-text--muted",
text: `${nowName || tx("Mat_NoPrice")} `, text: `${nowName || tx("Mat_NoPrice")} `,
attrs: { "data-mat-idx": String(i), "data-ref": brief?.ref ?? "" }, attrs: { "data-mat-idx": String(i), "data-ref": brief?.ref ?? "" },
}); });
@@ -141,7 +141,7 @@ export function machineControl(
again: () => void, again: () => void,
): HTMLElement | null { ): HTMLElement | null {
if (!isMachineInput(spec)) return null; if (!isMachineInput(spec)) return null;
const current = el("span", { className: "m01-logic__muted" }); const current = el("span", { className: "ui-text ui-text--muted" });
const paint = (): void => { const paint = (): void => {
const code = values[spec.이름] ?? ""; const code = values[spec.이름] ?? "";
const known = machines.get(code); const known = machines.get(code);
+13 -14
View File
@@ -17,6 +17,9 @@ import type {
NamedFormula, NamedFormula,
} from "./M01_MasterData_UI_Logic_Api"; } from "./M01_MasterData_UI_Logic_Api";
import { tx } from "./M01_MasterData_UI_Logic_Text"; import { tx } from "./M01_MasterData_UI_Logic_Text";
import { createBadge } from "@ui/ui_template_badge";
import { createText } from "@ui/ui_template_text";
import { createIconButton } from "@ui/ui_template_button";
export type PickMode = "element" | "table"; export type PickMode = "element" | "table";
export type PickDone = (item: ElementBrief, column?: string) => void; export type PickDone = (item: ElementBrief, column?: string) => void;
@@ -81,12 +84,8 @@ function choice(
} }
function dropButton(onClick: () => void): HTMLButtonElement { function dropButton(onClick: () => void): HTMLButtonElement {
const button = el("button", { const button = createIconButton({ glyph: "×", title: tx("DeleteRow"), size: 20, onClick });
className: "m01-logic__drop", button.classList.add("m01-logic__drop");
text: "×",
attrs: { type: "button", title: tx("DeleteRow") },
});
button.addEventListener("click", onClick);
return button; return button;
} }
@@ -100,7 +99,7 @@ function section(title: string, body: HTMLElement, onAdd?: () => void): HTMLElem
} }
function grid(headers: string[], rows: HTMLElement[][], className: string): HTMLElement { function grid(headers: string[], rows: HTMLElement[][], className: string): HTMLElement {
const table = el("table", { className: `m01-logic__grid ${className}` }); const table = el("table", { className: `ui-table ui-table--data m01-logic__grid ${className}` });
table.append( table.append(
el("thead", { children: [el("tr", { children: headers.map((h) => el("th", { text: h })) })] }), el("thead", { children: [el("tr", { children: headers.map((h) => el("th", { text: h })) })] }),
); );
@@ -133,7 +132,7 @@ export function buildEditor(host: HTMLElement, ctx: EditContext): void {
if (ctx.reasons.length) { if (ctx.reasons.length) {
host.append( host.append(
el("div", { el("div", {
className: "m01-logic__reasons", className: "ui-banner ui-banner--danger m01-logic__reasons",
children: [ children: [
el("strong", { text: tx("Head_Blocked") }), el("strong", { text: tx("Head_Blocked") }),
...ctx.reasons.map((r) => el("div", { text: r })), ...ctx.reasons.map((r) => el("div", { text: r })),
@@ -191,7 +190,7 @@ function head(ctx: EditContext, set: (p: () => void) => void, rerender: () => vo
field(tx("Head_File"), file, true), field(tx("Head_File"), file, true),
field( field(
tx("Head_Key"), tx("Head_Key"),
el("span", { className: "m01-logic__muted", text: row.키 || tx("New_Key") }), el("span", { className: "ui-text ui-text--muted", text: row.키 || tx("New_Key") }),
), ),
field( field(
tx("Head_Number"), tx("Head_Number"),
@@ -276,11 +275,11 @@ function priceCell(ctx: EditContext, item: HoLine, i: number): HTMLElement {
const line = ctx.lines?.[i]; const line = ctx.lines?.[i];
if (line) return el("span", { className: "m01-logic__money", text: formatNumber(line.단가) }); if (line) return el("span", { className: "m01-logic__money", text: formatNumber(line.단가) });
if (item.종류 === "로직" || item.요소.includes("{")) { if (item.종류 === "로직" || item.요소.includes("{")) {
return el("span", { className: "m01-logic__muted", text: tx("Ho_PriceLater") }); return el("span", { className: "ui-text ui-text--muted", text: tx("Ho_PriceLater") });
} }
const brief = ctx.prices[item.요소]; const brief = ctx.prices[item.요소];
if (brief === undefined) return el("span", { className: "m01-logic__muted", text: "" }); if (brief === undefined) return el("span", { className: "ui-text ui-text--muted", text: "" });
if (brief === null) return el("span", { className: "m01-logic__bad", text: tx("Ho_Missing") }); if (brief === null) return createText(tx("Ho_Missing"), "error");
return el("span", { className: "m01-logic__money", text: formatNumber(brief.값) }); return el("span", { className: "m01-logic__money", text: formatNumber(brief.값) });
} }
@@ -314,7 +313,7 @@ function hoSection(
set(() => (item.수량 = v)); set(() => (item.수량 = v));
tag.textContent = qtyKind(v); tag.textContent = qtyKind(v);
}); });
const tag = el("span", { className: "m01-logic__tag", text: qtyKind(item.수량) }); const tag = createBadge(qtyKind(item.수량), "accent");
const table = createButton({ const table = createButton({
label: tx("Ho_PickTable"), label: tx("Ho_PickTable"),
variant: "ghost", variant: "ghost",
@@ -339,7 +338,7 @@ function hoSection(
className: "m01-logic__stack", className: "m01-logic__stack",
children: [ children: [
textBox(item.이름 ?? "", (v) => set(() => (item.이름 = v))), textBox(item.이름 ?? "", (v) => set(() => (item.이름 = v))),
el("span", { className: "m01-logic__muted", text: spec }), el("span", { className: "ui-text ui-text--muted", text: spec }),
], ],
}), }),
textBox(item.단위 ?? "", (v) => set(() => (item.단위 = v)), "m01-logic__unit"), textBox(item.단위 ?? "", (v) => set(() => (item.단위 = v)), "m01-logic__unit"),
@@ -67,7 +67,7 @@ export function buildList(
const pick = { sub: "", detail: "" }; const pick = { sub: "", detail: "" };
const searchField = createInputField({ type: "search", placeholder: tx("List_Search") }); const searchField = createInputField({ type: "search", placeholder: tx("List_Search") });
const search = searchField.input; const search = searchField.input;
const grid = el("div", { className: "m01-master__grid-wrap" }); const grid = el("div", { className: "ui-table-wrap m01-master__grid-wrap" });
const draw = (): void => { const draw = (): void => {
const q = search.value; const q = search.value;
@@ -89,7 +89,7 @@ export function buildList(
pager.replaceWith((pager = buildPager(shown.length, SIZE, page, go))); pager.replaceWith((pager = buildPager(shown.length, SIZE, page, go)));
grid.replaceChildren( grid.replaceChildren(
el("table", { el("table", {
className: "m01-master__grid", className: "ui-table m01-master__grid",
children: [ children: [
el("thead", { el("thead", {
children: [el("tr", { children: COLS.map((c) => el("th", { text: c })) })], children: [el("tr", { children: COLS.map((c) => el("th", { text: c })) })],
+11 -6
View File
@@ -226,10 +226,12 @@ function tableView(find: RelatedFind, table: TableRow, ctx: EvalCtx): HTMLElemen
: ""; : "";
return [ return [
el("p", { el("p", {
className: "m01c__muted", className: "ui-text ui-text--muted",
text: `${find.source} · ${table.원문번호 ?? table.키} ${table.이름 ?? ""} (${table.출처 ?? ""})`, text: `${find.source} · ${table.원문번호 ?? table.키} ${table.이름 ?? ""} (${table.출처 ?? ""})`,
}), }),
...(use ? [el("p", { className: "m01c__muted", text: `${tx("Use_Of")} — ${use}` })] : []), ...(use
? [el("p", { className: "ui-text ui-text--muted", text: `${tx("Use_Of")} — ${use}` })]
: []),
el("table", { el("table", {
className: "m01c__table", className: "m01c__table",
children: [ children: [
@@ -252,17 +254,20 @@ export function buildNote(host: HTMLElement, opts: NoteOptions): void {
}); });
const plain = explain(opts.expr); const plain = explain(opts.expr);
const head: HTMLElement[] = [ const head: HTMLElement[] = [
el("p", { className: "m01c__muted", text: tx("Note_Plain") }), el("p", { className: "ui-text ui-text--muted", text: tx("Note_Plain") }),
el("p", { className: "m01c__plain", text: plain }), el("p", { className: "m01c__plain", text: plain }),
]; ];
if (!related.length) { if (!related.length) {
host.replaceChildren(...head, advanced); host.replaceChildren(...head, advanced);
return; return;
} }
const tables = el("div", { className: "m01c__muted", text: `${tx("Note_Searching")}…` }); const tables = el("div", {
className: "ui-text ui-text--muted",
text: `${tx("Note_Searching")}…`,
});
host.replaceChildren( host.replaceChildren(
...head, ...head,
el("p", { className: "m01c__muted", text: tx("Note_Table") }), el("p", { className: "ui-text ui-text--muted", text: tx("Note_Table") }),
tables, tables,
advanced, advanced,
); );
@@ -275,7 +280,7 @@ export function buildNote(host: HTMLElement, opts: NoteOptions): void {
? tableView(r, table, opts.ctx) ? tableView(r, table, opts.ctx)
: [ : [
el("p", { el("p", {
className: "m01c__muted", className: "ui-text ui-text--muted",
text: tx("Note_TableMissing", { v: r.find.table }), text: tx("Note_TableMissing", { v: r.find.table }),
}), }),
], ],
+20 -27
View File
@@ -27,6 +27,7 @@ import {
} from "./M01_MasterData_UI_Logic_Api"; } from "./M01_MasterData_UI_Logic_Api";
import { formatNumber, type PickDone, type PickMode } from "./M01_MasterData_UI_Logic_Edit"; import { formatNumber, type PickDone, type PickMode } from "./M01_MasterData_UI_Logic_Edit";
import { tx } from "./M01_MasterData_UI_Logic_Text"; import { tx } from "./M01_MasterData_UI_Logic_Text";
import { openModal } from "@ui/ui_template_modal";
const GROUPS: Record<PickMode, string[]> = { const GROUPS: Record<PickMode, string[]> = {
element: ["인력", "재료", "기계", "로직"], element: ["인력", "재료", "기계", "로직"],
@@ -43,30 +44,23 @@ export function openPicker(mode: PickMode, group: string, done: PickDone): void
const select = groupField.select; const select = groupField.select;
const searchField = createInputField({ type: "search", placeholder: tx("Pick_Search") }); const searchField = createInputField({ type: "search", placeholder: tx("Pick_Search") });
const search = searchField.input; const search = searchField.input;
const count = el("span", { className: "m01-logic__muted" }); const count = el("span", { className: "ui-text ui-text--muted" });
const list = el("div", { className: "m01-logic__pick-list" }); const list = el("div", { className: "m01-logic__pick-list" });
const close = (): void => backdrop.remove(); // 모달 부품 — 제목줄 [닫기] · 바깥 누르기 · Esc · 위 고정
const dialog = el("div", { const { close } = openModal({
className: "m01-logic__pick ui-popup", title: tx("Pick_Title"),
attrs: { role: "dialog", "aria-label": tx("Pick_Title") }, closeLabel: tx("Pick_Close"),
children: [ dialogClass: "m01-logic__pick",
el("div", { backdropClass: "m01-logic__backdrop",
className: "m01-logic__section-head", mount: (body) =>
children: [ body.append(
el("h3", { text: tx("Pick_Title") }), el("div", {
createButton({ label: tx("Pick_Close"), variant: "ghost", onClick: close }), className: "m01-logic__pick-bar",
], children: [groupField.root, searchField.root, count],
}), }),
el("div", { list,
className: "m01-logic__pick-bar", ),
children: [groupField.root, searchField.root, count],
}),
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());
const choose = (item: ElementBrief, column?: string): void => { const choose = (item: ElementBrief, column?: string): void => {
close(); close();
@@ -94,7 +88,6 @@ export function openPicker(mode: PickMode, group: string, done: PickDone): void
timer = window.setTimeout(load, 250); timer = window.setTimeout(load, 250);
}); });
select.addEventListener("change", load); select.addEventListener("change", load);
document.body.append(backdrop);
search.focus(); search.focus();
void load(); void load();
} }
@@ -110,7 +103,7 @@ function itemRow(
children: [ children: [
el("strong", { text: `${item.이름 ?? ""} ${item.규격 ?? ""}`.trim() }), el("strong", { text: `${item.이름 ?? ""} ${item.규격 ?? ""}`.trim() }),
el("span", { el("span", {
className: "m01-logic__muted", className: "ui-text ui-text--muted",
text: [item.ref, note].filter(Boolean).join(" · "), text: [item.ref, note].filter(Boolean).join(" · "),
}), }),
], ],
@@ -128,8 +121,8 @@ function itemRow(
const unit = typeof item.단위 === "string" ? item.단위 : ""; const unit = typeof item.단위 === "string" ? item.단위 : "";
const value = typeof item.값 === "object" && item.값 !== null ? "{…}" : formatNumber(item.값); const value = typeof item.값 === "object" && item.값 !== null ? "{…}" : formatNumber(item.값);
const row = el("button", { const row = el("button", {
className: "m01-logic__pick-row m01-logic__pick-row--button", className: "m01-logic__pick-row ui-list-row ui-sidebar-row",
attrs: { type: "button" }, attrs: { type: "button", "data-size": "32" },
children: [ children: [
text, text,
el("span", { className: "m01-logic__money", text: `${value} ${unit}`.trim() }), el("span", { className: "m01-logic__money", text: `${value} ${unit}`.trim() }),
@@ -276,7 +269,7 @@ function swapRow(one: LogicOne, line: HoLine, i: number, swaps: Swaps, onChange:
className: "m01-test__pick", className: "m01-test__pick",
children: [ children: [
el("strong", { text: `${line.종류} · ${line.이름 ?? String(line.요소)}${spec}` }), el("strong", { text: `${line.종류} · ${line.이름 ?? String(line.요소)}${spec}` }),
el("span", { className: "m01c__muted", text: `${line.단위 ?? ""}${cost}` }), el("span", { className: "ui-text ui-text--muted", text: `${line.단위 ?? ""}${cost}` }),
subRow, subRow,
itemBox.root, itemBox.root,
], ],
@@ -4,10 +4,11 @@
* 검색 칸을 채워 열고 · 잡힌 품목 칠함 · 한 줄 누르면 고름 · 저장 안 함(읽기만) * 검색 칸을 채워 열고 · 잡힌 품목 칠함 · 한 줄 누르면 고름 · 저장 안 함(읽기만)
* ========================================================================== */ * ========================================================================== */
import { createButton, createInputField, el, showToast } from "@ui/ui_template_elements"; import { createInputField, el, showToast } from "@ui/ui_template_elements";
import { fetchRows, type Row } from "./M01_MasterData_Api_Fetch"; import { fetchRows, type Row } from "./M01_MasterData_Api_Fetch";
import { buildPager, flatten, show } from "./M01_MasterData_UI_Cells"; import { buildPager, flatten, show } from "./M01_MasterData_UI_Cells";
import { tx } from "./M01_MasterData_UI_Logic_Text"; import { tx } from "./M01_MasterData_UI_Logic_Text";
import { openModal } from "@ui/ui_template_modal";
export type PickTableKind = "재료" | "기계"; export type PickTableKind = "재료" | "기계";
@@ -42,7 +43,6 @@ export interface PickTableOptions {
} }
export function openPickTable(opt: PickTableOptions): void { export function openPickTable(opt: PickTableOptions): void {
const close = (): void => back.remove();
const searchField = createInputField({ const searchField = createInputField({
type: "search", type: "search",
placeholder: tx("Pick_Search"), placeholder: tx("Pick_Search"),
@@ -50,24 +50,14 @@ export function openPickTable(opt: PickTableOptions): void {
}); });
const search = searchField.input; const search = searchField.input;
const host = el("div", { className: "m01-logic__pick-list" }); const host = el("div", { className: "m01-logic__pick-list" });
const dialog = el("div", { // 모달 부품 — 제목줄 [닫기] · 바깥 누르기 · Esc · 위 고정 · 폭만 표 모달 곁값
className: "m01-logic__pick m01-logic__pick--table ui-popup", const { close } = openModal({
attrs: { role: "dialog", "aria-label": opt.title }, title: opt.title,
children: [ closeLabel: tx("Pick_Close"),
el("div", { dialogClass: "m01-logic__pick m01-logic__pick--table",
className: "m01-logic__section-head", backdropClass: "m01-logic__backdrop",
children: [ mount: (body) => body.append(searchField.root, host),
el("h3", { text: opt.title }),
createButton({ label: tx("Pick_Close"), variant: "ghost", onClick: close }),
],
}),
searchField.root,
host,
],
}); });
const back = el("div", { className: "m01-logic__backdrop", children: [dialog] });
back.addEventListener("click", (ev) => ev.target === back && close());
back.addEventListener("keydown", (ev) => ev.key === "Escape" && close());
let page = 1; let page = 1;
const tried = new Set([opt.seed]); const tried = new Set([opt.seed]);
@@ -109,10 +99,10 @@ export function openPickTable(opt: PickTableOptions): void {
void load(); void load();
}), }),
el("div", { el("div", {
className: "m01-master__grid-wrap", className: "ui-table-wrap m01-master__grid-wrap",
children: [ children: [
el("table", { el("table", {
className: "m01-master__grid", className: "ui-table m01-master__grid",
children: [ children: [
el("thead", { el("thead", {
children: [el("tr", { children: cols.map((c) => el("th", { text: c })) })], children: [el("tr", { children: cols.map((c) => el("th", { text: c })) })],
@@ -136,7 +126,5 @@ export function openPickTable(opt: PickTableOptions): void {
void load(); void load();
}, 300); }, 300);
}); });
document.body.append(back);
search.focus();
void load(true); void load(true);
} }
@@ -54,18 +54,6 @@
background: var(--color-surface); background: var(--color-surface);
} }
.m01-logic__filters {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
}
.m01-logic__check {
display: flex;
align-items: center;
gap: var(--spacing-4);
}
.m01-logic__bar { .m01-logic__bar {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -79,10 +67,6 @@
color: var(--color-text); color: var(--color-text);
} }
.m01-logic__empty {
color: var(--color-text-muted);
}
.m01-logic__head { .m01-logic__head {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
@@ -156,23 +140,9 @@ input.m01-logic__input {
color: var(--color-text); color: var(--color-text);
} }
.m01-logic__grid { /* 겉 = 표 부품 데이터표(ui-table--data · 줄 28) · 여기는 위 맞춤만 */
width: 100%;
border-collapse: collapse;
}
.m01-logic__grid th,
.m01-logic__grid td { .m01-logic__grid td {
padding: 2px 4px;
border-bottom: 1px solid var(--color-border);
vertical-align: top; vertical-align: top;
text-align: left;
}
.m01-logic__grid th {
color: var(--color-text-secondary);
font-size: var(--text-caption);
font-weight: 600;
} }
.m01-logic__grid th, .m01-logic__grid th,
@@ -245,15 +215,6 @@ input.m01-logic__input {
width: 64px; width: 64px;
} }
.m01-logic__tag {
flex: none;
padding: 0 6px;
border-radius: var(--radius-pills);
background: var(--color-mist-violet);
color: var(--color-accent);
font-size: var(--text-caption);
}
.m01-logic__money { .m01-logic__money {
display: block; display: block;
text-align: right; text-align: right;
@@ -261,11 +222,6 @@ input.m01-logic__input {
white-space: nowrap; white-space: nowrap;
} }
.m01-logic__muted {
color: var(--color-text-muted);
font-size: var(--text-caption);
}
/* 목록 「별칭」 칸(208-1) — 칸 안 글 상자 · 테두리는 올렸을 때만 */ /* 목록 「별칭」 칸(208-1) — 칸 안 글 상자 · 테두리는 올렸을 때만 */
.m01-logic__alias { .m01-logic__alias {
width: 160px; width: 160px;
@@ -283,29 +239,10 @@ input.m01-logic__input {
background: var(--color-canvas); background: var(--color-canvas);
} }
.m01-logic__bad { /* 겉 = 띠 알림 부품(ui-banner--danger) · 여기는 제목 · 까닭 줄 쌓기만 */
color: var(--color-danger);
font-size: var(--text-caption);
}
.m01-logic__drop {
border: 0;
background: none;
color: var(--color-text-muted);
font-size: 16px;
cursor: pointer;
}
.m01-logic__drop:hover {
color: var(--color-danger);
}
.m01-logic__reasons { .m01-logic__reasons {
padding: var(--spacing-8); flex-direction: column;
border-left: 3px solid var(--color-danger); gap: 2px;
border-radius: var(--radius-sm);
background: var(--color-canvas);
color: var(--color-danger);
} }
.m01-logic__sums { .m01-logic__sums {
@@ -330,31 +267,7 @@ input.m01-logic__input {
gap: var(--spacing-8); gap: var(--spacing-8);
} }
.m01-logic__backdrop { /* 고르기 창 = 모달 부품(ui-modal · 720 · 위 고정) — 여기는 덧붙임 없음 · 표 · 복사 창만 폭 곁값 */
position: fixed;
inset: 0;
z-index: 1000;
display: flex;
align-items: flex-start;
justify-content: center;
background-color: rgba(38, 17, 74, 0.35); /* 공용 창 틀과 같이 뒤를 가림 */
}
.m01-logic__pick {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
width: min(720px, 92vw);
/* 위에서 고정 거리 — 공용 .ui-modal 과 같은 규칙(찾기 칸이 안 움직임) */
max-height: var(--modal-max, 80vh);
margin-top: calc((100dvh - var(--modal-max, 80vh)) / 2);
padding: var(--spacing-16);
border-radius: var(--radius-xl);
border: 1px solid var(--color-border);
background: var(--color-surface-raised);
box-shadow: var(--shadow-lg);
font-size: var(--text-body-sm);
}
.m01-logic__pick-bar { .m01-logic__pick-bar {
display: grid; display: grid;
@@ -367,6 +280,7 @@ input.m01-logic__input {
overflow: auto; overflow: auto;
} }
/* 고르는 줄 = 목록 줄 부품(button) · 열 고르기 줄(div)만 아래 선 */
.m01-logic__pick-row { .m01-logic__pick-row {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -374,20 +288,9 @@ input.m01-logic__input {
gap: var(--spacing-8); gap: var(--spacing-8);
width: 100%; width: 100%;
padding: var(--spacing-4) var(--spacing-8); padding: var(--spacing-4) var(--spacing-8);
border: 0; }
div.m01-logic__pick-row {
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border);
background: none;
color: inherit;
font: inherit;
text-align: left;
}
.m01-logic__pick-row--button {
cursor: pointer;
}
.m01-logic__pick-row--button:hover {
background: var(--color-paper);
} }
.m01-logic__chips { .m01-logic__chips {
+10 -1
View File
@@ -9,6 +9,12 @@
* ========================================================================== */ * ========================================================================== */
import "@ui/ui_template_workflow_layout.css"; import "@ui/ui_template_workflow_layout.css";
// 부품 상자 겉(553-5) — 페이지 CSS 앞에 실어 페이지 곁값이 이김
import "@ui/ui_template_banner.css";
import "@ui/ui_template_list_row.css";
import "@ui/ui_template_table.css";
import "@ui/ui_template_text.css";
import "@ui/ui_template_toolbar.css";
import { import {
createButton, createButton,
createInputField, createInputField,
@@ -71,7 +77,10 @@ function buildPage(): HTMLElement {
const summary = el("span", { className: "m01-master__summary" }); const summary = el("span", { className: "m01-master__summary" });
const save = createButton({ label: L("M01_Save"), variant: "filled" }); const save = createButton({ label: L("M01_Save"), variant: "filled" });
const drop = createButton({ label: L("M01_Discard"), variant: "ghost" }); const drop = createButton({ label: L("M01_Discard"), variant: "ghost" });
const notice = el("div", { className: "m01-master__notice", attrs: { hidden: "" } }); const notice = el("div", {
className: "ui-banner ui-banner--danger m01-master__notice",
attrs: { hidden: "" },
});
const body = el("div", { className: "m01-master__body" }); const body = el("div", { className: "m01-master__body" });
// 제목 줄 · 찾기 줄 = 「단가산출 로직」 화면과 같은 공용 조각 // 제목 줄 · 찾기 줄 = 「단가산출 로직」 화면과 같은 공용 조각
const headBar = buildHeadBar({ title, status: [summary], buttons: [drop, save] }); const headBar = buildHeadBar({ title, status: [summary], buttons: [drop, save] });
+16 -26
View File
@@ -4,6 +4,9 @@
* ========================================================================== */ * ========================================================================== */
import { createButton, createInputField, el, showToast } from "@ui/ui_template_elements"; import { createButton, createInputField, el, showToast } from "@ui/ui_template_elements";
import { createEmpty } from "@ui/ui_template_empty";
import { openModal } from "@ui/ui_template_modal";
import { createText } from "@ui/ui_template_text";
import { t as L } from "@ui/ui_template_locale"; import { t as L } from "@ui/ui_template_locale";
import { fetchPick, type PickItem, type PickKind } from "./M01_MasterData_Api_Fetch"; import { fetchPick, type PickItem, type PickKind } from "./M01_MasterData_Api_Fetch";
import { show } from "./M01_MasterData_UI_Cells"; import { show } from "./M01_MasterData_UI_Cells";
@@ -34,7 +37,6 @@ const field = (value: string, placeholder: string): ReturnType<typeof createInpu
createInputField({ type: "search", placeholder, value }); createInputField({ type: "search", placeholder, value });
export function openPickModal(opt: PickOptions): void { export function openPickModal(opt: PickOptions): void {
const close = (): void => back.remove();
const searchField = field(opt.seed, L("M01_ProcureSearch")); const searchField = field(opt.seed, L("M01_ProcureSearch"));
const search = searchField.input; const search = searchField.input;
const list = el("div", { className: "m01-procure__list" }); const list = el("div", { className: "m01-procure__list" });
@@ -44,8 +46,6 @@ export function openPickModal(opt: PickOptions): void {
opt.onPick(null); opt.onPick(null);
close(); close();
}); });
const shut = button(L("M01_ProcureClose"));
shut.addEventListener("click", close);
const condRow: HTMLElement[] = []; const condRow: HTMLElement[] = [];
if (opt.cond) { if (opt.cond) {
@@ -61,22 +61,15 @@ export function openPickModal(opt: PickOptions): void {
condRow.push(el("div", { className: "m01-procure__cond", children: [name, spec, go] })); condRow.push(el("div", { className: "m01-procure__cond", children: [name, spec, go] }));
} }
const box = el("div", { // 모달 부품 — 제목줄 [닫기] · 바깥 누르기 · Esc · 위 고정(공용 꼴) · [연결 끊기] 는 머리 단추 자리
className: "m01-procure__box", const { close } = openModal({
children: [ title: opt.title,
el("h3", { text: opt.title }), closeLabel: L("M01_ProcureClose"),
el("p", { className: "m01-master__muted", text: opt.current }), width: "40rem",
searchField.root, dialogClass: "m01-procure__box",
list, actions: [cut],
...condRow, mount: (body) => body.append(createText(opt.current), searchField.root, list, ...condRow),
el("div", { className: "m01-procure__foot", children: [cut, shut] }),
],
}); });
const back = el("div", { className: "m01-procure", children: [box] });
back.addEventListener("click", (e) => {
if (e.target === back) close();
});
document.body.append(back);
const paint = (items: PickItem[], total: number): void => { const paint = (items: PickItem[], total: number): void => {
list.replaceChildren( list.replaceChildren(
@@ -84,12 +77,12 @@ export function openPickModal(opt: PickOptions): void {
? items.map((it) => { ? items.map((it) => {
const tag = it.관급 ? ` · ${L("M01_Govt")}` : ""; const tag = it.관급 ? ` · ${L("M01_Govt")}` : "";
const b = el("button", { const b = el("button", {
className: "m01-procure__item", className: "m01-procure__item ui-list-row ui-sidebar-row",
attrs: { type: "button" }, attrs: { type: "button", "data-size": "36" },
children: [ children: [
el("strong", { text: it.이름 }), el("strong", { text: it.이름 }),
el("span", { text: `${it.규격} · ${it.단위} · ${show(it.값)}${tag}` }), el("span", { text: `${it.규격} · ${it.단위} · ${show(it.값)}${tag}` }),
el("span", { className: "m01-master__muted", text: it.ref }), el("span", { className: "ui-text", text: it.ref }),
], ],
}); });
b.addEventListener("click", () => { b.addEventListener("click", () => {
@@ -98,10 +91,8 @@ export function openPickModal(opt: PickOptions): void {
}); });
return b; return b;
}) })
: [el("p", { className: "m01-master__empty", text: L("M01_NoRows") })]), : [createEmpty({ text: L("M01_NoRows"), small: true })]),
...(total > items.length ...(total > items.length ? [createText(`${items.length} / ${total}`)] : []),
? [el("p", { className: "m01-master__muted", text: `${items.length} / ${total}` })]
: []),
); );
}; };
let timer: number | undefined; let timer: number | undefined;
@@ -123,5 +114,4 @@ export function openPickModal(opt: PickOptions): void {
timer = window.setTimeout(() => void run(), 300); timer = window.setTimeout(() => void run(), 300);
}); });
void run(true); void run(true);
search.focus();
} }
+5 -6
View File
@@ -28,6 +28,7 @@ import {
show, show,
withCell, withCell,
} from "./M01_MasterData_UI_Cells"; } from "./M01_MasterData_UI_Cells";
import { createEmpty } from "@ui/ui_template_empty";
const SIZE = 50; const SIZE = 50;
const MARKET = "재료_자재품목.json"; const MARKET = "재료_자재품목.json";
@@ -205,19 +206,17 @@ export function renderRows(host: HTMLElement, pick: Pick, q: string): () => void
addRow(file, d.version, sub && col ? { ...fresh, [col]: sub } : fresh); addRow(file, d.version, sub && col ? { ...fresh, [col]: sub } : fresh);
}); });
const empty = const empty =
d.rows.length || adds.length d.rows.length || adds.length ? [] : [createEmpty({ text: L("M01_NoRows"), small: true })];
? []
: [el("p", { className: "m01-master__empty", text: L("M01_NoRows") })];
host.replaceChildren( host.replaceChildren(
el("div", { el("div", {
className: "m01-master__bar", className: "ui-toolbar m01-master__bar",
children: [add, ...only, buildPager(d.total, SIZE, d.page, go)], children: [add, ...only, buildPager(d.total, SIZE, d.page, go)],
}), }),
el("div", { el("div", {
className: "m01-master__grid-wrap", className: "ui-table-wrap m01-master__grid-wrap",
children: [ children: [
el("table", { el("table", {
className: "m01-master__grid", className: "ui-table m01-master__grid",
children: [el("thead", { children: [head] }), body], children: [el("thead", { children: [head] }), body],
}), }),
], ],
+10 -76
View File
@@ -17,12 +17,6 @@
color: var(--color-accent); color: var(--color-accent);
} }
.m01-side__muted,
.m01-master__muted {
color: var(--color-text-muted);
font-size: var(--text-caption);
}
/* ── 우측 ── */ /* ── 우측 ── */
.m01-master__panel { .m01-master__panel {
display: flex; display: flex;
@@ -58,17 +52,12 @@ html.is-bm-route .m01-master__headbox {
font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold);
} }
/* 겉 = 띠 알림 부품(ui-banner--danger) · 여기는 쌓기 · 높이만 */
.m01-master__notice { .m01-master__notice {
display: flex;
flex-direction: column; flex-direction: column;
gap: var(--spacing-4); gap: var(--spacing-4);
padding: var(--spacing-8) var(--spacing-12);
max-height: 30vh; max-height: 30vh;
overflow: auto; overflow: auto;
border-left: 4px solid var(--color-danger);
background: color-mix(in srgb, var(--color-danger) 10%, var(--color-canvas));
color: var(--color-danger);
font-size: var(--text-body-sm);
} }
.m01-master__notice p { .m01-master__notice p {
@@ -95,25 +84,20 @@ html.is-bm-route .m01-master__headbox {
min-width: 0; min-width: 0;
} }
/* 겉 = 표 부품(ui-table-wrap · ui-table) · 여기는 높이 · 폭 · 머리 고정 · 칸 상태만 */
.m01-master__grid-wrap { .m01-master__grid-wrap {
max-height: calc(100dvh - 300px); max-height: calc(100dvh - 300px);
overflow: auto; overflow: auto;
border: 1px solid var(--color-border);
border-radius: var(--radius-cards);
} }
.m01-master__grid { table.m01-master__grid {
border-collapse: collapse; width: auto;
font-size: var(--text-body-sm);
} }
.m01-master__grid th, .m01-master__grid th,
.m01-master__grid td { .m01-master__grid td {
max-width: 320px; max-width: 320px;
padding: var(--spacing-8) var(--spacing-12);
overflow: hidden; overflow: hidden;
border-bottom: 1px solid var(--color-border);
text-align: left;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
@@ -122,9 +106,7 @@ html.is-bm-route .m01-master__headbox {
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 1; z-index: 1;
background: var(--color-surface); background: var(--color-surface-raised);
color: var(--color-text-secondary);
font-weight: var(--font-weight-medium);
} }
.m01-master__grid td.is-editable { .m01-master__grid td.is-editable {
@@ -161,77 +143,29 @@ html.is-bm-route .m01-master__headbox {
cursor: pointer; cursor: pointer;
} }
.m01-master__table-row:hover {
background: var(--color-paper);
}
.m01-master__table-row.is-changed { .m01-master__table-row.is-changed {
background: color-mix(in srgb, var(--color-warning) 28%, var(--color-canvas)); background: color-mix(in srgb, var(--color-warning) 28%, var(--color-canvas));
} }
.m01-master__empty { /* 조달 찾기 = 모달 부품(40rem) · 줄 = 목록 줄 부품 — 여기는 줄 안 세 줄 쌓기 · 목록 높이만 */
color: var(--color-text-muted);
}
.m01-master__logic-key {
padding: 0;
border: 0;
background: transparent;
color: var(--color-accent);
font: inherit;
cursor: pointer;
text-decoration: underline;
}
/* 조달 찾기 모달 */
.m01-procure {
position: fixed;
inset: 0;
z-index: 1000;
display: flex;
align-items: flex-start;
justify-content: center;
background: rgba(0, 0, 0, 0.45);
}
.m01-procure__box {
display: flex;
flex-direction: column;
gap: 8px;
width: min(640px, 92vw);
max-height: 80vh;
margin-top: 10vh; /* 위에서 고정 거리 — 찾기 칸이 안 움직임 */
padding: 16px;
border-radius: 8px;
background: var(--color-surface, #fff);
}
.m01-procure__list { .m01-procure__list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: var(--spacing-4);
overflow-y: auto; overflow-y: auto;
min-height: 120px; min-height: 120px;
} }
.m01-procure__item { .m01-procure__item {
display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
padding: 6px 8px; gap: 0;
border: 1px solid var(--color-border, #ccc); padding-block: var(--spacing-4);
border-radius: 4px;
background: transparent;
cursor: pointer;
text-align: left;
}
.m01-procure__foot {
display: flex;
justify-content: flex-end;
gap: 8px;
} }
.m01-procure__cond { .m01-procure__cond {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: var(--spacing-8);
} }
.m01-side__filter { .m01-side__filter {
+13 -18
View File
@@ -128,10 +128,10 @@ async function showList(
(p) => void showList(host, pick, q, usage, p, open, onUsageChange), (p) => void showList(host, pick, q, usage, p, open, onUsageChange),
), ),
el("div", { el("div", {
className: "m01-master__grid-wrap m01-master__tables", className: "ui-table-wrap m01-master__grid-wrap m01-master__tables",
children: [ children: [
el("table", { el("table", {
className: "m01-master__grid", className: "ui-table m01-master__grid",
children: [ children: [
el("thead", { el("thead", {
children: [el("tr", { children: LIST_COLS.map((c) => el("th", { text: c })) })], children: [el("tr", { children: LIST_COLS.map((c) => el("th", { text: c })) })],
@@ -184,11 +184,8 @@ function openTable(
now.forEach((item, i) => { now.forEach((item, i) => {
const tr = el("tr"); const tr = el("tr");
tr.classList.toggle("is-new", item.o === null); tr.classList.toggle("is-new", item.o === null);
const del = el("button", { const del = createButton({ label: L("M01_RowRemove"), variant: "ghost", size: "sm" });
className: "m01-master__row-btn", del.classList.add("m01-master__row-btn");
text: L("M01_RowRemove"),
attrs: { type: "button" },
});
del.addEventListener("click", () => commit(now.filter((_, j) => j !== i))); del.addEventListener("click", () => commit(now.filter((_, j) => j !== i)));
tr.append(el("td", { children: [del] })); tr.append(el("td", { children: [del] }));
for (const c of cols) { for (const c of cols) {
@@ -217,32 +214,30 @@ function openTable(
back(); back();
}); });
const notes = ((table.주 ?? []) as string[]).map((n) => const notes = ((table.주 ?? []) as string[]).map((n) =>
el("p", { className: "m01-master__muted", text: n }), el("p", { className: "ui-text ui-text--muted", text: n }),
); );
const usage = table.용도 as TableUsage | undefined; const usage = table.용도 as TableUsage | undefined;
const logicLinks = (usage?.로직키 ?? []).map((k, i) => { const logicLinks = (usage?.로직키 ?? []).map((k, i) => {
const btn = el("button", { const btn = createButton({ label: k, variant: "link" });
className: "m01-master__logic-key", btn.classList.add("m01-master__logic-key");
text: k, btn.title = L("M01_OpenLogic");
attrs: { type: "button", title: L("M01_OpenLogic") },
});
btn.addEventListener("click", () => openLogic(k)); btn.addEventListener("click", () => openLogic(k));
return i === 0 ? btn : el("span", { text: ", ", children: [btn] }); return i === 0 ? btn : el("span", { text: ", ", children: [btn] });
}); });
host.replaceChildren( host.replaceChildren(
el("div", { el("div", {
className: "m01-master__bar", className: "ui-toolbar m01-master__bar",
children: [ children: [
backButton, backButton,
el("strong", { text: String(table.이름 ?? key) }), el("strong", { text: String(table.이름 ?? key) }),
el("span", { el("span", {
className: "m01-master__muted", className: "ui-text ui-text--muted",
text: `${key} · ${table.기준 || "—"} · ${table.출처 || ""}`, text: `${key} · ${table.기준 || "—"} · ${table.출처 || ""}`,
}), }),
...(usage ...(usage
? [ ? [
el("span", { el("span", {
className: "m01-master__muted", className: "ui-text ui-text--muted",
text: `${L("M01_TableUsage")}: ${usage.공종} · ${usage.대상.join(", ")} · `, text: `${L("M01_TableUsage")}: ${usage.공종} · ${usage.대상.join(", ")} · `,
children: logicLinks, children: logicLinks,
}), }),
@@ -252,10 +247,10 @@ function openTable(
], ],
}), }),
el("div", { el("div", {
className: "m01-master__grid-wrap", className: "ui-table-wrap m01-master__grid-wrap",
children: [ children: [
el("table", { el("table", {
className: "m01-master__grid", className: "ui-table m01-master__grid",
children: [ children: [
el("thead", { el("thead", {
children: [ children: [
@@ -13,6 +13,7 @@ import { buildHeadFilters } from "../M01_MasterData/M01_MasterData_UI_Head";
import "../M01_MasterData/M01_MasterData_UI_Style.css"; import "../M01_MasterData/M01_MasterData_UI_Style.css";
import { fetchTree, type Layer, type TreeItem } from "./M02_MasterTemplete_Api_Fetch"; import { fetchTree, type Layer, type TreeItem } from "./M02_MasterTemplete_Api_Fetch";
import { folderOf, leafName, matchText } from "./M02_MasterTemplete_TreePanel_Model"; import { folderOf, leafName, matchText } from "./M02_MasterTemplete_TreePanel_Model";
import { createEmpty } from "@ui/ui_template_empty";
/** 목록 종류 — 하위 컨테이너 셋 + 항목 통째(복사 생성) */ /** 목록 종류 — 하위 컨테이너 셋 + 항목 통째(복사 생성) */
export type ListKind = "structure" | "area" | "labor" | "item"; export type ListKind = "structure" | "area" | "labor" | "item";
@@ -160,10 +161,10 @@ export function buildItemList(): ItemListHandle {
let page = 1; let page = 1;
const searchField = createInputField({ type: "search", placeholder: TEXT.search }); const searchField = createInputField({ type: "search", placeholder: TEXT.search });
const search = searchField.input; const search = searchField.input;
const grid = el("div", { className: "m01-master__grid-wrap" }); const grid = el("div", { className: "ui-table-wrap m01-master__grid-wrap" });
let pager = buildPager(0, SIZE, 1, () => {}); let pager = buildPager(0, SIZE, 1, () => {});
const note = (text: string): void => { const note = (text: string): void => {
grid.replaceChildren(el("p", { className: "m01-master__empty", text })); grid.replaceChildren(createEmpty({ text, small: true }));
}; };
const show = <R>( const show = <R>(
@@ -193,7 +194,7 @@ export function buildItemList(): ItemListHandle {
text, text,
attrs: { title: note ? `${text} ${note}` : text }, attrs: { title: note ? `${text} ${note}` : text },
children: note children: note
? [el("small", { className: "m01-master__muted", text: ` ${note}` })] ? [el("small", { className: "ui-text ui-text--muted", text: ` ${note}` })]
: [], : [],
}); });
}), }),
@@ -209,7 +210,7 @@ export function buildItemList(): ItemListHandle {
}); });
grid.replaceChildren( grid.replaceChildren(
el("table", { el("table", {
className: "m01-master__grid", className: "ui-table m01-master__grid",
children: [ children: [
el("thead", { el("thead", {
children: [el("tr", { children: cols.map((c) => el("th", { text: c.head })) })], children: [el("tr", { children: cols.map((c) => el("th", { text: c.head })) })],
@@ -1,13 +1,8 @@
/* 일위대가 화면 — 표는 칸 폭을 채움 · /* 일위대가 화면 — 표는 칸 폭을 채움 */
흐린 안내 글은 보조 글색(뮤트 글색은 밝은 테마에서 대비 3.1 로 모자람) */ .m01-combo table.m01-master__grid {
.m01-combo .m01-master__grid {
width: 100%; width: 100%;
} }
.m01-combo .m01-logic__muted {
color: var(--color-text-secondary);
}
/* 일위대가 상세(39-3) — 담은 로직 · 자재목록 표 · 금액 칸 두 줄(위 기준 작게 · 아래 반영) */ /* 일위대가 상세(39-3) — 담은 로직 · 자재목록 표 · 금액 칸 두 줄(위 기준 작게 · 아래 반영) */
.m02-labrow { .m02-labrow {
width: 100%; width: 100%;
@@ -75,6 +75,7 @@ import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css";
import "../M01_MasterData/M01_MasterData_UI_LogicLab_Style.css"; import "../M01_MasterData/M01_MasterData_UI_LogicLab_Style.css";
import "@ui/ui_template_general_layout.css"; import "@ui/ui_template_general_layout.css";
import "./M02_MasterTemplete_Labor.css"; import "./M02_MasterTemplete_Labor.css";
import { createText } from "@ui/ui_template_text";
export interface LaborDetailOptions { export interface LaborDetailOptions {
layer: Layer; layer: Layer;
@@ -160,10 +161,7 @@ export function mountLaborDetail(
}); });
function showSource(): void { function showSource(): void {
if (side?.current() !== "source") return; if (side?.current() !== "source") return;
if (!picked) if (!picked) return void sourceHost.replaceChildren(createText(tc("Labor_SourceHint")));
return void sourceHost.replaceChildren(
el("p", { className: "m01-logic__muted", text: tc("Labor_SourceHint") }),
);
source.show(logics.get(picked)?.출처 ?? ""); source.show(logics.get(picked)?.출처 ?? "");
if (sourceHost.firstChild !== source.root) sourceHost.replaceChildren(source.root); if (sourceHost.firstChild !== source.root) sourceHost.replaceChildren(source.root);
} }
@@ -28,6 +28,7 @@ import { precomboClasses } from "./M02_MasterTemplete_UI_Side_Precombo";
import { fetchStructureDef } from "./StructOptions/M02_SO_Api"; import { fetchStructureDef } from "./StructOptions/M02_SO_Api";
import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css"; // 창 겉틀 import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css"; // 창 겉틀
import "./M02_MasterTemplete_Labor.css"; import "./M02_MasterTemplete_Labor.css";
import { openModal } from "@ui/ui_template_modal";
export { withHopyo } from "./M02_MasterTemplete_Hopyo_Pick"; export { withHopyo } from "./M02_MasterTemplete_Hopyo_Pick";
@@ -92,7 +93,7 @@ export function openLogicPicker(
const sub = createSelectField({ options: opts([]), compact: true, label: tc("Sub") }); const sub = createSelectField({ options: opts([]), compact: true, label: tc("Sub") });
const detail = createSelectField({ options: opts([]), compact: true, label: tc("Detail") }); const detail = createSelectField({ options: opts([]), compact: true, label: tc("Detail") });
detail.select.disabled = true; detail.select.disabled = true;
const close = (): void => backdrop.remove(); let close = (): void => {};
/* 한 목록 — 구분(단가산출 · 일위조합) 칸 · 층 단추는 서고 줄(사전조합)의 층 */ /* 한 목록 — 구분(단가산출 · 일위조합) 칸 · 층 단추는 서고 줄(사전조합)의 층 */
const layerBar = el("div", { className: "ui-btn-group", attrs: { "data-pick-layers": "" } }); const layerBar = el("div", { className: "ui-btn-group", attrs: { "data-pick-layers": "" } });
@@ -266,24 +267,14 @@ export function openLogicPicker(
); );
}); });
const dialog = el("div", { // 모달 부품 — 제목줄 [닫기] · 바깥 누르기 · Esc · 위 고정
className: "m01-logic__pick m01-combo ui-popup", ({ close } = openModal({
attrs: { role: "dialog", "aria-label": tc("Pick_Title") }, title: lib?.title ?? tc("Pick_Title"),
children: [ closeLabel: tc("Pick_Close"),
el("div", { dialogClass: "m01-logic__pick m01-combo",
className: "m01-logic__section-head", backdropClass: "m01-logic__backdrop",
children: [ mount: (body) => body.append(finder.root),
el("h3", { text: lib?.title ?? tc("Pick_Title") }), }));
createButton({ label: tc("Pick_Close"), variant: "ghost", onClick: close }),
],
}),
finder.root,
],
});
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);
finder.search.focus(); finder.search.focus();
void lib?.more?.().then( void lib?.more?.().then(
@@ -255,11 +255,11 @@ export function mountLibraryList(
host.replaceChildren( host.replaceChildren(
buildHeadFilters([searchField.root, pager]), buildHeadFilters([searchField.root, pager]),
el("div", { el("div", {
className: "m01-master__grid-wrap", className: "ui-table-wrap m01-master__grid-wrap",
attrs: { "data-lib-list": opt.kind }, attrs: { "data-lib-list": opt.kind },
children: [ children: [
el("table", { el("table", {
className: "m01-master__grid", className: "ui-table m01-master__grid",
children: [ children: [
el("thead", { el("thead", {
children: [ children: [
@@ -9,6 +9,12 @@
* ========================================================================== */ * ========================================================================== */
import "@ui/ui_template_workflow_layout.css"; import "@ui/ui_template_workflow_layout.css";
// 부품 상자 겉(553-5) — 페이지 CSS 앞에 실어 페이지 곁값이 이김
import "@ui/ui_template_banner.css";
import "@ui/ui_template_list_row.css";
import "@ui/ui_template_table.css";
import "@ui/ui_template_text.css";
import "@ui/ui_template_toolbar.css";
import { ROUTES } from "@config/config_frontend"; import { ROUTES } from "@config/config_frontend";
import { el } from "@ui/ui_template_elements"; import { el } from "@ui/ui_template_elements";
import { t as L } from "@ui/ui_template_locale"; import { t as L } from "@ui/ui_template_locale";
+69
View File
@@ -0,0 +1,69 @@
"""553-5 M01 · M02 페이지 부품 옮김 지킴 시험 — 자체 변종이 부품으로 바뀐 자리 · 날 단추 · 손 모달 · 날 색 없음."""
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
OLD_ALIAS = re.compile(r"--color-(royal-amethyst|deep-iris|mist-violet|plum-velvet|lavender-wash)")
RAW_COLOR = re.compile(r"#[0-9a-fA-F]{3,8}\b|\brgba?\(")
def _src(folder: str, suffix: str) -> dict[str, str]:
return {p.name: p.read_text(encoding="utf-8") for p in (ROOT / folder).glob(f"*{suffix}")}
def test_m01_no_raw_buttons_or_hand_modals():
for name, ts in _src("M01_MasterData", ".ts").items():
# 날 단추 — 왼쪽 목록 줄(공용 ui-sidebar-row) · 목록 줄 부품 꼴 단추만 남음
for m in re.finditer(r'el\("button", \{\s*className: "([^"]*)"', ts):
assert "ui-sidebar-row" in m.group(1), f"{name}: 날 단추 {m.group(1)}"
assert "document.body.append(" not in ts, f"{name}: 손 모달 — openModal 부품을 씀"
def test_m01_parts_in_place():
ts = "".join(_src("M01_MasterData", ".ts").values())
for part in (
"openModal(",
"createBanner",
"createBadge(",
"createText(",
"createEmpty(",
"createIconButton(",
):
if part == "createBanner":
assert (
"ui-banner ui-banner--danger m01-master__notice" in ts
) # 알림 띠 = 띠 알림 부품 꼴
assert "ui-banner ui-banner--danger m01-logic__reasons" in ts
continue
assert part in ts, f"M01 이 부품 {part} 를 안 씀"
assert "ui-table m01-master__grid" in ts and "ui-table-wrap m01-master__grid-wrap" in ts
for gone in (
"m01-logic__muted",
"m01c__muted",
"m01-master__muted",
"m01-logic__tag",
"m01-logic__bad",
):
assert gone not in ts, f"옛 변종 {gone} 남음"
def test_m01_css_common_values_only():
for name, css in _src("M01_MasterData", ".css").items():
assert not OLD_ALIAS.search(css), f"{name}: 옛 보라 별칭"
body = re.sub(r"/\*.*?\*/", "", css, flags=re.S)
assert not RAW_COLOR.search(body), f"{name}: 날 색"
for gone in (
".m01-procure {",
".m01-logic__backdrop {",
".m01-logic__tag",
".m01lab__pills",
):
assert gone not in css, f"{name}: 죽은 · 옛 CSS {gone}"
def test_m02_borrowed_m01_classes_follow_parts():
ts = "".join(_src("M02_MasterTemplete", ".ts").values())
assert "m01-logic__muted" not in ts and "m01-master__muted" not in ts.replace("ui-text", "")
assert 'className: "m01-master__grid"' not in ts # 빌린 표도 표 부품 꼴
assert "ui-table m01-master__grid" in ts
@@ -42,17 +42,18 @@ def test_공용_모달_위_고정():
def test_따로_둔_모달도_위_고정(): def test_따로_둔_모달도_위_고정():
logic = _text("M01_MasterData/M01_MasterData_UI_Logic_Style.css") # M01 고르기 · 복사 · 조달 창 = 모달 부품(553-5) — 위 고정은 공용 .ui-modal 이 냄
assert "align-items: flex-start;" in _block(logic, ".m01-logic__backdrop")
assert "margin-top: calc((100dvh - var(--modal-max, 80vh)) / 2);" in _block(
logic, ".m01-logic__pick"
)
assert "--modal-max: 90vh;" in _block( assert "--modal-max: 90vh;" in _block(
_text("M01_MasterData/M01_MasterData_UI_LogicLab_Style.css"), ".m01copy__sheet" _text("M01_MasterData/M01_MasterData_UI_LogicLab_Style.css"), ".ui-modal.m01copy__sheet"
) )
m01 = _text("M01_MasterData/M01_MasterData_UI_Style.css") for rel in (
assert "align-items: flex-start;" in _block(m01, ".m01-procure") "M01_MasterData/M01_MasterData_UI_Pick.ts",
assert "margin-top: 10vh;" in _block(m01, ".m01-procure__box") "M01_MasterData/M01_MasterData_UI_Logic_Pick.ts",
"M01_MasterData/M01_MasterData_UI_Logic_PickTable.ts",
"M01_MasterData/M01_MasterData_UI_LogicLab_Copy.ts",
):
src = _text(rel)
assert "openModal(" in src and "document.body.append(" not in src, rel
b01 = _text("B01_Dashboard/B01_Dashboard_UI_Style.css") + _text( b01 = _text("B01_Dashboard/B01_Dashboard_UI_Style.css") + _text(
"B01_Dashboard/B01_Dashboard_UI_Style_Company.css" "B01_Dashboard/B01_Dashboard_UI_Style_Company.css"
) )