From e40726337046d13dbe72fbbedc0d954aeb2784bd Mon Sep 17 00:00:00 2001 From: GA Pro Date: Fri, 29 May 2026 22:58:49 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=ED=9A=8C=EA=B3=84=EB=B6=84=EA=B0=9C=20?= =?UTF-8?q?=EB=A9=B1=EB=93=B1=EC=84=B1=20=E2=80=94=20=EB=8F=99=EC=9D=BC=20?= =?UTF-8?q?=EC=A0=95=EC=82=B0=EC=9B=94=20=EC=9E=AC=EC=8B=A4=ED=96=89=20?= =?UTF-8?q?=EC=8B=9C=20=EB=B6=84=EA=B0=9C=20=EC=A4=91=EB=B3=B5=20INSERT=20?= =?UTF-8?q?=EB=B0=A9=EC=A7=80=20(V87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AccountingEntryGenerateStep(Step8b)이 selectBySettleMonth 결과를 무조건 insert 하여 동일 정산월 재정산 시 분개가 누적 중복되던 버그. payment_id 가 항상 NULL(원장 기반)이라 기존 주석의 existsByPaymentId 중복방어는 불가능했음. - V87: contract_accounting_entry.settle_month 컬럼 추가 + 기존데이터 백필(description 6자리) + 미전기 부분 인덱스 - core: VO settleMonth 필드, Mapper deleteUnpostedBySettleMonth, insertBatch/selectBySettleMonth 에 settle_month 반영 - batch: 재생성 전 deleteUnpostedBySettleMonth 호출(마감 채번분 journal_no IS NOT NULL 보존) 검증: ga-core/ga-batch/ga-api compileJava SUCCESS, Flyway v87 적용(백필 433건 NULL 0), 202605 정산 2회 실행 → 193건 유지(중복 0, 수정 전이라면 386). 멱등성 PASS. 테스트데이터 롤백 완료. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../step/AccountingEntryGenerateStep.java | 8 +++++++- .../migration/V87__회계분개_정산월_멱등성.sql | 19 +++++++++++++++++++ .../ContractAccountingEntryMapper.java | 6 ++++++ .../accounting/ContractAccountingEntryVO.java | 2 ++ .../ContractAccountingEntryMapper.xml | 13 +++++++++++-- 5 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 ga-common/src/main/resources/db/migration/V87__회계분개_정산월_멱등성.sql diff --git a/ga-batch/src/main/java/com/ga/batch/settlement/step/AccountingEntryGenerateStep.java b/ga-batch/src/main/java/com/ga/batch/settlement/step/AccountingEntryGenerateStep.java index f0922fd..e698aea 100644 --- a/ga-batch/src/main/java/com/ga/batch/settlement/step/AccountingEntryGenerateStep.java +++ b/ga-batch/src/main/java/com/ga/batch/settlement/step/AccountingEntryGenerateStep.java @@ -22,7 +22,7 @@ import java.util.List; * 정산월 기준 recruit_ledger + maintain_ledger → contract_accounting_entry INSERT. * - 차변(8350 지급수수료) / 대변(2530 미지급금) 한 쌍을 단일 행으로 저장. * - journal_no는 NULL(미전기). 마감 UI에서 채번. - * - 이미 분개된 payment는 existsByPaymentId로 중복 방어. + * - 멱등성: 재실행 시 해당 정산월의 미전기 분개를 먼저 삭제 후 재생성(마감 채번분은 보존). */ @Slf4j @Component @@ -40,6 +40,12 @@ public class AccountingEntryGenerateStep implements Tasklet { String settleMonth = ctx.getSettleMonth(); log.info("[AccountingEntry] settleMonth={}", settleMonth); + // 멱등성: 이번 정산월의 미전기 분개 제거 후 재생성 (마감 채번된 분개는 보존) + int deleted = accountingMapper.deleteUnpostedBySettleMonth(settleMonth); + if (deleted > 0) { + log.info("[AccountingEntry] deleted {} unposted entries before regenerate", deleted); + } + List source = accountingMapper.selectBySettleMonth(settleMonth); if (source.isEmpty()) { log.info("[AccountingEntry] no ledger records for month={}", settleMonth); diff --git a/ga-common/src/main/resources/db/migration/V87__회계분개_정산월_멱등성.sql b/ga-common/src/main/resources/db/migration/V87__회계분개_정산월_멱등성.sql new file mode 100644 index 0000000..8dd03cc --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V87__회계분개_정산월_멱등성.sql @@ -0,0 +1,19 @@ +-- V87: 회계분개(contract_accounting_entry) 정산월 멱등성 확보 +-- AccountingEntryGenerateStep(Step8b)이 동일 정산월 재실행 시 분개를 중복 INSERT 하던 버그 수정. +-- payment_id 가 항상 NULL(원장 기반)이라 기존 주석의 existsByPaymentId 중복방어는 불가능했음. +-- recruit/maintain ledger 와 동일하게 settle_month 키로 "미전기분 삭제 후 재생성" 패턴 적용. +-- (journal_no 채번된 마감 분개는 보존 — 미전기분만 재생성 대상) + +ALTER TABLE contract_accounting_entry ADD COLUMN IF NOT EXISTS settle_month CHAR(6); +COMMENT ON COLUMN contract_accounting_entry.settle_month IS '정산월 (YYYYMM). 배치 재생성 시 미전기분 멱등 삭제 키'; + +-- 기존 데이터 백필: description 의 6자리 정산월(예: "모집수수료 202604") 추출 +UPDATE contract_accounting_entry + SET settle_month = substring(description from '(\d{6})') + WHERE settle_month IS NULL + AND description ~ '\d{6}'; + +-- 미전기분 재생성 조회/삭제용 부분 인덱스 +CREATE INDEX IF NOT EXISTS idx_cae_settle_unposted + ON contract_accounting_entry(settle_month) + WHERE journal_no IS NULL; diff --git a/ga-core/src/main/java/com/ga/core/mapper/accounting/ContractAccountingEntryMapper.java b/ga-core/src/main/java/com/ga/core/mapper/accounting/ContractAccountingEntryMapper.java index 142ca34..e196c97 100644 --- a/ga-core/src/main/java/com/ga/core/mapper/accounting/ContractAccountingEntryMapper.java +++ b/ga-core/src/main/java/com/ga/core/mapper/accounting/ContractAccountingEntryMapper.java @@ -20,6 +20,12 @@ public interface ContractAccountingEntryMapper { /** 배치 step용: 정산월 기준 분개 대상 원장 조회 (recruit+maintain ledger UNION) */ List selectBySettleMonth(@Param("settleMonth") String settleMonth); + /** + * 배치 step용: 정산월의 미전기(journal_no IS NULL) 분개 삭제 — 재생성 멱등성 확보. + * 마감 채번된 분개는 보존. + */ + int deleteUnpostedBySettleMonth(@Param("settleMonth") String settleMonth); + /** * 분개 엔트리 일괄 INSERT. * 회계 자동분개 배치 step에서 호출. diff --git a/ga-core/src/main/java/com/ga/core/vo/accounting/ContractAccountingEntryVO.java b/ga-core/src/main/java/com/ga/core/vo/accounting/ContractAccountingEntryVO.java index 8025350..b0e4817 100644 --- a/ga-core/src/main/java/com/ga/core/vo/accounting/ContractAccountingEntryVO.java +++ b/ga-core/src/main/java/com/ga/core/vo/accounting/ContractAccountingEntryVO.java @@ -22,6 +22,8 @@ public class ContractAccountingEntryVO { private String creditAccount; private BigDecimal amount; private String description; + /** 정산월 (YYYYMM) — 배치 재생성 시 미전기분 멱등 삭제 키 */ + private String settleMonth; /** 마감 시 채번. NULL=미전기 */ private String journalNo; private LocalDateTime createdAt; diff --git a/ga-core/src/main/resources/mapper/accounting/ContractAccountingEntryMapper.xml b/ga-core/src/main/resources/mapper/accounting/ContractAccountingEntryMapper.xml index c5ced9e..9eb4190 100644 --- a/ga-core/src/main/resources/mapper/accounting/ContractAccountingEntryMapper.xml +++ b/ga-core/src/main/resources/mapper/accounting/ContractAccountingEntryMapper.xml @@ -66,14 +66,21 @@ INSERT INTO contract_accounting_entry (contract_id, payment_id, entry_date, - debit_account, credit_account, amount, description) + debit_account, credit_account, amount, description, settle_month) VALUES (#{e.contractId}, #{e.paymentId}, #{e.entryDate}, - #{e.debitAccount}, #{e.creditAccount}, #{e.amount}, #{e.description}) + #{e.debitAccount}, #{e.creditAccount}, #{e.amount}, #{e.description}, #{e.settleMonth}) + + + DELETE FROM contract_accounting_entry + WHERE settle_month = #{settleMonth} + AND journal_no IS NULL + + UPDATE contract_accounting_entry @@ -98,6 +105,7 @@ '2530' AS credit_account, l.agent_amount AS amount, CONCAT('모집수수료 ', l.settle_month) AS description, + l.settle_month AS settle_month, NULL AS journal_no FROM recruit_ledger l WHERE l.settle_month = #{settleMonth} @@ -110,6 +118,7 @@ '2530' AS credit_account, l.agent_amount AS amount, CONCAT('유지수수료 ', l.settle_month) AS description, + l.settle_month AS settle_month, NULL AS journal_no FROM maintain_ledger l WHERE l.settle_month = #{settleMonth}