# Guide #63 — 버그 수정 3건 (Pool 생성 누락 + 가스비 표시 + 외부지갑 주소)

**작성일**: 2026-03-26
**대상**: core (Spring), partner-api (Spring), partner-ui (Vue)

---

## Part A — POOL 지갑 생성 시 `deposit_address_pool` 레코드 누락 (Critical)

### 증상

```
NotFoundException: 사용 가능한 풀 주소가 없습니다.
  at DepositService.assignPoolAddress(DepositService.java:642)
```

### 원인

`WalletService.createPoolWallet()`이 `wallet_addresses` 테이블에 POOL 지갑을 생성하지만,
**`deposit_address_pool` 테이블에 레코드를 삽입하지 않습니다.**

소수점 매칭 입금 세션 생성 시 `assignPoolAddress()`는 `deposit_address_pool`을 조회하므로
항상 빈 결과 → `NotFoundException`.

### 흐름 비교

```
현재 (❌):
  POST /wallets/pool → wallet_addresses INSERT ✅
                      → wallet_balances INSERT ✅
                      → wallet_approvals INSERT ✅
                      → deposit_address_pool INSERT ❌ ← 누락!

수정 후 (✅):
  POST /wallets/pool → wallet_addresses INSERT ✅
                      → wallet_balances INSERT ✅
                      → wallet_approvals INSERT ✅
                      → deposit_address_pool INSERT ✅ (네트워크 활성 통화별 1건씩)
```

### 수정 1: PoolWalletCreateRequest에 currencyIds 추가 (선택)

**파일**: `partner-api/.../dto/request/PoolWalletCreateRequest.java`

```java
@NotNull(message = "네트워크 ID는 필수입니다.")
private Long networkId;

/**
 * 수신 대상 통화 ID 목록 (선택).
 * 미지정 시 해당 네트워크의 활성 통화 전체로 자동 등록.
 */
private List<Long> currencyIds;
```

### 수정 2: WalletService.createPoolWallet()에 deposit_address_pool INSERT 추가

**파일**: `core/.../wallet/WalletService.java` — `createPoolWallet()` 메서드

```java
public WalletAddress createPoolWallet(Long partnerId, Long networkId, List<Long> currencyIds) {
    WalletDeriveResponse derived = deriveWallet(networkId, "POOL", partnerId, null);
    WalletAddress poolWallet = walletAddressRepository.findOne(derived.getWalletAddressId());
    if (poolWallet == null) {
        throw new NotFoundException(ErrorCodes.WALLET_ADDRESS_NOT_FOUND);
    }

    initWalletBalances(poolWallet.getId(), networkId);
    int approvals = registerTokenApprovals(poolWallet.getId(), networkId);

    // ★ deposit_address_pool 레코드 생성
    List<Long> targetCurrencies = (currencyIds != null && !currencyIds.isEmpty())
            ? currencyIds
            : getActiveCurrencyIds(networkId);  // 네트워크의 활성 통화 전체

    for (Long currencyId : targetCurrencies) {
        DepositAddressPool pool = DepositAddressPool.builder()
                .partnerId(partnerId)
                .networkId(networkId)
                .currencyId(currencyId)
                .walletAddressId(poolWallet.getId())
                .address(poolWallet.getAddress())
                .decimalDigits(4)  // 기본값
                .isActive(true)
                .build();
        depositAddressPoolRepository.save(pool);
    }

    log.info("POOL wallet created: partnerId={}, networkId={}, address={}, currencies={}, approvals={}",
            partnerId, networkId, poolWallet.getAddress(), targetCurrencies.size(), approvals);
    return poolWallet;
}
```

### 수정 3: getActiveCurrencyIds 헬퍼 추가

```java
/**
 * 네트워크의 활성 통화 ID 목록.
 */
private List<Long> getActiveCurrencyIds(Long networkId) {
    return currencyRepository.findByNetworkId(networkId).stream()
            .filter(c -> Boolean.TRUE.equals(c.getIsActive()))
            .map(Currency::getId)
            .toList();
}
```

### 수정 4: 의존성 추가

`WalletService`에 `DepositAddressPoolRepository` 주입:

```java
private final DepositAddressPoolRepository depositAddressPoolRepository;
```

### 수정 5: PartnerWalletService 호출부 수정

```java
public WalletCreateResponse createPoolWallet(Long partnerId, PoolWalletCreateRequest request) {
    WalletAddress wallet = walletService.createPoolWallet(
            partnerId, request.getNetworkId(), request.getCurrencyIds());
    return toResponse(wallet);
}
```

### 검증

```sql
-- POOL 지갑 생성 후 확인
SELECT * FROM deposit_address_pool WHERE partner_id = ? AND network_id = ?;
-- 결과: currency별 1건씩 (예: USDT, USDC → 2건)

-- 소수점 매칭 세션 생성 → 정상 동작
```

---

## Part B — 가스비 기록 목록 표시 안됨 (네트워크, 가스비)

### 증상

가스비 기록 테이블에서 **네트워크**, **가스비(원본)**, **가스비(USD)** 컬럼이 모두 `-` 표시.

### 원인

필드명 불일치 + 네트워크 코드 JOIN 누락:

| 백엔드 엔티티 (GasCostRecord) | 프론트 타입 (gas.ts) | JSON 키 |
|---|---|---|
| `feeNative` | `gasCostNative` | `feeNative` |
| `feeUsd` | `gasCostUsd` | `feeUsd` |
| `networkId` (숫자) | `networkCode` (문자열) | `networkId` |

프론트엔드가 `gasCostNative`, `gasCostUsd`, `networkCode`를 참조하지만,
백엔드는 `feeNative`, `feeUsd`, `networkId`를 반환합니다.

### 수정 방법: 프론트엔드를 백엔드에 맞추기 + 네트워크 JOIN

#### B-1. 백엔드 Mapper에 네트워크 코드 JOIN 추가

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

```java
@Select("<script>" +
        "SELECT g.*, bn.chain_symbol AS network_code" +
        " FROM gas_cost_records g" +
        " LEFT JOIN blockchain_networks bn ON g.network_id = bn.id" +
        " WHERE g.partner_id = #{partnerId}" +
        " <if test='networkId != null'>AND g.network_id = #{networkId}</if>" +
        " <if test='from != null'>AND g.created_at &gt;= #{from}</if>" +
        " <if test='to != null'>AND g.created_at &lt; DATE_ADD(#{to}, INTERVAL 1 DAY)</if>" +
        "</script>")
XPage<GasCostRecord> searchGasCosts(XPagination pagination,
                                     @Param("partnerId") Long partnerId,
                                     @Param("from") String from,
                                     @Param("to") String to,
                                     @Param("networkId") Long networkId,
                                     Class<?> cls);
```

> `chain_symbol AS network_code` → `mapUnderscoreToCamelCase` → `networkCode`

#### B-2. GasCostRecord 엔티티에 networkCode 필드 추가 (조회 전용)

**파일**: `common/.../entity/GasCostRecord.java`에 추가

```java
/** JOIN용: blockchain_networks.chain_symbol (INSERT/UPDATE 무시) */
@XColumn(value = "network_code", insert = false, update = false)
private String networkCode;
```

#### B-3. 프론트엔드 타입 수정

**파일**: `partner-ui/src/api/types/gas.ts`

```typescript
export interface GasCostRecord {
  id: number
  networkId: number
  networkCode?: string     // ← 유지 (이제 백엔드에서 반환)
  txHash?: string
  feeNative?: number       // ← gasCostNative → feeNative
  feeUsd?: number          // ← gasCostUsd → feeUsd
  txType?: string
  createdAt?: string
}
```

#### B-4. GasCostsView.vue 컬럼 키 수정

**파일**: `partner-ui/.../gas/GasCostsView.vue`

```typescript
const columns: Column<GasCostRecord>[] = [
  { key: 'createdAt', label: '일시' },
  { key: 'txType', label: 'TX유형' },
  { key: 'networkCode', label: '네트워크' },      // 유지
  { key: 'txHash', label: 'TX Hash' },
  { key: 'feeNative', label: '가스비(원본)', class: 'text-right' },   // ← 변경
  { key: 'feeUsd', label: '가스비(USD)', class: 'text-right' },       // ← 변경
]
```

템플릿 슬롯도 변경:

```html
<template #feeNative="{ row }"><AmountDisplay :amount="row.feeNative" :decimals="8" /></template>
<template #feeUsd="{ row }"><AmountDisplay :amount="row.feeUsd" currency="USD" :decimals="4" /></template>
```

---

## Part C — 외부 지갑 주소 표시 개선

### 증상

외부 지갑 목록의 "주소" 컬럼이 `-`로 표시. `wallet_addresses`가 JSON 형태
(`{"2": "0xBSC...", "3": "0xPOLYGON...", "4": "TTRON..."}`)로 저장되어 단일 컬럼으로 표현 불가.

### 수정 방법: 체인 배지 + 확장 행

#### C-1. 프론트엔드 타입 수정

**파일**: 외부지갑 관련 타입 파일 (예: `wallet.ts`)

```typescript
export interface ExternalWallet {
  id: number
  partnerUserId?: string
  connectionType?: string
  walletAddresses?: string  // JSON string: {"2": "0xBSC...", "3": "0xPOL...", "4": "TTRON..."}
  status?: string
  connectedAt?: string
}

// JSON 파싱 헬퍼
export function parseWalletAddresses(json?: string): Record<string, string> {
  if (!json) return {}
  try { return JSON.parse(json) } catch { return {} }
}
```

#### C-2. 네트워크 ID → 이름 매핑 (상수)

```typescript
// constants.ts 또는 별도 파일
export const NETWORK_NAMES: Record<string, string> = {
  '2': 'BSC',
  '3': 'Polygon',
  '4': 'TRON',
}
```

#### C-3. 외부지갑 목록 뷰 수정

테이블의 "주소" 컬럼을 "연결 체인" 컬럼으로 변경:

```html
<!-- 컬럼 정의 -->
{ key: 'walletAddresses', label: '연결 체인' }

<!-- 슬롯 -->
<template #walletAddresses="{ row }">
  <div v-if="row.connectionType === 'AXIM'" class="flex gap-1">
    <span v-for="(addr, netId) in parseWalletAddresses(row.walletAddresses)" :key="netId"
      class="inline-flex items-center rounded-full bg-blue-100 text-blue-800 px-2 py-0.5 text-xs font-medium cursor-pointer"
      :title="addr"
    >
      {{ NETWORK_NAMES[netId] ?? `Net#${netId}` }}
    </span>
  </div>
  <span v-else class="font-mono text-xs">
    {{ truncateAddress(Object.values(parseWalletAddresses(row.walletAddresses))[0]) }}
  </span>
</template>
```

#### C-4. 행 확장 (클릭 시 상세 주소 표시)

```html
<!-- 행 클릭 시 토글 -->
<tr v-if="expandedRow === row.id" class="bg-muted/30">
  <td :colspan="columns.length" class="px-4 py-3">
    <div class="grid gap-2 text-sm font-mono">
      <div v-for="(addr, netId) in parseWalletAddresses(row.walletAddresses)" :key="netId"
        class="flex items-center gap-3">
        <span class="inline-flex w-16 justify-center rounded bg-blue-100 text-blue-800 px-2 py-0.5 text-xs font-medium">
          {{ NETWORK_NAMES[netId] ?? netId }}
        </span>
        <span class="text-xs break-all">{{ addr }}</span>
      </div>
    </div>
  </td>
</tr>
```

---

## Part D — 체크리스트

| # | 항목 | 모듈 | 심각도 | 상태 |
|---|------|------|--------|------|
| 1 | `WalletService.createPoolWallet()`에 `deposit_address_pool` INSERT 추가 | core | **Critical** | ☐ |
| 2 | `PoolWalletCreateRequest`에 `currencyIds` 필드 추가 | partner-api | Critical | ☐ |
| 3 | `PartnerWalletService.createPoolWallet()` 호출부 수정 | partner-api | Critical | ☐ |
| 4 | `DepositAddressPoolRepository` 의존성 주입 (`WalletService`) | core | Critical | ☐ |
| 5 | `PartnerGasMapper` — JOIN 추가 + `network_code` alias | partner-api | Medium | ☐ |
| 6 | `GasCostRecord` 엔티티 — `networkCode` 필드 추가 | common | Medium | ☐ |
| 7 | `gas.ts` 타입 — `feeNative`, `feeUsd` 필드명 수정 | partner-ui | Medium | ☐ |
| 8 | `GasCostsView.vue` — 컬럼 키 + 슬롯 수정 | partner-ui | Medium | ☐ |
| 9 | 외부지갑 타입 — `parseWalletAddresses` 헬퍼 추가 | partner-ui | Low | ☐ |
| 10 | 외부지갑 뷰 — 체인 배지 + 확장 행 구현 | partner-ui | Low | ☐ |
| 11 | E2E: POOL 지갑 생성 → 소수점 매칭 세션 생성 정상 확인 | 통합 | — | ☐ |
| 12 | E2E: 가스비 기록 목록 — 네트워크/금액 표시 확인 | 통합 | — | ☐ |

---

## 우선순위

**Part A (Pool 생성)가 최우선** — 이것 없이는 소수점 매칭 입금이 전혀 동작하지 않습니다.
Part B (가스비)와 Part C (외부지갑)는 표시 문제로 기능 자체는 동작합니다.
