# 관리자 에이전트 정산 상세 조회 설계 ## 문서 목적 - 관리자 페이지에서 에이전트 목록을 보고, 특정 에이전트 상세 화면에서 소속 크리에이터와 에이전트별 정산 현황을 조회할 수 있도록 백엔드 read API 설계를 고정한다. - 기존 `partner/agent/calculate` 계산 로직은 최대한 재사용하고, `ADMIN` 전용 조회 진입점만 별도로 추가한다. ## 요구사항 정리 - 관리자 화면에는 에이전트 리스트가 필요하다. - 에이전트 닉네임 - 에이전트에 속한 크리에이터 수 - 관리자 화면에는 크리에이터 검색이 필요하다. - 특정 에이전트에 크리에이터를 소속시키기 위한 검색 - 관리자 화면에는 특정 에이전트에 현재 소속된 크리에이터 목록이 필요하다. - 에이전트 소속 해제를 위한 목록 - 관리자 화면에는 특정 에이전트 기준 정산 상세가 필요하다. - 라이브 정산 현황 - 콘텐츠 판매 정산 현황 - 커뮤니티 정산 현황 - 채널 후원 정산 현황 - 콘텐츠 후원 정산 현황 ## 범위와 해석 - 이번 설계는 **B안: 에이전트 목록 → 에이전트 상세** 흐름을 기준으로 한다. - 에이전트 목록 화면에서는 요약 정보만 제공한다. - 실제 정산 데이터는 에이전트 상세 화면에서 조회한다. - 기존 `assignment`, `ratio`, `settlement/finalize` 쓰기 기능은 유지하고, 이번 범위에서는 관리자용 read API만 추가한다. ## 현재 코드 기준 확인 사항 - 관리자 전용 에이전트 관련 컨트롤러는 이미 존재한다. - `src/main/kotlin/kr/co/vividnext/sodalive/admin/partner/agent/assignment/AdminAgentCreatorController.kt` - `src/main/kotlin/kr/co/vividnext/sodalive/admin/partner/agent/ratio/AdminAgentSettlementRatioController.kt` - `src/main/kotlin/kr/co/vividnext/sodalive/admin/partner/agent/settlement/AdminAgentSettlementSnapshotController.kt` - 에이전트 본인 전용 정산 조회 컨트롤러도 이미 존재한다. - `src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateController.kt` - 따라서 현재 부족한 것은 정산 계산 로직이 아니라, `ADMIN`이 특정 `agentId`를 지정해 같은 데이터를 읽는 관리자 전용 read API 레이어다. ## 권장 아키텍처 ### 1. 관리자 전용 read 진입점 추가 - 신규 패키지: `kr.co.vividnext.sodalive.admin.partner.agent.read` - 신규 구성 - `AdminAgentReadController` - `AdminAgentReadService` - `AdminAgentReadQueryRepository` - 역할 - 에이전트 목록 조회 - 크리에이터 검색 조회 - 특정 에이전트 소속 크리에이터 목록 조회 - 특정 에이전트 기준 5종 정산 조회 ### 2. 기존 도메인 계산 로직 재사용 - 기존 계산/집계 로직은 계속 `kr.co.vividnext.sodalive.partner.agent.calculate` 아래에 둔다. - 관리자용 read 서비스는 기존 `AgentCalculateService`, `AgentCalculateQueryRepository`, snapshot 조회 경로를 재사용한다. - 현재 `AgentCalculateService`의 정산 조회 메서드는 이미 `agentId`를 직접 받으므로, 관리자 read 서비스는 이 메서드를 그대로 호출한다. - 최종안은 **정산 계산 로직은 `partner.agent.calculate`에 유지하고, ADMIN은 별도 read 서비스에서 기존 agentId 기반 조회 메서드를 재사용하는 구조**다. ### 3. 쓰기와 읽기 축 분리 - 쓰기 축 - `admin.partner.agent.assignment` - `admin.partner.agent.ratio` - `admin.partner.agent.settlement` - 읽기 축 - `admin.partner.agent.read` - `partner.agent.calculate` - 이렇게 나누면 “관리자 권한으로 읽는다”와 “정산 계산 규칙을 제공한다”의 책임이 섞이지 않는다. ## 화면 흐름 기준 API 설계 ### 1. 에이전트 목록 API - 목적: 관리자 화면의 첫 진입 리스트 - 권장 경로: `GET /admin/partner/agent/list` - 권한: `ADMIN` - 요청 파라미터 - `pageable` - 응답 필드 - `agentId` - `agentNickname` - `assignedCreatorCount` - `liveAgentSettlementAmount` - `contentAgentSettlementAmount` - `communityAgentSettlementAmount` - `contentDonationAgentSettlementAmount` - `channelDonationAgentSettlementAmount` - `totalCount` - 조회 기준 - `Member.role == AGENT` - 현재 활성 소속 크리에이터 수만 집계 - 활성 소속 판정은 현재 코드의 assignment window 규칙과 동일하게 현재 시각 기준 `assignedAt <= now < unassignedAt(or null)`를 따른다. - 정산 합계는 별도 날짜 입력 없이 **현재 월 기준**으로 계산한다. - 기준 시간대: `Asia/Seoul` - 시작 시각: 현재 월 1일 `00:00:00` - 종료 시각: 다음 달 1일 `00:00:00` 직전까지 포함되는 배타 상한 방식 - 실제 조회 구간은 위 KST 월 경계를 UTC `LocalDateTime`으로 변환해 DB의 UTC 저장 시각과 비교한다. - 각 합계 값의 의미는 해당 기간의 상세 조회 응답 `total.agentSettlementAmount`와 동일하다. - 해당 월에 정산 내역이 없더라도 에이전트 목록에서는 제외하지 않고, 5종 합계를 모두 `0`으로 내려준다. ### 2. 크리에이터 검색 API - 목적: 특정 에이전트에 크리에이터를 소속시키기 전 검색 - 권장 경로: `GET /admin/partner/agent/creator/search` - 권한: `ADMIN` - 요청 파라미터 - `search_word` - `pageable` - 응답 필드 - `creatorId` - `creatorNickname` - `currentAgentId` nullable - `currentAgentNickname` nullable - 동작 원칙 - 기존 `AdminMemberController.searchCreator()` 검색 관례를 따른다. - 검색 결과에 현재 활성 소속 agent 정보를 붙여서, 이미 다른 agent 소속인지 운영자가 바로 판단할 수 있게 한다. ### 3. 특정 에이전트 소속 크리에이터 목록 API - 목적: 상세 화면의 소속 크리에이터 탭 - 권장 경로: `GET /admin/partner/agent/{agentId}/creator/list` - 권한: `ADMIN` - 응답 필드 - `creatorId` - `creatorNickname` - `assignedAt` - 참고 - 현재 `GetAgentAssignedCreatorResponse`는 `creatorId`, `creatorNickname`만 제공한다. - 관리자 상세 화면에서는 운영 판단을 위해 `assignedAt`이 같이 내려가는 편이 자연스럽다. ### 4. 특정 에이전트 정산 상세 API 5종 - 목적: 에이전트 상세 화면의 정산 탭 - 권한: `ADMIN` - 권장 경로 - `GET /admin/partner/agent/{agentId}/calculate/live-by-creator` - `GET /admin/partner/agent/{agentId}/calculate/content-by-creator` - `GET /admin/partner/agent/{agentId}/calculate/community-by-creator` - `GET /admin/partner/agent/{agentId}/calculate/channel-donation-by-creator` - `GET /admin/partner/agent/{agentId}/calculate/content-donation-by-creator` - 공통 요청 파라미터 - `startDateStr` - `endDateStr` - `pageable` - 응답 원칙 - 기존 AGENT 전용 응답 계약을 가능한 그대로 유지한다. - `totalCount`, `total`, `items` 구조 유지 - 각 item의 집계 필드 유지 - `count` - `totalCan` - `krw` - `fee` - `settlementAmount` - `agentSettlementAmount` - 차이점 - 기존 AGENT API는 로그인 principal에서 `agentId`를 얻는다. - 관리자 API는 path variable `agentId`를 받는다. ## 데이터 모델/응답 설계 ### 1. 에이전트 목록 응답 DTO - 신규 DTO 필요 - DTO 명 - `GetAdminAgentListResponse` - `GetAdminAgentListItem` - 필수 필드 - response: `totalCount: Int`, `items: List` - item: - `agentId: Long` - `agentNickname: String` - `assignedCreatorCount: Int` - `liveAgentSettlementAmount: Int` - `contentAgentSettlementAmount: Int` - `communityAgentSettlementAmount: Int` - `contentDonationAgentSettlementAmount: Int` - `channelDonationAgentSettlementAmount: Int` ### 2. 크리에이터 검색 응답 DTO - 신규 DTO 필요 - 기존 `admin/member` 검색 응답을 그대로 쓰기보다, 현재 활성 agent 정보를 함께 주는 전용 DTO가 필요하다. - DTO 명 - `SearchAdminAgentAssignableCreatorResponse` - `SearchAdminAgentAssignableCreatorItem` - 필수 필드 - response: `totalCount: Int`, `items: List` - item: `creatorId: Long`, `creatorNickname: String`, `currentAgentId: Long?`, `currentAgentNickname: String?` ### 3. 관리자용 소속 크리에이터 목록 DTO - 기존 `GetAgentAssignedCreatorResponse`를 그대로 재사용하기보다는 관리자용 항목에 `assignedAt`을 포함한 전용 DTO가 적합하다. - DTO 명 - `GetAdminAgentAssignedCreatorResponse` - `GetAdminAgentAssignedCreatorItem` - 필수 필드 - response: `totalCount: Int`, `items: List` - item: `creatorId: Long`, `creatorNickname: String`, `assignedAt: LocalDateTime` ### 4. 관리자용 정산 상세 DTO - 가능하면 기존 아래 DTO를 그대로 재사용한다. - `GetAgentSettlementByCreatorResponse` - `GetAgentChannelDonationSettlementByCreatorResponse` - 이유 - 화면 주체만 ADMIN으로 바뀌고 데이터 shape는 동일하기 때문이다. - DTO까지 갈라지면 정산 계약이 중복될 가능성이 높다. ## 서비스 설계 ### 1. `AdminAgentReadService` - 책임 - 에이전트 목록 조회 - 크리에이터 검색 조회 - 에이전트 소속 크리에이터 목록 조회 - 에이전트 정산 상세 조회 진입 - 내부 동작 - 목록/검색/소속 목록은 전용 read query를 사용한다. - 에이전트 목록 조회 시 현재 월 기준 시작/종료 시각을 서비스에서 계산해 전용 read query에 전달한다. - 정산 상세는 기존 `AgentCalculateService`의 agentId 기반 public 조회 메서드를 사용한다. ### 2. 공통 정산 read 메서드 분리 - 현재 `AgentCalculateService`의 핵심 정산 조회 메서드는 이미 `agentId` 파라미터 중심 public 메서드다. - 따라서 아래 방향으로 정리한다. - controller는 AGENT/ADMIN 별도로 유지 - ADMIN read 서비스는 기존 `AgentCalculateService` public 메서드를 직접 호출한다. - 기대 효과 - 정산 계산 규칙 중복 제거 - snapshot 우선 조회 / live 계산 fallback 규칙 재사용 ## Repository / Query 방향 ### 1. 에이전트 목록용 조회 추가 - 필요 기능 - AGENT role member 목록 - 각 agent의 현재 활성 creator count - 각 agent의 현재 월 기준 5종 정산 합계 - 구현 방식 - `AdminAgentReadQueryRepository`에서 전용 projection query를 제공한다. - 기본 에이전트 목록과 활성 creator count는 기존 Querydsl projection query로 조회한다. - 현재 월 5종 정산 합계는 각 목록 item마다 기존 `AgentCalculateQueryRepository` total 조회 메서드를 재사용해 채운다. - 정산 row가 없는 agent도 목록에 남겨야 하므로 기존 total 조회 응답의 `0` 기본값을 그대로 사용한다. ### 2. 크리에이터 검색용 조회 추가 - 필요 기능 - creator nickname 기준 검색 - 현재 활성 소속 agent nullable join - 구현 방식 - `AdminAgentReadQueryRepository`에서 전용 검색 query를 제공한다. ### 3. 소속 크리에이터 목록 조회 추가 - 기존 `AgentCalculateQueryRepository.getAssignedCreators()`를 재사용할 수 있다. - 다만 `assignedAt`을 응답에 내려야 하면 projection을 확장하거나 관리자 전용 projection을 추가해야 한다. ## 인증/예외 처리 원칙 - 새 관리자 read 엔드포인트는 모두 `@PreAuthorize("hasRole('ADMIN')")`를 사용한다. - `agentId`가 존재하지 않거나 role이 `AGENT`가 아니면 `SodaException(messageKey = ...)`로 실패한다. - `creator/search`는 기존 `admin/member/search` 관례처럼 최소 검색어 길이 검증을 적용한다. - 정산 계산식, snapshot 우선 전략, assignment/ratio history 적용 규칙은 기존 `partner.agent.calculate` 동작을 그대로 사용한다. ## 테스트 설계 ### 1. 컨트롤러 테스트 - `ADMIN` 접근 성공 - 익명 사용자 접근 실패 - `AGENT` 또는 일반 사용자 접근 실패 ### 2. 서비스/리포지토리 테스트 - 에이전트 목록에서 닉네임과 현재 활성 creator count가 맞는지 검증 - 크리에이터 검색 결과에 현재 agent 소속 정보가 올바르게 붙는지 검증 - 특정 agent 소속 크리에이터 목록이 현재 활성 구간 기준으로만 내려오는지 검증 ### 3. 정산 parity 테스트 - 동일 기간, 동일 `agentId`에 대해 - AGENT 전용 조회 응답 - ADMIN 전용 조회 응답 - 두 결과의 `totalCount`, `total`, `items`가 동일한지 검증 - 대상 5종 - live - content - community - channel donation - content donation ### 4. 설계 대비 구현 계획 누락 체크리스트 - [x] 관리자 컨트롤러 테스트에 `ADMIN` 접근 성공, 익명 접근 실패, `AGENT` 또는 일반 사용자 접근 실패 시나리오가 모두 포함되어 있는지 확인한다. - [x] 동일 기간, 동일 `agentId` 기준으로 AGENT 전용 응답과 ADMIN 전용 응답의 `totalCount`, `total`, `items` parity를 5종 모두 검증하는 테스트가 구현 계획에 포함되어 있는지 확인한다. - [x] 구현 계획의 검증 기록 단계에 `무엇을/왜/어떻게`, 실제 실행 명령과 결과, 후속 수정 시 누적 기록 및 `정정` 추가 원칙이 명시되어 있는지 확인한다. - [x] `/admin/partner/agent/list`가 별도 날짜 입력 없이 현재 월 기준 5종 정산 합계를 계산하도록 구현 계획에 반영되어 있는지 확인한다. - [x] 에이전트 목록 응답 DTO와 리스트 조회 테스트에 `live/content/community/contentDonation/channelDonation` 5종 `agentSettlementAmount` summary 필드가 모두 반영되어 있는지 확인한다. - [x] 해당 월에 정산 내역이 없는 에이전트도 목록에서 제외하지 않고 5종 합계를 `0`으로 표기하는 쿼리/응답/테스트 계획이 포함되어 있는지 확인한다. - [x] 리스트 summary 값이 같은 월 기준 상세 조회 응답의 `total.agentSettlementAmount`와 일치하는지 검증하는 회귀 테스트가 구현 계획에 포함되어 있는지 확인한다. ## 구현 시 유의사항 - 관리자용 정산 API를 새로 만든다고 해서 계산 query를 복제하지 않는다. - 현재 assignment 활성 판정은 시간창 규칙을 반드시 동일하게 사용한다. - 정산 응답 계약은 기존 agent 응답과 불필요하게 분기하지 않는다. - 목록 화면은 요약 정보만 제공하고, 상세 정산은 상세 화면에서만 조회한다. - 목록의 5종 합계는 상세 API의 기간 필터와 별개로, 현재 월 기준 요약값이라는 의미를 문서와 코드에서 동일하게 유지한다. - 해당 월에 정산 내역이 없더라도 에이전트 행은 유지하고 금액은 `0`으로 표기한다. ## 최종 설계 결론 - 이번 기능은 “새 정산 엔진 추가”가 아니라 “기존 agent 정산 계산을 ADMIN에서 조회할 수 있게 read API를 보강”하는 작업으로 본다. - 관리자 페이지 흐름은 아래로 고정한다. 1. `/admin/partner/agent/list`로 에이전트 목록과 현재 월 기준 5종 정산 합계 조회 2. 상세 화면에서 `/admin/partner/agent/{agentId}/creator/list`로 현재 소속 크리에이터 조회 3. 필요 시 `/admin/partner/agent/creator/search`로 크리에이터 검색 후 기존 assignment API로 소속 지정 4. 상세 화면에서 `/admin/partner/agent/{agentId}/calculate/*` 5종으로 정산 현황 조회 - 목록의 5종 합계는 상세 페이지 이동 포인트용 현재 월 summary이며, 상세 화면의 날짜 범위 조회와 역할을 구분한다. - 현재 월에 정산 내역이 없는 에이전트도 목록에 포함되며, 합계는 0원으로 표시한다. - 이 설계를 기준으로 다음 단계에서는 구현 계획 문서를 작성한다. ## 검증 기록 - 1차 설계 - 무엇을: 관리자용 에이전트 목록/상세 read API 구조와 기존 agent 계산 로직 재사용 범위를 문서로 고정했다. - 왜: 현재 코드에는 관리자용 assignment/ratio/finalize는 있지만, 관리자용 agent 정산 상세 조회 API는 없어 read 레이어 설계가 먼저 필요했기 때문이다. - 어떻게: - `src/main/kotlin/kr/co/vividnext/sodalive/admin/partner/agent/assignment/AdminAgentCreatorController.kt` 확인 - `src/main/kotlin/kr/co/vividnext/sodalive/admin/partner/agent/ratio/AdminAgentSettlementRatioController.kt` 확인 - `src/main/kotlin/kr/co/vividnext/sodalive/admin/partner/agent/settlement/AdminAgentSettlementSnapshotController.kt` 확인 - `src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateController.kt` 확인 - `src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateService.kt` 확인 - `src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt` 확인 - 결과: 관리자용 read API 부재와 기존 계산 로직 재사용 가능성을 문서에 반영했다.