167 lines
5.7 KiB
TypeScript
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);
|
|
});
|
|
}
|