# 관리자 대시보드 확대 + 방식별 입금 집계 구현 지침

> 작성: 2026-06-27 (Cowork) · 대상: IntelliJ(admin-api) + admin-ui
> 우선순위: **P1 (Phase 3 방식별 집계 → Phase 4 현황 대시보드 확대)**
> 관련: `DashboardController/Service/Mapper`, `P2pDashboard*`

---

## 0. 배경 — "오늘 입금 16건 / 5,641" 이 적어 보이는 이유

현재 대시보드 "오늘 입금" = `SELECT COUNT(*), SUM(amount) FROM deposits WHERE DATE(created_at)=CURDATE()` (채널/상태 구분 없음).

2026-06-27 실측 `deposits` 16건의 실제 구성:

| 채널(판별) | 건수 | 금액(USDT) |
|---|---|---|
| P2P/TORQ 레그 정산 (`order_code` 보유, method=TORQ) | 10 | 5,575.53 |
| P2P 레그 정산 (`order_code` 보유, method=P2P) | 1 | 65.92 |
| **TRON 더스트/스팸** (order_code·session_id NULL, 0.00000x) | 5 | 0.000019 |

→ 실질 입금은 11건이고, **더스트 5건이 카운트를 부풀린다.** 또한 Axim Pay·일반 위젯·일반 온체인 입금은 이 날 0건이었다. 문제 3가지:

1. **더스트 스팸**이 건수에 포함됨 (금액 ≈ 0).
2. `deposits` 행만 세므로 **실패·취소·진행중 입금이 안 보임**(이 날 입금주문 취소 1·만료 2, TORQ 취소 3 추가 존재).
3. **채널 구분이 없어** P2P가 사실상 전부라는 걸 알 수 없음.

### 채널 판별 규칙 (`deposits` 컬럼 기준)

```
order_code  IS NOT NULL  → P2P/TORQ 레그 정산 입금  (leg 구분은 deposit_method = P2P | TORQ)
session_id  IS NOT NULL  → 위젯 세션 입금          (Axim/일반 구분은 payment_sessions.provider)
그 외 + amount ≥ 더스트임계 → 일반 온체인 입금 (HD/EXTERNAL/DECIMAL)
그 외 + amount < 더스트임계 → 더스트(스팸) — 집계 제외 / 별도 표기
```

> 더스트 임계값은 `system_settings` 로 관리 권장(예: `dashboard.dust_threshold_usdt = 0.01`). 우선 상수 0.01로 시작 가능.

---

## 표시 규칙 — 금액 소수점 2자리 (전 admin-ui 공통, 2026-06-27)

대시보드 "5,641.44976711" 처럼 USDT가 18자리까지 노출되는 문제 해결. **표시(display)만 2자리, 내부 저장/계산 정밀도(DECIMAL(36,18))는 절대 반올림하지 않는다.**

| 단위 | 표시 | 비고 |
|------|------|------|
| USDT / USD | 소수점 **2자리** (HALF_UP) | 천단위 콤마. 예: `5,641.45` |
| KRW | **0자리**(정수) | 천단위 콤마. 예: `8,730,000` |
| 비율(fee/bonus rate) | % 환산 후 2~3자리 | 예: `2.00%`, `0.20%` |

**admin-ui 공통 포매터** (예: `utils/format.ts`):
```ts
export const fmtUsdt = (v: string | number) =>
  Number(v).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
export const fmtKrw = (v: string | number) =>
  Number(v).toLocaleString('ko-KR', { maximumFractionDigits: 0 });
```

적용 대상: **모든 금액 표시 지점** — 대시보드 카드, 목록 컬럼, 상세 화면, 정산/원장/가스비/인보이스 금액. 합계는 표시값 기준 ±0.01 반올림 차가 날 수 있으므로, **정밀 원값이 필요한 화면(정산 명세·원장 상세)에는 툴팁/상세에 full precision 병기**.

> API 응답은 원값(full precision) 유지 — 반올림은 UI에서만. (정산/장부 계산에 표시값을 쓰면 안 됨.) 만약 백엔드에서 표시용 필드를 따로 내려야 하면 `*_display` 접미사로 별도 필드 추가하고 원값 필드는 유지.

---

## Phase 3 — 방식별 입금 집계 (P1)

### D1. `DashboardMapper` — 채널별 집계 쿼리 추가

`admin-api/.../mapper/DashboardMapper.java`

```java
/** 오늘 입금 — 채널별 집계 (더스트 제외). */
@Select("""
        SELECT
          CASE
            WHEN d.order_code IS NOT NULL AND d.deposit_method = 'P2P'  THEN 'P2P'
            WHEN d.order_code IS NOT NULL AND d.deposit_method = 'TORQ' THEN 'TORQ'
            WHEN d.session_id IS NOT NULL THEN COALESCE(ps.provider, 'SESSION')
            ELSE 'ONCHAIN'
          END AS channel,
          COUNT(*) AS cnt,
          COALESCE(SUM(d.amount), 0) AS amount
        FROM deposits d
        LEFT JOIN payment_sessions ps ON d.session_id = ps.id
        WHERE DATE(d.created_at) = CURDATE()
          AND d.amount >= #{dustThreshold}
        GROUP BY channel
        """)
List<ChannelDepositStat> sumTodayDepositsByChannel(@Param("dustThreshold") BigDecimal dustThreshold);

/** 오늘 더스트(스팸) 입금 건수 (참고용). */
@Select("SELECT COUNT(*) FROM deposits WHERE DATE(created_at)=CURDATE() AND amount < #{dustThreshold}")
Long countTodayDustDeposits(@Param("dustThreshold") BigDecimal dustThreshold);
```

> `payment_sessions.provider` 컬럼명은 실제 스키마로 확인할 것(Axim/일반 구분). 컬럼이 없으면 세션 타입 필드로 대체.
> `ChannelDepositStat` = `{ String channel; Long cnt; BigDecimal amount; }` DTO.

### D2. 진행중/실패 포함 "입금 흐름" 집계 (선택, 권장)

`deposits` 는 정산 완료분만 잡히므로, **시도 대비 결과**를 보려면 주문 단위 집계를 병행:

```java
/** 오늘 P2P/TORQ 입금주문 상태별 집계. */
@Select("""
        SELECT status, COUNT(*) cnt, COALESCE(SUM(krw_amount),0) krw
        FROM p2p_deposit_orders WHERE DATE(created_at)=CURDATE() GROUP BY status
        """)
List<StatusCount> sumTodayP2pDepositOrders();
```

→ COMPLETED/EXPIRED/CANCELLED/진행중을 한눈에. (이 날: COMPLETED 11, EXPIRED 2, CANCELLED 1)

### D3. 응답 DTO + 서비스

`DashboardSummaryResponse` 에 추가:
```java
/** 채널별 오늘 입금 (P2P/TORQ/Axim/일반/온체인) */
private List<ChannelDepositStat> todayDepositsByChannel;
/** 오늘 더스트(스팸) 입금 제외 건수 */
private Long todayDustCount;
/** 오늘 P2P 입금주문 상태별 (시도 대비 결과) */
private List<StatusCount> todayP2pOrderStats;
```

`DashboardService.getSummary()` 에서 위 매퍼 호출하여 채움. 기존 `todayDepositCount/Amount` 는 **더스트 제외 + 전 채널 합계**로 재정의(또는 별도 필드 유지). 더스트 임계는 `system_settings` 조회.

---

## Phase 4 — 현황 대시보드 확대 (P1)

현재 대시보드 위젯: 오늘 입금 / 오늘 출금 / 승인 대기 / 미식별 입금 + 이상 알림(논스/webhook). **P2P·분쟁·정산 현황이 메인에 없다.**

### D4. 메인 대시보드에 추가할 위젯

1. **방식별 입금 카드** (Phase 3) — P2P · TORQ · Axim Pay · 일반 · 온체인 채널별 건수/금액 + 더스트 제외 표기.
2. **분쟁 현황 카드** — `P2pDashboardMapper.countDisputedMatches()` (TORQ 분쟁 전파 수정 후 정확) + 미해결 분쟁 최장 경과시간. 클릭 시 분쟁 목록으로 이동.
3. **P2P 운영 현황** — 활성 출금/입금 주문, 오늘 매칭/정산, 잠금 USDT 합계(이미 `P2pDashboardMapper` 에 다 있음 — 메인에 노출만).
4. **실패/취소 현황** — 오늘 실패 정산(`countFailedSettlements`), TORQ 취소, 입금주문 만료/취소.
5. **이상 알림 확장** — 기존 논스/webhook 외에 **분쟁 미해결 N시간 초과**, **TORQ 레그 DISPUTED**, **상태 불일치(match FAILED ↔ torq DISPUTED)** 알림 추가.

### D5. 이상 알림 — 상태 불일치 감지 (신규)

`DashboardMapper` 에 추가:
```java
/** 상태 불일치: 매칭 FAILED 인데 TORQ 레그 DISPUTED (운영 점검 필요). */
@Select("""
        SELECT 'STATE_MISMATCH' AS alert_type, 'CRITICAL' AS severity,
               CONCAT('매칭 ', m.match_code, ' FAILED ↔ TORQ ', t.escrow_id, ' DISPUTED') AS message,
               'P2P_MATCH' AS entity_type, m.id AS entity_id, t.disputed_at AS detected_at
        FROM p2p_matches m
        JOIN torq_trades t ON m.torq_escrow_id = t.escrow_id
        WHERE m.status = 'FAILED' AND t.status = 'DISPUTED'
        """)
List<AlertResponse> findStateMismatchAlerts();

/** 분쟁 미해결 N시간 초과. */
@Select("""
        SELECT 'DISPUTE_STALE' AS alert_type, 'WARNING' AS severity,
               CONCAT('분쟁 미해결 ', match_code, ' (', TIMESTAMPDIFF(HOUR, disputed_at, NOW()), '시간)') AS message,
               'P2P_MATCH' AS entity_type, id AS entity_id, disputed_at AS detected_at
        FROM p2p_matches
        WHERE status = 'DISPUTED' AND disputed_at < DATE_SUB(NOW(), INTERVAL 6 HOUR)
        """)
List<AlertResponse> findStaleDisputeAlerts();
```

`DashboardService.getAlerts()` 에 두 목록 추가. `alertCount` 합산에도 반영.

> ⚠️ `<script>`/특수문자 규칙: 위 쿼리는 `<`,`>` 미사용이므로 안전. 추가 시 `&lt;`/CDATA 규칙(CLAUDE.md) 준수.

### D6. admin-ui

- 메인 대시보드 카드 그리드 확장 (방식별 입금 / 분쟁 / P2P 현황 / 실패).
- 이상 알림 리스트에 CRITICAL(상태 불일치) 상단 고정 + 색상 구분.
- 각 카드 → 해당 관리 화면 딥링크.
- 코딩 규칙: `ADMIN_UI_IMPLEMENTATION_GUIDE.md` 준수.

---

## 검증 체크리스트

- [ ] 방식별 집계 합계가 (더스트 제외) `deposits` 일별 합계와 일치
- [ ] 더스트 임계 적용 후 오늘 카운트가 16 → 11 로 보정됨 확인
- [ ] Axim/일반/온체인 입금 발생일에 채널 분리 정확(테스트 데이터로)
- [ ] 분쟁 카드 카운트가 TORQ 분쟁 전파(Phase 1) 후 정확
- [ ] 상태 불일치 알림이 기존 #101 같은 건을 감지(보정 전까지)
- [ ] 대시보드 응답 1초 내(필요 시 인덱스/캐시 검토 — `deposits(created_at)`, `p2p_matches(status,disputed_at)`)

## 배포 순서

1. `admin-api` 매퍼/DTO/서비스 → 2. `admin-ui` 카드 → 3. 각 레포 `git push origin main` (CI/CD 자동 배포). DDL 변경 없음(`system_settings` 더스트 임계는 INSERT 1행, 선택).
