Files
Aislo/B01_Dashboard/B01_Dashboard_UI_CalcSites.ts
T
eomsangdonandClaude Opus 5.5 282b481f81 feat(504-2 계산 자리 스위치): 시스템 관리자 「계산 자리」 카드 · 창 · 판 · G1 · D3 연결
- 판 = resources/master_template/calc_sites.json(기본값 = 등록부 자리 그대로 · 전부 both)
- common_util_calc_sites — 묶음 8 줄(등록부 45 #) · 읽기 · 검사(잠금 · 모르는 줄 · 값 = 400) · 통째 쓰기
- GET /api/dashboard/calc-sites(로그인) · PUT /api/dashboard/admin/calc-sites(시스템 관리자) · 바뀐 줄마다 CALC_SITE_CHANGE 시스템 로그
- 대시보드 시스템 설정 「계산 자리」 카드(시스템 관리자만 · ☰ 엔 없음) · 체크 창(칸 둘 · 잠긴 줄 까닭 · 지금 서버 N 줄 · 다음 열기부터)
- design_calc_sites — B05 · B06 열 때 한 번 받음 · G1 server = ground 단계 안에서만 서버 표본(cross 격자 그대로) · D3 server = 워커 안 띄움
- 시험 test_309_2_calc_sites 새 · test_b01_settings_cards · test_225_drawer_shortcuts 덧

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 12:23:11 +09:00

111 lines
4.1 KiB
TypeScript

/* =============================================================================
* B01_Dashboard_UI_CalcSites.ts
* 「계산 자리」 창(PLAN 504-2 · 요구 309-2) — 시스템 관리자만(카드 = `settingLinks` · 쓰기 = 서버
* `require_system_admin`). 묶음 8 줄 · 칸 둘(「조작 중 브라우저 셈」 · 「[저장] 서버 정본」 늘 켬 · 잠금).
* 바꿀 수 있는 줄 = 서버 판 `groups[].switchable` · 나머지는 잠긴 체크 + 까닭 글.
* 고침은 창 안 사본에만 · [저장] 때만 바뀐 줄을 PUT(선례 `app_settings.ts`).
* ========================================================================== */
import { createButton, el, showToast } from "@ui/ui_template_elements";
import { table } from "@ui/ui_template_general_blocks";
import { askLeave } from "@ui/ui_template_leave_dialog";
import { openModal } from "@ui/ui_template_modal";
import {
fetchCalcSites,
saveCalcSites,
type CalcSiteGroup,
type CalcSiteValue,
type CalcSitesBoard,
} from "../A00_Common/design/design_calc_sites";
import { L } from "./B01_Dashboard_UI_Common";
type LocaleKey = Parameters<typeof L>[0];
const K = (key: string): string => L(`B01_CalcSites_${key}` as LocaleKey);
function check(checked: boolean, locked: boolean): HTMLInputElement {
const input = el("input", { attrs: { type: "checkbox" } });
input.checked = checked;
input.disabled = locked;
return input;
}
function groupRow(
group: CalcSiteGroup,
draft: Record<string, CalcSiteValue>,
changed: () => void,
): HTMLElement[] {
const name = el("span", {
children: [el("strong", { text: K(group.key) }), ` (${group.nos.join(" ")})`],
});
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();
});
const server = check(group.fixed !== "browser", true);
return [name, browser, server, el("span", { text: K(`Why_${group.why ?? group.key}`) })];
}
/** 시스템 설정 「계산 자리」 카드 — 판을 받아 창을 엶. */
export async function openCalcSitesDialog(): Promise<void> {
let board: CalcSitesBoard;
try {
board = await fetchCalcSites();
} catch (error) {
showToast(error instanceof Error ? error.message : K("LoadFailed"), "error");
return;
}
const draft = { ...board.sites };
const dirty = (): Record<string, CalcSiteValue> =>
Object.fromEntries(Object.entries(draft).filter(([key, value]) => board.sites[key] !== value));
const count = el("p", { className: "b01-dashboard__calc-count" });
const recount = (): void => {
const n = Object.values(draft).filter((value) => value === "server").length;
count.textContent = K("ServerCount").replace("{value}", String(n));
};
recount();
const save = async (): Promise<boolean> => {
const changes = dirty();
if (!Object.keys(changes).length) return true;
try {
board = await saveCalcSites(changes);
} catch (error) {
showToast(error instanceof Error ? error.message : String(error), "error");
return false;
}
showToast(K("Saved"), "success");
return true;
};
const saveButton = createButton({
label: L("Common_Btn_Save"),
variant: "filled",
onClick: () => void save().then((ok) => ok && modal.close()),
});
const modal = openModal({
title: K("Title"),
closeLabel: L("Common_Btn_Close"),
dialogClass: "b01-dashboard__calc-sites",
wide: true,
actions: [saveButton],
beforeClose: async () => {
if (!Object.keys(dirty()).length) return true;
const choice = await askLeave(K("Leave"));
return choice === "discard" || (choice === "save" && (await save()));
},
mount: (body) => {
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)),
),
);
},
});
}