fix: 적대적 코드리뷰 후속 — 신규 도메인 5건 버그 수정

서브에이전트 4영역 적대적 리뷰(P7/P8 서비스·매퍼·배치·어댑터)에서 확정된 결함 수정.

- [HIGH] HqSettleItem update 시 status가 NULL로 덮어써짐(SaveReq에 status 없음)
  → status = COALESCE(#{status}, status) 로 수정 불변 보장.
- [MED] 소개·이관수수료 RATE/FIXED 분기를 요청 commissionRate 유무로 판정 → 활성 룰의
  calc_type 을 권위로 사용(FIXED=fixed_amount, RATE=요청rate||룰rate), 룰 없을 때만 요청rate 폴백.
- [MED] 지점장수당 cap 미적용(주석-코드 불일치) → SaveReq.capAmount 추가 + allowance = min(base×rate, cap).
- [MED] PUSH 외부호출 감사로그 요약 누락 → ExternalCallLoggingAspect applyRequest/applyResponse 에
  PushRequest/PushResponse 분기 추가(토큰 마스킹, messageId/sent/resultCode).
- [LOW] 공동계약 update 지분율 검증 키 불일치 → 기존 레코드 contractId 기준으로 검증/갱신(계약 이동 불가).

배치 통합 리뷰 결과 CLEAN(무결성·집계·upsert 정확). 전체 ./gradlew build(test 포함) SUCCESSFUL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
GA Pro
2026-05-31 21:27:14 +09:00
parent 51f340e36e
commit 5d5c2d53aa
6 changed files with 52 additions and 15 deletions
@@ -116,6 +116,15 @@ public class ExternalCallLoggingAspect {
m.put("refUserId", mr.getRefUserId());
m.put("contentPreview", truncate(mr.getContent(), 80));
vo.setRequestSummary(toJson(m));
} else if (a0 instanceof com.ga.common.adapter.push.PushRequest pr) {
vo.setTargetRefType(pr.getRefType());
vo.setTargetRefId(pr.getRefId());
Map<String, Object> m = new LinkedHashMap<>();
m.put("tokenMasked", maskToken(pr.getDeviceToken()));
m.put("title", pr.getTitle());
m.put("refUserId", pr.getRefUserId());
m.put("bodyPreview", truncate(pr.getBody(), 80));
vo.setRequestSummary(toJson(m));
} else if (a0 instanceof String s) {
// BankTransferAdapter.queryStatus(txId)
vo.setRequestSummary("{\"arg\":\"" + safeJsonString(s) + "\"}");
@@ -139,6 +148,14 @@ public class ExternalCallLoggingAspect {
m.put("sent", mr.isSent());
m.put("sentAt", mr.getSentAt());
vo.setResponseSummary(toJson(m));
} else if (result instanceof com.ga.common.adapter.push.PushResponse pr) {
vo.setResultCode(pr.getResultCode());
vo.setResultMessage(truncate(pr.getResultMessage(), 1000));
Map<String, Object> m = new LinkedHashMap<>();
m.put("messageId", pr.getMessageId());
m.put("sent", pr.isSent());
m.put("sentAt", pr.getSentAt());
vo.setResponseSummary(toJson(m));
}
}
@@ -173,6 +190,11 @@ public class ExternalCallLoggingAspect {
return name.charAt(0) + "*".repeat(Math.max(0, name.length() - 1));
}
private String maskToken(String token) {
if (token == null) return null;
return token.length() <= 8 ? "****" : token.substring(0, 8) + "****";
}
private String truncate(String s, int max) {
if (s == null) return null;
return s.length() <= max ? s : s.substring(0, max);
@@ -88,9 +88,9 @@ public class BranchAllowanceService {
.setScale(2, RoundingMode.HALF_UP);
BigDecimal clawback = req.getClawbackAmount() != null ? req.getClawbackAmount() : BigDecimal.ZERO;
// cap_amount는 SaveReq에 없으므로 allowanceRate와 동일 룰에서 cap 조회
// positionCode가 req에 없어 단순 구현: raw 그대로 사용 (배치에서 룰 참조 시 위임)
BigDecimal allowanceAmount = raw;
// cap 적용: allowance = min(base×rate, cap). cap 미지정(NULL)이면 상한 없음.
BigDecimal cap = req.getCapAmount();
BigDecimal allowanceAmount = (cap != null && raw.compareTo(cap) > 0) ? cap : raw;
BranchMgrAllowanceLedgerVO vo = new BranchMgrAllowanceLedgerVO();
vo.setManagerAgentId(req.getManagerAgentId());
@@ -56,10 +56,11 @@ public class JointContractService {
public void update(Long jointId, JointContractSaveReq req) {
JointContractResp resp = jointContractMapper.selectById(jointId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
validateShareRate(req.getContractId(), jointId, req.getShareRate());
// 계약 이동은 허용하지 않음 — 검증/갱신 모두 기존 레코드의 contractId 기준
validateShareRate(resp.getContractId(), jointId, req.getShareRate());
JointContractVO vo = new JointContractVO();
vo.setJointId(jointId);
vo.setContractId(req.getContractId());
vo.setContractId(resp.getContractId());
vo.setAgentId(req.getAgentId());
vo.setRole(req.getRole());
vo.setShareRate(req.getShareRate());
@@ -92,20 +92,30 @@ public class ReferralCommissionService {
vo.setBaseAmount(req.getBaseAmount());
vo.setCommissionRate(req.getCommissionRate());
// 소개·이관수수료 = base × rate (RATE) 또는 fixed_amount (FIXED)
// 소개·이관수수료 계산: 활성 룰의 calc_type 이 권위(RATE=base×rate / FIXED=fixed_amount).
// 룰이 없을 때만 요청 commissionRate 로 폴백(수동 등록 호환).
ReferralCommissionRuleVO rule = ruleMapper.selectActiveRule(
req.getReferralType(), java.time.LocalDate.now());
BigDecimal commissionAmount;
if (req.getCommissionRate() != null) {
if (rule != null) {
if ("FIXED".equals(rule.getCalcType())) {
if (rule.getFixedAmount() == null) throw new BizException(ErrorCode.NOT_FOUND);
commissionAmount = rule.getFixedAmount().setScale(2, RoundingMode.HALF_UP);
vo.setCommissionRate(null);
} else { // RATE
BigDecimal rate = req.getCommissionRate() != null
? req.getCommissionRate() : rule.getCommissionRate();
if (rate == null) throw new BizException(ErrorCode.NOT_FOUND);
vo.setCommissionRate(rate);
commissionAmount = req.getBaseAmount().multiply(rate).setScale(2, RoundingMode.HALF_UP);
}
} else if (req.getCommissionRate() != null) {
// 활성 룰 없음 → 요청 rate 로 수동 계산
commissionAmount = req.getBaseAmount()
.multiply(req.getCommissionRate())
.setScale(2, RoundingMode.HALF_UP);
} else {
// FIXED: 룰에서 fixed_amount 조회
ReferralCommissionRuleVO rule = ruleMapper.selectActiveRule(
req.getReferralType(), java.time.LocalDate.now());
if (rule == null || rule.getFixedAmount() == null) {
throw new BizException(ErrorCode.NOT_FOUND);
}
commissionAmount = rule.getFixedAmount().setScale(2, RoundingMode.HALF_UP);
throw new BizException(ErrorCode.NOT_FOUND);
}
vo.setCommissionAmount(commissionAmount);
vo.setStatus("CALCULATED");
@@ -36,4 +36,8 @@ public class BranchMgrAllowanceLedgerSaveReq {
@DecimalMin("0")
private BigDecimal clawbackAmount;
/** 수당 상한(룰의 cap_amount). 지정 시 allowance_amount = min(base×rate, cap). NULL이면 상한 없음. */
@DecimalMin("0")
private BigDecimal capAmount;
}
@@ -72,7 +72,7 @@
item_name = #{itemName},
amount = #{amount},
memo = #{memo},
status = #{status},
status = COALESCE(#{status}, status),
updated_at = NOW()
WHERE item_id = #{itemId}
</update>