docs(agent): 에이전트 정산 QA 기록을 최신화한다

This commit is contained in:
2026-04-10 13:51:43 +09:00
parent 0b61569522
commit c8898e8f7e

View File

@@ -12,6 +12,14 @@
- [x] 실패 시 재현 경로와 실제 증거를 확보해 결함 여부를 확정한다.
- [x] 최종 PASS/FAIL 판정을 정리하고 검증 기록을 남긴다.
## 구현 체크리스트
- [x] assignment/finalize 가드 미체크 항목 회귀 테스트를 추가한다.
- [x] calculate empty-result/snapshot pagination/finalized immutability 회귀 테스트를 추가한다.
- [x] ratio 목록 응답을 member 단위 current/history 구조로 확장한다.
- [x] generic 4종 calculate total 경로를 전용 total 계산 경로로 분리한다.
- [x] finalize snapshot 생성 경로의 중복 groupBy/row 변환을 줄인다.
- [x] 관련 테스트, 진단, 수동 검증 결과를 문서에 반영한다.
## 검증 대상 축
- assignment create/remove 시간 경계 동작
- ratio create/update 검증과 history 동작
@@ -24,7 +32,7 @@
### P0
- [x] assignment 생성: 유효한 agent/creator/assignedAt이면 신규 이력 row가 생성된다.
- [ ] assignment 생성: agentId와 creatorId가 같으면 거부된다.
- [x] assignment 생성: agentId와 creatorId가 같으면 거부된다.
- [x] assignment 생성: agent가 AGENT 역할이 아니면 거부된다.
- [x] assignment 생성: creator가 CREATOR 역할이 아니면 거부된다.
- [x] assignment 생성: 활성 소속과 시간이 겹치면 거부된다.
@@ -47,19 +55,56 @@
- [x] snapshot finalize: 동일 기간/타입/agent 재요청은 중복 저장 없이 alreadyFinalized=true를 반환한다.
### P1
- [ ] admin finalize: 대상 member가 없으면 실패한다.
- [ ] admin finalize: 대상 member가 AGENT 역할이 아니면 실패한다.
- [x] admin finalize: 대상 member가 없으면 실패한다.
- [x] admin finalize: 대상 member가 AGENT 역할이 아니면 실패한다.
- [x] agent controller: 익명 사용자는 creator/list 조회에 실패한다.
- [x] admin finalize controller: 익명 사용자는 finalize 호출에 실패한다.
- [x] channel donation 조회: 분할 정산 레코드가 있어도 후원 건수는 distinct useCan 기준이다.
- [ ] calculate 조회: 기간 내 결과가 없으면 total=0, items=[]로 일관되게 반환된다.
- [ ] assigned creator 목록 조회: 정렬/페이지네이션이 creatorId desc 기준으로 유지된다.
- [x] calculate 조회: 기간 내 결과가 없으면 total=0, items=[]로 일관되게 반환된다.
- [x] assigned creator 목록 조회: 정렬/페이지네이션이 creatorId desc 기준으로 유지된다.
### P2
- [ ] snapshot read: snapshot pagination이 `creatorId desc` 기준으로 안정적이다.
- [ ] ratio 목록 조회: current/history가 페이지 응답에서 누락 없이 노출된다.
- [x] snapshot read: snapshot pagination이 `creatorId desc` 기준으로 안정적이다.
- [x] ratio 목록 조회: current/history가 페이지 응답에서 누락 없이 노출된다.
- [x] provenance detail 합계는 snapshot summary 합계와 일치한다.
- [ ] finalized 이후 ratio/assignment 변경이 생겨도 동일 finalized 기간 응답은 변하지 않는다.
- [x] finalized 이후 ratio/assignment 변경이 생겨도 동일 finalized 기간 응답은 변하지 않는다.
## 미체크 항목 재확인 및 후속 방안
- `assignment 생성: agentId와 creatorId가 같으면 거부된다.`
- 현재 상태: `AdminAgentCreatorService.assignCreator()`가 동일 ID를 `partner.agent.assignment.invalid_relation`으로 즉시 거부한다 (`src/main/kotlin/kr/co/vividnext/sodalive/admin/partner/agent/assignment/AdminAgentCreatorService.kt:22-25`).
- 후속 방안: `AdminAgentCreatorServiceTest``agentId == creatorId` 요청을 추가해 동일 예외 키를 직접 검증한다.
- `admin finalize: 대상 member가 없으면 실패한다.`
- 현재 상태: `AdminAgentSettlementSnapshotService.getAgent()`가 member 미존재 시 `partner.agent.ratio.agent_not_found`를 던진다 (`src/main/kotlin/kr/co/vividnext/sodalive/admin/partner/agent/settlement/AdminAgentSettlementSnapshotService.kt:122-128`).
- 후속 방안: `AdminAgentSettlementSnapshotServiceTest``memberRepository.findById()`가 비어 있는 케이스를 추가해 finalize 진입 전 실패를 검증한다.
- `admin finalize: 대상 member가 AGENT 역할이 아니면 실패한다.`
- 현재 상태: 같은 `getAgent()`에서 role이 `AGENT`가 아니면 `partner.agent.ratio.invalid_agent`를 던진다 (`src/main/kotlin/kr/co/vividnext/sodalive/admin/partner/agent/settlement/AdminAgentSettlementSnapshotService.kt:122-128`).
- 후속 방안: `AdminAgentSettlementSnapshotServiceTest``USER` 또는 `CREATOR` role member를 주입해 예외 키를 직접 검증한다.
- `calculate 조회: 기간 내 결과가 없으면 total=0, items=[]로 일관되게 반환된다.`
- 현재 상태: 조회 repository는 total count를 `?: 0`으로 반환하고, 페이지 대상 creator가 없으면 빈 목록을 반환한다. 서비스도 이 값을 그대로 응답으로 조립한다 (`src/test/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateQueryRepositoryTest.kt:272-326`, `src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateService.kt:200-211`).
- 후속 방안: in-range 데이터가 전혀 없는 fixture로 `AgentCalculateService` 또는 query integration test를 추가해 `total.count/total.totalCan/...`가 모두 0이고 `items=[]`인 응답 계약을 직접 고정한다.
- `snapshot read: snapshot pagination이 creatorId desc 기준으로 안정적이다.`
- 현재 상태: snapshot 조회는 `findAllByPeriodStartAndPeriodEndAndSettlementTypeAndAgentIdOrderByCreatorIdDesc(...)`를 사용해 정렬 기준 자체는 명시되어 있다 (`src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/settlement/snapshot/AgentSettlementSnapshotRepository.kt:14-19`). 다만 다건 snapshot에서 offset/limit slicing 순서를 직접 검증하는 테스트는 없다.
- 후속 방안: `AgentCalculateServiceTest`에 creatorId가 다른 snapshot 3건 이상을 주입하고, `offset/limit`별 응답 순서와 크기를 검증하는 회귀 테스트를 추가한다.
- `ratio 목록 조회: current/history가 페이지 응답에서 누락 없이 노출된다.`
- 현재 상태: 현재 응답 DTO는 `items: List<GetAgentSettlementRatioItem>` 단일 flat 구조이고 (`src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/ratio/GetAgentSettlementRatioResponse.kt:6-17`), query도 ratio row를 flat list로만 조회한다 (`src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/ratio/AgentSettlementRatioRepository.kt:23-49`). 기존 서비스 테스트도 flat 목록 1건만 검증한다 (`src/test/kotlin/kr/co/vividnext/sodalive/admin/partner/agent/ratio/AgentSettlementRatioServiceTest.kt:344-367`).
- 구현 방안: `current/history`를 분리한 응답 DTO를 새로 정의하고, query/service 계층에서 member별 ratio row를 current 1건 + history n건으로 재구성하도록 응답 모델을 확장한다. 페이지 기준은 member 단위로 재정의하고, current/history 누락 회귀 테스트를 함께 추가한다.
- `finalized 이후 ratio/assignment 변경이 생겨도 동일 finalized 기간 응답은 변하지 않는다.`
- 현재 상태: finalize는 snapshot/source detail에 당시 값을 저장하고 (`src/test/kotlin/kr/co/vividnext/sodalive/admin/partner/agent/settlement/AdminAgentSettlementSnapshotServiceTest.kt:44-202`), 읽기 경로는 finalized snapshot을 live 계산보다 우선 사용한다 (`src/test/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateServiceTest.kt:458-598`). 다만 finalize 이후 assignment/ratio를 실제로 바꾼 뒤 같은 기간 재조회가 불변인지 직접 검증하는 테스트는 없다.
- 후속 방안: finalize 이후 assignment/ratio fixture를 변경한 뒤 동일 기간 read 응답이 snapshot 값 그대로 유지되는지를 검증하는 통합 회귀 테스트를 추가한다.
## 추가 구조/성능 메모
- `우선 반영 1순위: AgentCalculateService.buildSettlementByCreatorResponse()``totalRowsLoader` 기반 total 계산을 DB total query로 분리한다.
- 현재 경로: generic 4종(LIVE/CONTENT/COMMUNITY/CONTENT_DONATION) total은 `totalRowsLoader(startDate, endDate).toMergedResponseItems().toResponseTotal()`로 계산된다 (`src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateService.kt:200-203`).
- 현재 비용: 기간 전체 `GetAgentCreatorSettlementSummaryQueryData`를 메모리에 적재한 뒤, 각 row마다 `toResponseItem()`에서 `BigDecimal` 기반 금액 계산을 수행하고 (`src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/GetAgentCreatorSettlementSummaryQueryData.kt:17-40`), 다시 `groupBy { creatorId }`로 creator별 병합 후 (`.../GetAgentCreatorSettlementSummaryQueryData.kt:60-77`), 마지막에 `sumOf`로 grand total을 한 번 더 순회한다 (`src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/GetAgentSettlementByCreatorResponse.kt:33-43`).
- 왜 1순위인가: pagedRowsLoader는 페이지 범위만 읽지만 totalRowsLoader는 기간 전체 row를 읽는다. 따라서 메모리 사용량과 GC 비용, total 계산 지연을 동시에 줄이려면 total 경로를 먼저 제거하는 편이 효과가 가장 크다.
- row granularity 주의점: total을 단순 `sum(totalCan)`으로 바꾸면 안 된다. 현재 row는 creator 1건이 아니라 assignment/agent ratio/settlement ratio 기준으로 분할되어 있고 (`src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateQueryRepository.kt:392-596`), 같은 creator도 콘텐츠 개별 ratio 차이로 여러 row가 생긴다 (`src/test/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateQueryRepositoryTest.kt:145-176`). 또한 거래 시점 assignment/agent ratio 이력 때문에 row가 갈라진다 (`.../AgentCalculateQueryRepositoryTest.kt:328-381`, `421-470`).
- 핵심 관찰: total 계산에서 creator별 병합 자체는 필수가 아니다. 현재 로직은 row → creator 병합 → grand total 순서지만, 최종 total 값은 row별 계산 결과를 그대로 모두 더한 값과 동일하다. 즉 total 전용 경로는 creator 응답 shape를 만들 필요 없이 “현재 row granularity의 정산 결과 총합”만 DB에서 반환하면 된다.
- 권장 구현 방안: `AgentCalculateQueryRepository`에 generic 4종용 `getCalculate*Total()` 전용 query를 추가하고, `GetAgentSettlementByCreatorTotal`에 대응하는 total projection을 `fetchOne()`으로 바로 반환한다. 이때 현재 반올림/비율 적용 semantics를 유지하려면, 기존 grouped row granularity를 유지한 뒤 그 row 단위 정산 금액을 다시 합산하는 형태가 필요하다. Spring Boot 2.7 + Querydsl JPA 제약을 고려하면, 1차 권장은 파생 테이블(native SQL 또는 Querydsl SQL/JPASQLQuery fallback) 기반 total query이고, 로컬 선례는 total을 목록 쿼리와 분리한 `AdminChannelDonationCalculateQueryRepository.getChannelDonationSettlementTotal()` / `CreatorAdminChannelDonationCalculateQueryRepository.getChannelDonationSettlementTotal()`이다 (`src/main/kotlin/kr/co/vividnext/sodalive/admin/calculate/channelDonation/AdminChannelDonationCalculateQueryRepository.kt:33-54`, `src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/calculate/channelDonation/CreatorAdminChannelDonationCalculateQueryRepository.kt:18-38`).
- 차선책: DB query 분리가 바로 어렵다면, 최소한 total 계산에서 `.toMergedResponseItems()`를 제거하고 row를 단일 `fold`로 누적해 creator별 `groupBy`/중간 리스트 생성을 없앨 수 있다. 다만 이 방법은 기간 전체 row 적재 자체는 남기므로 임시 완화책으로만 본다.
- 구현 결과: 이번 단계에서는 `List<GetAgentCreatorSettlementSummaryQueryData>.toResponseTotal()` 전용 경로를 추가해 generic 4종 total 계산에서 creator별 `groupBy`와 중간 `GetAgentSettlementByCreatorItem` 리스트 병합을 제거했다. DB total query 분리는 Spring Boot 2.7 + Querydsl JPA 제약을 고려한 후속 최적화 후보로 남긴다.
- `AdminAgentSettlementSnapshotService.finalizeSnapshots()`는 DB가 assignment/ratio 단위로 이미 집계한 row를 creator별 snapshot 1건으로 다시 합산하는데, 이 합산 자체는 필요하지만 `rows.groupBy { creatorId }` 뒤에 `toMergedResponseItems()`가 다시 같은 key로 groupBy를 수행해 불필요한 재-groupBy가 한 번 더 발생한다. 반면 source detail은 query row 1건을 detail 1건으로 보존하므로 추가 집계는 없고, 동일 row의 `toResponseItem()` 변환만 summary/detail에서 각각 반복된다 (`src/main/kotlin/kr/co/vividnext/sodalive/admin/partner/agent/settlement/AdminAgentSettlementSnapshotService.kt:131-179`, `182-253`).
- 구현 방안: finalize 내부에서는 creator별 재-groupBy 없는 전용 merge 로직을 두고, row 변환 결과를 summary/detail 양쪽에서 재사용하도록 draft 구조를 조정한다.
- 구현 결과: `SnapshotAggregateDraft` 단일 패스 누적 구조로 summary/source detail 수치를 함께 합산하도록 바꿔 creator별 재-groupBy와 중복 `toResponseItem()` 변환을 제거했다.
## 검증 기록
@@ -85,3 +130,42 @@
- `docs/20260408_에이전트권한및정산기능추가.md:644-658` 확인 → 위 creator list window 버그가 17차 수정에서 실제 수정되고 테스트/빌드 검증까지 완료되었음을 확인했다.
- `src/test/kotlin/kr/co/vividnext/sodalive/admin/partner/agent/settlement/AdminAgentSettlementSnapshotServiceTest.kt:191-201` 확인 → provenance detail 각 수치 합계가 snapshot summary와 일치하는 검증이 이미 존재함을 확인했다.
- 위 세 근거에 따라 creator list 2개 항목의 실패 메모를 제거하고, `provenance detail 합계` 항목을 완료 처리했다.
### 3차 미체크 항목/구조 점검
- 무엇을:
- QA 문서에 남아 있던 미체크 항목을 구현/테스트 기준으로 다시 분류했다.
- `AgentCalculateService`의 creator 합산 경로와 `AdminAgentSettlementSnapshotService` finalize 경로의 중복 집계 여부를 코드 기준으로 재점검했다.
- 왜:
- 현재 문서에는 “구현은 이미 있으나 테스트가 빠진 항목”, “응답 모델 자체가 요구를 충족하지 못하는 항목”, “기능 결함은 아니지만 구조적으로 비효율적인 항목”이 섞여 있어 후속 작업 우선순위가 드러나지 않았기 때문이다.
- 어떻게:
- 명령 실행 없음: 이번 단계는 문서/코드 정합성 점검 목적이라 읽기 전용 파일 검토만 수행했다.
- `src/main/kotlin/kr/co/vividnext/sodalive/admin/partner/agent/assignment/AdminAgentCreatorService.kt:22-25` 확인 → 동일 agentId/creatorId 거부 로직은 구현돼 있으나 직접 테스트가 없음을 확인했다.
- `src/test/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateQueryRepositoryTest.kt:69-89``src/test/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateControllerTest.kt:37-62` 확인 → assigned creator 정렬/페이지네이션은 이미 검증돼 있어 완료 처리했다.
- `src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/ratio/GetAgentSettlementRatioResponse.kt:6-17``src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/ratio/AgentSettlementRatioRepository.kt:23-49` 확인 → ratio 목록은 flat 응답만 제공하므로 `current/history` 요구를 충족하려면 응답 모델과 조회 로직 확장이 필요함을 확인했다.
- `src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateService.kt:141-168,172-211` 확인 → finalized snapshot 경로가 전체 snapshot row를 메모리에서 page/total 처리함을 확인했다.
- `src/main/kotlin/kr/co/vividnext/sodalive/admin/partner/agent/settlement/AdminAgentSettlementSnapshotService.kt:131-179,182-253` 확인 → finalize summary 생성 시 creator별 재-groupBy가 한 번 더 수행되고, source detail은 집계 대신 row 변환만 반복함을 확인했다.
### 4차 totalRowsLoader 개선 방향 구체화
- 무엇을:
- `buildSettlementByCreatorResponse()``totalRowsLoader` 경로만 따로 떼어 메모리/성능 병목과 개선 우선순위를 구체화했다.
- 왜:
- 기존 성능 메모는 “live 계산 경로 전반을 DB 쿼리로 옮기자” 수준이라 범위가 넓었고, 실제로 어떤 부분을 먼저 고쳐야 효과가 큰지 드러나지 않았기 때문이다.
- 어떻게:
- 명령 실행 없음: 이번 단계도 읽기 전용 코드/문서 검토만 수행했다.
- `src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateService.kt:200-203` 확인 → total이 `totalRowsLoader(...).toMergedResponseItems().toResponseTotal()`로 계산됨을 다시 확인했다.
- `src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/GetAgentCreatorSettlementSummaryQueryData.kt:17-40,60-77``src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/GetAgentSettlementByCreatorResponse.kt:33-43` 확인 → row별 금액 계산, creator별 groupBy 병합, grand total 재합산이 모두 애플리케이션 메모리에서 수행됨을 확인했다.
- `src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateQueryRepository.kt:392-596` 확인 → generic 4종 쿼리가 assignment/agent ratio/settlement ratio 기준의 grouped row를 반환함을 확인했다.
- `src/test/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateQueryRepositoryTest.kt:145-176,328-381,421-470` 확인 → 같은 creator도 settlement ratio/assignment/agent ratio 이력 때문에 여러 row로 분할될 수 있어 total 계산이 단순 `sum(totalCan)`으로 대체되면 안 됨을 확인했다.
- `src/main/kotlin/kr/co/vividnext/sodalive/admin/calculate/channelDonation/AdminChannelDonationCalculateQueryRepository.kt:33-54``src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/calculate/channelDonation/CreatorAdminChannelDonationCalculateQueryRepository.kt:18-38` 확인 → 이 저장소 안에 total 전용 `fetchOne()` aggregate query 선례가 이미 있음을 확인했다.
### 5차 미체크 항목 구현 및 성능 완화
- 무엇을:
- assignment/finalize 가드, calculate empty-result, snapshot pagination, finalized immutability 회귀 테스트를 추가했다.
- ratio 목록 응답을 member 단위 `current/history` 구조로 확장하고, generic total/finalize 내부 계산 경로를 가볍게 정리했다.
- 왜:
- 문서에 남아 있던 미체크 항목을 실제 테스트와 응답 계약으로 고정해야 후속 회귀를 막을 수 있고, total/finalize 경로의 불필요한 groupBy/중간 객체 생성을 줄여야 현재 범위 안에서 안전하게 성능을 완화할 수 있기 때문이다.
- 어떻게:
- 성공: `./gradlew test --tests kr.co.vividnext.sodalive.admin.partner.agent.assignment.AdminAgentCreatorServiceTest --tests kr.co.vividnext.sodalive.admin.partner.agent.settlement.AdminAgentSettlementSnapshotServiceTest --tests kr.co.vividnext.sodalive.partner.agent.calculate.AgentCalculateServiceTest` → BUILD SUCCESSFUL
- 성공: `./gradlew test --tests kr.co.vividnext.sodalive.admin.partner.agent.ratio.AgentSettlementRatioServiceTest --tests kr.co.vividnext.sodalive.admin.partner.agent.ratio.AdminAgentSettlementRatioControllerTest` → BUILD SUCCESSFUL
- 성공: `./gradlew test --tests kr.co.vividnext.sodalive.admin.partner.agent.assignment.AdminAgentCreatorServiceTest --tests kr.co.vividnext.sodalive.admin.partner.agent.settlement.AdminAgentSettlementSnapshotServiceTest --tests kr.co.vividnext.sodalive.partner.agent.calculate.AgentCalculateServiceTest --tests kr.co.vividnext.sodalive.partner.agent.calculate.AgentCalculateQueryRepositoryTest --tests kr.co.vividnext.sodalive.admin.partner.agent.ratio.AgentSettlementRatioServiceTest --tests kr.co.vividnext.sodalive.admin.partner.agent.ratio.AdminAgentSettlementRatioControllerTest` → BUILD SUCCESSFUL
- 참고: `lsp_diagnostics`는 현재 환경에 Kotlin LSP가 없어 사용할 수 없었고, 대신 위 Gradle 실행에서 `compileKotlin`/`compileTestKotlin`까지 함께 통과한 것으로 컴파일 진단을 대체했다.