# Guide #78 — Axim Pay 목록 검색/필터 + 네트워크·심볼 바인딩 수정

> **대상**: partner-api (Mapper), partner-ui (AximPaymentsView.vue)
> **문제**: (1) 검색·날짜·상태 필터 UI 없음, (2) 네트워크 "-" 표시, (3) 수량에 심볼 미표시

---

## 문제 분석

| # | 현상 | 원인 |
|---|------|------|
| 1 | 검색, 날짜, 상태 필터 없음 | 프론트에서 `status`만 전달. `search`, `from`, `to` 미전달. 필터 UI 자체 없음 |
| 2 | 네트워크 컬럼 "-" | Mapper가 `SELECT * FROM axim_payments` — 단일 테이블이라 `networkCode` 컬럼 없음 |
| 3 | 수량 "1" (심볼 없음) | AmountDisplay에 `currency` prop으로 `row.currencyCode` 전달하나 같은 이유로 값이 null |

**백엔드 현황**: Controller는 이미 `from`, `to`, `status`, `partnerUserId`, `search` 5개 파라미터를 받고 있음. Mapper도 `<if>` 조건으로 동적 쿼리 지원. **JOIN만 추가하면 해결.**

---

## Part A. 백엔드 — Mapper JOIN 추가

**파일**: `partner-api/.../mapper/PartnerAximPaymentMapper.java`

현재:
```java
@Select("<script>" +
        "SELECT * FROM axim_payments" +
        " WHERE partner_id = #{partnerId}" +
        ...
```

변경:
```java
@Select("<script>" +
        "SELECT ap.*," +
        "  bn.chain_symbol AS network_code," +
        "  c.symbol AS currency_code" +
        " FROM axim_payments ap" +
        "  LEFT JOIN blockchain_networks bn ON ap.network_id = bn.id" +
        "  LEFT JOIN currencies c ON ap.currency_id = c.id" +
        " WHERE ap.partner_id = #{partnerId}" +
        " <if test='status != null'>AND ap.status = #{status}</if>" +
        " <if test='partnerUserId != null'>AND ap.partner_user_id = #{partnerUserId}</if>" +
        " <if test='keyword != null and keyword != \"\"'>" +
        "  AND (ap.payment_code LIKE CONCAT('%', #{keyword}, '%')" +
        "       OR ap.axim_payment_id LIKE CONCAT('%', #{keyword}, '%')" +
        "       OR ap.partner_user_id LIKE CONCAT('%', #{keyword}, '%'))" +
        " </if>" +
        " <if test='from != null'>AND ap.created_at &gt;= #{from}</if>" +
        " <if test='to != null'>AND ap.created_at &lt; DATE_ADD(#{to}, INTERVAL 1 DAY)</if>" +
        "</script>")
XPage<AximPayment> searchAximPayments(XPagination pagination,
                                       @Param("partnerId") Long partnerId,
                                       @Param("status") String status,
                                       @Param("partnerUserId") String partnerUserId,
                                       @Param("keyword") String keyword,
                                       @Param("from") String from,
                                       @Param("to") String to,
                                       Class<?> cls);
```

> **핵심**: `bn.chain_symbol AS network_code`, `c.symbol AS currency_code` 추가.
> MyBatis `mapUnderscoreToCamelCase=true`이므로 `network_code` → `networkCode`, `currency_code` → `currencyCode`로 자동 매핑.
>
> **⚠️ 중요**: 반환 타입을 `XPage<AximPayment>` (entity) → `XPage<AximPaymentResponse>` (DTO)로 변경해야 함.
> Entity에는 `networkCode`/`currencyCode` 필드가 없어서 JOIN 결과가 무시됨.

### A-2. 응답 DTO 추가 (필수)

**파일**: `partner-api/.../dto/response/AximPaymentResponse.java`

```java
package com.cryptoments.partnerapi.dto.response;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.*;

import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * Axim Pay 결제 목록 응답 DTO.
 * AximPayment entity + JOIN 필드 (networkCode, currencyCode).
 */
@Getter @Setter @Builder
@NoArgsConstructor @AllArgsConstructor
public class AximPaymentResponse {

    /** PK */
    private Long id;
    /** 결제 고유 코드 */
    private String paymentCode;
    /** 파트너 ID */
    private Long partnerId;
    /** 파트너 사용자 ID */
    private String partnerUserId;
    /** 외부 지갑 ID */
    private Long externalWalletId;
    /** Axim paymentId */
    private String aximPaymentId;
    /** 파트너 참조 */
    private String partnerReference;
    /** 결제 수량 */
    private BigDecimal amount;
    /** 통화 ID */
    private Long currencyId;
    /** 네트워크 ID */
    private Long networkId;
    /** 1토큰=?KRW */
    private BigDecimal priceKrw;
    /** 1토큰=?USD */
    private BigDecimal priceUsd;
    /** Axim 수수료 */
    private BigDecimal feeAmount;
    /** 수신 지갑 주소 */
    private String walletAddress;
    /** 상태 */
    private String status;
    /** 입금 연결 ID */
    private Long depositId;
    /** 확인 시각 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime confirmedAt;
    /** 만료 시각 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime expiredAt;
    /** 생성일 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createdAt;

    // ── JOIN 필드 ──
    /** 네트워크 심볼 (blockchain_networks.chain_symbol) */
    private String networkCode;
    /** 통화 심볼 (currencies.symbol) */
    private String currencyCode;
}
```

### A-3. Mapper 반환 타입 변경

**파일**: `partner-api/.../mapper/PartnerAximPaymentMapper.java`

```java
// 변경 전
XPage<AximPayment> searchAximPayments(...)

// 변경 후
XPage<AximPaymentResponse> searchAximPayments(...)
```

### A-4. Service 반환 타입 변경

**파일**: `partner-api/.../service/PartnerDepositService.java`

`getAximPayments()` 메서드의 반환 타입도 동일하게 변경:
```java
// 변경 전
public XPage<AximPayment> getAximPayments(...)

// 변경 후
public XPage<AximPaymentResponse> getAximPayments(...)
```

### A-5. Controller 반환 타입 변경

**파일**: `partner-api/.../controller/PartnerDepositController.java`

```java
// 변경 전
public XPage<AximPayment> getAximPayments(...)

// 변경 후
public XPage<AximPaymentResponse> getAximPayments(...)
```

### A-6. UserContext 서비스/컨트롤러도 동일 변경 (누락 주의)

`PartnerUserContextService`와 `PartnerUserContextController`도 같은 Mapper를 호출하므로 함께 변경:

**파일**: `partner-api/.../service/PartnerUserContextService.java`

```java
// 변경 전
public XPage<AximPayment> getUserAximPayments(Long partnerId, String partnerUserId,
                                               XPagination pagination) {
    return aximPaymentMapper.searchAximPayments(pagination, partnerId, null,
            partnerUserId, null, null, null, AximPayment.class);
}

// 변경 후
public XPage<AximPaymentResponse> getUserAximPayments(Long partnerId, String partnerUserId,
                                                       XPagination pagination) {
    return aximPaymentMapper.searchAximPayments(pagination, partnerId, null,
            partnerUserId, null, null, null, AximPaymentResponse.class);
}
```

**파일**: `partner-api/.../controller/PartnerUserContextController.java`

```java
// 변경 전
public XPage<AximPayment> getUserAximPayments(...)

// 변경 후
public XPage<AximPaymentResponse> getUserAximPayments(...)
```

> **⚠️ cls 파라미터도 변경 필수**: Mapper 호출 시 마지막 `AximPayment.class` → `AximPaymentResponse.class`

---

## Part B. 프론트엔드 — AximPaymentsView.vue 전면 수정

**파일**: `partner-ui/src/views/partner/deposits/AximPaymentsView.vue`

### 변경 내용

1. **필터 추가**: 검색(키워드), 상태(select), 날짜 범위(from/to)
2. **필터 파라미터 전달**: `depositService.getAximPayments(filters)`에 모든 필터 전달
3. **수량 심볼**: AmountDisplay `currency` prop 확인 (이미 전달 중 — JOIN 수정으로 자동 해결)

```vue
<script setup lang="ts">
import { onMounted, ref, reactive } from 'vue'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { depositService } from '@/api/services/deposit.service'
import type { XPage } from '@/api/types/common'
import PageHeader from '@/components/common/PageHeader.vue'
import DataTable from '@/components/common/DataTable.vue'
import type { Column } from '@/components/common/DataTable.vue'
import StatusBadge from '@/components/common/StatusBadge.vue'
import AmountDisplay from '@/components/common/AmountDisplay.vue'
import DateDisplay from '@/components/common/DateDisplay.vue'

const loading = ref(false)
const data = ref<XPage<Record<string, unknown>> | null>(null)
const filters = reactive({
  page: 1,
  size: 20,
  status: undefined as string | undefined,
  search: '',
  from: '',
  to: '',
})

const columns: Column<Record<string, unknown>>[] = [
  { key: 'paymentCode', label: '결제코드', class: 'font-mono text-xs' },
  { key: 'partnerUserId', label: '사용자 ID' },
  { key: 'networkCode', label: '네트워크' },
  { key: 'amount', label: '수량', class: 'text-right' },
  { key: 'status', label: '상태' },
  { key: 'createdAt', label: '생성일' },
]

async function fetchData() {
  loading.value = true
  try {
    data.value = await depositService.getAximPayments({
      page: filters.page,
      size: filters.size,
      status: filters.status || undefined,
      search: filters.search || undefined,
      from: filters.from || undefined,
      to: filters.to || undefined,
    })
  } finally { loading.value = false }
}

function onSearch() { filters.page = 1; fetchData() }
function onReset() {
  filters.status = undefined
  filters.search = ''
  filters.from = ''
  filters.to = ''
  filters.page = 1
  fetchData()
}
function onPageChange(page: number) { filters.page = page; fetchData() }

onMounted(fetchData)
</script>

<template>
  <div class="space-y-4">
    <PageHeader title="Axim Pay 결제" description="Axim Pay 결제 내역" />

    <Card>
      <CardContent class="pt-6 space-y-4">
        <!-- 필터 영역 -->
        <div class="flex flex-wrap items-end gap-3">
          <div>
            <label class="mb-1 block text-xs font-medium text-muted-foreground">검색</label>
            <Input v-model="filters.search" placeholder="결제코드, 사용자 ID" class="w-52"
                   @keyup.enter="onSearch" />
          </div>
          <div>
            <label class="mb-1 block text-xs font-medium text-muted-foreground">상태</label>
            <select v-model="filters.status"
                    class="h-9 w-36 rounded-md border border-input bg-background px-3 text-sm">
              <option :value="undefined">전체</option>
              <option value="CONFIRMED">확인</option>
              <option value="PENDING">대기</option>
              <option value="EXPIRED">만료</option>
              <option value="FAILED">실패</option>
              <option value="CANCELED">취소</option>
              <option value="DENIED">거부</option>
              <option value="REQUESTED">요청</option>
            </select>
          </div>
          <div>
            <label class="mb-1 block text-xs font-medium text-muted-foreground">시작일</label>
            <Input v-model="filters.from" type="date" class="w-40" />
          </div>
          <div>
            <label class="mb-1 block text-xs font-medium text-muted-foreground">종료일</label>
            <Input v-model="filters.to" type="date" class="w-40" />
          </div>
          <div class="flex gap-2">
            <Button size="sm" @click="onSearch">조회</Button>
            <Button size="sm" variant="outline" @click="onReset">초기화</Button>
          </div>
        </div>

        <!-- 데이터 테이블 -->
        <DataTable :columns="columns" :data="data" :loading="loading" @page-change="onPageChange">
          <template #amount="{ row }">
            <AmountDisplay :amount="row.amount as number" :currency="row.currencyCode as string" :decimals="2" />
          </template>
          <template #status="{ value }"><StatusBadge :status="value as string" /></template>
          <template #createdAt="{ value }"><DateDisplay :date="value as string" /></template>
        </DataTable>
      </CardContent>
    </Card>
  </div>
</template>
```

---

## 체크리스트

| # | 항목 | 모듈 |
|---|------|------|
| A | Mapper JOIN 추가 (`network_code`, `currency_code`) + 테이블 alias `ap.` | partner-api |
| B | AximPaymentsView 필터 UI (검색 + 상태 + 날짜) + 파라미터 전달 | partner-ui |

## 구현 순서

1. **A** — Mapper JOIN 수정 (partner-api)
2. **B** — Vue 필터 UI 추가 (partner-ui)
3. **확인**: 네트워크 컬럼에 `BSC`/`TRON`/`POLYGON` 표시, 수량에 `1 USDT` 심볼 표시, 필터 동작 확인
