# 파트너 관리자 콘솔 버그 수정 지침서

> 작성일: 2026-04-03
> 총 9건 — 백엔드 수정 4건, 프론트엔드 확인 4건, 인프라 1건

---

## 목차

1. [#1 입금 내역 목록 — 사용자 ID 검색 적용X](#1-입금-내역-목록--사용자-id-검색-적용x)
2. [#2 입금 예약 목록 — 사용자 ID 검색 적용X](#2-입금-예약-목록--사용자-id-검색-적용x)
3. [#3 결제 링크 랜딩 시 찾을 수 없음](#3-결제-링크-랜딩-시-찾을-수-없음)
4. [#4 화이트리스트 추가 500 에러](#4-화이트리스트-추가-500-에러)
5. [#5 POOL 지갑 approve 현황 실패](#5-pool-지갑-approve-현황-실패)
6. [#6 지갑 생성 후 생성일시 안맞음](#6-지갑-생성-후-생성일시-안맞음)
7. [#7 외부지갑 > 결제 링크 생성 URL](#7-외부지갑--결제-링크-생성-url)
8. [#8 외부지갑 > 입금 지갑생성 HTTP 메소드 에러](#8-외부지갑--입금-지갑생성-http-메소드-에러)
9. [#9 가스비 기록 TX 유형 Enum 노출](#9-가스비-기록-tx-유형-enum-노출)

---

## #1 입금 내역 목록 — 사용자 ID 검색 적용X

### 분류: 🟡 프론트엔드 확인 필요

### 분석

백엔드 코드는 **정상**입니다. 전체 파이프라인이 올바르게 연결되어 있습니다:

```
PartnerDepositController.getDeposits()
  @RequestParam(required = false) String partnerUserId  ← 파라미터 수신
    → PartnerDepositService.getDeposits(..., partnerUserId, ...)  ← 서비스 전달
      → PartnerDepositMapper.searchDeposits(..., partnerUserId, ...)  ← 매퍼 전달
        → SQL: <if test='partnerUserId != null'>AND d.partner_user_id = #{partnerUserId}</if>
```

**API 테스트**: `GET /api/partner/deposits?partnerUserId=TEST_USER` 로 직접 호출하여 필터가 동작하는지 확인.

### 수정 방향

**partner-front UI 확인 항목**:
- 검색 필드에서 `partnerUserId` 쿼리 파라미터를 API 호출에 포함하는지 확인
- 파라미터 이름이 `partnerUserId`인지 확인 (백엔드 기대 파라미터명)
- 검색 입력 값이 빈 문자열("")이 아닌 null로 전달되는지 확인 (SQL에서 `!= ""`도 체크하므로 빈 문자열이면 통과)

---

## #2 입금 예약 목록 — 사용자 ID 검색 적용X

### 분류: 🟡 프론트엔드 확인 필요

### 분석

#1과 동일한 상황. 백엔드 코드는 정상입니다:

```
PartnerReservationController.getReservations()
  @RequestParam(required = false) String partnerUserId
    → PartnerReservationService.getReservations(..., partnerUserId, ...)
      → PartnerReservationMapper.searchReservations(..., partnerUserId, ...)
        → SQL: <if test='partnerUserId != null'>AND r.partner_user_id = #{partnerUserId}</if>
```

### 수정 방향

#1과 동일 — partner-front에서 `partnerUserId` 파라미터 전달 여부 확인.

---

## #3 결제 링크 랜딩 시 찾을 수 없음

### 분류: 🟡 프론트엔드 + API 확인

### 분석

접속 URL: `https://widget.cryptoments.cc/link/0a4cb18dcd024b1f`

**경로 연결 체인**:
1. widget nginx: `try_files $uri $uri/ /index.html` → SPA 라우팅 정상
2. widget-ui 라우터: `path: "/link/:linkId"` → `views/link.vue` 렌더링 정상
3. widget API: `GET /widgets/payment/links/{linkId}` → `PaymentLinkController.getPaymentLinkInfo()` 정상

**가능한 원인**:
- `link.vue`에서 API 호출 시 `linkId`로 `0a4cb18dcd024b1f`를 전달하는데, `PaymentLinkRepository.findByLinkCode()`가 해당 코드를 찾지 못함
- 결제 링크가 아직 DB에 없거나, 만료/취소된 상태
- `link.vue`에서 호출하는 API 경로가 잘못되었을 수 있음

### 확인 필요

```sql
-- 운영 DB에서 해당 결제 링크 존재 여부 확인
SELECT * FROM payment_links WHERE link_code = '0a4cb18dcd024b1f';
```

**partner-front / widget-ui 확인**:
- `link.vue`에서 호출하는 API 엔드포인트 경로 확인 (예: `/widgets/payment/links/{linkId}`)
- open-api의 base URL이 올바르게 설정되어 있는지 확인

---

## #4 화이트리스트 추가 500 에러

### 분류: 🔴 백엔드 수정 필요

### 원인

DDL에서 `registered_by`가 **NOT NULL**인데, `addWhitelist()`에서 이 필드를 설정하지 않아서 INSERT 실패.

```sql
-- DDL (CRYPTOMENTS_V2_DDL.sql:1249)
registered_by VARCHAR(255) NOT NULL COMMENT '등록자 (관리자 ID 또는 API)',
```

```java
// PartnerWithdrawalService.java:289-298 — registered_by 누락!
WithdrawalAddressWhitelist whitelist = WithdrawalAddressWhitelist.builder()
        .partnerId(partnerId)
        .label(request.getLabel())
        .address(request.getAddress())
        .networkId(request.getNetworkId())
        .isActive(true)
        // ❌ .registeredBy() 누락
        // ❌ .verified() 누락 (DDL DEFAULT FALSE이므로 NULL도 동작하지만 명시 권장)
        .build();
```

### 수정

**파일**: `partner-api/src/main/java/com/cryptoments/partnerapi/service/PartnerWithdrawalService.java`

```java
// 수정 전 (line 289-298)
public WithdrawalAddressWhitelist addWhitelist(Long partnerId, AddWhitelistRequest request) {
    WithdrawalAddressWhitelist whitelist = WithdrawalAddressWhitelist.builder()
            .partnerId(partnerId)
            .label(request.getLabel())
            .address(request.getAddress())
            .networkId(request.getNetworkId())
            .isActive(true)
            .build();
    Long id = whitelistRepository.save(whitelist);
    return whitelistRepository.findOne(id);
}

// 수정 후
public WithdrawalAddressWhitelist addWhitelist(Long partnerId, AddWhitelistRequest request) {
    WithdrawalAddressWhitelist whitelist = WithdrawalAddressWhitelist.builder()
            .partnerId(partnerId)
            .label(request.getLabel())
            .address(request.getAddress())
            .networkId(request.getNetworkId())
            .registeredBy("PARTNER_API")   // ← 추가
            .verified(false)                // ← 추가 (명시적)
            .isActive(true)
            .build();
    Long id = whitelistRepository.save(whitelist);
    return whitelistRepository.findOne(id);
}
```

---

## #5 POOL 지갑 approve 현황 실패

### 분류: 🟠 백엔드 확인 + 운영 확인

### 분석

`PartnerWalletService.getWalletApprovals()` → `PartnerWalletMapper.findWalletApprovals(walletId)`
→ `wallet_approvals` 테이블 조회.

현재 운영 DB의 `wallet_approvals` 테이블은 **0건**입니다.
POOL 지갑 생성 후 approve가 아직 실행되지 않았습니다.

**POOL 지갑 approve 흐름**:
1. POOL 지갑 생성 (`createPoolWallet`) → `wallet_addresses`에 INSERT
2. wallet-activator가 GAS 전송 → ERC-20 approve 실행
3. approve TX 성공 시 `wallet_approvals`에 INSERT

**가능한 원인**:
- wallet-activator 서비스가 미작동 또는 POOL 지갑을 감지하지 못함
- approve TX가 실패 (가스 부족, 컨트랙트 미배포 등)
- wallet-activator가 approve 결과를 `wallet_approvals`에 기록하지 않음

### 확인 필요

```bash
# node-02에서 wallet-activator 상태 확인
ssh cryptoments-bastion "ssh node-02 'export PATH=/home/ubuntu/.local/share/fnm/node-versions/v20.20.2/installation/bin:\$PATH && pm2 list'"

# wallet-activator 로그 확인
ssh cryptoments-bastion "ssh node-02 'tail -100 /opt/cryptoments/node-service/logs/wallet-activator/*.log'"
```

---

## #6 지갑 생성 후 생성일시 안맞음

### 분류: 🔴 백엔드 수정 필요

### 원인

`WalletCreateResponse` DTO에 `createdAt` 필드가 **없습니다**.

```java
// WalletCreateResponse.java — createdAt 필드 누락
public class WalletCreateResponse {
    private Long walletAddressId;
    private String address;
    private Long networkId;
    private String walletType;
    private String derivationPath;
    // ❌ createdAt 없음
}
```

또한 `toResponse()` 메서드가 `walletService.createHotWallet()` 반환값을 바로 변환하는데,
Axim `save()`는 PK만 반환하므로 엔티티의 `createdAt`은 DB DEFAULT에 의해 생성됨.
**save 후 findOne()으로 재조회하지 않으면 createdAt은 null**.

### 수정

**파일 1**: `partner-api/src/main/java/com/cryptoments/partnerapi/dto/response/WalletCreateResponse.java`

```java
// 추가
import java.time.LocalDateTime;

public class WalletCreateResponse {
    private Long walletAddressId;
    private String address;
    private Long networkId;
    private String walletType;
    private String derivationPath;
    /** 생성 일시 */
    private LocalDateTime createdAt;   // ← 추가
}
```

**파일 2**: `partner-api/src/main/java/com/cryptoments/partnerapi/service/PartnerWalletService.java`

```java
// 수정 전 (line 124-132)
private WalletCreateResponse toResponse(WalletAddress wallet) {
    return WalletCreateResponse.builder()
            .walletAddressId(wallet.getId())
            .address(wallet.getAddress())
            .networkId(wallet.getNetworkId())
            .walletType(wallet.getWalletType().name())
            .derivationPath(wallet.getDerivationPath())
            .build();
}

// 수정 후
private WalletCreateResponse toResponse(WalletAddress wallet) {
    return WalletCreateResponse.builder()
            .walletAddressId(wallet.getId())
            .address(wallet.getAddress())
            .networkId(wallet.getNetworkId())
            .walletType(wallet.getWalletType().name())
            .derivationPath(wallet.getDerivationPath())
            .createdAt(wallet.getCreatedAt())   // ← 추가
            .build();
}
```

**주의**: `wallet.getCreatedAt()`이 null인 경우 → core `WalletService.createHotWallet()` 등에서 save 후 `findOne(id)`로 재조회하는지 확인 필요. 재조회하지 않으면 DB DEFAULT 값을 읽지 못해 null 반환됨.

추가로, "생성일시가 안맞음"이 **타임존 불일치**를 의미하는 경우:
- DB: `DATETIME(6)` (타임존 없음, 서버 로컬 타임)
- Java: `LocalDateTime` (타임존 없음)
- 문제: DB 서버 타임존(UTC)과 프론트 표시 타임존(KST)이 다르면 9시간 차이
- 해결: Spring Boot `application.yml`에서 `spring.datasource.url`에 `&serverTimezone=Asia/Seoul` 확인 또는 프론트에서 UTC→KST 변환

---

## #7 외부지갑 > 결제 링크 생성 URL

### 분류: 🔴 백엔드 수정 필요

### 원인

`PartnerDepositService.createPaymentLink()`가 `PaymentLink` 엔티티를 그대로 반환합니다.
엔티티에는 `linkCode`만 있고, **완성된 결제 링크 URL이 없습니다**.

```java
// createPaymentLink() 반환값: PaymentLink 엔티티
// linkCode = "0a4cb18dcd024b1f" (16자리 랜덤)
// 프론트가 이 코드로 URL을 조합해야 하는데, 기준 도메인을 모름
```

### 수정 방안 A (백엔드 — 추천)

`application.yml`에 widget base URL을 설정하고, 응답에 `linkUrl` 필드를 추가합니다.

**파일 1**: `partner-api/src/main/resources/application.yml`

```yaml
cryptoments:
  widget:
    base-url: ${WIDGET_BASE_URL:https://widget.cryptoments.cc}
```

**파일 2**: `PartnerDepositService.java`

```java
// createPaymentLink() 마지막 부분 수정
paymentLinkRepository.save(paymentLink);
// save 후 재조회 (createdAt 등 DB 기본값 반영)
PaymentLink saved = paymentLinkRepository.findOne(paymentLink.getId());
// linkUrl 설정 — PaymentLink 엔티티에 @XIgnoreColumn은 사용하지 않으므로
// 별도 응답 DTO를 만드는 것을 권장
return saved;
```

### 수정 방안 B (프론트엔드)

partner-front에서 환경변수로 widget base URL을 가지고 있고, `linkCode`를 조합하여 URL을 표시합니다:

```javascript
const linkUrl = `${WIDGET_BASE_URL}/link/${paymentLink.linkCode}`;
```

**방안 A를 추천** — 서버에서 완성된 URL을 반환하면 클라이언트가 환경별 도메인을 알 필요가 없습니다.

---

## #8 외부지갑 > 입금 지갑생성 > HTTP 메소드 에러

### 분류: 🟡 프론트엔드 확인 + 백엔드 엔드포인트 확인

### 분석

현재 `PartnerExternalWalletController`에는 3개 엔드포인트만 있습니다:
- `GET /api/partner/external-wallets` — 목록
- `GET /api/partner/external-wallets/{id}` — 상세
- `DELETE /api/partner/external-wallets/{id}` — 해제

**"입금 지갑 생성" 엔드포인트가 없습니다.**

외부지갑 사용자에 대한 입금 지갑 생성은 open-api widget의 `DepositController`에 있습니다:
- `POST /widgets/api/deposit-address` (WidgetSessionData 인증)

partner-api에서 직접 입금 지갑을 생성하는 API가 필요합니다.

### 수정

**파일**: `partner-api/src/main/java/com/cryptoments/partnerapi/controller/PartnerExternalWalletController.java`

```java
/**
 * 외부지갑 사용자에게 입금 주소 할당 (HD Wallet).
 * open-api의 DepositController와 동일한 로직.
 *
 * @group 지갑 관리
 * @auth true
 */
@PostMapping(name = "외부지갑 입금 주소 생성", value = "/{id}/deposit-address")
public WalletCreateResponse createDepositAddress(
        @PathVariable Long id,
        @RequestBody CreateDepositAddressRequest request) {
    Long partnerId = getSession().getPartnerId();
    // 외부지갑 소유권 검증
    ExternalWallet ew = externalWalletService.getExternalWallet(partnerId, id);
    // HD 지갑 할당
    WalletAddress wallet = walletService.assignWallet(
            partnerId, ew.getPartnerUserId(),
            request.getNetworkId(), request.getCurrencyId());
    return toResponse(wallet);
}
```

**새 DTO**: `CreateDepositAddressRequest`

```java
@Getter @Setter
public class CreateDepositAddressRequest {
    /** blockchain_networks.id */
    private Long networkId;
    /** currencies.id */
    private Long currencyId;
}
```

또는 **프론트엔드가 잘못된 HTTP 메소드를 사용**하는 경우:
- 프론트에서 `GET` 또는 `PUT`으로 호출하고 있지 않은지 확인
- 기존 HOT 지갑 생성 API (`POST /api/partner/wallets/hot`)를 사용해야 하는 상황인지 확인

---

## #9 가스비 기록 TX 유형 Enum 노출

### 분류: 🔴 백엔드 수정 필요

### 원인

`GasCostResponse.txType` 필드가 `GasCostTxType` enum 타입이므로 JSON 직렬화 시 raw enum name이 노출됩니다:
`"COLLECTION"`, `"WITHDRAWAL"`, `"GAS_SUPPORT"`, `"APPROVE"`, `"ENERGY_RENTAL"`

### 수정 방안 A (Enum에 한글 라벨 추가 — 추천)

**파일**: `common/src/main/java/com/cryptoments/common/enums/GasCostTxType.java`

```java
public enum GasCostTxType {
    COLLECTION("집금"),
    WITHDRAWAL("출금"),
    GAS_SUPPORT("가스 지원"),
    APPROVE("Approve"),
    ENERGY_RENTAL("에너지 임대");

    private final String label;

    GasCostTxType(String label) {
        this.label = label;
    }

    public String getLabel() {
        return label;
    }
}
```

**파일**: `partner-api/src/main/java/com/cryptoments/partnerapi/dto/response/GasCostResponse.java`

```java
// txType 필드 타입을 String으로 변경
private String txType;       // enum name (COLLECTION 등)
private String txTypeLabel;  // 한글 라벨 (집금 등) — 추가
```

또는 Mapper에서 직접 변환:
```java
// PartnerGasCostMapper 에서
CASE gcr.tx_type
  WHEN 'COLLECTION' THEN '집금'
  WHEN 'WITHDRAWAL' THEN '출금'
  WHEN 'GAS_SUPPORT' THEN '가스 지원'
  WHEN 'APPROVE' THEN 'Approve'
  WHEN 'ENERGY_RENTAL' THEN '에너지 임대'
END AS tx_type_label
```

### 수정 방안 B (프론트엔드에서 처리)

프론트에서 enum → 라벨 매핑:
```javascript
const TX_TYPE_LABELS = {
  COLLECTION: '집금',
  WITHDRAWAL: '출금',
  GAS_SUPPORT: '가스 지원',
  APPROVE: 'Approve',
  ENERGY_RENTAL: '에너지 임대',
};
```

**방안 A 추천** — 다른 enum들과의 일관성을 위해 서버에서 라벨을 제공하는 것이 좋습니다.

---

## 수정 우선순위 요약

| # | 버그 | 분류 | 수정 대상 | 긴급도 |
|---|------|------|-----------|--------|
| **4** | 화이트리스트 추가 500 | 🔴 백엔드 | `PartnerWithdrawalService.addWhitelist()` | **P0** |
| **6** | 지갑 생성일시 | 🔴 백엔드 | `WalletCreateResponse` + `toResponse()` | P1 |
| **7** | 결제 링크 URL | 🔴 백엔드 | `createPaymentLink()` 응답에 URL 포함 | P1 |
| **8** | 외부지갑 입금 지갑생성 | 🟡 양쪽 | 엔드포인트 추가 또는 프론트 API 경로 수정 | P1 |
| **9** | Enum 노출 | 🔴 백엔드 | `GasCostTxType` + `GasCostResponse` | P2 |
| **3** | 결제 링크 랜딩 | 🟡 확인 | DB 데이터 + widget API 경로 확인 | P1 |
| **5** | POOL approve | 🟠 운영 | wallet-activator 작동 확인 | P1 |
| **1** | 입금 목록 검색 | 🟡 프론트 | `partnerUserId` 파라미터 전달 확인 | P2 |
| **2** | 예약 목록 검색 | 🟡 프론트 | `partnerUserId` 파라미터 전달 확인 | P2 |
