Files
Aislo/B01_Dashboard/B01_Dashboard_UI_Admin.ts
T
eomsangdonandClaude Opus 5 4066504ba9 feat(B01): 시스템 로그에 대상·접속 정보 기록 + 1년 보관 정리 (2026-09-06 사용자 확정)
- 기록을 `common_util_audit.record_audit()` 한 곳으로 모음 — 여섯 자리에 흩어져 있던
  raw INSERT 제거
- 대상 식별자 칸 신설(`017_audit_log_detail.sql`, 적용 완료) — 기존 `resource_id` 는 INT 라
  프로젝트 UUID 를 못 담아 늘 NULL 이었음. 프로젝트 생성·수정·삭제·회사 생성이 대상을 남김
- 접속 주소·브라우저 기록 — 라우터가 요청을 넘겨 주고, 프록시 뒤에서는 X-Forwarded-For 우선
- 보관 기간 `AUDIT_LOG_RETENTION_DAYS` 기본 365일, 임시 보관함 정리 루프에 얹어 함께 정리
- 화면: 시스템 로그 표에 대상·접속 주소 열 추가(대상은 UUID 앞 8자만)

자체검증 — 프로젝트 생성·하드삭제를 실화면에서 돌려 두 줄 모두
`프로젝트 e1040640… · 127.0.0.1 · 2026-09-06 09:54/09:55` 로 남는 것 확인.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 18:57:22 +09:00

119 lines
3.9 KiB
TypeScript

import { createButton } from "@ui/ui_template_elements";
import type { AuditLog, DashboardUser } from "./B01_Dashboard_Api_Fetch";
import { canChangeRole, canDeleteUser } from "./B01_Dashboard_UI_Helper";
import {
openChangeRoleModal,
openDeleteUserModal,
openEditUserModal,
} from "./B01_Dashboard_UI_Modals";
import { table, text } from "@ui/ui_template_general_blocks";
import {
DASHBOARD_VISIBLE_ROWS,
formatDate,
formatTime,
L,
stackedCell,
} from "./B01_Dashboard_UI_Common";
export function userTable(users: DashboardUser[], currentUser: DashboardUser): HTMLElement {
return table(
[
L("B01_Dashboard_Table_Email"),
L("B01_Dashboard_Table_Name"),
L("B01_Dashboard_Table_Position"),
L("B01_Dashboard_Table_Department"),
L("B01_Account_Field_Phone"),
L("B01_Dashboard_Table_Role"),
L("B01_Dashboard_Table_Status"),
L("B01_Dashboard_Table_Action"),
],
users.map((user) => {
const actionsEl = document.createElement("div");
actionsEl.className = "b01-dashboard__actions";
actionsEl.append(
createButton({
label: L("Common_Btn_Edit"),
variant: "ghost",
onClick: () => openEditUserModal(currentUser, user),
}),
);
if (canChangeRole(currentUser, user)) {
actionsEl.append(
createButton({
label: L("B01_Dashboard_ChangeRole"),
variant: "ghost",
onClick: () => openChangeRoleModal(user),
}),
);
}
// 회사에서 빼기·계정 삭제 (2026-09-06 사용자 지시) — 범위는 백엔드가 다시 본다.
if (canDeleteUser(currentUser, user) && user.id !== currentUser.id) {
actionsEl.append(
createButton({
label: L("B01_Dashboard_DeleteUser"),
variant: "ghost",
onClick: () => openDeleteUserModal(user),
}),
);
}
return [
text(user.email),
text(user.name),
text(user.position),
text(user.department),
text(user.phone),
text(user.role),
text(user.status),
actionsEl,
];
}),
DASHBOARD_VISIBLE_ROWS,
);
}
/** 무엇에 한 일인지 — 표에 저장된 대상 종류·번호를 사람이 읽는 말로. */
function auditTarget(log: AuditLog): string {
// 저장값은 소문자(`project`)로 들어온다 — 대문자로 맞춰 찾는다.
const key = (log.resource_type ?? "").toUpperCase();
const kind = TARGET_LABELS[key] ?? log.resource_type ?? "";
const reference = log.resource_ref ?? (log.resource_id ? String(log.resource_id) : "");
if (!kind) return reference || "-";
// 프로젝트 UUID 는 길어 앞 8자만 — 어느 프로젝트인지 가리기에는 충분하다.
const shortened = reference.length > 12 ? `${reference.slice(0, 8)}…` : reference;
return shortened ? `${kind} ${shortened}` : kind;
}
const TARGET_LABELS: Record<string, string> = {
PROJECT: "프로젝트",
COMPANY: "회사",
USER: "사용자",
ASSET: "자산",
};
export function auditLogTable(logs: AuditLog[]): HTMLElement {
return table(
[
L("B01_Dashboard_Table_Email"),
// 관리 버튼 열과 같은 말(「관리」)을 돌려 쓰던 것을 갈랐다 (2026-09-06 사용자 지적).
L("B01_Dashboard_Table_Event"),
L("B01_Dashboard_Table_Target"),
L("B01_Dashboard_Table_Origin"),
L("B01_Dashboard_Table_When"),
],
logs.map((log) => [
text(log.email),
text(log.action),
text(auditTarget(log)),
// 접속 주소 — 기록이 없는 옛 줄은 빈칸으로 남는다 (2026-09-06부터 기록).
text(log.ip_address ?? "-"),
// 날짜와 시각을 두 줄로 — 아랫줄이 작은 글씨라 행 높이는 그대로다.
stackedCell(formatDate(log.timestamp), formatTime(log.timestamp)),
]),
DASHBOARD_VISIBLE_ROWS,
);
}