Files
sodalive-backend-spring-boot/docs/20260421_오리지널시리즈정산내역.md

11 KiB

오리지널 시리즈 작품 화별 정산 내역 추가 작업 계획

  • 기존 관리자 정산 API 패턴(/admin/calculate, list/excel 쌍, StreamingResponseBody)을 유지하는 신규 패키지 구조를 확정한다.
  • kr.co.vividnext.sodalive.admin.calculate.originalSeries 패키지에 오리지널 시리즈 소지 유저 조회 API를 추가한다.
  • kr.co.vividnext.sodalive.admin.calculate.originalSeries 패키지에 오리지널 시리즈 정산 내역 조회 API를 추가한다.
  • 정산 내역 조회 API가 start_date, end_date, creator_id를 받아 KST 입력 날짜를 UTC 조회 범위로 변환하도록 구현한다.
  • 정산 내역 조회 쿼리가 Series(isOriginal = true) -> SeriesContent -> AudioContent -> Order 경로를 사용해 Content.id, Order.type 기준으로 그룹화되도록 구현한다.
  • 정산 내역 결과에 시리즈 타이틀, 콘텐츠 타이틀, 가격, 대여/소장 여부, 판매 수, 합계(캔), 합계(포인트)를 포함하도록 DTO를 추가한다.
  • 가격은 주문에 저장된 값을 기준으로 사용하되, 대여는 ceil(price * 0.7) 규칙이 반영된 값으로 노출되도록 검증한다.
  • 오리지널 시리즈 정산 내역 엑셀 다운로드 API를 추가하고, 오리지널 시리즈를 소지한 유저별로 시트를 구성하도록 구현한다.
  • 엑셀 다운로드는 각 유저 시트에 헤더 행과 정산 내역 행을 기록하고, 데이터가 없어도 헤더가 있는 시트를 유지하도록 구현한다.
  • 신규 QueryRepository/Service/Controller에 대한 테스트를 먼저 작성하고 실패를 확인한 뒤 구현한다.
  • 관련 테스트, 정적 진단, 수동 검증 결과를 확인하고 이 문서 하단 검증 기록에 남긴다.

구현 메모

  • 오리지널 시리즈 여부는 Series.isOriginal 플래그를 기준으로 판단한다.
  • 소지 유저는 현재 코드베이스 기준 Series.member를 의미하는 것으로 해석한다.
  • 날짜 입력 파라미터는 start_date, end_date, creator_id로 받고, 날짜 값은 KST 기준 00:00:00 / 23:59:59로 해석한 뒤 convertLocalDateTime()으로 UTC LocalDateTime으로 변환한다.
  • 정산 내역 조회 API는 creator_id를 필수 필터로 사용하고, 엑셀 API는 전체 소지 유저를 순회해 시트를 생성하는 것으로 해석한다.

API 명세

1. 오리지널 시리즈 소지 유저 조회

URI

  • GET /admin/calculate/original-series/owners

Request

  • Header
    • 인증: 관리자 권한 필요 (hasRole('ADMIN'))
  • Query Parameter
    • 없음
  • Body
    • 없음

Response

  • Content-Type: application/json
  • 응답 구조
{
  "success": true,
  "message": null,
  "data": [
    {
      "creatorId": 1,
      "nickname": "owner-a"
    },
    {
      "creatorId": 2,
      "nickname": "owner-b"
    }
  ],
  "errorProperty": null
}

Response Field

  • success: 성공 여부
  • message: 성공 메시지, 현재 구현에서는 null
  • data: 오리지널 시리즈 소지 유저 목록
    • creatorId: 정산 내역 조회에 사용할 크리에이터 ID
    • nickname: 관리자 화면에 노출할 닉네임
  • errorProperty: 에러 시 사용되는 필드, 성공 시 null

2. 오리지널 시리즈 정산 내역 조회

URI

  • GET /admin/calculate/original-series/settlement-details

Request

  • Header
    • 인증: 관리자 권한 필요 (hasRole('ADMIN'))
  • Query Parameter
    • start_date: 시작일, 형식 yyyy-MM-dd, KST 기준
    • end_date: 종료일, 형식 yyyy-MM-dd, KST 기준
    • creator_id: 오리지널 시리즈 소지 유저 ID
    • page: 페이지 번호, Spring Pageable 규칙 사용
    • size: 페이지 크기, Spring Pageable 규칙 사용
  • Body
    • 없음

Request Example

GET /admin/calculate/original-series/settlement-details?start_date=2026-04-01&end_date=2026-04-30&creator_id=1&page=0&size=20

Response

  • Content-Type: application/json
  • 응답 구조
{
  "success": true,
  "message": null,
  "data": {
    "totalCount": 2,
    "items": [
      {
        "seriesTitle": "오리지널 시리즈",
        "contentTitle": "1화",
        "price": 70,
        "orderType": "대여",
        "salesCount": 2,
        "totalCan": 130,
        "totalPoint": 100
      },
      {
        "seriesTitle": "오리지널 시리즈",
        "contentTitle": "1화",
        "price": 100,
        "orderType": "소장",
        "salesCount": 2,
        "totalCan": 200,
        "totalPoint": 25
      }
    ]
  },
  "errorProperty": null
}

Response Field

  • data.totalCount: 조회 결과 전체 건수
  • data.items: 정산 내역 목록
    • seriesTitle: 시리즈 제목
    • contentTitle: 작품 화 제목
    • price: 표시 가격(캔)
      • 소장: audioContent.price
      • 대여: ceil(audioContent.price * 0.7)
    • orderType: 대여 또는 소장
    • salesCount: 판매 건수
    • totalCan: 합계 캔
    • totalPoint: 합계 포인트

3. 오리지널 시리즈 정산 내역 엑셀 다운로드

URI

  • GET /admin/calculate/original-series/settlement-details/excel

Request

  • Header
    • 인증: 관리자 권한 필요 (hasRole('ADMIN'))
  • Query Parameter
    • start_date: 시작일, 형식 yyyy-MM-dd, KST 기준
    • end_date: 종료일, 형식 yyyy-MM-dd, KST 기준
  • Body
    • 없음

Request Example

GET /admin/calculate/original-series/settlement-details/excel?start_date=2026-04-01&end_date=2026-04-30

Response

  • Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
  • Header
    • Content-Disposition: attachment; filename*=UTF-8''original-series-settlement-details.xlsx
  • Body
    • .xlsx 바이너리 스트림

Excel 구성

  • 오리지널 시리즈 소지 유저별로 시트 1개 생성
  • 시트명 형식: {nickname}
  • 각 시트의 헤더 열 순서
    1. 시리즈 제목
    2. 콘텐츠 제목
    3. 가격(캔)
    4. 구분
    5. 판매 수
    6. 합계(캔)
    7. 합계(포인트)
  • 해당 기간에 데이터가 없는 유저도 헤더만 있는 시트를 유지

클라이언트 기능 구현용 프롬프트

관리자 페이지에서 오리지널 시리즈 작품 화별 정산 내역을 조회하고 엑셀 다운로드하는 클라이언트 기능을 구현한다.

필수 API 계약은 아래와 같다.

1. 소지 유저 조회
- GET /admin/calculate/original-series/owners
- 응답은 ApiResponse<List<{ creatorId: number; nickname: string }>> 형태다.

2. 정산 내역 조회
- GET /admin/calculate/original-series/settlement-details
- query: start_date(yyyy-MM-dd), end_date(yyyy-MM-dd), creator_id(number), page(number), size(number)
- 응답은 ApiResponse<{ totalCount: number; items: Array<{ seriesTitle: string; contentTitle: string; price: number; orderType: string; salesCount: number; totalCan: number; totalPoint: number }> }> 형태다.

3. 엑셀 다운로드
- GET /admin/calculate/original-series/settlement-details/excel
- query: start_date(yyyy-MM-dd), end_date(yyyy-MM-dd)
- 응답은 xlsx 바이너리이며 파일명은 `original-series-settlement-details.xlsx` 이다.

구현 요구사항:
- 화면 진입 시 owners API를 먼저 호출해 멤버 선택 드롭다운 데이터를 로드한다.
- 사용자가 start_date, end_date, creator_id를 선택한 뒤 정산 내역 조회 API를 호출한다.
- 목록 테이블에는 시리즈 제목, 콘텐츠 제목, 가격, 구분, 판매 수, 합계(캔), 합계(포인트)를 그대로 표시한다.
- 페이지네이션은 page/size 기반으로 처리하고 totalCount를 사용해 총 페이지 수를 계산한다.
- 엑셀 다운로드 버튼은 현재 선택된 start_date/end_date만 사용해 excel API를 호출한다.
- JSON 응답은 항상 ApiResponse 래퍼의 success/data/message/errorProperty를 기준으로 처리한다.
- success가 false이면 message를 우선 노출한다.
- 날짜는 문자열 `yyyy-MM-dd` 형식으로 서버에 전달한다.

산출물 요구사항:
- API 호출 함수
- 타입 정의
- 목록 조회 상태 관리
- 엑셀 다운로드 처리
- 에러 처리 로직

임의로 API 계약을 바꾸지 말고, 위 URI/Request/Response를 그대로 사용한다.

검증 기록

1차 구현

  • 무엇을: /admin/calculate/original-series/owners, /admin/calculate/original-series/settlement-details, /admin/calculate/original-series/settlement-details/excel 3개 API와 전용 QueryRepository/Service/Controller/DTO를 추가했다.
  • 왜: 관리자 페이지에서 오리지널 시리즈 소지 유저를 먼저 조회하고, 선택한 유저의 작품 화별 정산 내역을 기간 기준으로 확인하며, 전체 소지 유저별 시트로 엑셀 다운로드할 수 있어야 했기 때문이다.
  • 어떻게:
    • kr.co.vividnext.sodalive.admin.calculate.originalSeries 패키지를 생성하고 Series(isOriginal = true) -> SeriesContent -> AudioContent -> Order 조인으로 정산 내역을 조회하도록 구현했다.
    • 정산 내역은 Content.id, Order.type 기준으로 그룹화하고 결과에 시리즈 제목, 콘텐츠 제목, 가격, 구분, 판매 수, 합계 캔, 합계 포인트를 담도록 구성했다.
    • 가격은 audioContent.price를 기준으로 계산하고, 대여는 응답 변환 시 ceil(price * 0.7)를 적용해 포인트 사용으로 order.can이 바뀐 주문도 동일한 표시 가격을 유지하도록 했다.
    • 엑셀은 SXSSFWorkbook(100) 기반 스트리밍으로 생성하고, 오리지널 시리즈 소지 유저별로 시트를 만들며 데이터가 없으면 헤더만 기록하도록 했다.
    • 테스트/검증 실행 결과:
      • lsp_diagnostics (신규 Kotlin 파일 경로) → Kotlin LSP 미설정으로 진단 불가
      • ./gradlew test --tests "*AdminOriginalSeriesCalculate*" → 성공
      • ./gradlew ktlintCheck → 성공
      • ./gradlew build → 성공
      • ./gradlew test --tests "*AdminOriginalSeriesCalculateServiceTest.shouldCreateOneSheetPerOwnerForExcel" → 성공

2차 문서화

  • 무엇을: 기존 작업 계획 문서에 오리지널 시리즈 정산 API 3종의 URI, Request, Response 상세 명세와 클라이언트 기능 구현용 프롬프트를 추가했다.
  • 왜: 클라이언트 기능 구현 시 서버 API 계약을 문서만 보고 그대로 사용할 수 있어야 하기 때문이다.
  • 어떻게:
    • AdminOriginalSeriesCalculateController, 응답 DTO, ApiResponse 구조를 기준으로 문서에 JSON 예시와 필드 설명을 정리했다.
    • 엑셀 다운로드 API는 바이너리 응답과 헤더 규격, 시트 구성 규칙을 함께 기록했다.
    • 클라이언트 개발자가 그대로 복사해 사용할 수 있도록 API 호출 순서와 상태 처리 조건을 포함한 프롬프트를 추가했다.
    • 실행 결과:
      • 수정 문서 재확인(read) → 성공