# Partner UI 프론트엔드 수정 가이드

> **작성일**: 2026-03-24
> **Guide #49** (Guide #48 백엔드 구현 완료 후속)
> **대상**: partner-ui (Vue 3 + TypeScript) 프론트엔드 프로젝트
> **목적**: Guide #48에서 추가/수정된 백엔드 API를 활용하여 UI를 개선
> **우선순위**: P0 → P1 → P2 순서로 작업

---

## 변경 요약

Guide #48에서 **백엔드 구현이 완료된 항목**:

| # | 항목 | 백엔드 상태 | UI 작업 |
|---|------|-----------|---------|
| P0 | 마스터 데이터 API (네트워크/통화 조회) | ✅ 3 endpoints 신규 | 공통 컴포넌트 + 4개 폼 수정 |
| P1-1 | 결제 링크 생성 API | ✅ `return null` → 정상 구현 | 생성 폼 연동 |
| P1-2 | Axim Pay 결제 요청 | ⚠️ 409 ConflictException (미구현 명시) | 에러 안내 개선 |
| P1-3 | Axim Pay 설정 수정 API | ✅ PUT endpoint 신규 | 설정 화면 리디자인 |
| P2 | 입금 주소 네트워크/통화 표시 | ⚠️ 백엔드 추가 작업 or 프론트 매핑 | 리스트 컬럼 표시 |

---

## P0. 네트워크/통화 드롭다운 공통 컴포넌트

### 배경

4개 폼에서 `통화 ID`, `네트워크 ID`를 **숫자로 직접 입력**하고 있음. 사용자가 내부 DB PK를 알아야 하는 구조로, **사용 불가 상태**.

### 신규 API (구현 완료)

```
GET  /api/partner/master/networks                      → List<NetworkOption>
GET  /api/partner/master/networks/{networkId}/currencies → List<CurrencyOption>
GET  /api/partner/master/currencies                     → List<CurrencyOption>
```

### API 응답 DTO

**NetworkOption**:
```typescript
interface NetworkOption {
  id: number            // 네트워크 ID (API 요청에 사용)
  chainSymbol: string   // "BSC", "POLYGON", "TRON"
  name: string          // "BNB Smart Chain"
  nativeCurrency: string // "BNB", "MATIC", "TRX"
  networkType: string   // "EVM" | "TVM"
}
```

**CurrencyOption**:
```typescript
interface CurrencyOption {
  id: number            // 통화 ID (API 요청에 사용)
  symbol: string        // "USDT", "USDC"
  name: string          // "Tether USD"
  decimals: number      // 6 | 18
  networkId: number     // 소속 네트워크 ID
  networkSymbol: string // 소속 네트워크 심볼 (nullable — 현재 미채워짐)
}
```

### 작업 1: API 서비스 추가

**파일**: `src/api/services/master.service.ts` (신규)

```typescript
import api from '../client'
import type { NetworkOption, CurrencyOption } from '../types/master'

/**
 * 활성 네트워크 목록 조회 (드롭다운용).
 */
export function getNetworks(): Promise<NetworkOption[]> {
  return api.get('/api/partner/master/networks').then(res => res.data)
}

/**
 * 네트워크별 활성 통화 목록 조회 (2단계 드롭다운용).
 */
export function getCurrenciesByNetwork(networkId: number): Promise<CurrencyOption[]> {
  return api.get(`/api/partner/master/networks/${networkId}/currencies`).then(res => res.data)
}

/**
 * 전체 활성 통화 목록 조회.
 */
export function getAllCurrencies(): Promise<CurrencyOption[]> {
  return api.get('/api/partner/master/currencies').then(res => res.data)
}
```

**파일**: `src/api/types/master.ts` (신규)

```typescript
export interface NetworkOption {
  id: number
  chainSymbol: string
  name: string
  nativeCurrency: string
  networkType: string
}

export interface CurrencyOption {
  id: number
  symbol: string
  name: string
  decimals: number
  networkId: number
  networkSymbol: string | null
}
```

### 작업 2: Composable 생성

**파일**: `src/composables/useNetworkCurrency.ts` (신규)

네트워크 → 통화 2단계 선택 로직을 공통화하여 4개 폼에서 재사용.

```typescript
import { ref, watch } from 'vue'
import { getNetworks, getCurrenciesByNetwork } from '@/api/services/master.service'
import type { NetworkOption, CurrencyOption } from '@/api/types/master'

/**
 * 네트워크 → 통화 2단계 선택 composable.
 *
 * @param options.requireCurrency - true면 통화 선택도 포함 (기본 true)
 * @returns 네트워크/통화 목록 + 선택값 + 로딩 상태
 *
 * @example
 * const {
 *   networks, currencies,
 *   selectedNetworkId, selectedCurrencyId,
 *   isLoadingNetworks, isLoadingCurrencies
 * } = useNetworkCurrency()
 */
export function useNetworkCurrency(options?: { requireCurrency?: boolean }) {
  const requireCurrency = options?.requireCurrency ?? true

  // 목록
  const networks = ref<NetworkOption[]>([])
  const currencies = ref<CurrencyOption[]>([])

  // 선택값
  const selectedNetworkId = ref<number | null>(null)
  const selectedCurrencyId = ref<number | null>(null)

  // 로딩
  const isLoadingNetworks = ref(false)
  const isLoadingCurrencies = ref(false)

  // 1) 네트워크 목록 로드 (페이지 마운트 시)
  async function loadNetworks() {
    isLoadingNetworks.value = true
    try {
      networks.value = await getNetworks()
    } finally {
      isLoadingNetworks.value = false
    }
  }

  // 2) 네트워크 선택 시 → 통화 목록 연동 로드
  watch(selectedNetworkId, async (networkId) => {
    // 통화 초기화
    selectedCurrencyId.value = null
    currencies.value = []

    if (!networkId || !requireCurrency) return

    isLoadingCurrencies.value = true
    try {
      currencies.value = await getCurrenciesByNetwork(networkId)
    } finally {
      isLoadingCurrencies.value = false
    }
  })

  // 3) 초기 로드
  loadNetworks()

  return {
    networks,
    currencies,
    selectedNetworkId,
    selectedCurrencyId,
    isLoadingNetworks,
    isLoadingCurrencies,
    loadNetworks,
  }
}
```

### 작업 3: 공통 드롭다운 컴포넌트

**파일**: `src/components/common/NetworkCurrencySelect.vue` (신규)

네트워크 + 통화 2단계 선택 UI 컴포넌트. 각 폼에서 이 컴포넌트 하나로 통일.

```vue
<script setup lang="ts">
/**
 * 네트워크/통화 2단계 선택 컴포넌트.
 *
 * Props:
 * - modelValue: { networkId, currencyId } — v-model 바인딩
 * - showCurrency: 통화 선택 표시 여부 (default: true)
 *   예: 화이트리스트 추가 폼은 네트워크만 필요 → :showCurrency="false"
 * - networkLabel / currencyLabel: 라벨 커스터마이징
 * - required: 필수 여부 (default: true)
 *
 * Emits:
 * - update:modelValue — { networkId: number | null, currencyId: number | null }
 */
import { computed, watch } from 'vue'
import { useNetworkCurrency } from '@/composables/useNetworkCurrency'
import {
  Select, SelectContent, SelectItem, SelectTrigger, SelectValue
} from '@/components/ui/select'
import { Label } from '@/components/ui/label'

interface Props {
  modelValue?: { networkId: number | null; currencyId: number | null }
  showCurrency?: boolean
  networkLabel?: string
  currencyLabel?: string
  required?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  modelValue: () => ({ networkId: null, currencyId: null }),
  showCurrency: true,
  networkLabel: '네트워크',
  currencyLabel: '통화',
  required: true,
})

const emit = defineEmits<{
  'update:modelValue': [value: { networkId: number | null; currencyId: number | null }]
}>()

const {
  networks, currencies,
  selectedNetworkId, selectedCurrencyId,
  isLoadingNetworks, isLoadingCurrencies,
} = useNetworkCurrency({ requireCurrency: props.showCurrency })

// v-model 동기화
watch([selectedNetworkId, selectedCurrencyId], ([nId, cId]) => {
  emit('update:modelValue', { networkId: nId, currencyId: cId })
})

// 외부 modelValue → 내부 상태 동기화 (편집 모드 등)
watch(() => props.modelValue, (val) => {
  if (val.networkId !== selectedNetworkId.value) {
    selectedNetworkId.value = val.networkId
  }
  if (val.currencyId !== selectedCurrencyId.value) {
    selectedCurrencyId.value = val.currencyId
  }
}, { immediate: true })
</script>

<template>
  <div class="grid grid-cols-2 gap-4">
    <!-- 네트워크 선택 -->
    <div class="space-y-2">
      <Label>
        {{ networkLabel }}
        <span v-if="required" class="text-destructive">*</span>
      </Label>
      <Select v-model="selectedNetworkId" :disabled="isLoadingNetworks">
        <SelectTrigger>
          <SelectValue :placeholder="isLoadingNetworks ? '로딩 중...' : '네트워크 선택'" />
        </SelectTrigger>
        <SelectContent>
          <SelectItem
            v-for="network in networks"
            :key="network.id"
            :value="network.id"
          >
            {{ network.chainSymbol }} ({{ network.name }})
          </SelectItem>
        </SelectContent>
      </Select>
    </div>

    <!-- 통화 선택 (showCurrency가 true일 때만) -->
    <div v-if="showCurrency" class="space-y-2">
      <Label>
        {{ currencyLabel }}
        <span v-if="required" class="text-destructive">*</span>
      </Label>
      <Select
        v-model="selectedCurrencyId"
        :disabled="!selectedNetworkId || isLoadingCurrencies"
      >
        <SelectTrigger>
          <SelectValue
            :placeholder="
              !selectedNetworkId
                ? '네트워크를 먼저 선택'
                : isLoadingCurrencies
                  ? '로딩 중...'
                  : '통화 선택'
            "
          />
        </SelectTrigger>
        <SelectContent>
          <SelectItem
            v-for="currency in currencies"
            :key="currency.id"
            :value="currency.id"
          >
            {{ currency.symbol }} ({{ currency.name }})
          </SelectItem>
        </SelectContent>
      </Select>
    </div>
  </div>
</template>
```

### 작업 4: 4개 폼 수정

#### 4-1. 출금 요청 (`WithdrawalNewView.vue` — PCR-3010)

**현재**: `통화 ID [숫자 입력]`, `네트워크 ID [숫자 입력]`
**수정 후**: 네트워크 → 통화 2단계 드롭다운

```vue
<!-- 변경 전 -->
<FormField name="currencyId">
  <FormItem>
    <FormLabel>통화 ID *</FormLabel>
    <FormControl><Input type="number" v-model="form.currencyId" /></FormControl>
  </FormItem>
</FormField>
<FormField name="networkId">
  <FormItem>
    <FormLabel>네트워크 ID *</FormLabel>
    <FormControl><Input type="number" v-model="form.networkId" /></FormControl>
  </FormItem>
</FormField>

<!-- 변경 후 -->
<NetworkCurrencySelect
  v-model="networkCurrency"
  networkLabel="네트워크"
  currencyLabel="통화"
/>
```

**스크립트 변경**:
```typescript
import NetworkCurrencySelect from '@/components/common/NetworkCurrencySelect.vue'

const networkCurrency = ref({ networkId: null, currencyId: null })

// 폼 제출 시
async function handleSubmit() {
  const payload: CreateWithdrawalRequest = {
    currencyId: networkCurrency.value.currencyId!,    // ← 드롭다운에서 선택
    networkId: networkCurrency.value.networkId!,       // ← 드롭다운에서 선택
    toAddress: form.toAddress,
    whitelistId: form.whitelistId,
    amount: form.amount,
    partnerUserId: form.partnerUserId,
  }
  await createWithdrawal(payload)
}
```

**CreateWithdrawalRequest DTO** (참고):
```typescript
interface CreateWithdrawalRequest {
  currencyId: number      // @NotNull
  networkId: number       // @NotNull
  toAddress?: string      // 직접 입력 시
  whitelistId?: number    // 화이트리스트 선택 시
  amount: number          // @NotNull
  partnerUserId?: string
}
```

#### 4-2. 입금 세션 생성 (`DepositSessionNewView.vue` — PCR-2015)

동일하게 `NetworkCurrencySelect` 적용.

**CreateDepositSessionRequest DTO** (참고):
```typescript
interface CreateDepositSessionRequest {
  partnerUserId?: string
  currencyId: number       // @NotNull
  networkId: number        // @NotNull
  depositMethod: string    // @NotNull — "HD_WALLET" | "DECIMAL_MATCH" | "DIRECT" | "EXTERNAL_WALLET"
  amount?: number
  amountCurrency?: string  // "CRYPTO" | "KRW"
  expiresInMinutes?: number
}
```

**추가 개선 — `amountCurrency` 드롭다운화**:

현재 텍스트 입력 `KRW` → Select 드롭다운으로 변경:

```vue
<Select v-model="form.amountCurrency">
  <SelectTrigger><SelectValue placeholder="단위 선택" /></SelectTrigger>
  <SelectContent>
    <SelectItem value="CRYPTO">토큰 기준</SelectItem>
    <SelectItem value="KRW">KRW</SelectItem>
  </SelectContent>
</Select>
```

#### 4-3. 쉐어 출금 (`SettlementWithdrawView.vue` — PCR-5030)

동일하게 `NetworkCurrencySelect` 적용.

**SettlementWithdrawRequest DTO** (참고):
```typescript
interface SettlementWithdrawRequest {
  currencyId: number      // @NotNull
  networkId: number       // @NotNull
  toAddress?: string
  whitelistId?: number
  amount: number          // @NotNull
}
```

#### 4-4. 화이트리스트 추가 (`WhitelistView.vue` — PCR-3020)

**특이점**: 이 폼은 **네트워크만** 필요 (통화 불필요).

```vue
<NetworkCurrencySelect
  v-model="networkCurrency"
  :showCurrency="false"
  networkLabel="네트워크"
/>
```

**AddWhitelistRequest DTO** (참고):
```typescript
interface AddWhitelistRequest {
  label: string      // @NotBlank
  address: string    // @NotBlank
  networkId: number  // @NotNull
}
```

**스크립트**:
```typescript
async function handleSubmit() {
  const payload: AddWhitelistRequest = {
    label: form.label,
    address: form.address,
    networkId: networkCurrency.value.networkId!,  // ← 드롭다운에서 선택
  }
  await addWhitelist(payload)
}
```

### P0 UI 호출 흐름 정리

```
┌─────────────────────────────────────────────────────────┐
│ 페이지 마운트                                            │
│   └→ GET /api/partner/master/networks                    │
│       └→ 네트워크 드롭다운 렌더링                          │
│           [BSC (BNB Smart Chain)]                         │
│           [POLYGON (Polygon)]                            │
│           [TRON (TRON)]                                  │
│                                                          │
│ 사용자가 "BSC" 선택                                       │
│   └→ GET /api/partner/master/networks/1/currencies       │
│       └→ 통화 드롭다운 렌더링                              │
│           [USDT (Tether USD)]                            │
│           [USDC (USD Coin)]                              │
│                                                          │
│ 사용자가 "USDT" 선택                                      │
│   └→ 폼에 networkId=1, currencyId=3 자동 세팅             │
│                                                          │
│ 폼 제출                                                  │
│   └→ POST /api/partner/withdrawals                       │
│       Body: { networkId: 1, currencyId: 3, ... }         │
└─────────────────────────────────────────────────────────┘
```

---

## P1-1. 결제 링크 생성 폼 연동

### 배경

기존 `POST /api/partner/payment-links`가 `return null` (NPE) → **정상 구현 완료**.
UI에서 "결제 링크 생성 API는 현재 준비 중입니다 (501)" 메시지를 제거하고 실제 폼 연동.

### 신규/수정 API

```
POST  /api/partner/payment-links   → PaymentLink (201 Created)
GET   /api/partner/payment-links   → XPage<PaymentLink>
```

### 작업: PaymentLinkListView + 생성 폼

**파일**: `PaymentLinkListView.vue` (PCR-2020)

1. **501 에러 핸들링 제거** — 기존 stub 안내 메시지 삭제
2. **목록 조회 연동** — `GET /api/partner/payment-links` 호출
3. **"결제 링크 생성" 버튼** 추가 → 생성 폼(모달 또는 별도 페이지) 열기

**생성 폼 필드**:

```vue
<template>
  <!-- 결제 링크 생성 Dialog -->
  <Dialog v-model:open="showCreateDialog">
    <DialogContent class="max-w-lg">
      <DialogHeader>
        <DialogTitle>결제 링크 생성</DialogTitle>
      </DialogHeader>

      <form @submit.prevent="handleCreate" class="space-y-4">
        <!-- 제목 (필수) -->
        <div class="space-y-2">
          <Label>제목 <span class="text-destructive">*</span></Label>
          <Input v-model="form.title" placeholder="결제 링크 제목" />
        </div>

        <!-- 네트워크/통화 (선택 — null이면 결제 페이지에서 사용자가 직접 선택) -->
        <NetworkCurrencySelect
          v-model="networkCurrency"
          :required="false"
          networkLabel="네트워크 (선택)"
          currencyLabel="통화 (선택)"
        />
        <p class="text-xs text-muted-foreground">
          미선택 시 결제 페이지에서 고객이 직접 선택합니다.
        </p>

        <!-- 결제 금액 (선택) -->
        <div class="space-y-2">
          <Label>결제 금액 (선택)</Label>
          <Input type="number" v-model="form.amount" placeholder="미입력 시 고객이 직접 입력" />
        </div>

        <!-- 입금 방식 (선택) -->
        <div class="space-y-2">
          <Label>입금 방식 (선택)</Label>
          <Select v-model="form.depositMethod">
            <SelectTrigger>
              <SelectValue placeholder="미선택 시 고객이 선택" />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="HD_WALLET">HD Wallet</SelectItem>
              <SelectItem value="DECIMAL_MATCH">소수점 매칭</SelectItem>
              <SelectItem value="DIRECT">직접 전송</SelectItem>
              <SelectItem value="EXTERNAL_WALLET">외부 지갑</SelectItem>
            </SelectContent>
          </Select>
        </div>

        <!-- 참조 코드 (선택) -->
        <div class="space-y-2">
          <Label>참조 코드 (선택)</Label>
          <Input v-model="form.partnerReference" placeholder="주문번호 등" />
        </div>

        <!-- 고객 ID (선택) -->
        <div class="space-y-2">
          <Label>고객 ID (선택)</Label>
          <Input v-model="form.partnerUserId" placeholder="파트너 사용자 ID" />
        </div>

        <!-- 만료 시간 -->
        <div class="space-y-2">
          <Label>만료 시간</Label>
          <Select v-model="form.expiresInMinutes">
            <SelectTrigger>
              <SelectValue placeholder="만료 없음" />
            </SelectTrigger>
            <SelectContent>
              <SelectItem :value="null">만료 없음</SelectItem>
              <SelectItem :value="30">30분</SelectItem>
              <SelectItem :value="60">1시간</SelectItem>
              <SelectItem :value="1440">24시간</SelectItem>
            </SelectContent>
          </Select>
        </div>

        <DialogFooter>
          <Button variant="outline" @click="showCreateDialog = false">취소</Button>
          <Button type="submit" :disabled="!form.title">생성</Button>
        </DialogFooter>
      </form>
    </DialogContent>
  </Dialog>
</template>
```

**CreatePaymentLinkRequest DTO** (참고):
```typescript
interface CreatePaymentLinkRequest {
  title: string            // @NotBlank (필수)
  partnerUserId?: string
  partnerReference?: string
  currencyId?: number      // nullable — null이면 고객 선택
  networkId?: number       // nullable — null이면 고객 선택
  amount?: number          // nullable — null이면 고객 입력
  depositMethod?: string   // nullable — null이면 고객 선택
  expiresInMinutes?: number // nullable — null이면 만료 없음
}
```

**API 서비스 추가** (`src/api/services/deposit.service.ts`):

```typescript
export function createPaymentLink(req: CreatePaymentLinkRequest): Promise<PaymentLink> {
  return api.post('/api/partner/payment-links', req).then(res => res.data)
}
```

**목록 테이블 컬럼**:

| 컬럼 | 필드 | 비고 |
|------|------|------|
| 제목 | `title` | |
| 링크 코드 | `linkCode` | 복사 버튼 포함 |
| 상태 | `status` | StatusBadge (ACTIVE=green, EXPIRED=gray, COMPLETED=blue) |
| 금액 | `amount` | nullable — "미지정" 표시 |
| 통화 | `currencyId` | 마스터 데이터 캐시에서 심볼 매핑 |
| 생성일 | `createdAt` | |
| 만료일 | `expiresAt` | nullable |

---

## P1-2. Axim Pay 결제 요청 에러 개선

### 현재 상태

백엔드가 `ConflictException` (409)을 반환하며 `"Axim Pay 결제 요청 기능은 현재 준비 중입니다."` 메시지를 전달.

### 작업

**AximPaymentListView.vue** (PCR-2050):

1. 결제 요청 버튼은 **비활성화 상태로 표시** (disabled)
2. 툴팁 또는 안내 배너: "Axim Pay 결제 요청 기능은 현재 준비 중입니다."
3. 기존 501 에러 핸들링을 409 ConflictException 핸들링으로 변경

```vue
<div class="mb-4 rounded-lg border border-yellow-200 bg-yellow-50 p-3 text-sm text-yellow-800">
  Axim Pay 결제 요청 기능은 현재 준비 중입니다. 결제 내역 조회는 정상 동작합니다.
</div>

<Button disabled>
  결제 요청 (준비 중)
</Button>
```

결제 **내역 조회**(`GET /api/partner/axim-payments`)는 정상 동작하므로, 목록 테이블은 활성화.

---

## P1-3. Axim Pay 설정 화면 리디자인

### 배경

현재 `/partner/settings/axim` 화면이 **JSON raw 디버그 뷰**로 표시됨.
V1에서는 Api Key / Api Secret 입력 + ON/OFF 토글 + 저장 구조.

### 신규 API (구현 완료)

```
GET  /api/partner/integration/axim   → PartnerAximSettings (기존)
PUT  /api/partner/integration/axim   → PartnerAximSettings (신규)
```

**UpdateAximSettingsRequest DTO**:
```typescript
interface UpdateAximSettingsRequest {
  apiKey?: string       // Axim Pay API Key
  apiSecret?: string    // Axim Pay API Secret
  siteId?: string       // 사이트 ID
  isEnabled?: boolean   // 활성화 여부
}
```

### 작업: AximPayView.vue (PCR-9030) 전면 리디자인

**변경 전**: JSON raw 필드 (id, partnerId, isEnabled, apiKey, createdAt, updatedAt) 나열
**변경 후**: 입력 폼 + 토글

```vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { getAximSettings, updateAximSettings } from '@/api/services/integration.service'
import { useToast } from '@/composables/useToast'

const { toast } = useToast()

const form = ref({
  apiKey: '',
  apiSecret: '',
  siteId: '',
  isEnabled: false,
})
const isLoading = ref(false)
const isNew = ref(true) // 최초 설정 여부

onMounted(async () => {
  try {
    const settings = await getAximSettings()
    if (settings) {
      isNew.value = false
      form.value.apiKey = settings.apiKey || ''
      form.value.apiSecret = '' // Secret은 조회 시 마스킹 — 빈값 표시
      form.value.siteId = settings.siteId || ''
      form.value.isEnabled = settings.isEnabled || false
    }
  } catch (e: any) {
    // 403 또는 404면 설정 없음 → 최초 설정 모드
    if (e.response?.status === 403 || e.response?.status === 404) {
      isNew.value = true
    }
  }
})

async function handleSave() {
  isLoading.value = true
  try {
    // null 제거: 빈 문자열은 보내지 않음 (PATCH 스타일)
    const payload: Record<string, any> = {}
    if (form.value.apiKey) payload.apiKey = form.value.apiKey
    if (form.value.apiSecret) payload.apiSecret = form.value.apiSecret
    if (form.value.siteId) payload.siteId = form.value.siteId
    payload.isEnabled = form.value.isEnabled

    await updateAximSettings(payload)
    toast({ title: '저장 완료', description: 'Axim Pay 설정이 저장되었습니다.' })
  } catch (e) {
    toast({ title: '저장 실패', description: '설정 저장 중 오류가 발생했습니다.', variant: 'destructive' })
  } finally {
    isLoading.value = false
  }
}
</script>

<template>
  <div class="space-y-6">
    <PageHeader title="Axim Pay 연동 설정" />

    <Card class="max-w-xl">
      <CardContent class="space-y-6 pt-6">
        <!-- 활성화 토글 -->
        <div class="flex items-center justify-between">
          <div>
            <Label class="text-base font-medium">Axim Pay 활성화</Label>
            <p class="text-sm text-muted-foreground">활성화 시 Axim Pay 결제를 수신합니다.</p>
          </div>
          <Switch v-model:checked="form.isEnabled" />
        </div>

        <Separator />

        <!-- API Key -->
        <div class="space-y-2">
          <Label>API Key</Label>
          <Input
            v-model="form.apiKey"
            placeholder="Axim Pay에서 발급받은 API Key"
          />
        </div>

        <!-- API Secret -->
        <div class="space-y-2">
          <Label>API Secret</Label>
          <Input
            v-model="form.apiSecret"
            type="password"
            :placeholder="isNew ? 'API Secret 입력' : '변경 시에만 입력 (미입력 시 유지)'"
          />
        </div>

        <!-- 사이트 ID -->
        <div class="space-y-2">
          <Label>사이트 ID</Label>
          <Input
            v-model="form.siteId"
            placeholder="Axim Pay 사이트 ID"
          />
        </div>

        <!-- 저장 -->
        <Button @click="handleSave" :disabled="isLoading" class="w-full">
          {{ isLoading ? '저장 중...' : (isNew ? '설정 등록' : '설정 저장') }}
        </Button>
      </CardContent>
    </Card>
  </div>
</template>
```

**API 서비스 추가** (`src/api/services/integration.service.ts`):

```typescript
export function getAximSettings(): Promise<PartnerAximSettings> {
  return api.get('/api/partner/integration/axim').then(res => res.data)
}

export function updateAximSettings(req: UpdateAximSettingsRequest): Promise<PartnerAximSettings> {
  return api.put('/api/partner/integration/axim', req).then(res => res.data)
}
```

---

## P2. 입금 주소 네트워크/통화 표시

### 배경

`/partner/deposit-addresses` 화면에서 주소 데이터는 표시되지만, 네트워크/통화 컬럼이 모두 `-`로 표시됨.

### 해결 방안 (프론트엔드에서 처리)

P0에서 추가한 마스터 데이터 API를 활용하여 프론트에서 ID → 이름 매핑.

**파일**: `src/composables/useMasterDataCache.ts` (신규)

```typescript
import { ref } from 'vue'
import { getNetworks, getAllCurrencies } from '@/api/services/master.service'
import type { NetworkOption, CurrencyOption } from '@/api/types/master'

// 싱글톤 캐시 (앱 전역에서 1회만 로드)
const networksCache = ref<Map<number, NetworkOption>>(new Map())
const currenciesCache = ref<Map<number, CurrencyOption>>(new Map())
const isLoaded = ref(false)

export function useMasterDataCache() {
  async function ensureLoaded() {
    if (isLoaded.value) return

    const [networks, currencies] = await Promise.all([
      getNetworks(),
      getAllCurrencies(),
    ])

    networks.forEach(n => networksCache.value.set(n.id, n))
    currencies.forEach(c => currenciesCache.value.set(c.id, c))
    isLoaded.value = true
  }

  function getNetworkName(networkId: number | null): string {
    if (!networkId) return '-'
    return networksCache.value.get(networkId)?.chainSymbol ?? '-'
  }

  function getNetworkDisplayName(networkId: number | null): string {
    if (!networkId) return '-'
    return networksCache.value.get(networkId)?.name ?? '-'
  }

  function getCurrencySymbol(currencyId: number | null): string {
    if (!currencyId) return '-'
    return currenciesCache.value.get(currencyId)?.symbol ?? '-'
  }

  function getCurrencyName(currencyId: number | null): string {
    if (!currencyId) return '-'
    return currenciesCache.value.get(currencyId)?.name ?? '-'
  }

  return {
    ensureLoaded,
    getNetworkName,
    getNetworkDisplayName,
    getCurrencySymbol,
    getCurrencyName,
    networksCache,
    currenciesCache,
  }
}
```

**DepositAddressListView.vue** (PCR-2030) 수정:

```typescript
import { useMasterDataCache } from '@/composables/useMasterDataCache'

const { ensureLoaded, getNetworkName, getCurrencySymbol } = useMasterDataCache()

onMounted(async () => {
  await ensureLoaded()
  // 기존 주소 목록 로드...
})
```

테이블 컬럼에서:
```vue
<!-- 변경 전 -->
<td>{{ row.networkId ?? '-' }}</td>

<!-- 변경 후 -->
<td>{{ getNetworkName(row.networkId) }}</td>
```

**적용 대상 화면** (네트워크/통화 ID를 이름으로 표시해야 하는 모든 리스트):

| 화면 | ID 필드 | 매핑 |
|------|---------|------|
| 입금 주소 | `networkId` | → `getNetworkName()` |
| 결제 링크 목록 | `currencyId`, `networkId` | → `getCurrencySymbol()`, `getNetworkName()` |
| 집금 현황 | `networkId` | → `getNetworkName()` |
| 화이트리스트 | `networkId` | → `getNetworkName()` |

---

## 추가 개선 사항

### A. 출금 요청 — 화이트리스트 연동

현재 `수신 주소`를 직접 텍스트 입력만 가능. 개선: 화이트리스트에서 선택 옵션 추가.

```vue
<div class="space-y-2">
  <Label>수신 방식</Label>
  <RadioGroup v-model="addressMode" class="flex gap-4">
    <RadioGroupItem value="whitelist">화이트리스트에서 선택</RadioGroupItem>
    <RadioGroupItem value="direct">직접 입력</RadioGroupItem>
  </RadioGroup>
</div>

<!-- 화이트리스트 선택 모드 -->
<div v-if="addressMode === 'whitelist'" class="space-y-2">
  <Label>화이트리스트</Label>
  <Select v-model="form.whitelistId">
    <SelectTrigger><SelectValue placeholder="화이트리스트 선택" /></SelectTrigger>
    <SelectContent>
      <SelectItem
        v-for="item in whitelistItems"
        :key="item.id"
        :value="item.id"
      >
        {{ item.label }} ({{ item.address.slice(0, 8) }}...{{ item.address.slice(-6) }})
      </SelectItem>
    </SelectContent>
  </Select>
</div>

<!-- 직접 입력 모드 -->
<div v-else class="space-y-2">
  <Label>수신 주소</Label>
  <Input v-model="form.toAddress" placeholder="0x..." />
</div>
```

**필요 API**: `GET /api/partner/withdrawals/whitelist` (이미 구현됨)

**참고**: `CreateWithdrawalRequest`에 `whitelistId`와 `toAddress`가 이미 존재하며, 둘 중 하나 필수.

### B. API 키 화면 — Secret 표시 추가

V1에서는 API Secret도 표시(마스킹+복사) 가능했으나, V2에서는 미표시.

**확인 필요**: `ApiKeyResponse` DTO에 `apiSecret` 필드가 포함되어 있는지 → 포함되어 있다면 마스킹 표시 + "보기" 버튼 추가. 미포함이라면 백엔드에 필드 추가 필요 (별도 Guide 필요).

---

## API 엔드포인트 전체 참조

### 이번 가이드에서 사용하는 API

| Method | Endpoint | 용도 | 상태 |
|--------|----------|------|------|
| `GET` | `/api/partner/master/networks` | 네트워크 드롭다운 | ✅ 신규 |
| `GET` | `/api/partner/master/networks/{id}/currencies` | 통화 드롭다운 | ✅ 신규 |
| `GET` | `/api/partner/master/currencies` | 전체 통화 (캐시용) | ✅ 신규 |
| `POST` | `/api/partner/payment-links` | 결제 링크 생성 | ✅ 수정 |
| `GET` | `/api/partner/payment-links` | 결제 링크 목록 | ✅ 기존 |
| `GET` | `/api/partner/integration/axim` | Axim 설정 조회 | ✅ 기존 |
| `PUT` | `/api/partner/integration/axim` | Axim 설정 수정 | ✅ 신규 |
| `POST` | `/api/partner/axim-payments/request` | Axim 결제 요청 | ⚠️ 409 (준비 중) |

### 기존 API (폼 연동에 필요)

| Method | Endpoint | 용도 |
|--------|----------|------|
| `POST` | `/api/partner/withdrawals` | 출금 요청 생성 |
| `POST` | `/api/partner/deposit-sessions` | 입금 세션 생성 |
| `POST` | `/api/partner/settlement/withdraw` | 쉐어 출금 |
| `POST` | `/api/partner/withdrawals/whitelist` | 화이트리스트 추가 |
| `GET` | `/api/partner/withdrawals/whitelist` | 화이트리스트 목록 |
| `GET` | `/api/partner/deposit-addresses` | 입금 주소 목록 |

---

## 작업 순서 요약

| 순서 | 항목 | 수정 파일 | 의존성 |
|------|------|---------|--------|
| **1** | API 타입 + 서비스 추가 | `types/master.ts`, `services/master.service.ts` | 없음 |
| **2** | Composable 생성 | `useNetworkCurrency.ts`, `useMasterDataCache.ts` | #1 |
| **3** | 공통 컴포넌트 | `NetworkCurrencySelect.vue` | #2 |
| **4** | 4개 폼 수정 (P0) | `WithdrawalNewView`, `DepositSessionNewView`, `SettlementWithdrawView`, `WhitelistView` | #3 |
| **5** | 결제 링크 폼 (P1-1) | `PaymentLinkListView` | #3 |
| **6** | Axim 설정 리디자인 (P1-3) | `AximPayView` | `integration.service.ts` |
| **7** | Axim 결제 안내 (P1-2) | `AximPaymentListView` | 없음 |
| **8** | 입금 주소 표시 (P2) | `DepositAddressListView` | #2 |
| **9** | 화이트리스트 연동 (추가) | `WithdrawalNewView`, `SettlementWithdrawView` | 기존 API |

---

## 체크리스트

- [ ] P0: `master.service.ts` + `types/master.ts` 생성
- [ ] P0: `useNetworkCurrency.ts` composable 생성
- [ ] P0: `NetworkCurrencySelect.vue` 공통 컴포넌트 생성
- [ ] P0: 출금 요청 폼 — 숫자 입력 → 드롭다운
- [ ] P0: 입금 세션 생성 폼 — 숫자 입력 → 드롭다운
- [ ] P0: 쉐어 출금 폼 — 숫자 입력 → 드롭다운
- [ ] P0: 화이트리스트 추가 폼 — 숫자 입력 → 드롭다운 (네트워크만)
- [ ] P1-1: 결제 링크 생성 폼 구현 + 목록 연동
- [ ] P1-2: Axim Pay 결제 요청 — 비활성 안내 UI
- [ ] P1-3: Axim Pay 설정 — JSON raw → 입력 폼 + 토글
- [ ] P2: `useMasterDataCache.ts` 생성
- [ ] P2: 입금 주소 리스트 — 네트워크/통화 컬럼 매핑
- [ ] 추가: 출금 요청 화이트리스트 선택 옵션
- [ ] 추가: `amountCurrency` 텍스트 → Select 드롭다운
