diff --git a/B01_Dashboard/B01_Dashboard_UI_CalcSites.ts b/B01_Dashboard/B01_Dashboard_UI_CalcSites.ts index 7d54714c7..e73d2d505 100644 --- a/B01_Dashboard/B01_Dashboard_UI_CalcSites.ts +++ b/B01_Dashboard/B01_Dashboard_UI_CalcSites.ts @@ -1,8 +1,9 @@ /* ============================================================================= * B01_Dashboard_UI_CalcSites.ts - * 「계산 자리」 창(PLAN 504-2 · 요구 309-2) — 시스템 관리자만(카드 = `settingLinks` · 쓰기 = 서버 - * `require_system_admin`). 묶음 8 줄 · 칸 둘(「조작 중 브라우저 셈」 · 「[저장] 서버 정본」 늘 켬 · 잠금). - * 바꿀 수 있는 줄 = 서버 판 `groups[].switchable` · 나머지는 잠긴 체크 + 까닭 글. + * 「계산 자리」 창(PLAN 504-2 · 504-3 · 요구 309-2 · 312) — 시스템 관리자만(카드 = `settingLinks` · 쓰기 = 서버 + * `require_system_admin`). 묶음 8 머리 + 그 아래 계산 45 줄(번호 · 이름 = 등록부 `label`) · 칸 둘(「조작 중 + * 브라우저 셈」 · 「[저장] 서버 정본」 늘 켬 · 잠금). 묶음 머리 체크 = 그 묶음 전부. + * 바꿀 수 있는 줄 = 서버 판 `groups[].switchable`(판 열쇠 = 묶음) · 나머지는 잠긴 체크 + 까닭 글. * 고침은 창 안 사본에만 · [저장] 때만 바뀐 줄을 PUT(선례 `app_settings.ts`). * ========================================================================== */ @@ -17,6 +18,7 @@ import { type CalcSiteValue, type CalcSitesBoard, } from "../A00_Common/design/design_calc_sites"; +import { CALC_REGISTRY } from "../A00_Common/design/logic/design_calc_registry"; import { L } from "./B01_Dashboard_UI_Common"; type LocaleKey = Parameters[0]; @@ -29,24 +31,70 @@ function check(checked: boolean, locked: boolean): HTMLInputElement { return input; } -function groupRow( +const LABELS = new Map(CALC_REGISTRY.map((entry) => [entry.no, entry.label])); + +/** 묶음 머리 줄 + 계산 한 줄씩. 바꿀 수 있는 묶음은 머리 · 줄 체크가 같은 값(서로 따라감). */ +function groupRows( group: CalcSiteGroup, draft: Record, changed: () => void, -): HTMLElement[] { - const name = el("span", { - children: [el("strong", { text: K(group.key) }), ` (${group.nos.join(" ")})`], +): HTMLElement[][] { + const server = (): HTMLInputElement => check(group.fixed !== "browser", true); + const why = K(`Why_${group.why ?? group.key}`); + const head = el("span", { + children: [el("strong", { text: K(group.key) }), ` (${group.nos.length})`], }); - const browser = group.switchable - ? check(draft[group.key] !== "server", false) - : check(group.fixed !== "server", true); - browser.dataset.calcSite = group.key; - browser.addEventListener("change", () => { - draft[group.key] = browser.checked ? "both" : "server"; - changed(); + if (!group.switchable) { + const browser = (): HTMLInputElement => check(group.fixed !== "server", true); + return [ + [head, browser(), server(), el("span")], + ...group.nos.map((no) => [calcName(no), browser(), server(), el("span", { text: why })]), + ]; + } + const boxes = [group.key, ...group.nos].map((id, index) => { + const box = check(draft[group.key] !== "server", false); + if (index === 0) box.dataset.calcSite = group.key; + else box.dataset.calcNo = id; + return box; }); - const server = check(group.fixed !== "browser", true); - return [name, browser, server, el("span", { text: K(`Why_${group.why ?? group.key}`) })]; + for (const box of boxes) { + box.addEventListener("change", () => { + draft[group.key] = box.checked ? "both" : "server"; + for (const other of boxes) other.checked = box.checked; + changed(); + }); + } + return [ + [head, boxes[0], server(), el("span")], + ...group.nos.map((no, index) => [ + calcName(no), + boxes[index + 1], + server(), + el("span", { text: why }), + ]), + ]; +} + +const calcName = (no: string): HTMLElement => + el("span", { className: "b01-dashboard__calc-name", text: `${no} · ${LABELS.get(no) ?? ""}` }); + +function grid( + groups: CalcSiteGroup[], + draft: Record, + changed: () => void, +): HTMLElement { + const rows = groups.flatMap((group) => groupRows(group, draft, changed)); + const wrap = table([K("Col_Group"), K("Col_Browser"), K("Col_Server"), K("Col_Why")], rows, 14); + const heads = new Set(); + let at = 0; + for (const group of groups) { + heads.add(at); + at += 1 + group.nos.length; + } + wrap.querySelectorAll("tbody tr").forEach((row, index) => { + if (heads.has(index)) row.classList.add("b01-dashboard__calc-group"); + }); + return wrap; } /** 시스템 설정 「계산 자리」 카드 — 판을 받아 창을 엶. */ @@ -100,10 +148,7 @@ export async function openCalcSitesDialog(): Promise { body.append( count, el("p", { className: "b01-dashboard__calc-note", text: K("NextOpen") }), - table( - [K("Col_Group"), K("Col_Browser"), K("Col_Server"), K("Col_Why")], - board.groups.map((group) => groupRow(group, draft, recount)), - ), + grid(board.groups, draft, recount), ); }, }); diff --git a/B01_Dashboard/B01_Dashboard_UI_Style.css b/B01_Dashboard/B01_Dashboard_UI_Style.css index 2e658221f..c25e72392 100644 --- a/B01_Dashboard/B01_Dashboard_UI_Style.css +++ b/B01_Dashboard/B01_Dashboard_UI_Style.css @@ -796,6 +796,16 @@ .b01-dashboard__calc-sites .b01-dashboard__table td:nth-child(3) { text-align: center; } +.b01-dashboard__calc-sites .b01-dashboard__calc-group td { + background: var(--color-surface); + font-weight: var(--font-weight-medium); +} +.b01-dashboard__calc-sites .b01-dashboard__calc-name { + padding-left: var(--spacing-16); +} +.b01-dashboard__calc-sites .b01-dashboard__calc-group .b01-dashboard__calc-name { + padding-left: 0; +} .b01-dashboard__calc-count, .b01-dashboard__calc-note { margin: 0 0 0.5rem; diff --git a/resources/tester/test_309_2_calc_sites.py b/resources/tester/test_309_2_calc_sites.py index 4ec5fa4ec..b835aaa8e 100644 --- a/resources/tester/test_309_2_calc_sites.py +++ b/resources/tester/test_309_2_calc_sites.py @@ -258,3 +258,4 @@ def test_카드_시스템_관리자만_글_다_있음(): ui = _text("B01_Dashboard/B01_Dashboard_UI_CalcSites.ts") assert 'check(group.fixed !== "browser", true)' in ui # 서버 정본 칸 = 늘 잠금 assert "saveCalcSites(changes)" in ui and "dirty()" in ui # 바뀐 줄만 PUT + assert "CALC_REGISTRY" in ui and "group.nos.map" in ui # 묶음 머리 아래 계산 줄(등록부 label) diff --git a/resources/tester/test_309_2_calc_sites_browser.py b/resources/tester/test_309_2_calc_sites_browser.py index 4bc81026d..1f880bac4 100644 --- a/resources/tester/test_309_2_calc_sites_browser.py +++ b/resources/tester/test_309_2_calc_sites_browser.py @@ -121,12 +121,18 @@ def test_시스템_관리자_카드_창_G1_서버_길(browser): dialog = page.wait_for_selector(".b01-dashboard__calc-sites", timeout=30000) rows = page.eval_on_selector_all( ".b01-dashboard__calc-sites tbody tr", - "(trs) => trs.map((tr) => [...tr.querySelectorAll('input')]" - ".map((i) => [i.checked, i.disabled]))", + "(trs) => trs.map((tr) => ({ head: tr.classList.contains('b01-dashboard__calc-group')," + " name: tr.cells[0].textContent.trim()," + " boxes: [...tr.querySelectorAll('input')].map((i) => [i.checked, i.disabled]) }))", ) - assert len(rows) == 8 - assert rows[0] == [[True, False], [True, True]] # G1 바꿀 수 있음 · 서버 정본 잠금 - assert all(row[0][1] and row[1][1] for row in rows[2:]) # 나머지 잠금 + heads = [row for row in rows if row["head"]] + calcs = [row for row in rows if not row["head"]] + assert len(heads) == 8 and len(calcs) == 45 + assert heads[0]["boxes"] == [[True, False], [True, True]] # G1 묶음 머리 = 바꿀 수 있음 + editable = [row["name"] for row in calcs if not row["boxes"][0][1]] + assert [name.split(" · ")[0] for name in editable] == ["G1", "D3"] # 줄도 둘만 켬 + assert all(row["name"].split(" · ")[1] for row in calcs) # 이름 = 등록부 label + assert all(row["boxes"][1][1] for row in calcs) # 서버 정본 칸은 늘 잠금 assert "지금 서버 0 줄" in dialog.inner_text() SHOTS.mkdir(parents=True, exist_ok=True) dialog.screenshot(path=str(SHOTS / "light.png")) @@ -136,6 +142,7 @@ def test_시스템_관리자_카드_창_G1_서버_길(browser): page.click("input[data-calc-site='ground_sample']") assert "지금 서버 1 줄" in dialog.inner_text() + assert not page.is_checked("input[data-calc-no='G1']") # 머리 따라 줄도 꺼짐 page.locator(".b01-dashboard__calc-sites .ui-modal__actions button").click() page.wait_for_selector(".b01-dashboard__calc-sites", state="detached", timeout=30000) got = ctx.request.get(f"{BASE}/api/dashboard/calc-sites").json()["sites"]