diff --git a/Z01_MasterData/Z01_MasterData_UI_Cells.ts b/Z01_MasterData/Z01_MasterData_UI_Cells.ts index 77a7d5db..3fd34c1c 100644 --- a/Z01_MasterData/Z01_MasterData_UI_Cells.ts +++ b/Z01_MasterData/Z01_MasterData_UI_Cells.ts @@ -85,14 +85,29 @@ export function outdatedBanner(sources?: TableSource[]): string[] { }); } +/** 알림은 한 줄로도 목록으로도 옴 — 받은 대로 줄 목록으로. */ +export function noticeLines(notice?: string | string[]): string[] { + if (!notice) return []; + return Array.isArray(notice) ? notice.filter(Boolean) : [notice]; +} + +/** 서버 글자의 `**굵게**` 를 조각으로 — 별표가 화면에 글자로 남으면 안 됨. */ +export function boldSegments(text: string): { text: string; bold: boolean }[] { + return text + .split(/\*\*(.+?)\*\*/g) + .map((part, index) => ({ text: part, bold: index % 2 === 1 })) + .filter((part) => part.text !== ""); +} + /** 기초단가 표 머리 — 열 하나에 한 문장이라 표에 한 번만 옴(2026-09-15 브레인 ④). */ export interface TableMeta { /** 한 표가 모은 원본마다 한 줄 */ source?: TableSource[]; - /** 서버가 주는 알림 한 줄(요율 — 법이 정한 값) — 화면이 사전을 가지면 두 벌이 됨 */ - notice?: string; - /** 고친 값이 어디에 닿는지 — 새로 만드는 프로젝트부터 쓰임(서버가 줌) */ - scope_notice?: string; + /** + * 표 머리 알림 — 서버가 한 벌로 줌(첫 줄 「새로 만드는 프로젝트부터」 · 요율은 법정값 경고까지). + * 화면이 사전을 가지면 두 벌이 됨. + */ + notice?: string | string[]; editable?: string[]; /** 열key → 왜 못 고치나 */ locked?: Record; diff --git a/Z01_MasterData/Z01_MasterData_UI_Rows.ts b/Z01_MasterData/Z01_MasterData_UI_Rows.ts index 0763909c..d09f1747 100644 --- a/Z01_MasterData/Z01_MasterData_UI_Rows.ts +++ b/Z01_MasterData/Z01_MasterData_UI_Rows.ts @@ -11,10 +11,12 @@ import { createButton, createInputField, el, showToast } from "@ui/ui_template_e import { t as L } from "@ui/ui_template_locale"; import type { MasterRows, RowsQuery } from "./Z01_MasterData_Api_Fetch"; import { + boldSegments, cellInfo, clampPage, columnTitle, latestOnly, + noticeLines, outdatedBanner, pageCount, parseCellInput, @@ -97,8 +99,7 @@ export function buildRowsView(): RowsView { className: "z01-master__check", children: [hiddenToggle, L("Z01_MasterData_ShowHidden")], }); - const notice = el("p", { className: "z01-master__notice" }); - const scope = el("p", { className: "z01-master__scope" }); + const notice = el("div", { className: "z01-master__notice" }); const banner = el("div", { className: "z01-master__outdated" }); const detail = el("div", { className: "z01-master__detail" }); const grid = el("div", { className: "z01-master__grid-wrap" }); @@ -127,9 +128,8 @@ export function buildRowsView(): RowsView { className: "z01-master__panel", children: [ el("div", { children: [title, titleKey] }), - scope, - banner, notice, + banner, toolbar, detail, grid, @@ -141,7 +141,6 @@ export function buildRowsView(): RowsView { detail.hidden = true; notice.hidden = true; banner.hidden = true; - scope.hidden = true; function goTo(page: number): void { const pages = pageCount(last?.total ?? 0, PAGE_SIZE); @@ -191,15 +190,22 @@ export function buildRowsView(): RowsView { function draw(): void { if (!last) return; const meta = last; - // 표 머리 세 줄 — 모두 서버가 주는 글자 그대로(화면이 사전을 가지면 두 벌이 됨). - // ① 고친 값이 닿는 자리 ② 뒤처진 원본(서버 outdated 만 · 날짜 비교 안 함) ③ 그 밖 알림 - scope.textContent = meta.scope_notice ?? ""; - scope.hidden = !meta.scope_notice; + // 표 머리 — 모두 서버 글자 그대로(화면이 사전을 가지면 두 벌이 됨). + // ① 알림(첫 줄 「새로 만드는 프로젝트부터」 · 요율은 법정값 경고까지) ② 뒤처진 원본(서버 outdated 만) + const notices = noticeLines(meta.notice); + notice.replaceChildren( + ...notices.map((text) => + el("p", { + children: boldSegments(text).map((part) => + part.bold ? el("strong", { text: part.text }) : el("span", { text: part.text }), + ), + }), + ), + ); + notice.hidden = notices.length === 0; const outdated = outdatedBanner(meta.source); banner.replaceChildren(...outdated.map((text) => el("p", { text: `⚠ ${text}` }))); banner.hidden = outdated.length === 0; - notice.textContent = meta.notice ?? ""; - notice.hidden = !meta.notice; const columns = shownColumns(meta.columns, showHidden); const headRow = el("tr", { children: columns.map((column) => headCell(meta, column)) }); const body = el("tbody"); @@ -366,7 +372,6 @@ export function buildRowsView(): RowsView { titleKey.textContent = picked.key; notice.hidden = true; banner.hidden = true; - scope.hidden = true; search.root.hidden = picked.searchable === false; toolbar.hidden = false; pager.hidden = false; diff --git a/Z01_MasterData/Z01_MasterData_UI_Style.css b/Z01_MasterData/Z01_MasterData_UI_Style.css index c4894ce4..d25b3950 100644 --- a/Z01_MasterData/Z01_MasterData_UI_Style.css +++ b/Z01_MasterData/Z01_MasterData_UI_Style.css @@ -125,17 +125,6 @@ font-size: var(--text-body); } -/* 고친 값이 닿는 자리 — 흐리면 「저장이 안 됐다」로 읽힘(2026-09-16 브레인) */ -.z01-master__scope { - margin: 0; - padding: var(--spacing-8) var(--spacing-12); - border-left: 4px solid var(--color-accent); - background: var(--color-mist-violet); - color: var(--color-accent); - font-size: var(--text-body-sm); - font-weight: var(--font-weight-bold); -} - /* 뒤처진 자료 띠 — 받는 자리까지 적힘 · 또렷이 · 원본마다 한 줄 */ .z01-master__outdated p { margin: 0; @@ -151,16 +140,23 @@ font-weight: var(--font-weight-bold); } -/* 요율 알림 — 막지 않고 알리기만 */ +/* 표 머리 알림 — 고친 값이 닿는 자리·법정값 경고. 흐리면 「저장이 안 됐다」로 읽힘(2026-09-16 브레인) */ .z01-master__notice { + display: flex; + flex-direction: column; + gap: var(--spacing-4); margin: 0; padding: var(--spacing-8) var(--spacing-12); - border-left: 4px solid var(--color-warning); - background: var(--color-paper); - color: var(--color-text-body); + border-left: 4px solid var(--color-accent); + background: var(--color-mist-violet); + color: var(--color-accent); font-size: var(--text-body-sm); } +.z01-master__notice p { + margin: 0; +} + .z01-master__toolbar { display: flex; flex-wrap: wrap; diff --git a/resources/tester/test_z01_master_cells.py b/resources/tester/test_z01_master_cells.py index 823fc746..13983ba2 100644 --- a/resources/tester/test_z01_master_cells.py +++ b/resources/tester/test_z01_master_cells.py @@ -25,9 +25,20 @@ _RUNNER = """ import { writeFileSync } from "node:fs"; import { cellInfo, cellText, clampPage, columnTitle, latestOnly, pageCount, parseCellInput, rowCells, - outdatedBanner, shownColumns, valueWithUnit, + boldSegments, noticeLines, outdatedBanner, shownColumns, valueWithUnit, } from "./Z01_MasterData_UI_Cells.js"; +// 표 수준 알림 — 서버가 한 벌로 목록을 줌(첫 줄 「새로 만드는 프로젝트부터」) · 글자에 **굵게** 가 섞임 +const notices = [ + noticeLines(["여기서 고친 값은 **새로 만드는 프로젝트부터** 쓰임", "법이 정한 값 — 고치면…"]), + noticeLines("한 줄짜리"), + noticeLines(undefined), +]; +const bolds = [ + boldSegments("여기서 고친 값은 **새로 만드는 프로젝트부터** 쓰임"), + boldSegments("굵게 없음"), +]; + // 뒤처짐 띠 — 판정은 서버 outdated 만(화면이 날짜를 안 견줌) · 받는 자리를 띠에 적음(2026-09-15 브레인) // ⚠ 한 표가 원본 여럿을 모은 것이라 출처는 목록으로 옴(노임 = 건설업 + 제조업). const laborSources = [ @@ -155,6 +166,8 @@ writeFileSync(process.argv[2], JSON.stringify({ valueWithUnit("5억 미만", ""), ], banners, + notices, + bolds, })); """ @@ -248,3 +261,18 @@ def test_칸_글자와_숨김_열(tmp_path: Path) -> None: [], [], ] + # 알림은 목록도 한 줄도 받음 · 없으면 빈 목록 + assert got["notices"] == [ + ["여기서 고친 값은 **새로 만드는 프로젝트부터** 쓰임", "법이 정한 값 — 고치면…"], + ["한 줄짜리"], + [], + ] + # **…** 는 굵게 조각으로 갈라 보임 — 별표가 글자로 남으면 안 됨 + assert got["bolds"] == [ + [ + {"text": "여기서 고친 값은 ", "bold": False}, + {"text": "새로 만드는 프로젝트부터", "bold": True}, + {"text": " 쓰임", "bold": False}, + ], + [{"text": "굵게 없음", "bold": False}], + ]