c0bd420c66
- 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) 통일 자동 만족
164 lines
6.6 KiB
Java
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;
|
|
}
|
|
}
|
|
}
|