Files
sodalive-backend-spring-boot/docs/20260226_관리자크리에이터관리자채널후원정산페이지api생성.md

17 KiB

관리자/크리에이터 관리자 채널 후원 정산 페이지 API 작업 계획

  • 기존 정산 API 패턴(admin.calculate, creator.admin.calculate)과 채널 후원 데이터 소스(ChannelDonationMessage, CanUsage.CHANNEL_DONATION)를 확인한다.
  • 기존 패키지에 직접 누적하지 않도록 신규 하위 패키지를 설계한다.
    • 관리자: kr.co.vividnext.sodalive.admin.calculate.channelDonation
    • 크리에이터 관리자: kr.co.vividnext.sodalive.creator.admin.calculate.channelDonation
  • 관리자 채널 후원 정산 조회 API를 추가하고, 날짜 범위(startDateStr, endDateStr)로 전체 데이터를 조회한 뒤 응답을 크리에이터별로 그룹화해 반환하도록 설계한다.
  • 크리에이터 관리자 채널 후원 정산 조회 API를 추가하고, 날짜 범위(startDateStr, endDateStr)만 입력받아 인증 사용자 본인 데이터만 조회한다.
  • 서비스 계층에서 날짜 문자열을 convertLocalDateTime()으로 변환하고 종료일은 23:59:59로 보정해 조회 구간을 통일한다.
  • 저장소(QueryRepository) 계층에 날짜 범위 조건(createdAt >= startDate, createdAt <= endDate)과 크리에이터 기준 그룹화(groupBy(member.id) 등)를 반영한 집계 조회를 추가한다.
  • API URL을 기존 정산 URL 규칙에 맞춰 확정하고 문서화한다.
    • 관리자: GET /admin/calculate/channel-donation-by-creator
    • 크리에이터 관리자: GET /creator-admin/calculate/channel-donation
  • 정산 계산 공식을 공통 로직으로 구현하고, 사람이 이해하기 쉬운 한글 주석을 추가한다.
    • 원화 = 캔 * 100
    • 수수료 = 원화 * 6.6%
    • 정산금액 = (원화 - 수수료) * 85%
    • 원천세 = 정산금액 * 3.3%
    • 입금액 = 정산금액 - 원천세
  • 계산 정밀도 정책을 정의한다(BigDecimal, RoundingMode.HALF_UP, 반올림 시점 고정).
  • 성능/효율 개선 항목을 반영한다(집계 쿼리 중심 처리, 불필요한 애플리케이션 후처리 최소화, count 조회 최적화 검토).
  • 응답 DTO 스펙을 아래 필드로 고정하고 권한 정책(관리자=전체, 크리에이터 관리자=본인)을 함께 검증한다.
    • 날짜(yyyy-MM-dd)
    • 크리에이터
    • 건수(count)
    • 총 받은 캔 수(totalCan)
    • 원화
    • 수수료
    • 정산금액
    • 원천세
    • 입금액
  • 테스트를 추가한다(관리자 날짜 필터 + 크리에이터별 그룹화 응답, 크리에이터 관리자 날짜 필터/본인 범위, 계산식 정확성, 경계값).
  • 검증을 수행한다(./gradlew test, ./gradlew build, 필요 시 ./gradlew ktlintCheck).

API URL 선정 근거

  • 기본 경로는 권한 범위별 정산 컨트롤러 관례를 따른다.
    • 관리자: @RequestMapping("/admin/calculate")
    • 크리에이터 관리자: @RequestMapping("/creator-admin/calculate")
  • 하위 경로는 기존 정산 API와 동일하게 소문자 하이픈(kebab-case) 명사 조합을 사용한다.
    • 예: content-donation-list, cumulative-sales-by-content, community-by-creator
  • channel-donation 토큰은 기존 채널 후원 API 경로(@RequestMapping("/explorer/profile/channel-donation"))와 용어를 맞춰 도메인 표현을 통일한다.
  • 관리자 정산은 조회 결과가 크리에이터별 그룹화 응답이므로 기존 *-by-creator 패턴을 적용해 channel-donation-by-creator로 정한다.
  • 크리에이터 관리자 정산은 인증 사용자 본인 범위로 고정되므로 -by-creator 접미사를 제외하고 channel-donation으로 정한다.

검증 기록

계획 수립

  • 무엇을: 관리자/크리에이터 관리자 채널 후원 정산 페이지 API 구현을 위한 작업 계획 문서를 작성했다.
  • 왜: 구현 전에 패키지 구조, 날짜 범위 조회, 정산 계산식, 성능 검증 기준을 명확히 하기 위해서다.
  • 어떻게:
    • docs의 기존 작업 계획 문서 형식(체크박스 + 검증 기록)을 기준으로 템플릿을 맞췄다.
    • admin.calculate, creator.admin.calculate, explorer.profile.channelDonation 경로를 탐색해 반영했다.
    • 사용자 요청에 따라 실제 코드 구현/테스트는 수행하지 않고 계획 문서만 작성했다.

2차 계획 수정

  • 무엇을: 조회 조건을 관리자=날짜+크리에이터 구분, 크리에이터 관리자=날짜만으로 명확히 분리했고, 응답 필드를 날짜(yyyy-MM-dd), 크리에이터, 원화, 수수료, 정산금액, 원천세, 입금액으로 고정했다.
  • 왜: 추가 요구사항(조회 조건 분리, Response 필드 고정)을 계획 단계에서 누락 없이 반영하기 위해서다.
  • 어떻게:
    • admin.calculate/creator.admin.calculate의 기존 날짜 파라미터 및 인증 기반 필터링 패턴을 재탐색해 계획 항목을 수정했다.
    • lsp_diagnostics(대상: 본 문서) 결과 No diagnostics found를 확인했다.
    • ./gradlew tasks --all 실행 결과 BUILD SUCCESSFUL을 확인했다.
    • 문서만 작성해야 하는 요청 범위를 유지하기 위해 코드 구현/테스트 변경은 수행하지 않았다.

3차 계획 수정

  • 무엇을: 관리자 조회 요구사항을 크리에이터 식별값으로 필터가 아닌 조회 결과를 크리에이터별로 그룹화하여 반환으로 정정했다.
  • 왜: 사용자 의도가 “조회 조건 추가”가 아니라 “응답 결과 구성 방식(크리에이터별 그룹화)”이었기 때문이다.
  • 어떻게:
    • AdminCalculateController*-by-creator 엔드포인트가 날짜/페이지 파라미터만 받고(creatorId/memberId 미입력), 서비스/리포지토리에서 GetCalculateByCreatorResponsegroupBy(member.id) 기반으로 결과를 구성하는 패턴을 확인했다.
    • 위 근거를 바탕으로 체크리스트를 관리자=날짜 필터 + 크리에이터별 그룹화 응답 기준으로 수정했다.
    • lsp_diagnostics(대상: 본 문서) 결과 No diagnostics found를 확인했다.
    • ./gradlew tasks --all 실행 결과 BUILD SUCCESSFUL을 확인했다.

4차 계획 수정

  • 무엇을: 작업 계획 문서에 API URL을 어떤 기준으로 정했는지(경로 규칙, 용어 선택, 최종 URL) 근거를 추가했다.
  • 왜: 구현 전에 URL 명명 기준을 명확히 남겨, 이후 개발 시 경로 해석 차이와 재작업을 방지하기 위해서다.
  • 어떻게:
    • AdminCalculateController, CreatorAdminCalculateController, ChannelDonationController@RequestMapping/@GetMapping 패턴을 비교해 기준 경로와 하위 경로 규칙을 도출했다.
    • 관리자 URL은 *-by-creator 관례를 적용해 /admin/calculate/channel-donation-by-creator, 크리에이터 관리자 URL은 본인 범위 고정 특성에 맞춰 /creator-admin/calculate/channel-donation으로 문서화했다.
    • lsp_diagnostics(대상: 본 문서) 결과 No diagnostics found를 확인했다.
    • ./gradlew tasks --all 실행 결과 BUILD SUCCESSFUL을 확인했다.

5차 구현

  • 무엇을: 관리자/크리에이터 관리자 채널 후원 정산 API를 신규 하위 패키지로 구현하고, 날짜 범위 조회/크리에이터별 그룹화/정산 공식 공통 계산 로직을 적용했다.
  • 왜: 기존 정산 코드에 얽히지 않고 유지보수 가능한 구조로 요구사항(관리자=크리에이터별 그룹 응답, 크리에이터 관리자=본인 범위 조회)을 정확히 반영하기 위해서다.
  • 어떻게:
    • 신규 패키지 생성: admin.calculate.channelDonation, creator.admin.calculate.channelDonation, 공통 계산기 calculate.channelDonation.
    • API 구현: GET /admin/calculate/channel-donation-by-creator, GET /creator-admin/calculate/channel-donation.
    • QueryDSL 집계: UseCan + UseCanCalculate를 사용해 CanUsage.CHANNEL_DONATION, 날짜 범위, 환불 제외 조건을 적용하고 관리자 응답은 날짜+크리에이터 기준 그룹화, 크리에이터 관리자 응답은 날짜 기준 그룹화로 구현.
    • 정산 계산식 공통화: ChannelDonationSettlementCalculator에서 BigDecimal("0.066"), BigDecimal("0.85"), BigDecimal("0.033"), RoundingMode.HALF_UP 정책으로 계산하고 공식 설명 한글 주석을 추가.
    • 테스트 추가: 계산식/반올림 단위 테스트 및 관리자·크리에이터 관리자 컨트롤러/서비스 경로 테스트를 추가.
    • 검증 실행:
      • ./gradlew test --tests "kr.co.vividnext.sodalive.calculate.channelDonation.ChannelDonationSettlementCalculatorTest" → 성공
      • ./gradlew test --tests "*channelDonation*" → 성공
      • ./gradlew test → 성공
      • ./gradlew build → 성공
    • 참고: Kotlin LSP 서버 미설정 환경이라 .kt 파일에 대한 lsp_diagnostics는 실행 시 서버 미설정 오류를 반환했다.

6차 수정

  • 무엇을: 정산 계산식을 단계별 반올림 후 다음 단계 계산하는 방식으로 수정하고, 크리에이터 관리자 조회 쿼리/카운트에서 불필요한 member 조인을 제거했다.
  • 왜: 정산 항목 간 관계(입금액 = 정산금액 - 원천세)를 정수 기준으로 일관되게 맞추고, 조회 성능 최적화를 위해 불필요 조인을 줄이기 위해서다.
  • 어떻게:
    • ChannelDonationSettlementCalculator를 단계별 반올림 파이프라인으로 변경했다.
      • 수수료 = round(원화 * 6.6%)
      • 정산금액 = round((원화 - 수수료) * 85%)
      • 원천세 = round(정산금액 * 3.3%)
      • 입금액 = 정산금액 - 원천세
    • 크리에이터 관리자 경로는 인증 사용자 닉네임을 서비스 인자로 전달해 응답 creator를 구성하고, QueryRepository의 member 조인/닉네임 select를 제거했다.
    • 관리자 totalCount는 member 조인 없이 recipientCreatorId 기반 distinct 키로 계산하도록 변경했다.
  • 검증 실행:
    • ./gradlew test --tests "*channelDonation*" → 성공
    • ./gradlew test → 성공
    • ./gradlew build → 성공

7차 수정

  • 무엇을: 요청한 2번/3번 최적화를 반영해 QueryDSL @QueryProjection 기반 매핑으로 전환하고, 날짜 그룹 조회 경로 인덱스 전략 DDL을 추가했다. 또한 테스트 가독성을 위해 @DisplayName을 추가했다.
  • 왜: Projections.constructor 대비 타입 안전성과 유지보수성을 높이고, 채널 후원 정산 조회의 날짜 범위/조인 필터 성능 개선 근거를 DDL로 명확히 남기기 위해서다.
  • 어떻게:
    • Query DTO 전환:
      • GetAdminChannelDonationSettlementQueryData, GetCreatorChannelDonationSettlementQueryData@QueryProjection을 적용했다.
      • 각 QueryRepository의 Projections.constructorQGet*QueryData(...) 호출로 교체했다.
    • 인덱스 전략 반영:
      • docs/20260226_channel_donation_settlement_index_ddl.sql 파일을 추가해 use_can, use_can_calculate 인덱스 DDL을 정의했다.
    • 테스트 가독성 개선:
      • 채널 후원 정산 관련 신규 테스트에 @DisplayName(한글)을 추가해 테스트 의도를 명확히 했다.
    • 검증 실행:
      • ./gradlew test --tests "*channelDonation*" → 성공
      • ./gradlew test → 성공
      • ./gradlew build → 성공
    • 참고: ./gradlew test./gradlew build를 병렬 실행하면 테스트 결과 XML 파일 쓰기 충돌이 재발할 수 있어, 순차 실행 기준으로 최종 검증했다.

8차 수정

  • 무엇을: 채널 후원 정산 Item 응답(GetAdminChannelDonationSettlementItem, GetCreatorChannelDonationSettlementItem)에 count 필드를 추가하고, QueryData/Repository/Test를 함께 갱신했다.
  • 왜: 사용자 요청에 따라 정산 응답에서 그룹별 건수를 직접 확인할 수 있도록 하기 위해서다.
  • 어떻게:
    • Item DTO에 @JsonProperty("count") val count: Int를 추가했다.
    • QueryDTO(GetAdminChannelDonationSettlementQueryData, GetCreatorChannelDonationSettlementQueryData)에 count: Long을 추가하고 toResponseItem()에서 count.toInt()로 매핑했다.
    • Repository projection에 useCan.id.count()를 추가해 count 값을 조회하도록 반영했다.
    • 컨트롤러/서비스 테스트 fixture 및 assertion에 count 검증을 추가했다.
    • 검증 실행:
      • ./gradlew test --tests "*channelDonation*" → 성공
      • ./gradlew test → 성공
      • ./gradlew build → 성공

9차 수정

  • 무엇을: 채널 후원 정산 count가 분할 정산 레코드 수로 과집계되던 문제를 수정하고, 동일 후원(UseCan 1건) + 분할 정산(UseCanCalculate 2건) 회귀 테스트를 관리자/크리에이터 관리자 경로에 추가했다.
  • 왜: 결제 게이트웨이별 분할 정산이 발생하면 기존 useCan.id.count()가 실제 후원 건수보다 크게 집계되어 정산 화면 count가 잘못 표시되기 때문이다.
  • 어떻게:
    • AdminChannelDonationCalculateQueryRepository, CreatorAdminChannelDonationCalculateQueryRepository의 집계 countuseCan.id.countDistinct()로 변경했다.
    • QueryRepository 통합 테스트(AdminChannelDonationCalculateQueryRepositoryTest, CreatorAdminChannelDonationCalculateQueryRepositoryTest)를 추가해 분할 정산 시 count=1, totalCan 합산(50) 동작을 검증했다.
    • H2 환경에서 MySQL 함수(DATE_FORMAT, CONVERT_TZ)를 테스트 가능하게 하기 위해 H2MySqlFunctionDialect, H2MysqlDateFunctions 테스트 지원 코드를 추가하고 각 테스트에서 alias를 등록했다.
    • 검증 실행:
      • ./gradlew test --tests "*channelDonation*" → 성공
      • ./gradlew build → 성공
    • 참고: Kotlin LSP 미설정 환경이라 .kt 대상 lsp_diagnostics는 실행 시 서버 미설정 오류가 발생했다.

10차 수정

  • 무엇을: 관리자 채널 후원 정산의 totalCount 쿼리에 member innerJoin을 추가해 목록 조회와 동일한 조인 조건으로 집계하도록 정렬했다.
  • 왜: 기존에는 totalCountmember 조인 없이 계산하고 목록은 member innerJoin을 사용해, 데이터 정합성 이슈(고아 recipientCreatorId)가 있을 때 totalCountitems가 불일치할 수 있었다.
  • 어떻게:
    • AdminChannelDonationCalculateQueryRepository.getChannelDonationByCreatorTotalCount(...)member 조인(member.id = useCanCalculate.recipientCreatorId)을 추가했다.
    • distinct 그룹 키를 recipientCreatorId 문자열 대신 member.id 문자열 기준으로 변경해 목록 쿼리의 그룹 축(날짜+멤버)과 맞췄다.
    • 검증 실행:
      • ./gradlew test --tests "*channelDonation*" → 성공
      • ./gradlew build → 성공
      • 참고: ./gradlew test./gradlew build를 병렬 실행했을 때 test result XML 쓰기 충돌이 1회 발생해, 이후 순차 실행으로 재검증했다.

10차 수정

  • 무엇을: 정산 페이지 Item 응답(GetAdminChannelDonationSettlementItem, GetCreatorChannelDonationSettlementItem)에 totalCan 필드를 추가했다.
  • 왜: 사용자 요청대로 화면에서 건수 다음에 총 받은 캔 수를 함께 노출하기 위해서다.
  • 어떻게:
    • Item DTO에 @JsonProperty("totalCan") val totalCan: Intcount 다음 위치로 추가했다.
    • QueryData(GetAdminChannelDonationSettlementQueryData, GetCreatorChannelDonationSettlementQueryData)의 toResponseItem()에서 totalCan ?: 0을 응답 Item의 totalCan으로 매핑했다.
    • 컨트롤러/서비스 테스트 fixture와 assertion에 totalCan 검증을 추가했다.
  • 검증 실행:
    • ./gradlew test --tests "*channelDonation*" → 성공
    • ./gradlew test → 성공
    • ./gradlew build → 성공

11차 수정

  • 무엇을: 채널 후원 정산 인덱스 DDL(docs/20260226_channel_donation_settlement_index_ddl.sql)을 재실행 가능한 멱등 스크립트로 수정했다.
  • 왜: 동일 DB에 DDL을 재적용할 때 기존 ADD INDEXDuplicate key name으로 실패할 수 있어, 운영 재적용/롤백 후 재적용 시 안정성을 확보해야 했기 때문이다.
  • 어떻게:
    • information_schema.statistics에서 table_schema = DATABASE() 기준으로 인덱스 존재 여부를 조회하도록 변경했다.
    • 인덱스가 없을 때만 ALTER TABLE ... ADD INDEX를 실행하고, 이미 존재하면 안내 SELECT를 실행하는 동적 SQL(PREPARE/EXECUTE) 패턴을 적용했다.
    • 대상 인덱스 3개(idx_use_can_channel_donation_filter, idx_use_can_calculate_settlement_join, idx_use_can_calculate_creator_settlement) 모두 동일 규칙으로 반영했다.
  • 검증 실행:
    • lsp_diagnostics(대상: docs/20260226_channel_donation_settlement_index_ddl.sql) → .sql LSP 서버 미설정으로 진단 불가(환경 제약)
    • lsp_diagnostics(대상: 본 문서) → No diagnostics found
    • ./gradlew test --tests "*channelDonation*" → 성공
    • ./gradlew build → 성공