# TORQ P0 구현 지침서

> **목적**: TORQ 온램프 아키텍처를 "KRW Deposit 생성 방식"에서 "USDT on-chain Deposit (blockchain-monitor 감지)" 방식으로 전환한다.
> **우선순위**: P0 (서비스 런칭 전 필수)
> **날짜**: 2026-05-14

---

## 1. 아키텍처 변경 요약

### AS-IS (현재 — 잘못된 구조)
```
TORQ Webhook (ESCROW_RELEASED)
  → TorqService.handleEscrowReleased()
    → KRW Deposit 생성 (currencyId=99, networkId=99)
    → Ledger CREDIT (KRW)
    → 즉시 SETTLED
```

### TO-BE (변경 후 — 올바른 구조)
```
[경로 A] blockchain-monitor가 LP→MASTER USDT 전송 감지
  → WebhookProcessingService.processDeposit()
    → USDT Deposit 생성 (실제 체인/통화)
    → torq_trades.deposit_id 연결 시도 (txHash 매칭)

[경로 B] TORQ Webhook (ESCROW_RELEASED)
  → TorqService.handleEscrowReleased()
    → torq_trades 상태만 COMPLETED + txHash 저장
    → deposits 테이블에서 txHash 매칭 시도 → deposit_id 연결

두 경로 중 어느 것이 먼저 도착해도 양방향 매칭으로 연결됨.
```

### 핵심 원칙
- **Deposit 생성은 blockchain-monitor가 담당** (on-chain 감지)
- **TORQ Webhook은 torq_trades 상태 갱신만 담당** (Deposit 생성 하지 않음)
- **txHash를 기반으로 양방향 매칭**: Webhook이 먼저 오면 deposits에서 찾고, blockchain-monitor가 먼저 오면 torq_trades에서 찾음

---

## 2. 수정 대상 파일 목록

| # | 파일 | 모듈 | 변경 유형 |
|---|------|------|----------|
| 1 | `TorqService.java` | core | **대폭 수정** — handleEscrowReleased 로직 교체 |
| 2 | `TorqWidgetController.java` | open-api | **수정** — receiveAddress 자동 조회 |
| 3 | `TorqCreateTradeRequest.java` | open-api | **수정** — receiveAddress 필드 제거 |
| 4 | `WebhookProcessingService.java` | open-api | **추가** — TORQ Deposit 매칭 로직 |
| 5 | `TorqTradeRepository.java` | common | **추가** — findByTxHash 메서드 |
| 6 | `TorqWebhookController.java` | open-api | **수정** — Webhook payload 파싱 확장 |

---

## 3. 상세 수정 지침

### 3.1 TorqTradeRepository — findByTxHash 추가

**파일**: `common/src/main/java/com/cryptoments/common/repository/TorqTradeRepository.java`

현재:
```java
@XRepository
public interface TorqTradeRepository extends IXRepository<Long, TorqTrade> {
    TorqTrade findByEscrowId(Long escrowId);
    TorqTrade findByDepositId(Long depositId);
    List<TorqTrade> findByPartnerIdAndStatus(Long partnerId, TorqTradeStatus status);
}
```

추가할 메서드:
```java
    /** TX hash로 조회 (양방향 매칭용 — Webhook/blockchain-monitor 어느 쪽이 먼저 오든 매칭) */
    TorqTrade findByTxHash(String txHash);
```

---

### 3.2 TorqService.handleEscrowReleased — 전면 교체

**파일**: `core/src/main/java/com/cryptoments/core/torq/TorqService.java`

#### 제거할 것
- `KRW_CURRENCY_ID`, `FIAT_NETWORK_ID` 상수 (라인 44~47)
- `SettlementService` 의존성 (라인 52, 59, 63)
- `handleEscrowReleased()` 내부의 Deposit 생성 로직 전체 (라인 302~399)
  - Deposit.builder() → depositRepository.save()
  - settlementService.credit()
  - Deposit SETTLED 상태 변경
  - recordStatusHistory() 호출
  - notificationService.send()

#### 새로운 handleEscrowReleased 구현

```java
/**
 * ESCROW_RELEASED 처리.
 *
 * <p>Deposit 생성은 하지 않는다 — blockchain-monitor가 LP→MASTER USDT 전송을
 * 감지하여 Deposit을 생성한다.
 *
 * <p>여기서는:
 * (1) torq_trades 상태를 COMPLETED로 전환 + txHash 저장
 * (2) deposits 테이블에서 txHash로 매칭 시도 → 찾으면 deposit_id 연결
 *
 * <p>blockchain-monitor가 먼저 Deposit을 생성한 경우 여기서 연결되고,
 * 아직 생성 전이면 deposit_id는 null로 남는다.
 * (WebhookProcessingService.processDeposit에서 반대 방향 매칭 수행)
 */
private void handleEscrowReleased(TorqTrade trade, Map<String, Object> payload) {
    LocalDateTime now = LocalDateTime.now();

    // 멱등: 이미 COMPLETED 상태이면 skip
    if (trade.getStatus() == TorqTradeStatus.COMPLETED) {
        log.info("TORQ Webhook ESCROW_RELEASED: 이미 COMPLETED, skip. tradeId={}", trade.getId());
        return;
    }

    // Payload에서 txHash 추출
    String txHash = payload != null ? (String) payload.get("txHash") : null;

    TorqTrade completed = trade.toBuilder()
            .status(TorqTradeStatus.COMPLETED)
            .completedAt(now)
            .txHash(txHash)
            .build();

    // ── txHash로 Deposit 매칭 시도 ──
    if (txHash != null && trade.getDepositId() == null) {
        List<Deposit> deposits = depositRepository.findByTxHash(txHash);
        if (!deposits.isEmpty()) {
            Deposit matched = deposits.get(0);
            completed.setDepositId(matched.getId());
            log.info("TORQ Webhook → Deposit 매칭 성공: tradeId={}, depositId={}, txHash={}",
                    trade.getId(), matched.getId(), txHash);
        } else {
            log.info("TORQ Webhook → Deposit 아직 미생성, deposit_id null 유지: tradeId={}, txHash={}",
                    trade.getId(), txHash);
        }
    }

    torqTradeRepository.modify(completed);

    log.info("TORQ 거래 완료(Webhook): tradeId={}, escrowId={}, txHash={}",
            trade.getId(), trade.getEscrowId(), txHash);
}
```

#### import 정리
- **제거**: `LedgerReferenceType`, `DepositStatus`, `DepositType`, `DepositMethod` (handleEscrowReleased에서만 사용하는 경우)
- **제거**: `SettlementService` import 및 생성자 주입
- **유지**: `DepositRepository` (txHash 매칭에 사용)
- **유지**: `List` import (findByTxHash 반환타입)

#### 생성자 수정
```java
// 제거: SettlementService settlementService 파라미터 및 필드
// 제거: NotificationService notificationService 파라미터 및 필드
//       (handleEscrowReleased에서 알림 발송 제거 — blockchain-monitor가 알림 발송)
// 제거: TransactionStatusHistoryRepository statusHistoryRepository 파라미터 및 필드
//       (Deposit 상태 이력은 blockchain-monitor가 기록)

public TorqService(TorqClient torqClient,
                   TorqTradeRepository torqTradeRepository,
                   DepositRepository depositRepository) {
    this.torqClient = torqClient;
    this.torqTradeRepository = torqTradeRepository;
    this.depositRepository = depositRepository;
}
```

> **주의**: `NotificationService`, `SettlementService`, `TransactionStatusHistoryRepository`는
> `handleEscrowReleased` 외 다른 메서드에서 사용하지 않으므로 모두 제거 가능.
> 단, 향후 TORQ 관련 알림(예: 분쟁 알림)에 필요할 수 있으므로 `NotificationService`만 남기는 것도 가능.
> 판단하여 선택할 것.

---

### 3.3 TorqWidgetController.createTrade — receiveAddress 자동 조회

**파일**: `open-api/src/main/java/com/cryptoments/openapi/controller/widget/TorqWidgetController.java`

#### 변경 목적
Widget 클라이언트가 `receiveAddress`를 전송하지 않도록 한다.
서버에서 파트너의 TRON 체인 MASTER 지갑 주소를 자동으로 조회한다.

#### 의존성 추가

```java
import com.cryptoments.common.entity.WalletAddress;
import com.cryptoments.common.enums.WalletType;
import com.cryptoments.common.repository.WalletAddressRepository;
```

#### 생성자 수정

```java
private final TorqService torqService;
private final AximPayClient aximPayClient;
private final PartnerAximSettingsRepository aximSettingsRepository;
private final WalletAddressRepository walletAddressRepository;

/** TRON 네트워크 ID (blockchain_networks 테이블 seed data) */
private static final Long TRON_NETWORK_ID = 4L;

public TorqWidgetController(TorqService torqService,
                             AximPayClient aximPayClient,
                             PartnerAximSettingsRepository aximSettingsRepository,
                             WalletAddressRepository walletAddressRepository) {
    this.torqService = torqService;
    this.aximPayClient = aximPayClient;
    this.aximSettingsRepository = aximSettingsRepository;
    this.walletAddressRepository = walletAddressRepository;
}
```

> **TRON_NETWORK_ID = 4L 확인 필요**: `blockchain_networks` 테이블에서 TRON의 실제 ID를 확인할 것.
> `SELECT id, name FROM blockchain_networks WHERE name = 'TRON';`

#### createTrade 메서드 수정

eKYC 조회 후, TorqService.createTrade 호출 전에 receiveAddress 자동 조회 추가:

```java
// ── MASTER 지갑 자동 조회 (TRON) ──
WalletAddress masterWallet = walletAddressRepository
        .findByPartnerIdAndNetworkIdAndWalletType(partnerId, TRON_NETWORK_ID, WalletType.MASTER);
if (masterWallet == null) {
    throw new NotFoundException(ErrorCodes.WALLET_NOT_FOUND.code(),
            "파트너 TRON MASTER 지갑이 등록되지 않았습니다.");
}
String receiveAddress = masterWallet.getAddress();

log.info("TORQ 거래 생성: partnerId={}, buyer={}, krwAmount={}, receiveAddress={}(auto)",
        partnerId, buyerName, request.getKrwAmount(), receiveAddress);

// ── TorqService 호출 ──
TorqTrade trade = torqService.createTrade(
        partnerId, partnerUserId,
        request.getKrwAmount(), receiveAddress,   // ← request.getReceiveAddress() 대신 자동 조회 주소
        buyerName, buyerPhone, buyerBank, buyerAccountHolder);
```

#### TorqCreateTradeRequest 수정

**파일**: `open-api/src/main/java/com/cryptoments/openapi/dto/request/TorqCreateTradeRequest.java`

`receiveAddress` 필드를 제거한다. Widget에서 전송할 필요 없음.

```java
@Getter @Setter @NoArgsConstructor @AllArgsConstructor
public class TorqCreateTradeRequest {
    /** KRW 입금 금액 */
    private long krwAmount;
    // receiveAddress 제거 — 서버에서 MASTER 지갑 자동 조회
}
```

#### Widget UI (torq.vue) 수정

`widgetApis.js`의 `torqCreateTrade` 호출 시 `receiveAddress` 파라미터를 제거한다.
`torq.vue`에서 `receiverWallet` URL 파라미터도 제거한다 (서버가 자동 조회하므로 불필요).

---

### 3.4 WebhookProcessingService.processDeposit — TORQ Deposit 매칭

**파일**: `open-api/src/main/java/com/cryptoments/webhook/service/WebhookProcessingService.java`

#### 의존성 추가

```java
import com.cryptoments.common.repository.TorqTradeRepository;
```

생성자에 `TorqTradeRepository torqTradeRepository` 추가.

#### 수정 위치: processDeposit() 내부, Deposit 생성 후 (라인 317~319 근처)

현재 코드 (라인 297~319):
```java
Deposit deposit = Deposit.builder()
        ...
        .build();
Long depositId = depositRepository.save(deposit);
log.info("신규 입금 생성(DETECTED): txHash={}, ...");
```

이 직후, 후처리(`depositService.onTxConfirmed`) 전에 TORQ 매칭 로직을 추가한다:

```java
Long depositId = depositRepository.save(deposit);
log.info("신규 입금 생성(DETECTED): txHash={}, to={}, amount={}, partnerId={}, currencyId={}, depositMethod={}",
        dto.getTxHash(), dto.getTo(), amount, partnerId, currencyId, depositMethod);

// ── TORQ 거래 매칭 (blockchain-monitor → torq_trades 방향) ──
if (dto.getTxHash() != null) {
    try {
        TorqTrade torqTrade = torqTradeRepository.findByTxHash(dto.getTxHash());
        if (torqTrade != null && torqTrade.getDepositId() == null) {
            torqTrade.setDepositId(depositId);
            torqTradeRepository.modify(torqTrade);

            // depositMethod를 TORQ로 변경
            deposit.setId(depositId);
            deposit.setDepositMethod(DepositMethod.TORQ);
            depositRepository.modify(deposit);

            log.info("TORQ 거래 매칭 성공 (on-chain 감지): tradeId={}, depositId={}, txHash={}",
                    torqTrade.getId(), depositId, dto.getTxHash());
        }
    } catch (Exception e) {
        log.warn("TORQ 매칭 시도 실패 (무시): txHash={}, error={}", dto.getTxHash(), e.getMessage());
    }
}

// ── 후처리: 원장 기록 + 집금 enqueue ──
try {
    depositService.onTxConfirmed(depositId);
    ...
```

#### 동작 설명
1. blockchain-monitor가 LP→MASTER USDT 전송을 감지하여 Deposit 생성
2. MASTER 지갑이므로 현재 로직상 `depositType = PARTNER_CHARGE`, `depositMethod = DIRECT`로 분류됨
3. **새로 추가되는 로직**: txHash로 `torq_trades`를 검색하여 매칭되면:
   - `torq_trades.deposit_id`에 생성된 depositId 연결
   - `deposits.deposit_method`를 `TORQ`로 변경
4. 이후 정상적으로 `onTxConfirmed()` → 원장 CREDIT + 집금 enqueue 진행

> **참고**: depositType은 `PARTNER_CHARGE`로 유지해도 무방하나, 추후 TORQ 입금을 명확히
> 구분하고 싶다면 `USER_DEPOSIT`으로 변경하는 것도 고려할 수 있음.
> 단, partnerUserId가 null이므로 기존 로직과의 호환성 확인 필요.

---

### 3.5 TORQ Webhook Payload 스펙 (TORQ 서비스 개발용)

TORQ 서비스에서 Cryptoments로 전송하는 Webhook payload 스펙.
현재 TorqWebhookController에서 파싱하는 필드를 기준으로 확장한다.

#### ESCROW_RELEASED Payload

```json
{
  "event": "ESCROW_RELEASED",
  "escrowId": 12345,
  "txHash": "0xabc123...def456",
  "chain": "TRON",
  "fromAddress": "TLP...xxx",
  "toAddress": "TMA...yyy",
  "amount": "100.000000",
  "currency": "USDT"
}
```

| 필드 | 타입 | 필수 | 설명 |
|------|------|------|------|
| event | String | Y | 이벤트 타입. `ESCROW_RELEASED`, `ESCROW_CANCELLED`, `ESCROW_DISPUTED`, `ESCROW_EXPIRED` |
| escrowId | Long | Y | TORQ 에스크로 ID |
| txHash | String | Y* | USDT 전송 TX hash (*RELEASED에서만 필수) |
| chain | String | N | 블록체인 네트워크 (`TRON`, `ETHEREUM`, `BSC`, `POLYGON`) |
| fromAddress | String | N | LP 지갑 주소 (송금자) |
| toAddress | String | N | MASTER 지갑 주소 (수취자) |
| amount | String | N | USDT 전송 금액 (소수점 포함 문자열) |
| currency | String | N | 토큰 심볼 (`USDT`) |

> `chain`, `fromAddress`, `toAddress`, `amount`, `currency`는 현재 Cryptoments에서 직접 사용하지 않지만
> (blockchain-monitor가 on-chain에서 감지하므로), 로깅 및 검증 목적으로 포함한다.
> 향후 cross-validation (Webhook 금액 vs on-chain 금액 대조) 에 활용 가능.

#### ESCROW_CANCELLED / EXPIRED Payload

```json
{
  "event": "ESCROW_CANCELLED",
  "escrowId": 12345,
  "reason": "매수자 취소"
}
```

#### ESCROW_DISPUTED Payload

```json
{
  "event": "ESCROW_DISPUTED",
  "escrowId": 12345
}
```

#### HMAC-SHA256 서명
- Header: `X-TORQ-Signature`
- 서명 대상: Request Body (JSON 문자열 그대로)
- 서명 키: 양측 합의 Secret Key (`torq.webhook.secret-key`)
- 검증: `TorqWebhookController`에서 기존 로직 그대로 유지

---

### 3.6 TorqWebhookController — payload 필드 파싱 확장 (선택)

**파일**: `open-api/src/main/java/com/cryptoments/openapi/webhook/controller/TorqWebhookController.java`

현재 `escrowId`와 `event`만 추출하여 `torqService.handleWebhook(escrowId, event, payload)`에 전달한다.
payload Map 자체를 넘기므로 TorqService에서 `txHash`를 추출하는 현재 구조는 유지해도 된다.

추가로 로깅 강화만 수행:
```java
log.info("TORQ Webhook 수신: event={}, escrowId={}, txHash={}, chain={}, from={}, to={}",
        event, escrowId,
        payload.get("txHash"), payload.get("chain"),
        payload.get("fromAddress"), payload.get("toAddress"));
```

---

## 4. 수정하지 않는 것

| 항목 | 이유 |
|------|------|
| `TorqTrade.java` (Entity) | 변경 불필요 — 기존 필드로 충분 |
| `TorqTradeStatus.java` (Enum) | 변경 불필요 — COMPLETED 상태 이미 존재 |
| `TorqClient.java` | 변경 불필요 — TORQ API 호출 로직은 동일 |
| `torq_trades DDL` | 변경 불필요 — deposit_id, tx_hash 컬럼 이미 존재 |
| `TorqService`의 다른 메서드 | createTrade, acceptTrade, submitTransfer, cancelTrade, submitEvidence — 변경 불필요 |
| Widget 폴링 로직 | 거래 상태 폴링은 그대로 유지 (COMPLETED가 되면 UI에 성공 표시) |

---

## 5. 검증 체크리스트

### 시나리오 A: blockchain-monitor가 먼저 감지
1. LP가 USDT를 파트너 MASTER 지갑으로 전송 (TRON)
2. blockchain-monitor가 감지 → `WebhookProcessingService.processDeposit()` 호출
3. Deposit 생성 (DETECTED → CONFIRMED)
4. txHash로 `torq_trades` 검색 → **TORQ Webhook이 아직 안 옴** → deposit_id NULL 상태
5. **이후** TORQ Webhook (ESCROW_RELEASED) 도착
6. `TorqService.handleEscrowReleased()` → txHash로 deposits 검색 → **매칭 성공** → deposit_id 연결
7. 결과: `torq_trades.deposit_id` = 생성된 Deposit ID

### 시나리오 B: TORQ Webhook이 먼저 도착
1. TORQ Webhook (ESCROW_RELEASED, txHash 포함) 도착
2. `TorqService.handleEscrowReleased()` → txHash로 deposits 검색 → **아직 없음** → deposit_id NULL
3. **이후** blockchain-monitor가 LP→MASTER 전송 감지
4. `WebhookProcessingService.processDeposit()` → Deposit 생성
5. txHash로 `torq_trades` 검색 → **매칭 성공** → deposit_id 연결 + depositMethod=TORQ
6. 결과: `torq_trades.deposit_id` = 생성된 Deposit ID

### 시나리오 C: 멱등성 테스트
1. 동일 txHash로 Webhook이 2회 도착
2. 첫 번째: COMPLETED 전환 + deposit_id 연결 (또는 null)
3. 두 번째: `trade.getStatus() == COMPLETED` → skip 처리 → 정상

### 확인 SQL
```sql
-- TORQ 거래와 Deposit 연결 상태 확인
SELECT
    t.id AS trade_id,
    t.escrow_id,
    t.status AS trade_status,
    t.tx_hash,
    t.deposit_id,
    d.id AS deposit_id_check,
    d.status AS deposit_status,
    d.deposit_method,
    d.amount,
    d.from_address,
    d.to_address
FROM torq_trades t
LEFT JOIN deposits d ON t.deposit_id = d.id
ORDER BY t.created_at DESC
LIMIT 20;
```

---

## 6. 작업 순서

1. `TorqTradeRepository` — `findByTxHash` 추가
2. `TorqService.handleEscrowReleased` — 전면 교체 (Deposit/Ledger 생성 제거)
3. `TorqService` — 불필요 의존성 제거 (SettlementService 등)
4. `TorqWidgetController` — receiveAddress 자동 조회 + WalletAddressRepository 주입
5. `TorqCreateTradeRequest` — receiveAddress 필드 제거
6. `WebhookProcessingService` — TORQ Deposit 매칭 로직 추가
7. 컴파일 확인: `./gradlew :common:compileJava && ./gradlew :core:compileJava && ./gradlew :open-api:compileJava`
8. TRON_NETWORK_ID 확인: `SELECT id, name FROM blockchain_networks WHERE name LIKE '%TRON%';`

---

## 7. Widget UI 수정 사항

`widget-ui/src/views/torq.vue` 및 `widget-ui/src/api/widgetApis.js`:

- `torqCreateTrade` API 호출에서 `receiveAddress` 파라미터 제거
- `torq.vue`에서 `receiverWallet` URL 파라미터 제거 (불필요)
- 나머지 UI 흐름 (견적 조회, 입금 의사, 이체 신고, 폴링, 취소, 분쟁)은 변경 없음
