원장 전체 재계산
기업 원장의 balance_after 와 remain_credit 을 첫 행부터 다시 굴려 한 번에 맞춥니다.
원장 전체 재계산
기업 원장의 balanceAfter 를 첫 행부터 끝까지 다시 계산하고, 기업 잔액(remainCredit)을
원장 마지막 값에 맞춥니다.
기존 수정·삭제 API(PUT /ledger/{txId} 등)는 고친 행 이후만 다시 계산합니다.
이 API는 앵커 없이 전 구간을 다시 계산합니다.
언제 쓰나
- 원장과 기업 잔액이 어긋나 있을 때 (과거 데이터 정리, 마이그레이션 후 보정)
- 과거 내역을 여러 건 넣은 뒤 한 번에 정리할 때
- 원장 도입 이전 이월분(기초 잔액)이 틀려서 바로잡을 때
지금까지 이런 상황에서 DB 에 직접 UPDATE ... SUM(amount) OVER (...) 를 돌렸다면,
이 API 로 대체하세요. 손으로 쓴 쿼리는 BUDGET_ADJUST 행을 합산에서 빼지 않아
잔액이 어긋나기 쉽고, TB_BUSINESS.remain_credit 도 함께 갱신되지 않습니다.
HTTP 요청
POST /ai/admin/finance/ledger/business/{businessAccountId}/recalculate
Authorization: Bearer {access_token}Path Parameters
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
businessAccountId | Long | 예 | 기업 계정 id |
Query Parameters
| 파라미터 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
apply | boolean | 아니오 | false | false 면 미리보기(아무것도 저장 안 함). true 여야 실제로 저장 |
adminId | String | 아니오 | — | 처리자 ID (로그 기록용) |
apply 를 빠뜨리면 아무 일도 일어나지 않습니다. 기업 원장을 통째로 다시 쓰는
작업이라 미리보기를 기본으로 뒀습니다. 화면에서는 "미리보기 → 결과 확인 → 적용"
2단계로 붙여주세요.
계산 규칙
| 항목 | 규칙 |
|---|---|
| 정렬 | occurredAt ASC, id ASC |
| 대상 | 소프트 삭제되지 않은 행 (deletedAt IS NULL) |
| 합산 제외 | BUDGET_ADJUST — 예산 수정 audit 행이라 amount 가 0 이 아니어도 잔액을 움직이지 않음 |
| 시작값 | 첫 행에서 역산한 이월분 = 첫 행.balanceAfter - 첫 행.amount |
이월분(기초 잔액)이란
원장은 나중에 도입됐습니다. 그 이전의 충전·차감은 행이 없고 결과만 기업 잔액에
남아 있습니다. 그래서 첫 행의 balanceAfter 가 그 행의 amount 보다 큽니다.
| 행 | 타입 | amount | balanceAfter |
|---|---|---|---|
| 1 | CREDIT_ADD | +50,000 | 150,000 |
| 2 | CAMPAIGN_DEPOSIT | -30,000 | 120,000 |
| 3 | CREDIT_ADD | +20,000 | 140,000 |
첫 행의 150,000 - 50,000 = 100,000 이 원장에 행으로 없는 이월분입니다. 재계산은
이 값에서 출발하므로 기존 잔액이 보존됩니다.
이월분이 틀렸을 때
이 API 는 이월분을 바꾸지 않습니다. 지금 값을 그대로 쓰고 아래로 굴리기만 합니다. 이월분 자체가 틀렸다면 첫 행 잔액 지정 API 를 쓰세요. 응답 형태는 같습니다.
응답
성공 (200 OK)
{
"status": 200,
"code": null,
"message": "원장 재계산 미리보기",
"data": {
"businessAccountId": 418,
"openingBalance": 100000,
"firstTxId": 451,
"openingOverridden": false,
"applied": false,
"scannedCount": 168,
"mismatchedCount": 12,
"remainCreditBefore": 140000,
"remainCreditAfter": 120000,
"skippedEmptyLedger": false,
"diffs": [
{
"txId": 619,
"occurredAt": "2026-08-05T17:55:06",
"transactionType": "CAMPAIGN_DEPOSIT",
"amount": -30000,
"balanceAfterBefore": 999,
"balanceAfterCalculated": 120000
}
],
"diffsTruncated": 0
}
}필드
| 필드 | 타입 | 설명 |
|---|---|---|
openingBalance | int | 이번 계산에 쓴 이월분(시작값) |
firstTxId | Long | 원장 첫 행 id. 원장이 비었으면 null |
openingOverridden | boolean | 이월분을 덮어썼는가. 이 API 에서는 항상 false (첫 행 잔액 지정에서만 true) |
applied | boolean | 실제로 저장했는가. false 면 미리보기 |
scannedCount | int | 훑은 행 수 |
mismatchedCount | int | 계산값과 달랐던 행 수. 0 이면 원장이 이미 맞습니다 |
remainCreditBefore / remainCreditAfter | Integer | 기업 잔액 변화. applied=false 면 "바뀔 값" |
skippedEmptyLedger | boolean | 원장에 행이 없어 잔액을 건드리지 않았는가 |
diffs | array | 어긋난 행 샘플 (최대 100건) |
diffsTruncated | int | 샘플에 못 담고 잘린 행 수 |
에러
| 상태 코드 | 설명 |
|---|---|
404 | 기업을 찾을 수 없음 |
화면 연동 순서
apply없이 호출 →mismatchedCount와diffs로 무엇이 어긋났는지 표시- 확인되면
apply=true로 실행 openingBalance자체가 틀렸다면 첫 행 잔액 지정 API 로 먼저 바로잡기
현재 첫 행보다 더 오래된 행을 새로 넣으면 첫 행이 바뀝니다. 그러면 이월분도
새 첫 행 기준으로 다시 역산되므로 값이 달라질 수 있습니다. 응답의 firstTxId 로
첫 행이 무엇인지 확인하세요.
안전장치
- 원장에 행이 없으면 잔액을 건드리지 않습니다 (
0으로 밀면 원장 밖에서 들어온 잔액이 사라짐) apply=true시 기업 행을 잠급니다 (동시에 들어온 거래가 덮어써지지 않도록)- 미리보기는 읽기 전용 트랜잭션이라 저장 경로가 아예 없습니다