feat(대무자): 결재 시스템 어댑터 통합 — USER_SUBSTITUTES read + IN(effective_user_ids)

- approval.xml selectActiveProxyForLine: APPROVAL_PROXY_SETTINGS → USER_SUBSTITUTES 참조
  (BOOLEAN IS_ACTIVE, START_DATE NULL 허용)
- approval.xml 3곳 (selectRequests/countRequests/selectMyPendingLines):
  APPROVER_ID = #{user_id} → APPROVER_ID IN (effective_user_ids) foreach
- ApprovalService.ensureEffectiveUserIds helper — mybatis 빈 IN() 방지
- ApprovalController: getRequests/getMyPendingLines 에 @RequestAttribute(effective_user_ids) 추가
- ApprovalService.processApproval 마지막에 AuditLogService.insertAuditLog 호출 추가
  (user_id=A, processor_id=B 분리 기록)
This commit is contained in:
2026-05-12 08:06:39 +09:00
parent 4a83bfc8e8
commit 6ab7c3e780
3 changed files with 63 additions and 8 deletions
@@ -190,9 +190,11 @@ public class ApprovalController {
public ResponseEntity<ApiResponse<Map<String, Object>>> getRequests(
@RequestParam Map<String, Object> params,
@RequestAttribute("company_code") String companyCode,
@RequestAttribute("user_id") String userId) {
@RequestAttribute("user_id") String userId,
@RequestAttribute(name = "effective_user_ids", required = false) List<String> effectiveUserIds) {
params.put("company_code", companyCode);
params.put("user_id", userId);
params.put("effective_user_ids", effectiveUserIds);
return ResponseEntity.ok(ApiResponse.success(approvalService.getRequests(params)));
}
@@ -277,10 +279,12 @@ public class ApprovalController {
@GetMapping("/my-pending")
public ResponseEntity<ApiResponse<List<Map<String, Object>>>> getMyPendingLines(
@RequestAttribute("company_code") String companyCode,
@RequestAttribute("user_id") String userId) {
@RequestAttribute("user_id") String userId,
@RequestAttribute(name = "effective_user_ids", required = false) List<String> effectiveUserIds) {
Map<String, Object> params = new HashMap<>();
params.put("company_code", companyCode);
params.put("user_id", userId);
params.put("effective_user_ids", effectiveUserIds);
return ResponseEntity.ok(ApiResponse.success(approvalService.getMyPendingLines(params)));
}
@@ -17,6 +17,26 @@ public class ApprovalService extends BaseService {
@Autowired
private ObjectMapper objectMapper;
@Autowired
private AuditLogService auditLogService;
/**
* IN (:effective_user_ids) 쿼리용 fallback.
* SubstituteContextFilter 가 attribute 를 못 채운 경로(통합 테스트/배치 등) 에서도
* 빈 IN () SQL 에러를 막기 위해 항상 최소 [user_id] 가 들어가도록 한다.
*/
@SuppressWarnings("unchecked")
private void ensureEffectiveUserIds(Map<String, Object> params) {
Object v = params.get("effective_user_ids");
boolean empty = v == null || (v instanceof Collection<?> && ((Collection<?>) v).isEmpty());
if (empty) {
Object userId = params.get("user_id");
if (userId != null) {
params.put("effective_user_ids", List.of(userId));
}
}
}
// ═══════════════════════════════════════════════════════════════
// approval_definitions
// ═══════════════════════════════════════════════════════════════
@@ -149,6 +169,7 @@ public class ApprovalService extends BaseService {
// ═══════════════════════════════════════════════════════════════
public Map<String, Object> getRequests(Map<String, Object> params) {
ensureEffectiveUserIds(params);
int page = toInt(params.getOrDefault("page", "1"));
int limit = toInt(params.getOrDefault("limit", "20"));
params.put("page_limit", limit);
@@ -359,6 +380,7 @@ public class ApprovalService extends BaseService {
// ═══════════════════════════════════════════════════════════════
public List<Map<String, Object>> getMyPendingLines(Map<String, Object> params) {
ensureEffectiveUserIds(params);
return sqlSession.selectList("approval.selectMyPendingLines", params);
}
@@ -456,6 +478,24 @@ public class ApprovalService extends BaseService {
activateNextStep(requestId, stepOrder, totalSteps, lineCC, userId, comment);
}
}
// 결재 처리 audit log — 대무 시 user_id(A)와 processor_id(B) 분리 기록.
// 실패는 본 처리를 막지 않음 (가용성 우선).
try {
Map<String, Object> auditP = new HashMap<>();
auditP.put("company_code", lineCC);
auditP.put("user_id", approverId); // 위임자 A
auditP.put("user_name", line.get("approver_name"));
auditP.put("processor_id", userId); // 실제 처리자 B
// processor_name 은 AuditLogService 가 USER_INFO 에서 lookup (T14)
auditP.put("action", "approval." + action);
auditP.put("resource_type", "approval_line");
auditP.put("resource_id", String.valueOf(lineId));
auditP.put("summary", "결재 " + action + (proxyFor != null ? " (대무)" : ""));
auditLogService.insertAuditLog(auditP);
} catch (Exception e) {
log.warn("결재 audit log 기록 실패 (line={}): {}", lineId, e.getMessage());
}
}
// ═══════════════════════════════════════════════════════════════
@@ -214,7 +214,10 @@
AND EXISTS (
SELECT 1 FROM APPROVAL_LINES L
WHERE L.REQUEST_ID = R.REQUEST_ID
AND L.APPROVER_ID = #{user_id}
AND L.APPROVER_ID IN
<foreach collection="effective_user_ids" item="uid" open="(" separator="," close=")">
#{uid}
</foreach>
AND L.STATUS = 'pending'
AND L.COMPANY_CODE = R.COMPANY_CODE
)
@@ -248,7 +251,10 @@
AND EXISTS (
SELECT 1 FROM APPROVAL_LINES L
WHERE L.REQUEST_ID = R.REQUEST_ID
AND L.APPROVER_ID = #{user_id}
AND L.APPROVER_ID IN
<foreach collection="effective_user_ids" item="uid" open="(" separator="," close=")">
#{uid}
</foreach>
AND L.STATUS = 'pending'
AND L.COMPANY_CODE = R.COMPANY_CODE
)
@@ -463,7 +469,10 @@
FROM APPROVAL_LINES L
JOIN APPROVAL_REQUESTS R
ON L.REQUEST_ID = R.REQUEST_ID AND L.COMPANY_CODE = R.COMPANY_CODE
WHERE L.APPROVER_ID = #{user_id}
WHERE L.APPROVER_ID IN
<foreach collection="effective_user_ids" item="uid" open="(" separator="," close=")">
#{uid}
</foreach>
AND L.STATUS = 'pending'
AND (L.COMPANY_CODE = #{company_code} OR L.COMPANY_CODE = '*')
ORDER BY R.CREATED_DATE ASC
@@ -536,12 +545,14 @@
AND (COMPANY_CODE = #{company_code} OR COMPANY_CODE = '*')
</delete>
<!-- 어댑터: USER_SUBSTITUTES 참조 (T7, 086 마이그레이션 이후). -->
<select id="selectActiveProxyForLine" parameterType="map" resultType="map">
SELECT * FROM APPROVAL_PROXY_SETTINGS
SELECT *
FROM USER_SUBSTITUTES
WHERE ORIGINAL_USER_ID = #{original_user_id}
AND PROXY_USER_ID = #{proxy_user_id}
AND IS_ACTIVE = 'Y'
AND START_DATE &lt;= CURRENT_DATE
AND IS_ACTIVE = TRUE
AND (START_DATE IS NULL OR START_DATE &lt;= CURRENT_DATE)
AND END_DATE &gt;= CURRENT_DATE
AND (COMPANY_CODE = #{company_code} OR COMPANY_CODE = '*')
LIMIT 1