# 파트너 콘솔 — 일별 정산 내역 구현 지침서

- 작성일: 2026-08-10
- 대상: `partner-api`, `cryptoments-admin/partner-ui`
- DDL 변경: **없음** (기존 테이블 집계만)
- 배경: 파트너가 하루 단위로 입금·출금·수수료·수익을 한 화면에서 보고 싶다는 요청.
  현재는 `FeesView`(일별 수수료) / `RealizationsView`(실현) / `RebatesView`(리베이트)로
  **흩어져 있어 하루치 전체 그림이 안 잡힌다.**

---

## 0. 만들 것

파트너 콘솔 정산 메뉴에 **"일별 정산"** 화면 신규 추가.

### 표 구조 (2026-08-10 확정)

**요약 행은 `사용자 / 파트너` 축으로 대칭 분리**하고, 그 아래 세분화는 **행 펼침 세부**로 내린다.

| # | 요약 컬럼 | 소스 |
|---|---|---|
| 1 | 사용자 입금 | `deposits` `deposit_type = 'USER_DEPOSIT'` |
| 2 | 파트너 충전 | `deposits` 그 외(`PARTNER_CHARGE` 등) |
| 3 | 사용자 지급 | `withdrawals` `withdrawal_type IN ('USER_PAYOUT','KRW_WITHDRAW')` |
| 4 | 파트너 출금 | `withdrawals` 그 외 전부(`PARTNER_WITHDRAW` + `SETTLEMENT_WITHDRAW` + 기타) |
| 5 | 수수료 | `deposits.fee_amount` — 파트너가 **낸** 비용 |
| 6 | 수익 | 수수료 몫 + 리베이트 **합산 1컬럼** |

> **왜 이 축인가**: 사용자 입금/지급은 **파트너의 영업 흐름**이고, 파트너 충전/출금은 **파트너 자신의
> 자금 이동**이다. 성격이 달라 합치면 어느 쪽 숫자도 의미를 잃는다. 요약에서 이 둘만 갈라 놓으면
> "우리 사업이 얼마나 돌았나"와 "내가 얼마를 넣고 뺐나"를 한눈에 구분할 수 있다.

### 행 펼침 세부

| 세부 블록 | 세분화 축 |
|---|---|
| 입금 세부 | `deposit_method` × `deposit_type` (예: `TORQ · 사용자`, `직접 · 파트너충전`) |
| 출금 세부 | `USER_PAYOUT` / `KRW_WITHDRAW` / `PARTNER_WITHDRAW` / `SETTLEMENT_WITHDRAW` / 기타 |
| 수익 세부 | 수수료 몫 / 리베이트 |

- 값이 0인 항목도 **숨기지 않는다.** 세부를 펼치는 이유는 "왜 0인지" 확인하려는 경우가 많다.
- 리베이트 항목 아래에 `지급일에 반영` 주석. 요약 컬럼에는 넣지 않는다.

---

## 1. ★집계 정의 — 이 절이 이 문서의 핵심이다

각 컬럼의 소스 테이블과 **날짜 귀속 기준**을 아래대로 정확히 따를 것.
귀속 기준을 임의로 바꾸면 숫자가 기존 화면과 어긋난다.

### 1-1. 입금

```sql
FROM deposits
WHERE partner_id = :partnerId
  AND status IN ('CONFIRMED','NOTIFIED','COLLECTING','SETTLED')
날짜 = DATE(created_at)
  사용자입금 = SUM(CASE WHEN deposit_type = 'USER_DEPOSIT' THEN amount ELSE 0 END)
  자기충전   = SUM(CASE WHEN deposit_type = 'USER_DEPOSIT' THEN 0 ELSE amount END)
```

⚠️ **상태를 `SETTLED` 하나로 보면 안 된다 (2026-08-10 정정).** `DepositStatus` 는 7종
(`DETECTED`→`CONFIRMING`→`CONFIRMED`→`NOTIFIED`→`COLLECTING`→`SETTLED`, 분기 `FAILED`)이고,
`SETTLED` 는 **집금까지 끝난** 최종 상태다. `SETTLED` 만 세면 **오늘 들어온 돈이 집금될 때까지
화면에 안 보인다**(실측 `CONFIRMED` 187건 24,015 / `COLLECTING` 2건 213 누락).
파트너 잔액에는 이미 반영되는 금액이라 빼면 잔액 화면과 어긋난다.
→ **돈이 실제로 도착한 단계 전부**를 포함한다. 제외는 `DETECTED`·`CONFIRMING`(확정 전)·`FAILED`.
⚠️ **둘을 합치지 말 것.** `PARTNER_CHARGE`(파트너 자기 충전)는 매출이 아니고 **수수료 대상도 아니다.**
합산하면 "입금은 큰데 수수료가 왜 이것뿐이냐"는 문의가 반드시 나온다(§45 에서 실제로 오판했던 지점).

### 1-2. 출금  ← **3분할 (2026-08-10 정정)**

```sql
FROM withdrawals
WHERE partner_id = :partnerId AND status IN ('CONFIRMED','COMPLETED')
날짜 = DATE(COALESCE(confirmed_at, created_at))
  사용자지급   = SUM(CASE WHEN withdrawal_type IN ('USER_PAYOUT','KRW_WITHDRAW') THEN amount ELSE 0 END)
  파트너출금   = SUM(CASE WHEN withdrawal_type = 'PARTNER_WITHDRAW'     THEN amount ELSE 0 END)
  수수료인출   = SUM(CASE WHEN withdrawal_type = 'SETTLEMENT_WITHDRAW'  THEN amount ELSE 0 END)
```

⚠️ **컬럼명은 `confirmed_at` 이다.** 초판에 `completed_at` 으로 적었으나 그런 컬럼은 없다(팬텀).
`Withdrawal` 엔티티도 `confirmedAt` 하나뿐이다.

⚠️⚠️ **요약 축은 3개지만 원천 `withdrawal_type` 은 그보다 많다.** 초판은
"일반 / 수수료인출" 이분법이었는데, 2026-08-10 실측 당시에도 `USER_PAYOUT` 335건 /
`PARTNER_WITHDRAW` 185건 / `SETTLEMENT_WITHDRAW` 6건이었고,
**10개 파트너가 두 유형을 섞어 쓴다.** 파트너36 만 봐도 파트너출금 1,162,339 vs 사용자지급 199,494 로
규모·의미가 전혀 다르다. 합치면 §1-1 에서 입금을 분리한 이유와 정면으로 모순된다.

- `USER_PAYOUT` — 파트너가 **자기 사용자에게 지급**한 것 (영업 유출)
- `KRW_WITHDRAW` — 파트너 사용자의 **원화 오프램프 출금**. 목록 유형은 분리하지만 사용자 지급
  합계에는 `USER_PAYOUT` 과 함께 센다
- `PARTNER_WITHDRAW` — **파트너 본인이 자기 잔액을 인출**한 것
- `SETTLEMENT_WITHDRAW` — 매일 01:00 실현 후 **수수료 자동 인출**. 이걸 다른 출금과 섞으면
  파트너가 **"내가 출금한 적 없는 금액이 빠져나갔다"**고 문의한다
  (지식 repo `dashboard-fee-sweep-ux` 의 정형 문의).

⚠️⚠️ **`WithdrawalStatus` 에 `SETTLED` 는 존재하지 않는다 (2026-08-10 정정).** 초판이 적어 넣은
**팬텀 값**이다. 실제 종결 상태는 `CONFIRMED`(온체인 확정) 와 `COMPLETED`(P2P 원화 경로 종료) 둘이며,
`COMPLETED` 를 빼면 실측 36건 14,067 이 통째로 누락된다
(파트너36 7월 `USER_PAYOUT` 만 봐도 3,808.00 → 5,203.40 으로 **1,395.40 차이**).
제외: 진행 중(`REQUESTED`~`BROADCASTING`, `P2P_PENDING`) 및 `REJECTED`/`FAILED`/`CANCELLED`/`STALE`.

★★ **교훈 — 이 문서에서만 네 번 반복된 실패다.**
`withdrawal_type` 2종 가정, `withdrawals.completed_at` 팬텀 컬럼, `deposits.status` 단일값 가정,
`WithdrawalStatus.SETTLED` 팬텀 값. 전부 **값·컬럼 도메인을 확인하지 않고 "이럴 것이다"로 쓴 것**이다.
쿼리를 짜기 전에 **반드시** 아래를 실행해 도메인부터 확정하라.
```sql
SHOW COLUMNS FROM <table>;                     -- 컬럼 존재 확인
SELECT <col>, COUNT(*) FROM <table> GROUP BY 1; -- 값 도메인 확인 (+ enum 소스도 함께 볼 것)
```
⚠️ **DB `GROUP BY` 만으로도 부족하다** — 아직 발생하지 않은 enum 값은 데이터에 없다.
`WithdrawalType` 은 당시 데이터에 3종뿐이었지만 enum 에는 5종(`REFUND`·`INTERNAL` 추가)이 있었다.
현재는 `KRW_WITHDRAW` 까지 포함한 6종이다.
**enum 소스와 DB 실측을 둘 다** 볼 것.

### 1-3. 수수료 (파트너가 지불한 비용)

```sql
FROM deposits
WHERE partner_id = :partnerId
  AND status IN ('CONFIRMED','NOTIFIED','COLLECTING','SETTLED')   -- §1-1 과 동일 집합
날짜 = DATE(created_at)
  지불수수료 = SUM(fee_amount)
```
⚠️ **§1-1 입금과 반드시 같은 상태 집합·같은 스캔을 쓸 것.** 조건이 갈리면 같은 날 입금은 잡히는데
수수료는 안 잡히는(또는 반대) 상태가 되어 화면에서 대조가 불가능해진다.
⚠️ **TORQ 입금은 `fee_amount = 0` 이 정상이다.** 수수료가 환율 스프레드(`torq_fee`)에 내장돼 있다
(`P2pSettlementService.createTorqDeposit` 주석: `FEE 0 — torq_fee 내장`).
TORQ 물량이 대부분인 파트너는 이 컬럼이 계속 0으로 보인다 — **버그 아님.**
화면에 이 취지의 안내(툴팁)를 넣을 것.

### 1-4. 수익 — 수수료 몫

```sql
FROM settlement_daily_fees
WHERE participant_partner_id = :partnerId      -- ★ partner_id 아님!
날짜 = settlement_date
  수수료몫 = SUM(share_amount)
```
★★ **`participant_partner_id` 를 써야 한다.** `source_partner_id` 는 수수료가 **발생한** 파트너이고
`participant_partner_id` 가 **그 몫을 받는** 파트너다. 혼동하면 남의 수익이 내 화면에 뜬다
(지식 repo `realization-participant-fix` — `settlement_realizations` 에서 실제로 났던 버그).

⚠️ 대부분의 파트너는 이 값이 **0 또는 매우 작다.** 하위 파트너 요율이 하한과 같으면
총판 마진이 0이고 시스템이 전액을 가져가는 구조이기 때문이다(§45 검증표). 정상이다.

### 1-5. 수익 — 리베이트  ← **귀속 기준 주의**

```sql
FROM torq_rebate_items i
JOIN torq_rebate_settlements s ON s.id = i.rebate_settlement_id
WHERE i.partner_id = :partnerId
  AND i.confirmed_rate > 0        -- CLOSED_ZERO(잔차 없음) 제외
  AND i.credited = 1              -- 실제 크레딧된 것만
  AND s.paid_at IS NOT NULL
날짜 = DATE(s.paid_at)            -- ★ 거래일 아님, 지급일
  리베이트 = SUM(i.rebate_amount)
```

**오너 결정(2026-08-10): 리베이트는 거래일 안분이 아니라 `paid_at`(지급일)에 몰아서 계상한다.**
코드도 그렇게 동작한다 — `TorqRebateService` 가 배치 PAID 시점에 `creditRealizedDirect` 로
파트너 realized 에 직접 크레딧한다.

⚠️ 그 결과 **주 1회(월요일) 큰 봉우리**가 생긴다. 다른 컬럼은 발생주의인데 이 컬럼만 현금주의라
합계를 "그날의 수익"으로 읽으면 오해가 생긴다. → **컬럼 헤더에 `지급일 기준` 을 명시**하고
툴팁으로 "주간 정산분이 지급일에 일괄 반영됩니다"를 안내할 것.

⚠️ `confirmed_rate` 는 **연속값**이다(0.004 / 0.412 / 0.8 등). "0 아니면 0.8" 로 가정하지 말 것.
`> 0` 조건만 쓰면 안전하다. (§46 — 이걸 이진값으로 착각해 오탐을 낸 사례가 있다.)

⚠️ `torq_trades.updated_at` 은 리베이트 잡이 01:30 에 덮어쓰므로 **기간 기준으로 쓰지 말 것.**

### 1-6. 통화 / 네트워크

모든 소스에 `currency_id`·`network_id` 가 있다. **기본은 통화별로 행을 나누지 말고 USDT 합산**,
상단에 통화 선택 필터를 둔다(현재 운영은 사실상 USDT 단일). `deposits`/`withdrawals` 는
`currency_id` 기준으로 필터한다.

---

## 2. API 스펙

### `GET /api/v1/settlements/daily`

| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
| `startDate` | `LocalDate` | N | 기본 = 오늘 - 29일 |
| `endDate` | `LocalDate` | N | 기본 = 오늘 |
| `currencyId` | `Long` | N | 미지정 시 전체 |
| `networkId` | `Long` | N | 미지정 시 전체 |

응답: `PartnerDailySettlementResponse`

```java
/** 일별 정산 내역 응답 */
public class PartnerDailySettlementResponse {
    /** 일자별 행 (최신순) */
    private List<Row> rows;
    /** 조회 기간 합계 */
    private Row total;

    public static class Row {
        /** 기준 일자 (합계 행은 null) */
        private LocalDate date;
        /** 사용자 입금 합계 */
        private BigDecimal userDepositAmount;
        /** 파트너 자기 충전 합계 */
        private BigDecimal partnerChargeAmount;
        /** 일반 출금 합계 */
        private BigDecimal withdrawalAmount;
        /** 수수료 인출 합계 (SETTLEMENT_WITHDRAW) */
        private BigDecimal settlementWithdrawAmount;
        /** 파트너가 지불한 입금 수수료 합계 */
        private BigDecimal paidFeeAmount;
        /** 수수료 배분으로 받은 몫 (settlement_daily_fees.share_amount) */
        private BigDecimal feeShareAmount;
        /** TORQ 리베이트 — 지급일(paid_at) 기준 */
        private BigDecimal rebateAmount;
        /** 수익 합계 = feeShareAmount + rebateAmount */
        private BigDecimal revenueAmount;
    }
}
```

**구현 방식**: 5개 소스를 각각 `GROUP BY 날짜` 로 뽑아 `UNION ALL` 한 뒤 바깥에서 날짜로 재집계한다.
페이지네이션은 **쓰지 않는다**(기간 제한으로 충분, 합계 행이 필요하므로 전체를 받아야 함).
→ `XPage` 규칙(첫 파라미터 `XPagination`, 마지막 `Class<?> cls`) 대상 아님. 일반 `@Select` 로 작성.

⚠️ `<script>` 내부에 `<`, `<=`, `<>` **직접 사용 금지** — `&lt;`, `&lt;=`, `!=` 또는 `<![CDATA[ ]]>`.
(2026-06-11 전 서비스 기동 실패 원인. CLAUDE.md 참조.)

기간 상한을 **최대 366일**로 서버에서 강제하고, 초과 시 `XRestException` 으로 거부한다.

---

## 3. 화면 스펙 — `partner-ui`

- 위치: `src/views/partner/settlement/DailySettlementView.vue`
- 라우팅·메뉴: 기존 정산 메뉴(`FeesView`/`RealizationsView`/`RebatesView`) 옆에 **"일별 정산"** 추가.
  **정산 메뉴의 첫 항목**으로 배치한다(요약 성격이므로).
- 기존 화면들의 레이아웃·필터·숫자 포맷 컨벤션을 그대로 따를 것.
  특히 `FeesView.vue` 를 먼저 읽고 테이블/필터 구조를 재사용한다.

### 표 구성

> ⚠️ **아래 표 이미지와 "행 클릭 → 기존 상세 화면 이동" 항목은 §0(2026-08-10 확정본)으로 대체됐다.**
> 현재 구현은 **요약 6컬럼 + 행 펼침 세부 3블록**이며, 셀 클릭 화면 이동은 제거됐다.
> 입금 세부는 `GET /api/partner/settlement/daily/deposits?date=` 로 펼칠 때 lazy 조회한다
> (집계 조건은 `/daily` 요약의 입금 절과 동일해야 한다).

```
[기간 필터: 시작일 ~ 종료일]  [통화]  [빠른선택: 최근7일 / 30일 / 이번달 / 지난달]

일자        | 입금                    | 출금                  | 수수료   | 수익
            | 사용자      자기충전    | 일반       수수료인출 | 지불     | 수수료몫   리베이트   합계
2026-08-10  | 16,548.87   0.00        | 5,000.00   0.00       | 0.000000 | 0.000000   518.972099  518.972099
...
합계        | ...
```

- 상단에 기간 합계 **요약 카드 4개**(입금 / 출금 / 지불 수수료 / 수익).
- 금액은 소수 6자리, 천단위 구분. 0은 흐린 색으로 처리해 유의미한 값이 눈에 띄게.
- 리베이트 컬럼 헤더에 `지급일 기준` 뱃지 + 툴팁.
- 수수료 컬럼에 툴팁: "TORQ 입금은 수수료가 환율에 포함되어 별도 부과되지 않습니다."
- 행 클릭 시 해당 일자의 **기존 상세 화면으로 이동**(수수료 몫 → `FeesView` 해당일 필터,
  리베이트 → `RebatesView`). 새 상세 화면을 만들지 말 것.
- CSV 내보내기 버튼(현재 조회 결과 그대로).
- 빈 기간엔 "해당 기간에 정산 내역이 없습니다" 안내.

---

## 4. 완료 기준

1. `./gradlew :partner-api:compileJava` 성공, `partner-ui` `npm run build` 성공.
2. `participant_partner_id` 로 수수료 몫을 조회할 것 (`source_partner_id` 사용 시 불합격).
3. 리베이트가 `paid_at` 기준이고 `confirmed_rate > 0 AND credited = 1` 로 필터될 것.
4. 입금이 `USER_DEPOSIT` / 그 외로 **분리**될 것.
5. 출금이 `SETTLEMENT_WITHDRAW` / 그 외로 **분리**될 것.
6. 합계 행이 각 컬럼의 단순 합과 일치할 것.
7. 기간 미지정 시 최근 30일이 조회될 것. 366일 초과 요청은 거부될 것.
8. `<script>` 내 부등호 이스케이프 준수. XML 매퍼 미사용. CLAUDE.md 규칙 준수.
9. DTO 멤버 변수에 JavaDoc 주석 필수.

## 5. 검증 (구현 후 운영 DB 대조)

파트너 36(피터) 2026-08-03 ~ 2026-08-10 기준 기대값:

| 일자 | 사용자입금 | 일반출금 | 수수료인출 | 리베이트 |
|---|---|---|---|---|
| 08-03 | 23,829.38 | 40,028.00 | 353.00 | 353.206871 |
| 08-04 | 52,596.06 | 62,954.00 | 0 | 0 |
| 08-09 | 21,560.36 | 33,275.00 | 0 | 0 |
| 08-10 | 16,548.87 | 5,000.00 | 0 | **518.972099** |

- 리베이트 총합은 `RebatesView` 의 배치 합계와 일치해야 한다.
- 수수료 몫 총합은 `FeesView` 의 같은 기간 합계와 일치해야 한다.

## 6. 범위 밖

- **어드민 콘솔 버전** — 파트너 콘솔 먼저. 어드민은 별건(전 파트너 합계 + 시스템 몫 포함).
- 리베이트 거래일 안분 뷰 — 오너가 지급일 기준으로 결정.
- 원장(`ledger_entries`) 대조 기능.
- 미실현/실현 구분 표시 — `settlement_daily_fees.status` 는 이번에 쓰지 않는다(발생 기준 전액 집계).
