feat(504-3 계산 자리 45줄): 창 = 묶음 머리 8 + 계산 45 줄 · 이름 = 등록부 label · 켤 수 있는 줄 G1 · D3 만 · 머리 체크 = 묶음 전부 · 판 꼴 그대로
- B01_Dashboard_UI_CalcSites: 머리 줄 + 계산 줄(번호 · 이름) · 잠긴 줄 까닭 글 · 머리 · 줄 체크 서로 따라감 - 시험: test_309_2 두 파일 줄 수 · 켤 수 있는 줄 · 머리 따라 줄 꺼짐 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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<typeof L>[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<string, CalcSiteValue>,
|
||||
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<string, CalcSiteValue>,
|
||||
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<number>();
|
||||
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<void> {
|
||||
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),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user