feat: P6 외부연동 호출 감사 이력 (V85)
펌뱅킹/SMS·카카오 어댑터 모든 호출을 external_call_log 테이블에 AOP 자동 영속화.
외부 SDK 통합 후에도 그대로 재사용. 운영 감사·장애 추적·SDK 검증 용도.
- V85: external_call_log 테이블 + 인덱스 4종 (called_at desc / type / target / failures)
+ SYSTEM_EXTERNAL_CALL 메뉴 + READ 권한 (SUPER_ADMIN/ADMIN)
- ga-core: ExternalCallLogVO/Resp/SearchParam + Mapper (insertOne / selectList / selectById)
- ga-api/aop: ExternalCallLoggingAspect — BankTransferAdapter+/MessageAdapter+
모든 메서드 @Around 영속화. 호출자 트랜잭션과 분리(REQUIRES_NEW),
request/response 는 PII 마스킹된 JSON 한 줄 요약(계좌·전화 뒤 4자리, 이름 첫글자만),
영속화 자체가 실패해도 비즈니스 흐름 비차단(warn 로그만), 예외는 그대로 propagate.
- ga-api: ExternalCallLogService + Controller GET /api/external-call-logs[/{id}]
- ga-frontend: ExternalCallLogList.tsx + api/externalCallLog.ts + App.tsx 라우트
/system/external-call-log (ProTable + Drawer 상세)
라이브 검증:
- Flyway V85 자동 적용 → 운영 DB schema v85
- 5모듈 컴파일 BUILD SUCCESSFUL + ga-frontend tsc --noEmit 통과
- 시나리오: withdraw#3 신규 → 결재 advance ×2 → 펌뱅킹 호출
external_call_log #1: BANK/MockBankTransferAdapter/requestTransfer/
success=true/durMs=3/target=WITHDRAW#3/accountNoMasked="****0123"
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
package com.ga.core.mapper.external;
|
||||
|
||||
import com.ga.core.vo.external.ExternalCallLogResp;
|
||||
import com.ga.core.vo.external.ExternalCallLogSearchParam;
|
||||
import com.ga.core.vo.external.ExternalCallLogVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ExternalCallLogMapper {
|
||||
|
||||
/** AOP @Around 영속화. selectKey 로 logId 채움. */
|
||||
int insertOne(ExternalCallLogVO vo);
|
||||
|
||||
ExternalCallLogResp selectById(@Param("logId") Long logId);
|
||||
|
||||
List<ExternalCallLogResp> selectList(ExternalCallLogSearchParam param);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.ga.core.vo.external;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class ExternalCallLogResp {
|
||||
private Long logId;
|
||||
private String callType;
|
||||
private String adapterClass;
|
||||
private String methodName;
|
||||
private String targetRefType;
|
||||
private Long targetRefId;
|
||||
private String requestSummary;
|
||||
private String responseSummary;
|
||||
private String resultCode;
|
||||
private String resultMessage;
|
||||
private Boolean success;
|
||||
private Integer durationMs;
|
||||
private String errorClass;
|
||||
private String errorMessage;
|
||||
private LocalDateTime calledAt;
|
||||
private Long createdBy;
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.ga.core.vo.external;
|
||||
|
||||
import com.ga.common.model.SearchParam;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ExternalCallLogSearchParam extends SearchParam {
|
||||
/** BANK / MESSAGE */
|
||||
private String callType;
|
||||
private String methodName;
|
||||
/** true 만 / false 만 (실패 조회) — null 이면 전체 */
|
||||
private Boolean success;
|
||||
private String targetRefType;
|
||||
private Long targetRefId;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.ga.core.vo.external;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* external_call_log — 외부 연동(펌뱅킹/SMS·카카오) 호출 감사 이력 (V85).
|
||||
* AOP @Around 가 INSERT. 운영 감사·장애 추적용. PII 는 이미 어댑터에서 마스킹된 값만 들어간다.
|
||||
*/
|
||||
@Data
|
||||
public class ExternalCallLogVO {
|
||||
private Long logId;
|
||||
/** BANK / MESSAGE */
|
||||
private String callType;
|
||||
/** 실제 호출된 구현체 (Mock/Kftc/Toast) */
|
||||
private String adapterClass;
|
||||
/** requestTransfer / queryStatus / send */
|
||||
private String methodName;
|
||||
/** WITHDRAW / APPROVAL 등 (가능 시) */
|
||||
private String targetRefType;
|
||||
private Long targetRefId;
|
||||
/** 요청 요약 (마스킹된 JSON 한 줄) */
|
||||
private String requestSummary;
|
||||
private String responseSummary;
|
||||
private String resultCode;
|
||||
private String resultMessage;
|
||||
private Boolean success;
|
||||
private Integer durationMs;
|
||||
private String errorClass;
|
||||
private String errorMessage;
|
||||
private LocalDateTime calledAt;
|
||||
private Long createdBy;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ga.core.mapper.external.ExternalCallLogMapper">
|
||||
|
||||
<resultMap id="RespMap" type="com.ga.core.vo.external.ExternalCallLogResp">
|
||||
<id property="logId" column="log_id"/>
|
||||
<result property="callType" column="call_type"/>
|
||||
<result property="adapterClass" column="adapter_class"/>
|
||||
<result property="methodName" column="method_name"/>
|
||||
<result property="targetRefType" column="target_ref_type"/>
|
||||
<result property="targetRefId" column="target_ref_id"/>
|
||||
<result property="requestSummary" column="request_summary"/>
|
||||
<result property="responseSummary" column="response_summary"/>
|
||||
<result property="resultCode" column="result_code"/>
|
||||
<result property="resultMessage" column="result_message"/>
|
||||
<result property="success" column="success"/>
|
||||
<result property="durationMs" column="duration_ms"/>
|
||||
<result property="errorClass" column="error_class"/>
|
||||
<result property="errorMessage" column="error_message"/>
|
||||
<result property="calledAt" column="called_at"/>
|
||||
<result property="createdBy" column="created_by"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="cols">
|
||||
log_id, call_type, adapter_class, method_name,
|
||||
target_ref_type, target_ref_id,
|
||||
request_summary, response_summary,
|
||||
result_code, result_message, success, duration_ms,
|
||||
error_class, error_message, called_at, created_by
|
||||
</sql>
|
||||
|
||||
<insert id="insertOne" parameterType="com.ga.core.vo.external.ExternalCallLogVO"
|
||||
useGeneratedKeys="true" keyProperty="logId" keyColumn="log_id">
|
||||
INSERT INTO external_call_log (
|
||||
call_type, adapter_class, method_name,
|
||||
target_ref_type, target_ref_id,
|
||||
request_summary, response_summary,
|
||||
result_code, result_message, success, duration_ms,
|
||||
error_class, error_message, called_at, created_by
|
||||
) VALUES (
|
||||
#{callType},
|
||||
#{adapterClass},
|
||||
#{methodName},
|
||||
#{targetRefType, jdbcType=VARCHAR},
|
||||
#{targetRefId, jdbcType=BIGINT},
|
||||
#{requestSummary, jdbcType=LONGVARCHAR},
|
||||
#{responseSummary, jdbcType=LONGVARCHAR},
|
||||
#{resultCode, jdbcType=VARCHAR},
|
||||
#{resultMessage, jdbcType=LONGVARCHAR},
|
||||
#{success, jdbcType=BOOLEAN},
|
||||
#{durationMs, jdbcType=INTEGER},
|
||||
#{errorClass, jdbcType=VARCHAR},
|
||||
#{errorMessage, jdbcType=LONGVARCHAR},
|
||||
COALESCE(#{calledAt,jdbcType=TIMESTAMP}, CURRENT_TIMESTAMP),
|
||||
#{createdBy, jdbcType=BIGINT}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<select id="selectById" resultMap="RespMap">
|
||||
SELECT <include refid="cols"/>
|
||||
FROM external_call_log
|
||||
WHERE log_id = #{logId}
|
||||
</select>
|
||||
|
||||
<select id="selectList" resultMap="RespMap"
|
||||
parameterType="com.ga.core.vo.external.ExternalCallLogSearchParam">
|
||||
SELECT <include refid="cols"/>
|
||||
FROM external_call_log
|
||||
<where>
|
||||
<if test="callType != null and callType != ''">
|
||||
AND call_type = #{callType}
|
||||
</if>
|
||||
<if test="methodName != null and methodName != ''">
|
||||
AND method_name = #{methodName}
|
||||
</if>
|
||||
<if test="success != null">
|
||||
AND success = #{success}
|
||||
</if>
|
||||
<if test="targetRefType != null and targetRefType != ''">
|
||||
AND target_ref_type = #{targetRefType}
|
||||
</if>
|
||||
<if test="targetRefId != null">
|
||||
AND target_ref_id = #{targetRefId}
|
||||
</if>
|
||||
<if test="startDate != null and startDate != ''">
|
||||
AND called_at >= (#{startDate}::date)
|
||||
</if>
|
||||
<if test="endDate != null and endDate != ''">
|
||||
AND called_at < (#{endDate}::date + INTERVAL '1 day')
|
||||
</if>
|
||||
</where>
|
||||
ORDER BY called_at DESC, log_id DESC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user