Files
invyone/backend-spring/src/main/java/com/erp/service/AuditLogService.java
T
johngreen c0bd420c66 feat(대무자): SYSTEM_AUDIT_LOG processor 분리 기록 + USER_INFO lookup
- auditLog.xml insertAuditLog INSERT 절에 PROCESSOR_ID/PROCESSOR_NAME 컬럼 추가
- auditLog.xml selectUserNameById — 처리자 이름 lookup
- AuditLogService.insertAuditLog:
  · processor_id null → user_id 로 자동 채움 (평시 = 동일)
  · processor_id != user_id 이고 processor_name null → USER_INFO 단건 조회 (대무 이벤트만)
- notes: 도메인 테이블 CREATED_BY/UPDATED_BY broad scan — actual processor(B) 통일 자동 만족
2026-05-12 08:06:55 +09:00

164 lines
6.6 KiB
Java

package com.erp.service;
import com.erp.common.BaseService;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
@Slf4j
public class AuditLogService extends BaseService {
@Autowired
private ObjectMapper objectMapper;
private static final String SECURITY_MASK = "(보안 항목 - 값 비공개)";
private static final List<String> SECURED_TABLES = List.of("table_type_columns");
/**
* 감사 로그 목록 조회 (페이지네이션 + 보안 마스킹)
*/
public Map<String, Object> queryLogs(Map<String, Object> filters, boolean isSuperAdmin) {
int page = toInt(filters.get("page"), 1);
int limit = toInt(filters.get("limit"), 50);
int offset = (page - 1) * limit;
Map<String, Object> params = new HashMap<>(filters);
params.put("limit", limit);
params.put("offset", offset);
Number totalNum = sqlSession.selectOne("auditLog.selectAuditLogsCount", params);
int total = totalNum != null ? totalNum.intValue() : 0;
List<Map<String, Object>> data = sqlSession.selectList("auditLog.selectAuditLogs", params);
// changes 필드 처리 (JSONB → Java Map) + 보안 마스킹
processChanges(data, isSuperAdmin);
Map<String, Object> result = new HashMap<>();
result.put("data", data);
result.put("total", total);
return result;
}
/**
* 통계 조회 (일별 / 리소스타입 / 액션 / 상위사용자)
*/
public Map<String, Object> getStats(String companyCode, int days) {
Map<String, Object> params = new HashMap<>();
params.put("company_code", companyCode);
params.put("days", days);
Map<String, Object> result = new HashMap<>();
result.put("daily_counts", sqlSession.selectList("auditLog.selectDailyCounts", params));
result.put("resource_type_counts", sqlSession.selectList("auditLog.selectResourceTypeCounts", params));
result.put("action_counts", sqlSession.selectList("auditLog.selectActionCounts", params));
result.put("top_users", sqlSession.selectList("auditLog.selectTopUsers", params));
return result;
}
/**
* 감사 로그가 있는 활성 사용자 목록
*/
public List<Map<String, Object>> getAuditLogUsers(String companyCode, boolean isSuperAdmin) {
Map<String, Object> params = new HashMap<>();
params.put("company_code", companyCode);
params.put("exclude_wildcard", !isSuperAdmin);
return sqlSession.selectList("auditLog.selectAuditLogUsers", params);
}
/**
* 감사 로그 1건 기록.
*
* PROCESSOR 처리 (대무 추적):
* - processor_id 미지정 → user_id 로 채움 (평시 = 동일 = 본인 처리)
* - processor_id 가 user_id 와 다르고 processor_name 미지정 → USER_INFO 에서 lookup
* (대무 이벤트만 USER_INFO 단건 조회 1회 — 평시는 추가 DB 호출 없음)
*/
public void insertAuditLog(Map<String, Object> params) {
// changes가 Map이면 JSON 문자열로 직렬화
Object changes = params.get("changes");
if (changes instanceof Map || changes instanceof List) {
try {
params.put("changes", objectMapper.writeValueAsString(changes));
} catch (Exception e) {
log.warn("changes 직렬화 실패, null로 처리: {}", e.getMessage());
params.put("changes", null);
}
}
Object processorId = params.get("processor_id");
Object userId = params.get("user_id");
if (processorId == null) {
params.put("processor_id", userId);
if (params.get("processor_name") == null) {
params.put("processor_name", params.get("user_name"));
}
} else if (!processorId.equals(userId) && params.get("processor_name") == null) {
try {
Map<String, Object> p = new HashMap<>();
p.put("user_id", processorId);
p.put("company_code", params.get("company_code"));
String name = sqlSession.selectOne("auditLog.selectUserNameById", p);
params.put("processor_name", name);
} catch (Exception e) {
log.warn("processor_name lookup 실패 (processor_id={}): {}", processorId, e.getMessage());
}
}
sqlSession.insert("auditLog.insertAuditLog", params);
}
// ── 내부 유틸 ───────────────────────────────────────────────────────────
/**
* JSONB 컬럼(changes) 역직렬화 및 보안 마스킹 처리
*/
private void processChanges(List<Map<String, Object>> entries, boolean isSuperAdmin) {
for (Map<String, Object> entry : entries) {
Object raw = entry.get("changes");
if (raw == null) continue;
String json = raw.toString(); // PGobject.toString() = JSON 문자열
try {
Map<String, Object> changes = objectMapper.readValue(
json, new TypeReference<Map<String, Object>>() {});
if (!isSuperAdmin) {
String tableName = (String) entry.get("table_name");
if (tableName != null && SECURED_TABLES.contains(tableName)) {
maskMapValues(changes, "before");
maskMapValues(changes, "after");
}
}
entry.put("changes", changes);
} catch (Exception e) {
log.warn("changes JSON 파싱 실패, 원본 유지: {}", e.getMessage());
entry.put("changes", json);
}
}
}
@SuppressWarnings("unchecked")
private void maskMapValues(Map<String, Object> changes, String key) {
Object section = changes.get(key);
if (!(section instanceof Map)) return;
Map<String, Object> sectionMap = (Map<String, Object>) section;
sectionMap.replaceAll((k, v) -> SECURITY_MASK);
}
private int toInt(Object value, int defaultVal) {
if (value == null) return defaultVal;
try {
return Integer.parseInt(value.toString());
} catch (NumberFormatException e) {
return defaultVal;
}
}
}