# TORQ V49 연동 구현 지침서

> **작성일**: 2026-05-16
> **근거 문서**: `/Users/dudgh/git/laas-chain/docs/CRYPTOMENTS_V49_RESPONSE.md`
> **호환성**: additive 변경만 — 기존 호스트 통합 그대로 작동. 새 필드는 무시해도 됨.

---

## 1. 변경 사항 요약

### TORQ 측 변경 (배포 예정)

| 항목 | 변경 내용 |
|------|----------|
| `FORCE_RELEASED` / `COMPLETED` payload | `resolution` 필드 추가 |
| `CANCELLED` payload | `reason` enum 확장 (5종) |

### Cryptoments 측 작업 (이 지침서 범위)

| # | 작업 | 파일 |
|---|------|------|
| 1 | `resolution` 필드 분기 처리 | `TorqService.java` |
| 2 | `CANCELLED.reason` enum 분기 세분화 | `TorqService.java` |
| 3 | TorqLink ↔ TorqTrade 상태 동기화 | `TorqService.java` |
| 4 | `TorqTradeStatus.RESOLVED` 활용 | `TorqTradeStatus.java`, `TorqService.java` |
| 5 | TorqTrade 엔티티 필드 추가 | `TorqTrade.java` |

---

## 2. resolution 필드 분기 (#1)

### TORQ payload 변경

`FORCE_RELEASED` 및 `COMPLETED` webhook payload의 `data` 객체에 `resolution` 필드가 추가된다.

```json
{
  "eventType": "FORCE_RELEASED",
  "data": {
    "escrowId": 123,
    "txHash": "0x...",
    "senderWallet": "0x...",
    "resolution": "RELEASE_TO_BUYER"
  }
}
```

### resolution 값 매핑

| TORQ resolution | Cryptoments 해석 | 처리 |
|----------------|-----------------|------|
| `RELEASE_TO_BUYER` | **LP 승 (seller_win)** — USDT가 구매자에게 방출됨 | deposit 정상 생성 (기존 handleCompleted 로직) |
| `REFUND_TO_BUYER` | **구매자 승 (buyer_win)** — KRW 환불, USDT 미방출 | deposit 미생성, 거래 종결 |
| `null` (미전송) | 하위 호환 — 기존과 동일 | deposit 정상 생성 (기존 로직 유지) |

### handleForceReleased() 수정

**현재 코드** (TorqService.java:542-545):
```java
private void handleForceReleased(TorqTrade trade, Map<String, Object> payload) {
    handleCompleted(trade, payload);
    log.info("TORQ 강제 릴리즈 (분쟁 해결): ...");
}
```

**변경 후**:
```java
private void handleForceReleased(TorqTrade trade, Map<String, Object> payload) {
    String resolution = extractDataField(payload, "resolution");

    if ("REFUND_TO_BUYER".equals(resolution)) {
        // 구매자 승 — deposit 미생성, 거래 종결
        TorqTrade resolved = trade.toBuilder()
                .status(TorqTradeStatus.RESOLVED)
                .resolvedAt(LocalDateTime.now())
                .resolution(resolution)
                .build();
        torqTradeRepository.modify(resolved);

        // Link 상태 동기화 → FAILED (재시도 가능)
        syncLinkStatus(resolved);

        log.info("TORQ 분쟁 해결 (구매자 승): tradeId={}, escrowId={}, resolution={}",
                trade.getId(), trade.getEscrowId(), resolution);
    } else {
        // LP 승 (RELEASE_TO_BUYER) 또는 null (하위 호환)
        // → 기존 COMPLETED 로직 (deposit 매칭)
        TorqTrade resolved = trade.toBuilder()
                .resolution(resolution)
                .build();
        torqTradeRepository.modify(resolved);

        handleCompleted(resolved, payload);

        log.info("TORQ 분쟁 해결 (LP 승): tradeId={}, escrowId={}, resolution={}",
                trade.getId(), trade.getEscrowId(), resolution);
    }
}
```

### handleCompleted() 수정

`COMPLETED` 이벤트에도 `resolution` 필드가 올 수 있다. 정상 완료의 경우 `resolution`은 항상 `RELEASE_TO_BUYER`이거나 `null`이므로 기존 로직 유지하되, `resolution` 값을 저장만 추가한다.

```java
private void handleCompleted(TorqTrade trade, Map<String, Object> payload) {
    // ... 기존 멱등 체크 ...

    String txHash = extractDataField(payload, "txHash");
    String senderWallet = extractDataField(payload, "senderWallet");
    String resolution = extractDataField(payload, "resolution");  // ← 추가

    TorqTrade completed = trade.toBuilder()
            .status(TorqTradeStatus.COMPLETED)
            .completedAt(now)
            .txHash(txHash)
            .fromAddress(senderWallet)
            .resolution(resolution)       // ← 추가
            .build();

    boolean matched = tryMatchDeposit(completed, txHash);
    torqTradeRepository.modify(completed);

    // Link 상태 동기화 → USED (최종 완료)
    syncLinkStatus(completed);                // ← 추가

    // ... 기존 지연 재시도 로직 유지 ...
}
```

---

## 3. CANCELLED.reason enum 분기 세분화 (#2)

### TORQ reason 값 확장

| reason | 의미 | Trade 상태 | Link 상태 |
|--------|------|-----------|----------|
| `EXPIRED` | 시간 초과 만료 | EXPIRED | EXPIRED |
| `TIMEOUT` | LP 응답 시간 초과 | EXPIRED | EXPIRED |
| `BUYER_CANCELLED` | 구매자 직접 취소 | CANCELLED | CANCELLED |
| `DISPUTE_REJECTED` | 분쟁 기각 (구매자 패소) | CANCELLED | CANCELLED |
| `ADMIN_FORCE_CANCEL` | 관리자 강제 취소 | CANCELLED | CANCELLED |

### handleCancelled() 수정

**현재 코드** (TorqService.java:511-526):
```java
private void handleCancelled(TorqTrade trade, Map<String, Object> payload) {
    String reason = extractDataField(payload, "reason");
    boolean isExpired = "EXPIRED".equals(reason);
    TorqTradeStatus newStatus = isExpired ? TorqTradeStatus.EXPIRED : TorqTradeStatus.CANCELLED;
    // ...
}
```

**변경 후**:
```java
private void handleCancelled(TorqTrade trade, Map<String, Object> payload) {
    String reason = extractDataField(payload, "reason");

    // EXPIRED / TIMEOUT → 만료 처리, 나머지 → 취소 처리
    boolean isExpired = "EXPIRED".equals(reason) || "TIMEOUT".equals(reason);
    TorqTradeStatus newStatus = isExpired ? TorqTradeStatus.EXPIRED : TorqTradeStatus.CANCELLED;

    TorqTrade updated = trade.toBuilder()
            .status(newStatus)
            .cancelledAt(LocalDateTime.now())
            .cancelReason(reason)
            .build();
    torqTradeRepository.modify(updated);

    // Link 상태 동기화
    syncLinkStatus(updated);

    log.info("TORQ 거래 {} (Webhook): tradeId={}, escrowId={}, reason={}",
            isExpired ? "만료" : "취소", trade.getId(), trade.getEscrowId(), reason);
}
```

---

## 4. TorqLink ↔ TorqTrade 상태 동기화 (#3)

### 정책

Link는 **1회성** — 완료/취소/만료 모두 terminal 상태. 실패(FAILED)만 재시도 가능.

| Trade 최종 상태 | Link 동기화 상태 | 재시도 가능 |
|----------------|-----------------|-----------|
| COMPLETED | **USED** | ❌ |
| CANCELLED | **CANCELLED** | ❌ |
| EXPIRED | **EXPIRED** | ❌ |
| FAILED | **FAILED** → 재시도 시 ACTIVE 복귀 | ✅ |
| RESOLVED (구매자 승) | **FAILED** → 재시도 가능 | ✅ |
| RESOLVED (LP 승) | **USED** | ❌ |

### syncLinkStatus() 구현

TorqService.java에 추가:

```java
/**
 * TorqTrade terminal 상태 도달 시 연결된 TorqLink 상태를 동기화.
 *
 * <p>Link 는 1회성 — COMPLETED/CANCELLED/EXPIRED 는 terminal.
 * FAILED 와 RESOLVED(구매자 승)만 재시도 가능(FAILED 상태로 전환).
 */
private void syncLinkStatus(TorqTrade trade) {
    if (trade.getId() == null) return;

    // torq_links.torq_trade_id 로 연결된 Link 조회
    TorqLink link = torqLinkRepository.findByTorqTradeId(trade.getId());
    if (link == null) {
        log.debug("TORQ Link 동기화: 연결된 Link 없음, tradeId={}", trade.getId());
        return;
    }

    TorqLinkStatus newLinkStatus;
    switch (trade.getStatus()) {
        case COMPLETED -> newLinkStatus = TorqLinkStatus.USED;
        case CANCELLED -> newLinkStatus = TorqLinkStatus.CANCELLED;
        case EXPIRED   -> newLinkStatus = TorqLinkStatus.EXPIRED;
        case FAILED    -> newLinkStatus = TorqLinkStatus.FAILED;
        case RESOLVED  -> {
            // 구매자 승 → FAILED (재시도 가능), LP 승 → USED (완료)
            if ("REFUND_TO_BUYER".equals(trade.getResolution())) {
                newLinkStatus = TorqLinkStatus.FAILED;
            } else {
                newLinkStatus = TorqLinkStatus.USED;
            }
        }
        default -> {
            log.debug("TORQ Link 동기화: 비-terminal 상태, skip. tradeId={}, status={}",
                    trade.getId(), trade.getStatus());
            return;
        }
    }

    // 이미 같은 상태면 skip
    if (link.getStatus() == newLinkStatus) return;

    TorqLink updated = link.toBuilder()
            .status(newLinkStatus)
            .build();
    torqLinkRepository.modify(updated);

    log.info("TORQ Link 상태 동기화: linkId={}, linkCode={}, {} → {}, tradeId={}",
            link.getId(), link.getLinkCode(), link.getStatus(), newLinkStatus, trade.getId());
}
```

### Link 재시도 로직 (기존 isTradeReusable 연동)

Link가 FAILED 상태일 때 고객이 같은 링크로 재접근하면:
1. `isTradeReusable()` → true 확인
2. Link의 `torqTradeId`를 null로 초기화
3. Link 상태를 ACTIVE로 복귀
4. 새 Trade 생성 후 Link에 다시 연결

이 로직은 **Widget Controller** (TorqLinkWidgetController)에서 처리:

```java
// Widget 에서 링크 접근 시
if (link.getStatus() == TorqLinkStatus.FAILED && link.getTorqTradeId() != null) {
    if (torqService.isTradeReusable(link.getTorqTradeId())) {
        // 기존 실패 거래 해제 + Link 재활성화
        TorqLink reactivated = link.toBuilder()
                .status(TorqLinkStatus.ACTIVE)
                .torqTradeId(null)
                .build();
        torqLinkRepository.modify(reactivated);
        link = reactivated;
        log.info("TORQ Link 재활성화: linkCode={}, 이전 tradeId={}", link.getLinkCode(), link.getTorqTradeId());
    }
}
```

---

## 5. TorqTrade 엔티티 필드 추가 (#5)

### TorqTrade.java 추가 필드

```java
/** 분쟁 해결 판정 결과 (TORQ resolution: RELEASE_TO_BUYER / REFUND_TO_BUYER) */
@XColumn("resolution")
private String resolution;

/** 분쟁 해결 시각 */
@XColumn("resolved_at")
private LocalDateTime resolvedAt;
```

### DDL 추가 (torq_trades 테이블)

```sql
ALTER TABLE torq_trades
    ADD COLUMN resolution VARCHAR(30) NULL COMMENT 'TORQ 판정 결과 (RELEASE_TO_BUYER / REFUND_TO_BUYER)' AFTER dispute_evidence_url,
    ADD COLUMN resolved_at DATETIME NULL COMMENT '분쟁 해결 시각' AFTER resolution;
```

---

## 6. TorqService 의존성 추가

### 새 의존성

```java
private final TorqLinkRepository torqLinkRepository;
```

생성자에 `TorqLinkRepository` 추가:

```java
public TorqService(TorqClient torqClient,
                   TorqTradeRepository torqTradeRepository,
                   DepositRepository depositRepository,
                   TorqLinkRepository torqLinkRepository) {  // ← 추가
    this.torqClient = torqClient;
    this.torqTradeRepository = torqTradeRepository;
    this.depositRepository = depositRepository;
    this.torqLinkRepository = torqLinkRepository;  // ← 추가
}
```

### TorqLinkRepository 메서드 추가

```java
@XRepository
public interface TorqLinkRepository extends IXRepository<Long, TorqLink> {
    TorqLink findByLinkCode(String linkCode);
    TorqLink findByTorqTradeId(Long torqTradeId);  // ← 추가 (Link 동기화용)
    List<TorqLink> findByPartnerId(Long partnerId);
}
```

---

## 7. handleDisputed() 수정

분쟁 진입 시에도 Link 상태를 동기화 — 다만 DISPUTED는 Trade의 중간 상태이므로 Link는 USED 유지. 로그만 추가.

```java
private void handleDisputed(TorqTrade trade) {
    TorqTrade disputed = trade.toBuilder()
            .status(TorqTradeStatus.DISPUTED)
            .disputedAt(LocalDateTime.now())
            .build();
    torqTradeRepository.modify(disputed);

    // DISPUTED 는 중간 상태 — Link 는 USED 유지, 최종 해결(RESOLVED/COMPLETED) 시 갱신
    log.info("TORQ 분쟁 시작 (Webhook): tradeId={}, escrowId={}", trade.getId(), trade.getEscrowId());
}
```

---

## 8. 전체 상태 전이 요약

### TorqTrade

```
QUOTED → CREATED → ACCEPTED → TRANSFERRED → COMPLETED (정상)
                                           → DISPUTED → RESOLVED (분쟁 해결)
                                                      → COMPLETED (LP 승 via FORCE_RELEASED)
         CREATED → CANCELLED (BUYER_CANCELLED / ADMIN_FORCE_CANCEL)
         ACCEPTED → CANCELLED (BUYER_CANCELLED / DISPUTE_REJECTED)
         * → EXPIRED (EXPIRED / TIMEOUT)
         * → FAILED (시스템 오류)
```

### TorqLink (Trade 동기화)

```
ACTIVE → USED (Trade 생성)
USED   → USED (Trade COMPLETED / RESOLVED LP 승) — terminal
       → CANCELLED (Trade CANCELLED)             — terminal
       → EXPIRED (Trade EXPIRED)                  — terminal
       → FAILED (Trade FAILED / RESOLVED 구매자 승) — 재시도 가능 → ACTIVE
```

---

## 9. 구현 순서

1. **DDL 실행**: `resolution`, `resolved_at` 컬럼 추가
2. **TorqTrade.java**: `resolution`, `resolvedAt` 필드 추가
3. **TorqLinkRepository.java**: `findByTorqTradeId()` 메서드 추가
4. **TorqService.java**: 
   - 생성자에 `TorqLinkRepository` 주입
   - `syncLinkStatus()` private 메서드 추가
   - `handleCompleted()` — resolution 저장 + syncLinkStatus 호출
   - `handleCancelled()` — TIMEOUT 분기 추가 + syncLinkStatus 호출
   - `handleForceReleased()` — resolution 분기 (REFUND_TO_BUYER vs RELEASE_TO_BUYER)
5. **TorqLinkWidgetController**: FAILED 링크 재활성화 로직
6. **테스트**: 각 webhook 이벤트별 Link 상태 동기화 검증

---

## 10. 체크리스트

- [ ] DDL: `ALTER TABLE torq_trades ADD COLUMN resolution, resolved_at`
- [ ] `TorqTrade.java` — resolution, resolvedAt 필드 추가
- [ ] `TorqLinkRepository.java` — `findByTorqTradeId()` 추가
- [ ] `TorqService` 생성자 — `TorqLinkRepository` 주입
- [ ] `syncLinkStatus()` 메서드 구현
- [ ] `handleCompleted()` — resolution 저장 + Link 동기화
- [ ] `handleCancelled()` — TIMEOUT 분기 + Link 동기화
- [ ] `handleForceReleased()` — resolution 분기 (REFUND vs RELEASE) + RESOLVED 상태 활용
- [ ] `TorqLinkWidgetController` — FAILED 링크 재활성화
- [ ] 빌드 검증: `./gradlew :core:compileJava`
