# 파트너 생성 2건 수정 지침서 (Spring Boot)

> **작성일**: 2026-03-30
> **우선순위**: P0 (파트너 온보딩 차단)
> **수정 대상**: `admin-api`, `core`

---

## 이슈 요약

| # | 증상 | 근본 원인 |
|---|------|----------|
| 1 | 파트너 생성 후 응답의 `partner.id`가 undefined | `save()` 반환값(ID)을 partner 객체에 set하지 않음 |
| 2 | MASTER 지갑 approve가 등록되지 않음 | (A) 트랜잭션 격리로 findOne 실패 + (B) 기존 지갑 존재 시 early return |

---

## 이슈 1: partner.id undefined 수정

### 파일

`admin-api/src/main/java/com/cryptoments/adminapi/service/PartnerManagementService.java`

### 변경 전 (line ~201)

```java
partnerRepository.save(partner);
```

### 변경 후

```java
Long partnerId = partnerRepository.save(partner);
partner.setId(partnerId);
```

### 설명

Axim `IXRepository.save()`는 auto-increment PK(Long)를 반환한다. Builder로 생성된 partner 객체의 id 필드는 null이므로, 반환된 ID를 명시적으로 set해야 한다. 이 패턴은 `CLAUDE.md`에도 명시되어 있음.

---

## 이슈 2: Approve 등록 실패 수정

### 2-A: 트랜잭션 격리 문제 (REPEATABLE READ)

#### 현상

`createPartner()`가 `@Transactional` → blockchain-api(Node.js)가 MySQL에 직접 INSERT → Spring의 같은 트랜잭션 내 `findOne(walletAddressId)`이 **REPEATABLE READ 스냅샷** 때문에 새 행을 못 봄 → `NotFoundException`.

#### 해결: findOne 대신 응답 데이터로 WalletAddress 구성

blockchain-api가 반환하는 `WalletDeriveResponse`에 이미 `address`, `walletAddressId`가 있으므로, 굳이 DB를 다시 조회할 필요가 없다.

### 2-B: 기존 지갑 존재 시 approve 건너뜀

#### 현상

```java
if (existing != null) {
    return existing;  // ← approve 등록 없이 early return!
}
```

두 번째 호출(지갑 탭 "지갑 생성" 버튼)에서 이미 존재하는 MASTER를 찾아 바로 반환하면서 approve 등록을 생략.

#### 해결: early return 전에 approve 보완 등록

---

### 수정 파일

`core/src/main/java/com/cryptoments/core/wallet/WalletService.java`

### 변경 1: `createMasterWallet` — 기존 지갑 approve 보완 + findOne 제거

#### 변경 전

```java
public WalletAddress createMasterWallet(Long partnerId, Long networkId) {
    WalletAddress existing = walletAddressRepository
            .findByPartnerIdAndNetworkIdAndWalletType(partnerId, networkId, WalletType.MASTER);
    if (existing != null) {
        log.info("MASTER wallet already exists: partnerId={}, networkId={}, address={}",
                partnerId, networkId, existing.getAddress());
        return existing;
    }

    WalletDeriveResponse derived = deriveWallet(networkId, "MASTER", partnerId, null);
    WalletAddress masterWallet = walletAddressRepository.findOne(derived.getWalletAddressId());
    if (masterWallet == null) {
        throw new NotFoundException(ErrorCodes.WALLET_ADDRESS_NOT_FOUND);
    }

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

    log.info("MASTER wallet created: partnerId={}, networkId={}, address={}, approvals={}",
            partnerId, networkId, masterWallet.getAddress(), approvals);
    return masterWallet;
}
```

#### 변경 후

```java
public WalletAddress createMasterWallet(Long partnerId, Long networkId) {
    WalletAddress existing = walletAddressRepository
            .findByPartnerIdAndNetworkIdAndWalletType(partnerId, networkId, WalletType.MASTER);
    if (existing != null) {
        // 기존 지갑이 있어도 approve가 누락됐을 수 있으므로 보완 등록
        int approvals = registerTokenApprovals(existing.getId(), networkId);
        log.info("MASTER wallet already exists: partnerId={}, networkId={}, address={}, approvals补완={}",
                partnerId, networkId, existing.getAddress(), approvals);
        return existing;
    }

    WalletDeriveResponse derived = deriveWallet(networkId, "MASTER", partnerId, null);

    // ── blockchain-api 응답 데이터로 WalletAddress 구성 ──
    // (REPEATABLE READ 트랜잭션 내에서 Node.js INSERT를 findOne으로 읽을 수 없으므로)
    WalletAddress masterWallet = WalletAddress.builder()
            .id(derived.getWalletAddressId())
            .networkId(networkId)
            .address(derived.getAddress())
            .walletType(WalletType.MASTER)
            .partnerId(partnerId)
            .build();

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

    log.info("MASTER wallet created: partnerId={}, networkId={}, address={}, approvals={}",
            partnerId, networkId, masterWallet.getAddress(), approvals);
    return masterWallet;
}
```

### 변경 2: `createHotWallet` — 동일한 findOne 제거 패턴 적용

#### 변경 전

```java
public WalletAddress createHotWallet(Long partnerId, Long networkId, String partnerUserId) {
    if (partnerUserId != null) {
        WalletAddress existing = walletAddressRepository
                .findByPartnerIdAndPartnerUserIdAndNetworkId(partnerId, partnerUserId, networkId);
        if (existing != null) {
            return existing;
        }
    }

    WalletDeriveResponse derived = deriveWallet(networkId, "HOT", partnerId, partnerUserId);
    WalletAddress hotWallet = walletAddressRepository.findOne(derived.getWalletAddressId());
    if (hotWallet == null) {
        throw new NotFoundException(ErrorCodes.WALLET_ADDRESS_NOT_FOUND);
    }

    initWalletBalances(hotWallet.getId(), networkId);

    monitorRegistrationService.register(hotWallet);

    log.info("HOT wallet created: partnerId={}, networkId={}, partnerUserId={}, address={}",
            partnerId, networkId, partnerUserId, hotWallet.getAddress());
    return hotWallet;
}
```

#### 변경 후

```java
public WalletAddress createHotWallet(Long partnerId, Long networkId, String partnerUserId) {
    if (partnerUserId != null) {
        WalletAddress existing = walletAddressRepository
                .findByPartnerIdAndPartnerUserIdAndNetworkId(partnerId, partnerUserId, networkId);
        if (existing != null) {
            return existing;
        }
    }

    WalletDeriveResponse derived = deriveWallet(networkId, "HOT", partnerId, partnerUserId);

    // ── blockchain-api 응답 데이터로 WalletAddress 구성 ──
    WalletAddress hotWallet = WalletAddress.builder()
            .id(derived.getWalletAddressId())
            .networkId(networkId)
            .address(derived.getAddress())
            .walletType(WalletType.HOT)
            .partnerId(partnerId)
            .partnerUserId(partnerUserId)
            .build();

    initWalletBalances(hotWallet.getId(), networkId);

    monitorRegistrationService.register(hotWallet);

    log.info("HOT wallet created: partnerId={}, networkId={}, partnerUserId={}, address={}",
            partnerId, networkId, partnerUserId, hotWallet.getAddress());
    return hotWallet;
}
```

### 변경 3: `createSettlementWallet` — 동일 패턴 (해당되면 적용)

Settlement도 같은 findOne 패턴이 있다면 동일하게 수정. 다만 Settlement은 인프라 지갑이라 파트너 생성 플로우에서 호출되지 않으므로 우선순위 낮음.

### 변경 4: `createPoolWallet` — 동일 패턴 (해당되면 적용)

Pool도 마찬가지. 향후 사용 시 동일 수정 필요.

---

## WalletDeriveResponse 필드 확인

`common/src/main/java/com/cryptoments/common/client/dto/WalletDeriveResponse.java`에 아래 필드가 있는지 확인:

```java
private String address;
private String publicKey;
private Long walletAddressId;
```

Node.js가 반환하는 JSON과 일치해야 한다:
```json
{ "address": "0x...", "publicKey": "0x...", "walletAddressId": 157 }
```

---

## 수정 요약 (3건)

| # | 파일 | 수정 내용 |
|---|------|----------|
| 1 | `PartnerManagementService.java` | `save()` 반환값으로 `partner.setId(partnerId)` |
| 2 | `WalletService.java` `createMasterWallet` | (A) 기존 지갑 early return 전 approve 보완 등록 + (B) findOne 제거, 응답 데이터로 WalletAddress 구성 |
| 3 | `WalletService.java` `createHotWallet` | findOne 제거, 응답 데이터로 WalletAddress 구성 |

---

## 테스트 데이터 재초기화

수정 배포 전에 기존 테스트 파트너 데이터를 다시 정리:

```sql
-- 기존 파트너 지갑 approve 레코드 삭제 (있다면)
DELETE FROM wallet_approvals WHERE wallet_address_id IN (
    SELECT id FROM wallet_addresses WHERE partner_id IS NOT NULL
);

-- 기존 파트너 지갑 잔액/키 삭제
DELETE FROM wallet_keys WHERE wallet_address_id IN (
    SELECT id FROM wallet_addresses WHERE partner_id IS NOT NULL
);
DELETE FROM wallet_balances WHERE wallet_address_id IN (
    SELECT id FROM wallet_addresses WHERE partner_id IS NOT NULL
);

-- 파트너 지갑 주소 삭제
DELETE FROM wallet_addresses WHERE partner_id IS NOT NULL;

-- 인덱스 관리자 초기화
DELETE FROM wallet_index_manager;

-- 파트너 체인 설정 삭제
DELETE FROM partner_chain_configs;

-- 파트너 삭제
DELETE FROM partners;

-- 확인
SELECT 'wallet_addresses' AS tbl, COUNT(*) AS cnt FROM wallet_addresses
UNION ALL SELECT 'wallet_approvals', COUNT(*) FROM wallet_approvals
UNION ALL SELECT 'wallet_index_manager', COUNT(*) FROM wallet_index_manager
UNION ALL SELECT 'partners', COUNT(*) FROM partners;
```

---

## 검증 절차

1. DB 초기화 SQL 실행
2. Spring 코드 수정 + 빌드 + 배포
3. admin UI에서 파트너 등록
4. 확인 사항:
   - [ ] 응답의 `partner.id`가 정상 숫자 (undefined/null 아님)
   - [ ] 파트너 상세 페이지로 정상 이동 (NaN/undefined 없음)
   - [ ] 지갑 탭에서 3개 네트워크 MASTER/HOT 지갑 표시
   - [ ] `wallet_approvals` 테이블에 approve 레코드 생성 확인
   - [ ] approve 상태가 `PENDING`

```sql
-- 검증 쿼리
SELECT wa.address, wa.wallet_type, wa.partner_id, wa.network_id,
       wap.currency_id, wap.status AS approve_status
FROM wallet_addresses wa
LEFT JOIN wallet_approvals wap ON wa.id = wap.wallet_address_id
WHERE wa.partner_id IS NOT NULL
ORDER BY wa.network_id, wa.wallet_type;
```
