Files
Aislo/B01_Dashboard/B01_Dashboard_UI_View_SysLogs.ts
T

167 lines
5.7 KiB
TypeScript

/* =============================================================================
* 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<string, string> = {
PROJECT_CREATE: "프로젝트 생성",
PROJECT_UPDATE: "프로젝트 수정",
PROJECT_DELETE: "프로젝트 삭제",
PROJECT_HARD_DELETE: "프로젝트 영구 삭제",
COMPANY_CREATE: "회사 생성",
CALC_SITE_CHANGE: "계산 자리 변경",
RATE_ANOMALY: "호출 이상",
M02_OTHER_LAYER_WRITE: "마스터 다른 층 수정",
};
const TARGET_KO: Record<string, string> = {
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<void> {
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);
});
}