From 3866025475fe4d2dac16e2fa40b861a986becf28 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 21 Oct 2026 16:06:17 +0900 Subject: [PATCH] =?UTF-8?q?feat(504-3=20=EA=B3=84=EC=82=B0=20=EC=9E=90?= =?UTF-8?q?=EB=A6=AC=2045=EC=A4=84):=20=EC=B0=BD=20=3D=20=EB=AC=B6?= =?UTF-8?q?=EC=9D=8C=20=EB=A8=B8=EB=A6=AC=208=20+=20=EA=B3=84=EC=82=B0=204?= =?UTF-8?q?5=20=EC=A4=84=20=C2=B7=20=EC=9D=B4=EB=A6=84=20=3D=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=EB=B6=80=20label=20=C2=B7=20=EC=BC=A4=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8A=94=20=EC=A4=84=20G1=20=C2=B7=20D3=20=EB=A7=8C?= =?UTF-8?q?=20=C2=B7=20=EB=A8=B8=EB=A6=AC=20=EC=B2=B4=ED=81=AC=20=3D=20?= =?UTF-8?q?=EB=AC=B6=EC=9D=8C=20=EC=A0=84=EB=B6=80=20=C2=B7=20=ED=8C=90=20?= =?UTF-8?q?=EA=BC=B4=20=EA=B7=B8=EB=8C=80=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - B01_Dashboard_UI_CalcSites: 머리 줄 + 계산 줄(번호 · 이름) · 잠긴 줄 까닭 글 · 머리 · 줄 체크 서로 따라감 - 시험: test_309_2 두 파일 줄 수 · 켤 수 있는 줄 · 머리 따라 줄 꺼짐 Co-Authored-By: Claude Sonnet 5.5 --- B01_Dashboard/B01_Dashboard_UI_CalcSites.ts | 85 ++++++++++++++----- B01_Dashboard/B01_Dashboard_UI_Style.css | 10 +++ resources/tester/test_309_2_calc_sites.py | 1 + .../tester/test_309_2_calc_sites_browser.py | 17 ++-- 4 files changed, 88 insertions(+), 25 deletions(-) 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"]