# Partner API 미구현 항목 완성 가이드

> 작성일: 2026-03-24
> Guide #48
> 대상: `partner-api` 모듈
> 목적: UI에서 발견된 미구현/UX 문제를 백엔드에서 해소하기 위한 작업 지침

---

## 현황 요약

| 구분 | 수치 |
|------|------|
| 전체 Controller | 14개 |
| 전체 Endpoint | 75개 |
| 구현 완료 | 73개 (97%) |
| **TODO (return null)** | **2개** |
| **누락 API (UI 필요)** | **3개** |
| Service | 13개 (모두 구현됨) |
| DTO | 39개 (Request 19 + Response 20) |

---

## P0. 마스터 데이터 조회 API 신규 추가

### 문제

4개 폼(출금 요청, 입금 세션 생성, 쉐어 출금, 화이트리스트 추가)에서 `currencyId`, `networkId`를 **숫자로 직접 입력**해야 하는데, UI에서 이 ID를 해석할 수 있는 **마스터 데이터 조회 API가 partner-api에 존재하지 않음**.

### 작업: `PartnerMasterDataController` 신규 생성

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

```java
@RestController
@RequestMapping("/api/partner/master")
public class PartnerMasterDataController extends XSessionController<PartnerSessionData> {

    private final PartnerMasterDataService masterDataService;

    /**
     * 파트너에게 할당된 네트워크 목록.
     * UI 드롭다운 렌더링용 — 네트워크명, 네이티브 토큰 심볼 포함.
     *
     * @response 200 네트워크 목록
     * @group 마스터 데이터
     * @auth true
     */
    @GetMapping(name = "네트워크 목록 조회", value = "/networks")
    public List<NetworkOption> getNetworks() {
        Long partnerId = getSession().getPartnerId();
        return masterDataService.getActiveNetworks(partnerId);
    }

    /**
     * 특정 네트워크에서 사용 가능한 통화 목록.
     * 네트워크 선택 후 통화 드롭다운 렌더링용.
     *
     * @param networkId 네트워크 ID
     * @response 200 통화 목록
     * @group 마스터 데이터
     * @auth true
     */
    @GetMapping(name = "네트워크별 통화 목록 조회", value = "/networks/{networkId}/currencies")
    public List<CurrencyOption> getCurrencies(@PathVariable Long networkId) {
        Long partnerId = getSession().getPartnerId();
        return masterDataService.getActiveCurrencies(partnerId, networkId);
    }

    /**
     * 전체 통화 목록 (네트워크 무관).
     * 네트워크 선택 전 전체 통화를 보여줄 때 사용.
     *
     * @response 200 통화 목록
     * @group 마스터 데이터
     * @auth true
     */
    @GetMapping(name = "전체 통화 목록 조회", value = "/currencies")
    public List<CurrencyOption> getAllCurrencies() {
        Long partnerId = getSession().getPartnerId();
        return masterDataService.getAllActiveCurrencies(partnerId);
    }
}
```

### DTO

**`NetworkOption.java`** (Response DTO):
```java
@Getter @Setter @Builder
@NoArgsConstructor @AllArgsConstructor
public class NetworkOption {
    /** 네트워크 ID (API 요청에 사용) */
    private Long id;
    /** 네트워크명 (예: "BSC") */
    private String name;
    /** 표시명 (예: "BNB Smart Chain") */
    private String displayName;
    /** 네이티브 토큰 (예: "BNB") */
    private String nativeSymbol;
    /** 체인 타입 (EVM / TRON) */
    private String chainType;
}
```

**`CurrencyOption.java`** (Response DTO):
```java
@Getter @Setter @Builder
@NoArgsConstructor @AllArgsConstructor
public class CurrencyOption {
    /** 통화 ID (API 요청에 사용) */
    private Long id;
    /** 심볼 (예: "USDT") */
    private String symbol;
    /** 표시명 (예: "Tether USD") */
    private String name;
    /** 소수점 자릿수 */
    private Integer decimals;
    /** 소속 네트워크 ID */
    private Long networkId;
    /** 소속 네트워크명 */
    private String networkName;
}
```

### Service

**`PartnerMasterDataService.java`**:
```java
@Service
@RequiredArgsConstructor
public class PartnerMasterDataService {

    private final BlockchainNetworkRepository networkRepository;
    private final CurrencyRepository currencyRepository;
    // 파트너별 활성 네트워크 판단을 위한 추가 Repository 필요 시

    public List<NetworkOption> getActiveNetworks(Long partnerId) {
        // 방안 A: 전체 ACTIVE 네트워크 반환 (심플)
        // 방안 B: partner_network_configs 기반 파트너별 필터 (향후)
        List<BlockchainNetwork> networks = networkRepository.findByStatus(NetworkStatus.ACTIVE);
        return networks.stream()
            .map(n -> NetworkOption.builder()
                .id(n.getId())
                .name(n.getNetworkCode())
                .displayName(n.getNetworkName())
                .nativeSymbol(n.getNativeSymbol())
                .chainType(n.getChainType().name())
                .build())
            .collect(Collectors.toList());
    }

    public List<CurrencyOption> getActiveCurrencies(Long partnerId, Long networkId) {
        List<Currency> currencies = currencyRepository.findByNetworkIdAndStatus(networkId, CurrencyStatus.ACTIVE);
        return currencies.stream()
            .map(c -> CurrencyOption.builder()
                .id(c.getId())
                .symbol(c.getSymbol())
                .name(c.getCurrencyName())
                .decimals(c.getDecimals())
                .networkId(networkId)
                .build())
            .collect(Collectors.toList());
    }

    public List<CurrencyOption> getAllActiveCurrencies(Long partnerId) {
        List<Currency> currencies = currencyRepository.findByStatus(CurrencyStatus.ACTIVE);
        // networkName JOIN은 Repository에서 처리하거나 후처리
        return currencies.stream()
            .map(c -> CurrencyOption.builder()
                .id(c.getId())
                .symbol(c.getSymbol())
                .name(c.getCurrencyName())
                .decimals(c.getDecimals())
                .networkId(c.getNetworkId())
                .build())
            .collect(Collectors.toList());
    }
}
```

### UI 적용 대상 (4개 폼)

| 화면 | 현재 | 수정 후 |
|------|------|---------|
| 출금 요청 | `통화 ID: [1]`, `네트워크 ID: [1]` | `네트워크: [▾ BSC]`, `통화: [▾ USDT]` |
| 입금 세션 생성 | 동일 | 동일 |
| 쉐어 출금 | 동일 | 동일 |
| 화이트리스트 추가 | `네트워크 ID: [1]` | `네트워크: [▾ BSC]` |

### UI 호출 흐름

```
1. 페이지 로드 → GET /api/partner/master/networks
2. 네트워크 드롭다운 렌더링
3. 사용자가 네트워크 선택 → GET /api/partner/master/networks/{id}/currencies
4. 통화 드롭다운 렌더링
5. 폼 제출 시 선택된 networkId, currencyId를 기존 API에 전달
```

---

## P1-1. 결제 링크 생성 구현

### 현재 상태

- **Controller:** `POST /api/partner/payment-links` → `return null` (TODO 주석)
- **Service:** `PartnerDepositService.createPaymentLink()` 메서드 없음
- **DTO:** `CreatePaymentLinkRequest` 이미 정의됨
- **Entity:** `PaymentLink` (common 모듈에 존재)
- **UI 상태:** "결제 링크 생성 API는 현재 준비 중입니다 (501)"

### 작업

**1) `PartnerDepositService`에 메서드 추가:**

```java
public PaymentLink createPaymentLink(Long partnerId, CreatePaymentLinkRequest request) {
    // 1. 파트너 검증
    Partner partner = partnerRepository.findById(partnerId);
    if (partner == null) throw new NotFoundException(ErrorCodes.PARTNER_NOT_FOUND);

    // 2. currencyId / networkId 검증 (nullable)
    if (request.getCurrencyId() != null) {
        Currency currency = currencyRepository.findById(request.getCurrencyId());
        if (currency == null) throw new BadRequestException(ErrorCodes.CURRENCY_NOT_FOUND);
    }
    if (request.getNetworkId() != null) {
        BlockchainNetwork network = networkRepository.findById(request.getNetworkId());
        if (network == null) throw new BadRequestException(ErrorCodes.NETWORK_NOT_FOUND);
    }

    // 3. 링크 코드 생성
    String linkCode = UUID.randomUUID().toString().replace("-", "").substring(0, 16);

    // 4. PaymentLink 엔티티 생성
    PaymentLink paymentLink = PaymentLink.builder()
        .partnerId(partnerId)
        .linkCode(linkCode)
        .title(request.getTitle())
        .partnerUserId(request.getPartnerUserId())
        .partnerReference(request.getPartnerReference())
        .currencyId(request.getCurrencyId())
        .networkId(request.getNetworkId())
        .amount(request.getAmount())
        .depositMethod(request.getDepositMethod())
        .status(PaymentLinkStatus.ACTIVE)
        .expiresAt(request.getExpiresInMinutes() != null
            ? LocalDateTime.now().plusMinutes(request.getExpiresInMinutes())
            : null)
        .build();

    // 5. 저장
    paymentLinkRepository.save(paymentLink);
    return paymentLink;
}
```

**2) Controller 수정:**

```java
@PostMapping(name = "결제 링크 생성", value = "/payment-links")
@ResponseStatus(HttpStatus.CREATED)
public PaymentLink createPaymentLink(@Valid @RequestBody CreatePaymentLinkRequest request) {
    Long partnerId = getSession().getPartnerId();
    return partnerDepositService.createPaymentLink(partnerId, request);
}
```

### 검증 포인트

- `PaymentLink` 엔티티의 컬럼 매핑 확인 (`@XEntity("payment_links")`)
- `PaymentLinkRepository`에 `save()` 동작 확인
- `PaymentLinkStatus` enum 존재 확인 (ACTIVE, EXPIRED, COMPLETED, CANCELLED)

---

## P1-2. Axim Pay 결제 요청 구현

### 현재 상태

- **Controller:** `POST /api/partner/axim-payments/request` → `return null` (TODO: Axim Pay 외부 API 연동)
- **Service:** 메서드 없음
- **DTO:** `RequestAximPaymentRequest` 정의됨
- **UI 상태:** "결제 요청 API는 현재 준비 중입니다 (501)"

### 작업

이건 **Axim Pay 외부 API 연동**이 필요하므로, 외부 연동 규격이 확정된 후 구현.

**스텁 → 명시적 501 응답으로 변경 (현재 `return null`은 NPE 위험):**

```java
@PostMapping(name = "Axim Pay 결제 요청", value = "/axim-payments/request")
@ResponseStatus(HttpStatus.CREATED)
public AximPayment requestAximPayment(@Valid @RequestBody RequestAximPaymentRequest request) {
    // Axim Pay 외부 API 연동 완료 후 구현
    throw new BadRequestException("AXIM_NOT_READY", "Axim Pay 결제 요청 기능은 현재 준비 중입니다.");
}
```

---

## P1-3. Axim Pay 설정 수정 API 추가

### 현재 상태

- **Controller:** `GET /api/partner/integration/axim` → 조회만 존재
- **PUT/PATCH 없음** → UI에서 설정 수정 불가
- **UI:** JSON raw 데이터 표시 (수정 폼 없음)

### 작업

**1) `UpdateAximSettingsRequest` DTO 생성:**

```java
@Getter @Setter
public class UpdateAximSettingsRequest {
    /** Axim Pay API Key */
    private String apiKey;
    /** Axim Pay API Secret */
    private String apiSecret;
    /** 활성화 여부 */
    private Boolean isEnabled;
}
```

**2) Controller에 PUT 추가:**

```java
@PutMapping(name = "Axim Pay 연동 설정 수정", value = "/axim")
public PartnerAximSettings updateAximSettings(
        @Valid @RequestBody UpdateAximSettingsRequest request) {
    Long partnerId = getSession().getPartnerId();
    return partnerIntegrationService.updateAximSettings(partnerId, request);
}
```

**3) Service 메서드 추가:**

```java
public PartnerAximSettings updateAximSettings(Long partnerId, UpdateAximSettingsRequest request) {
    PartnerAximSettings settings = partnerAximSettingsRepository.findByPartnerId(partnerId);
    if (settings == null) {
        // 최초 설정
        settings = PartnerAximSettings.builder()
            .partnerId(partnerId)
            .apiKey(request.getApiKey())
            .apiSecret(request.getApiSecret())
            .isEnabled(request.getIsEnabled() != null ? request.getIsEnabled() : false)
            .build();
    } else {
        if (request.getApiKey() != null) settings.setApiKey(request.getApiKey());
        if (request.getApiSecret() != null) settings.setApiSecret(request.getApiSecret());
        if (request.getIsEnabled() != null) settings.setIsEnabled(request.getIsEnabled());
    }
    partnerAximSettingsRepository.save(settings);
    return settings;
}
```

---

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

### 현재 상태

- `GET /api/partner/deposit-addresses` → `XPage<WalletAddress>` 반환
- `WalletAddress` 엔티티에 `networkId`는 있지만, **네트워크명/통화명이 없음**
- UI에서 네트워크/통화 컬럼이 모두 `-` 표시

### 수정 방안

**방안 A (권장): Response DTO 도입**

```java
@Getter @Setter @Builder
public class DepositAddressResponse {
    private Long id;
    private String address;
    private String networkName;     // JOIN 결과
    private String networkCode;     // JOIN 결과
    private String currencySymbol;  // JOIN 결과 (assigned currency)
    private Boolean isActive;
    private LocalDateTime createdAt;
}
```

Service에서 `WalletAddress` + `BlockchainNetwork` JOIN 후 DTO 변환.

**방안 B (간단): UI에서 마스터 데이터 캐싱**

P0에서 추가하는 `/api/partner/master/networks`, `/api/partner/master/currencies` API를 UI 초기화 시 호출하여 캐싱하고, `networkId` → 네트워크명 매핑을 프론트에서 처리.

---

## 작업 순서 요약

| 순서 | 항목 | 예상 규모 | 의존성 |
|------|------|---------|--------|
| **1** | P0: 마스터 데이터 API (네트워크/통화 목록) | Controller + Service + DTO 2개 | 없음 |
| **2** | P1-1: 결제 링크 생성 | Service 메서드 1개 + Controller 수정 | 없음 |
| **3** | P1-3: Axim 설정 수정 API | DTO + Controller + Service 각 1개 | 없음 |
| **4** | P2: 입금 주소 네트워크/통화 표시 | DTO + Service 수정 | P0 완료 후 |
| **5** | P1-2: Axim Pay 결제 요청 | 외부 연동 | Axim Pay 규격 확정 후 |
| **6** | UI 수정: 4개 폼 드롭다운 전환 | 프론트엔드 | P0 완료 후 |

---

## 참고: 현재 Controller-Service 매핑

```
PartnerMasterDataController (신규)
  └─ PartnerMasterDataService (신규)
     ├─ BlockchainNetworkRepository (common — 기존)
     └─ CurrencyRepository (common — 기존)

PartnerDepositController
  └─ PartnerDepositService
     └─ createPaymentLink() ← 추가

PartnerIntegrationController
  └─ PartnerIntegrationService
     └─ updateAximSettings() ← 추가
```

---

## 기존 common 모듈 확인 필요 항목

| 항목 | 확인 내용 |
|------|---------|
| `BlockchainNetworkRepository` | `findByStatus(NetworkStatus)` 메서드 존재 여부 |
| `CurrencyRepository` | `findByNetworkIdAndStatus(Long, CurrencyStatus)` 메서드 존재 여부 |
| `PaymentLinkRepository` | `save()` + 필요 findBy 메서드 |
| `PartnerAximSettingsRepository` | `findByPartnerId(Long)` 메서드 존재 여부 |
| `PaymentLinkStatus` enum | ACTIVE, EXPIRED, COMPLETED, CANCELLED |
| `PaymentLink` 엔티티 | 컬럼 매핑 (`link_code`, `title`, `partner_reference` 등) |
