From 6af23d42e1e724bd22665e97081c031d68b85784 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 2 Nov 2026 22:10:18 +0900 Subject: [PATCH] =?UTF-8?q?feat(748-1):=20=EB=8B=A8=EA=B0=80=EC=82=B0?= =?UTF-8?q?=EC=B6=9C=20=EC=83=81=EC=84=B8=20=EC=A4=84=20=EB=AA=A8=EB=8B=AC?= =?UTF-8?q?=20=E2=80=94=20=EC=A0=9C=EB=AA=A9=20=EC=98=A4=EB=A5=B8=EC=AA=BD?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0=20=ED=82=A4=20=C2=B7=20[=EC=A0=81?= =?UTF-8?q?=EC=9A=A9]=20[=EC=82=AD=EC=A0=9C]=20[=EB=8B=AB=EA=B8=B0]=20?= =?UTF-8?q?=EC=B0=A8=EB=A1=80=20=C2=B7=20=EC=9A=94=EC=86=8C=20=EB=8B=A8?= =?UTF-8?q?=EA=B0=80=20=EC=A0=95=EB=B3=B4=20=EC=B9=B8=20=EA=B1=B7=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 제목 오른쪽 (연결 키 · 예 보통인부 (LB000002)) — 요소 · 고른 품목 · 부른 로직 키 - [적용] · [삭제](옛 빼기 이름 바꿈)를 모달 머리 [닫기] 왼쪽으로 · [기초 빼기] · 별도계상은 고침 칸 아래 그대로 - 고침 칸이 있는 모달은 요소 단가 정보 칸(이름 · 키 · 값 · 출처 · 마스터 보기) 걷음 — 자재품목 물가 넷 · 후보 목록 · 한 줄 풀이는 그대로 - M01 단가산출 기초 상세 모달도 같은 꼴([적용] [닫기]) - 낡은 시험 고침: test_722 모달 edit 인자(close, head) Co-Authored-By: Claude Sonnet 5.5 --- .../M01_MasterData_UI_LogicLab_Cover.ts | 12 +++++++- .../M01_MasterData_UI_LogicLab_Ho.ts | 12 ++++---- .../M01_MasterData_UI_LogicLab_Modal.ts | 28 +++++++++++++------ .../M01_MasterData_UI_LogicLab_Style.css | 7 +++++ .../M02_MasterTemplete_Sangeun_Detail.ts | 10 +++++-- resources/tester/test_722_ho_detail.py | 2 +- ui_template/ui_template_locale_m2.ts | 2 +- 7 files changed, 53 insertions(+), 20 deletions(-) diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Cover.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Cover.ts index b1841a53f..deb5fec0e 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Cover.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Cover.ts @@ -93,6 +93,8 @@ export interface CoverFormOptions { /** 「m02」 = 덮개 보관 안내 */ scope?: "m01" | "m02"; onApply: (cover: LineCover | null) => void; + /** 모달 머리 단추 자리(닫기 왼쪽 · 748-1) — 주면 [적용] 이 거기로 */ + head?: HTMLElement; } const num = (raw: string): number | null => { @@ -140,10 +142,17 @@ export function coverForm(o: CoverFormOptions): HTMLElement { error.hidden = !bad; if (!bad) o.onApply(Object.keys(next).length ? next : null); }; + const applyButton = createButton({ + label: tc("Apply"), + variant: "filled", + size: 32, + onClick: apply, + }); + o.head?.append(applyButton); const actions = el("div", { className: "m01lab-cover__actions", children: [ - createButton({ label: tc("Apply"), variant: "filled", size: 32, onClick: apply }), + ...(o.head ? [] : [applyButton]), ...(o.cover ? [ createButton({ @@ -156,6 +165,7 @@ export function coverForm(o: CoverFormOptions): HTMLElement { : []), ], }); + actions.hidden = !actions.childElementCount; return el("section", { className: "m01lab-cover", attrs: { "data-cover-form": "" }, diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Ho.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Ho.ts index 988efffe4..1af682d93 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Ho.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Ho.ts @@ -122,8 +122,8 @@ export interface HoEdit { covers: Record; set: (idx: number, cover: LineCover | null) => void; scope?: "m01" | "m02"; - /** (M02) 모달 고침 칸 아래 덧 동작(줄 빼기 · 별도계상 등) */ - more?: (idx: number, close: () => void) => HTMLElement[]; + /** (M02) 모달 덧 동작 — `head` = 닫기 왼쪽 단추(삭제) · `body` = 고침 칸 아래(기초 빼기 · 별도계상 등) */ + more?: (idx: number, close: () => void) => { head: HTMLElement[]; body: HTMLElement[] }; } /** 고친 줄 꼬리표 — M01 = 시험 고침 · M02 = 변경 항목 · 고침 없음 = "" */ @@ -279,8 +279,9 @@ function modalOf(e: Entry, ctx: HoContext, extra: { cond?: PickCond; item?: HoLi // 덧줄(%) = M02 덮개만 율 고침(M01 시험 값은 호표 줄만) ...(edit && (e.extra ? edit.scope === "m02" : item) ? { - edit: (close: () => void) => { + edit: (close: () => void, head: HTMLElement) => { const form = coverForm({ + head, qty: line ? pctView(line).qty : null, prices: e.extra ? {} @@ -295,8 +296,9 @@ function modalOf(e: Entry, ctx: HoContext, extra: { cond?: PickCond; item?: HoLi scope: edit.scope, onApply: (c) => (close(), edit.set(e.idx, c)), }); - const more = edit.more?.(e.idx, close) ?? []; - return more.length ? el("div", { children: [form, ...more] }) : form; + const more = edit.more?.(e.idx, close); + head.append(...(more?.head ?? [])); + return more?.body.length ? el("div", { children: [form, ...more.body] }) : form; }, } : {}), diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts index d6e8dede0..0e1f69ead 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts @@ -92,8 +92,8 @@ export interface ModalTarget { usedSlot?: string; /** 있으면 후보 줄을 눌러 고름(시험 입력 · 저장 안 함) — 고르면 모달 닫힘 */ onPick?: (item: { ref: string; label: string }) => void; - /** 722-2 그 자리 고침 칸(맨 위) — 받는 것 = 모달 닫기 */ - edit?: (close: () => void) => HTMLElement; + /** 722-2 그 자리 고침 칸(맨 위) — 받는 것 = 모달 닫기 · 머리 단추 자리(닫기 왼쪽 · 748-1) */ + edit?: (close: () => void, head: HTMLElement) => HTMLElement; } /** 수량 칸 표 알약 하나가 여는 자료 — 서버 조각 `참조`(키 · 행 · 열)가 곧 찾은 자리 */ @@ -287,7 +287,10 @@ function priceView(ref: string, brief: ElementBrief, usedSlot?: string): HTMLEle } /** 모달 겉틀(배경·닫기·포커스) — 몸은 `mount` 이 채움(닫기 함수도 받음) */ -function openDialog(title: string, mount: (body: HTMLElement, close: () => void) => void): void { +function openDialog( + title: string, + mount: (body: HTMLElement, close: () => void, head: HTMLElement) => void, +): void { openModal({ title, closeLabel: tl("Modal_Close"), @@ -428,7 +431,7 @@ function pickedOf(cond: PickCond, values: Record, fallback: stri /** 줄을 눌렀을 때 — 그 줄의 단가(재료·인력·기계 또는 다른 로직)를 바로 보임(알약 없이) · * 재료 고르기 줄(`cond`)은 잡힌 품목 단가 + 후보 목록 */ export function openMaterialModal(target: ModalTarget): void { - openDialog(target.title, (body, close) => { + openDialog(target.title, (body, close, head) => { const cond = target.cond; const element = target.element ?? ""; const base = element.split(".")[0]; @@ -445,6 +448,15 @@ export function openMaterialModal(target: ModalTarget): void { ? target.unitRef || pickedOf(cond, target.values, known?.ref ?? "") : element; const logicKey = /^로직\((\w+)/.exec(element)?.[1] ?? ""; + // 748-1 연결 키 = 제목 오른쪽 (괄호) · 고침 칸이 있으면 요소 단가 정보 칸은 없음(키 · 값은 제목 · 단가 칸에) + const key = logicKey || (cond ? priceRef : element.includes("{") ? target.unitRef : base); + if (key) + body + .closest(".ui-modal") + ?.querySelector("h3") + ?.append(el("span", { className: "m01lab__modalkey", text: ` (${key})` })); + const info = (ref: string, brief: ElementBrief, slot?: string): HTMLElement => + target.edit && !ref.startsWith("MT") ? el("span") : priceView(ref, brief, slot); const sum = el("p", { className: "m01lab__sum" }); const setSum = (price: ElementBrief | null | undefined): void => { const unit = price ? `${tl("Modal_UnitPrice")} ${formatPrice(price.값)} × ` : ""; @@ -457,9 +469,7 @@ export function openMaterialModal(target: ModalTarget): void { /** `prices` 에 있으면 그대로 · 없으면 키로 찾아 옴 — 못 찾으면 「관리자 값 미확정」 */ const lookup = (ref: string, label: string): void => { const show = (brief: ElementBrief | null): void => { - rate.replaceChildren( - brief ? priceView(ref, brief) : muted(`${label} — ${tl("Pill_NoValue")}`), - ); + rate.replaceChildren(brief ? info(ref, brief) : muted(`${label} — ${tl("Pill_NoValue")}`)); setSum(brief); }; rate.append(muted("…")); @@ -471,7 +481,7 @@ export function openMaterialModal(target: ModalTarget): void { .catch(() => show(null)); }; if (known && priceRef) { - rate.append(priceView(priceRef, known, target.usedSlot)); + rate.append(info(priceRef, known, target.usedSlot)); } else if (logicKey) { rate.append( el("p", { @@ -505,7 +515,7 @@ export function openMaterialModal(target: ModalTarget): void { lookup(element, element); // 더한 줄(M02) 등 `prices` 에 없는 요소 — 키로 찾아 옴(725-4) } body.replaceChildren( - ...(target.edit ? [target.edit(close)] : []), + ...(target.edit ? [target.edit(close, head)] : []), rate, ...(cond || target.kind === "재료" ? [candidateView(target, priceRef, close)] : []), muted(tl("Modal_Sum")), diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css b/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css index ed1d313d3..43d0a8ac4 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css @@ -121,6 +121,13 @@ overflow: auto; } +/* 748-1 모달 제목 오른쪽 연결 키 — 제목보다 가볍게 */ +.m01lab__modalkey { + font-size: var(--text-body); + font-weight: var(--font-weight-regular, 400); + color: var(--color-text-secondary); +} + .m01lab__modal-body { display: flex; flex-direction: column; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts index b0e55e5b9..187e032ef 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts @@ -399,11 +399,11 @@ export function mountSangeunDetail( }, more: (idx, close) => { const r = at(idx); - if (!r) return []; + if (!r) return { head: [], body: [] }; const drop = createButton({ label: L("M02_SangeunDrop"), variant: "danger-ghost", - size: 28, + size: 32, onClick: () => (close(), dropLine(doc, r), touch()), }); drop.dataset.skDrop = ""; @@ -432,7 +432,11 @@ export function mountSangeunDetail( }).root, ] : []; - return [el("div", { className: "m02-sk__tags", children: [...apart, drop, ...whole] })]; + const rest = [...apart, ...whole]; + return { + head: [drop], + body: rest.length ? [el("div", { className: "m02-sk__tags", children: rest })] : [], + }; }, }; } diff --git a/resources/tester/test_722_ho_detail.py b/resources/tester/test_722_ho_detail.py index b13834ea6..34d4eed23 100644 --- a/resources/tester/test_722_ho_detail.py +++ b/resources/tester/test_722_ho_detail.py @@ -69,7 +69,7 @@ def test_시험_고침은_캐시_밖(): cover = _text(COVER) assert "drafts" not in cover and "touch" not in cover and "/save" not in cover modal = _text(M01 / "M01_MasterData_UI_LogicLab_Modal.ts") - assert "...(target.edit ? [target.edit(close)] : [])" in modal + assert "...(target.edit ? [target.edit(close, head)] : [])" in modal @pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") diff --git a/ui_template/ui_template_locale_m2.ts b/ui_template/ui_template_locale_m2.ts index f892dae58..1c91f2ef9 100644 --- a/ui_template/ui_template_locale_m2.ts +++ b/ui_template/ui_template_locale_m2.ts @@ -180,7 +180,7 @@ export const ui_locales_m2 = { M02_SangeunSwapWeight: ["단위중량", "Unit weight"], M02_SangeunSwapPick: ["고르세요", "Choose"], M02_SangeunAddLine: ["항목 추가", "Add item"], - M02_SangeunDrop: ["빼기", "Remove"], + M02_SangeunDrop: ["삭제", "Delete"], M02_SangeunDropBase: ["기초 빼기", "Remove base"], M02_SangeunBases: ["더한 기초", "Added bases"], M02_SangeunBaseQty: ["수량(기준단위 몇 개)", "Quantity (base units)"],