/* ============================================================================= * B01_Dashboard_UI_View_SysLogs.ts * 시스템 관리 › 시스템 로그(549-2) — 동작 · 대상 · 접속 주소 · 일시 표 + 거르기(동작 · 기간 · 찾기). * 서버는 최신순 쪽(100 줄)만 줌 → 받은 줄 안에서 거르고 [이전 기록 더 보기] 로 다음 쪽을 이어 붙임. * ========================================================================== */ import { fetchAuditLogPage, type AuditLog } from "./B01_Dashboard_Api_Fetch"; import { formatDate, formatTime } from "./B01_Dashboard_UI_Common"; import { createButton } from "@ui/ui_template_elements"; import { dataTable, emptyState, pageFrame } from "./B01_Dashboard_UI_PageKit"; import { el as h, loadPage, searchBox, selectBox, toolbar } from "./B01_Dashboard_UI_View_Parts"; import type { ViewCtx } from "./B01_Dashboard_UI_Views"; const PAGE = 100; export const ACTION_KO: Record = { PROJECT_CREATE: "프로젝트 생성", PROJECT_UPDATE: "프로젝트 수정", PROJECT_DELETE: "프로젝트 삭제", PROJECT_HARD_DELETE: "프로젝트 영구 삭제", COMPANY_CREATE: "회사 생성", CALC_SITE_CHANGE: "계산 자리 변경", RATE_ANOMALY: "호출 이상", M02_OTHER_LAYER_WRITE: "마스터 다른 층 수정", }; const TARGET_KO: Record = { PROJECT: "프로젝트", COMPANY: "회사", USER: "사용자", ASSET: "자산", M02_PERSONAL: "개인 마스터", CALC_SITE: "계산 자리", }; export const actionLabel = (action: string): string => ACTION_KO[action] ?? action; /** 무엇에 한 일인지 — 종류 + 번호(UUID 는 앞 8 자) */ export function targetText(log: AuditLog): string { const kind = TARGET_KO[(log.resource_type ?? "").toUpperCase()] ?? log.resource_type ?? ""; const ref = log.resource_ref ?? (log.resource_id ? String(log.resource_id) : ""); const short = ref.length > 12 ? `${ref.slice(0, 8)}…` : ref; return kind ? `${kind} ${short}`.trim() : short || "-"; } export interface LogFilter { action: string; from: string; to: string; q: string; } /** 동작 · 기간(날짜 YYYY-MM-DD 양끝 포함) · 찾기(이메일 · 대상 · 접속 주소) */ export function filterLogs(logs: AuditLog[], f: LogFilter): AuditLog[] { return logs.filter((l) => { const day = (l.timestamp ?? "").slice(0, 10); if (f.action && l.action !== f.action) return false; if (f.from && day < f.from) return false; if (f.to && day > f.to) return false; if (!f.q) return true; return [l.email, targetText(l), l.ip_address, actionLabel(l.action)] .filter(Boolean) .join(" ") .toLowerCase() .includes(f.q); }); } function dateInput(label: string, onChange: (v: string) => void): HTMLInputElement { const input = h("input", "b01-sys-input"); input.type = "date"; input.ariaLabel = label; input.title = label; input.addEventListener("change", () => onChange(input.value)); return input; } const COLUMNS = [ { label: "사용자" }, { label: "동작" }, { label: "대상" }, { label: "접속 주소" }, { label: "일시" }, ]; export function mountSysLogs(host: HTMLElement, _ctx: ViewCtx): void { loadPage(host, async (stage) => { const first = await fetchAuditLogPage(1, PAGE); let logs = first.items; let page = 1; const total = first.total; const f: LogFilter = { action: "", from: "", to: "", q: "" }; const body = h("div"); const count = h("span", "b01-sys-count"); const more = createButton({ label: "이전 기록 더 보기", variant: "ghost", onClick: () => void loadMore(), }); const actionBox = h("span"); const drawActions = (): void => { const kinds = [...new Set(logs.map((l) => l.action))].sort(); const select = selectBox( "동작", [["", "모든 동작"], ...kinds.map((a): [string, string] => [a, actionLabel(a)])], (v) => { f.action = v; draw(); }, ); select.value = f.action; actionBox.replaceChildren(select); }; const draw = (): void => { const shown = filterLogs(logs, f); count.textContent = `${shown.length} / ${logs.length} 줄 (전체 ${total} 줄)`; more.hidden = logs.length >= total; body.replaceChildren( shown.length ? dataTable( COLUMNS, shown.map((l) => { const action = h("span", "", actionLabel(l.action)); action.title = l.action; return [ l.email ?? "-", action, targetText(l), l.ip_address ?? "-", `${formatDate(l.timestamp)} ${formatTime(l.timestamp)}`.trim(), ]; }), ) : emptyState({ icon: "fileText", text: logs.length ? "조건에 맞는 기록이 없습니다." : "기록이 없습니다.", }), ); }; async function loadMore(): Promise { page += 1; logs = logs.concat((await fetchAuditLogPage(page, PAGE)).items); drawActions(); draw(); } drawActions(); draw(); const search = searchBox("사용자 · 대상 · 접속 주소 찾기", (v) => { f.q = v; draw(); }); const frame = pageFrame( stage, "시스템 로그", "누가 무엇을 했는지 최신순으로 봅니다. 기록은 1 년 보관됩니다.", ); const from = dateInput("시작일", (v) => { f.from = v; draw(); }); const to = dateInput("종료일", (v) => { f.to = v; draw(); }); frame.body.append(toolbar(search, actionBox, from, h("span", "", "~"), to, count), body, more); }); }