# Guide #33 — Telegram Push Notification 구현 지침서

**작성일**: 2026-03-22
**대상 모듈**: `core` (NotificationService), `common` (TelegramEventType, PartnerTelegramConfig)
**구현 환경**: IntelliJ (Spring Boot)
**선행 조건**: Guide #32 (GAS_COST + withdrawal_fee_fixed 제거) 적용 완료

---

## 1. 현재 상태 분석

### 1-1. NotificationService.sendTelegram() — 미완성

현재 `sendTelegram()` 메서드는 구독 확인까지만 수행하고 **실제 Telegram Bot API 호출을 하지 않는다**.

```java
// 현재 (L178~180): 로그만 남기고 끝남
log.info("Telegram 발송 대상 확인: partnerId={}, chatId={}, eventType={}",
        partnerId, config.getChatId(), telegramEvent);
```

### 1-2. DepositService — 알림 호출 누락

`DepositService`는 `NotificationService`를 **import조차 하지 않는다**.
입금 확정(CONFIRMED) 시 파트너에게 알림이 전혀 발송되지 않는 상태.

### 1-3. 현재 알림 트리거 포인트

| 서비스 | 메서드 | notificationService.send() 호출 | 비고 |
|--------|--------|------|------|
| WithdrawalService | `reject()` L155 | ✅ 호출 | WITHDRAWAL_REJECTED |
| WithdrawalService | `onTxConfirmed()` L225 | ✅ 호출 | WITHDRAWAL_CONFIRMED |
| WithdrawalService | `onTxFailed()` L245 | ✅ 호출 | WITHDRAWAL_FAILED |
| DepositService | `onTxConfirmed()` L422 | ❌ **미호출** | 입금 알림 누락 |
| DepositService | `onCollectionConfirmed()` L516 | ❌ 미호출 | 집금은 알림 불필요 |

---

## 2. 이벤트 타입 ↔ 트리거 매핑

### 2-1. 5대 Telegram 이벤트

| TelegramEventType | 트리거 서비스 | 트리거 메서드 | 트리거 시점 |
|---|---|---|---|
| `DEPOSIT_CONFIRMED` | DepositService | `onTxConfirmed()` | 입금 TX 블록 확정 |
| `WITHDRAWAL_CONFIRMED` | WithdrawalService | `onTxConfirmed()` | 출금 TX 블록 확정 |
| `WITHDRAWAL_FAILED` | WithdrawalService | `onTxFailed()` | 출금 TX 실패 |
| `LARGE_DEPOSIT` | DepositService | `onTxConfirmed()` | 입금 금액 > 임계값 |
| `BALANCE_LOW` | Scheduler (향후) | `checkBalanceLow()` | 잔액 < 임계값 |

> **WITHDRAWAL_REJECTED**는 현재 `mapToTelegramEventType()`에서 WITHDRAWAL → WITHDRAWAL_CONFIRMED으로 매핑됨.
> 출금 거부는 별도 이벤트 타입이 필요할 수 있으나, 현재 DDL/enum에 정의되지 않았으므로 Phase 2에서 검토.

### 2-2. TransactionType → TelegramEventType 매핑 개선

현재 매핑 (`NotificationService.mapToTelegramEventType()`):
```java
// 현재: WITHDRAWAL 거부/확정/실패를 모두 WITHDRAWAL_CONFIRMED으로 매핑 (부정확)
case DEPOSIT -> TelegramEventType.DEPOSIT_CONFIRMED;
case WITHDRAWAL -> TelegramEventType.WITHDRAWAL_CONFIRMED;
```

**개선**: `send()` 메서드에 이벤트 구분 정보를 전달해야 함 → **eventData JSON에서 event 필드 파싱**하여 정확한 매핑.

---

## 3. 구현 항목

### 3-1. TelegramBotClient 신규 생성

**파일**: `core/src/main/java/com/cryptoments/core/notification/TelegramBotClient.java`

```java
package com.cryptoments.core.notification;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

/**
 * Telegram Bot API HTTP 클라이언트.
 *
 * <p>Bot API sendMessage 엔드포인트를 호출하여 실제 메시지를 전송한다.
 * bot_token_ref 방식: partner_telegram_configs.bot_token_ref가 'default'이면
 * application.yml의 기본 봇 토큰을 사용한다.
 */
@Component
public class TelegramBotClient {

    private static final Logger log = LoggerFactory.getLogger(TelegramBotClient.class);
    private static final String TELEGRAM_API_BASE = "https://api.telegram.org/bot";

    private final RestClient restClient;

    /** 기본 봇 토큰 (application.yml에서 주입) */
    @Value("${cryptoments.telegram.bot-token:}")
    private String defaultBotToken;

    public TelegramBotClient(RestClient.Builder restClientBuilder) {
        this.restClient = restClientBuilder.build();
    }

    /**
     * Telegram 메시지 발송.
     *
     * @param botTokenRef 봇 토큰 참조 키 ('default' → 기본 토큰 사용)
     * @param chatId      대상 채팅방 ID
     * @param text        메시지 본문 (Markdown V2 또는 HTML)
     * @param parseMode   파싱 모드 ("MarkdownV2" 또는 "HTML")
     * @return 성공 여부
     */
    public boolean sendMessage(String botTokenRef, String chatId, String text, String parseMode) {
        String botToken = resolveBotToken(botTokenRef);
        if (botToken == null || botToken.isBlank()) {
            log.warn("Telegram 봇 토큰 미설정: botTokenRef={}", botTokenRef);
            return false;
        }

        String url = TELEGRAM_API_BASE + botToken + "/sendMessage";

        try {
            TelegramSendRequest request = new TelegramSendRequest(chatId, text, parseMode);

            String response = restClient.post()
                    .uri(url)
                    .header("Content-Type", "application/json")
                    .body(request)
                    .retrieve()
                    .body(String.class);

            log.info("Telegram 발송 성공: chatId={}, responseLength={}",
                    chatId, response != null ? response.length() : 0);
            return true;
        } catch (Exception e) {
            log.error("Telegram 발송 실패: chatId={}, error={}", chatId, e.getMessage());
            return false;
        }
    }

    /**
     * 봇 토큰 참조 키를 실제 토큰으로 변환.
     * 현재는 'default' 또는 NULL → 기본 토큰.
     * 향후 파트너별 전용 봇 토큰 지원 시 확장.
     */
    private String resolveBotToken(String botTokenRef) {
        if (botTokenRef == null || "default".equalsIgnoreCase(botTokenRef)) {
            return defaultBotToken;
        }
        // 향후: system_settings 또는 별도 저장소에서 파트너별 토큰 조회
        log.warn("알 수 없는 botTokenRef: {}, 기본 토큰 사용", botTokenRef);
        return defaultBotToken;
    }

    /**
     * Telegram sendMessage 요청 DTO.
     */
    record TelegramSendRequest(
            /** 대상 채팅방 ID */
            String chat_id,
            /** 메시지 본문 */
            String text,
            /** 파싱 모드 (MarkdownV2, HTML) */
            String parse_mode
    ) {}
}
```

### 3-2. TelegramMessageFormatter 신규 생성

**파일**: `core/src/main/java/com/cryptoments/core/notification/TelegramMessageFormatter.java`

```java
package com.cryptoments.core.notification;

import com.cryptoments.common.enums.TelegramEventType;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * Telegram 알림 메시지 포맷터.
 *
 * <p>이벤트 타입별로 파트너에게 발송할 메시지를 생성한다.
 * parse_mode = "HTML" 사용 (MarkdownV2 특수문자 이스케이프 복잡성 회피).
 */
public class TelegramMessageFormatter {

    private static final DateTimeFormatter DT_FORMAT =
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    private TelegramMessageFormatter() {}

    /**
     * 입금 확정 알림.
     */
    public static String depositConfirmed(String depositCode, BigDecimal amount,
                                            String currency, String network,
                                            String txHash, String fromAddress) {
        return """
                ✅ <b>입금 확정</b>

                입금코드: <code>%s</code>
                금액: <b>%s %s</b>
                네트워크: %s
                보낸주소: <code>%s</code>
                TX: <code>%s</code>
                시각: %s"""
                .formatted(depositCode, amount.toPlainString(), currency,
                        network, truncateAddress(fromAddress),
                        truncateHash(txHash), LocalDateTime.now().format(DT_FORMAT));
    }

    /**
     * 출금 확정 알림.
     */
    public static String withdrawalConfirmed(String withdrawalCode, BigDecimal amount,
                                               String currency, String network,
                                               String txHash, String toAddress) {
        return """
                ✅ <b>출금 완료</b>

                출금코드: <code>%s</code>
                금액: <b>%s %s</b>
                네트워크: %s
                받는주소: <code>%s</code>
                TX: <code>%s</code>
                시각: %s"""
                .formatted(withdrawalCode, amount.toPlainString(), currency,
                        network, truncateAddress(toAddress),
                        truncateHash(txHash), LocalDateTime.now().format(DT_FORMAT));
    }

    /**
     * 출금 실패 알림.
     */
    public static String withdrawalFailed(String withdrawalCode, BigDecimal amount,
                                            String currency, String network,
                                            String errorMessage) {
        return """
                ❌ <b>출금 실패</b>

                출금코드: <code>%s</code>
                금액: <b>%s %s</b>
                네트워크: %s
                사유: %s
                시각: %s

                ⚠️ 관리 콘솔에서 재시도 또는 취소 처리해 주세요."""
                .formatted(withdrawalCode, amount.toPlainString(), currency,
                        network, errorMessage != null ? errorMessage : "TX 실패",
                        LocalDateTime.now().format(DT_FORMAT));
    }

    /**
     * 대량 입금 알림 (LARGE_DEPOSIT).
     */
    public static String largeDeposit(String depositCode, BigDecimal amount,
                                        String currency, String network,
                                        BigDecimal threshold) {
        return """
                🔔 <b>대량 입금 감지</b>

                입금코드: <code>%s</code>
                금액: <b>%s %s</b> (임계값: %s)
                네트워크: %s
                시각: %s

                ⚠️ 이상 거래 여부를 확인해 주세요."""
                .formatted(depositCode, amount.toPlainString(), currency,
                        threshold.toPlainString(), network,
                        LocalDateTime.now().format(DT_FORMAT));
    }

    /**
     * 잔액 부족 알림 (BALANCE_LOW).
     */
    public static String balanceLow(String walletType, String network,
                                      String currency, BigDecimal currentBalance,
                                      BigDecimal threshold) {
        return """
                ⚠️ <b>잔액 부족 경고</b>

                지갑유형: %s
                네트워크: %s
                통화: %s
                현재잔액: <b>%s</b>
                임계값: %s
                시각: %s

                💡 자금 충전이 필요합니다."""
                .formatted(walletType, network, currency,
                        currentBalance.toPlainString(), threshold.toPlainString(),
                        LocalDateTime.now().format(DT_FORMAT));
    }

    /**
     * 출금 거부 알림 (WITHDRAWAL_REJECTED → WITHDRAWAL_CONFIRMED 이벤트로 분류).
     * 현재 TelegramEventType에 별도 REJECTED 없으므로 메시지로 구분.
     */
    public static String withdrawalRejected(String withdrawalCode, BigDecimal amount,
                                              String currency, String reason) {
        return """
                🚫 <b>출금 거부</b>

                출금코드: <code>%s</code>
                금액: <b>%s %s</b>
                사유: %s
                시각: %s

                ℹ️ 출금 요청이 관리자에 의해 거부되었습니다."""
                .formatted(withdrawalCode, amount.toPlainString(), currency,
                        reason != null ? reason : "사유 미입력",
                        LocalDateTime.now().format(DT_FORMAT));
    }

    // ── 유틸 ──

    private static String truncateAddress(String address) {
        if (address == null || address.length() <= 12) return address != null ? address : "-";
        return address.substring(0, 6) + "..." + address.substring(address.length() - 4);
    }

    private static String truncateHash(String hash) {
        if (hash == null || hash.length() <= 16) return hash != null ? hash : "-";
        return hash.substring(0, 10) + "..." + hash.substring(hash.length() - 6);
    }
}
```

### 3-3. NotificationService.sendTelegram() 수정

**파일**: `core/src/main/java/com/cryptoments/core/notification/NotificationService.java`

#### 변경 1: TelegramBotClient 의존성 추가 + Subscription 제거

```java
// 기존 필드에서 제거
// private final PartnerTelegramSubscriptionRepository telegramSubscriptionRepository;  ← 삭제

// 추가
private final TelegramBotClient telegramBotClient;

// 생성자 — PartnerTelegramSubscriptionRepository 제거, TelegramBotClient 추가
public NotificationService(PartnerRepository partnerRepository,
                           WebhookDeliveryLogRepository webhookDeliveryLogRepository,
                           PartnerTelegramConfigRepository telegramConfigRepository,
                           TelegramBotClient telegramBotClient) {  // ← Subscription 제거, BotClient 추가
    // 기존 할당...
    // this.telegramSubscriptionRepository = ...  ← 삭제
    this.telegramBotClient = telegramBotClient;  // ← 추가
}
```

#### 변경 2: sendTelegram() 메서드 교체

기존 `sendTelegram()` (L158~180) 전체를 아래로 교체:

```java
/**
 * Telegram 알림 발송.
 * config.is_active 확인 후 TelegramBotClient로 실제 전송.
 * (partner_telegram_subscriptions 테이블 제거됨 — 연결되면 모든 이벤트 수신)
 */
public void sendTelegram(TransactionType transactionType, Long partnerId, String eventData) {
    PartnerTelegramConfig config = telegramConfigRepository.findByPartnerId(partnerId);
    if (config == null || !Boolean.TRUE.equals(config.getIsActive())) {
        log.debug("Telegram 발송 생략: partnerId={}, config 없음 또는 비활성", partnerId);
        return;
    }

    // 실제 발송
    boolean success = telegramBotClient.sendMessage(
            config.getBotTokenRef(),
            config.getChatId(),
            eventData,  // 호출 측에서 TelegramMessageFormatter로 포맷팅된 메시지
            "HTML"
    );

    if (success) {
        log.info("Telegram 발송 완료: partnerId={}, chatId={}", partnerId, config.getChatId());
    }
}
```

#### 변경 3: mapToTelegramEventType() 시그니처 변경

기존 `mapToTelegramEventType(TransactionType)` → eventData도 받아서 정밀 매핑:

```java
/**
 * TransactionType + eventData → TelegramEventType 매핑.
 * eventData JSON의 "event" 필드로 정확한 이벤트 구분.
 */
private TelegramEventType mapToTelegramEventType(TransactionType transactionType, String eventData) {
    if (transactionType == TransactionType.DEPOSIT) {
        // LARGE_DEPOSIT 판별은 호출 측에서 별도 send() 호출
        return TelegramEventType.DEPOSIT_CONFIRMED;
    }
    if (transactionType == TransactionType.WITHDRAWAL) {
        if (eventData != null && eventData.contains("WITHDRAWAL_FAILED")) {
            return TelegramEventType.WITHDRAWAL_FAILED;
        }
        // WITHDRAWAL_CONFIRMED, WITHDRAWAL_REJECTED 모두 여기
        return TelegramEventType.WITHDRAWAL_CONFIRMED;
    }
    return null;
}
```

#### 변경 4: sendTestTelegram() 실제 전송으로 교체

```java
/**
 * Telegram 테스트 발송.
 */
public boolean sendTestTelegram(Long partnerId) {
    PartnerTelegramConfig config = telegramConfigRepository.findByPartnerId(partnerId);
    if (config == null || !Boolean.TRUE.equals(config.getIsActive())) {
        return false;
    }

    String testMessage = "✅ <b>Cryptoments Telegram 연결 테스트</b>\n\n연결이 정상입니다.";
    return telegramBotClient.sendMessage(config.getBotTokenRef(), config.getChatId(),
            testMessage, "HTML");
}
```

### 3-4. send() 메서드의 eventData 변경

현재 `send()`는 JSON 문자열을 `eventData`로 받는다. Telegram 발송 시에는 **포맷된 메시지**가 필요하므로, **호출 측에서 메시지를 생성**하는 패턴을 사용한다.

**방법 A** (권장): `send()` 오버로드 추가

```java
/**
 * 통합 발송 — Webhook + Telegram.
 * telegramMessage가 null이면 Telegram 발송 생략.
 */
public void send(TransactionType transactionType, Long partnerId,
                 Long referenceId, String eventData, String telegramMessage) {
    sendWebhook(transactionType, partnerId, referenceId, eventData);
    if (telegramMessage != null) {
        sendTelegram(transactionType, partnerId, telegramMessage);
    }
}

/**
 * 기존 호환용 — telegramMessage가 없으면 eventData를 그대로 전달.
 */
public void send(TransactionType transactionType, Long partnerId,
                 Long referenceId, String eventData) {
    sendWebhook(transactionType, partnerId, referenceId, eventData);
    sendTelegram(transactionType, partnerId, eventData);
}
```

### 3-5. DepositService에 NotificationService 추가

**파일**: `core/src/main/java/com/cryptoments/core/deposit/DepositService.java`

#### 변경 1: import + 필드 + 생성자

```java
// import 추가
import com.cryptoments.core.notification.NotificationService;
import com.cryptoments.core.notification.TelegramMessageFormatter;

// 필드 추가
private final NotificationService notificationService;

// 생성자 파라미터 추가
public DepositService(WalletService walletService,
                      SettlementService settlementService,
                      NotificationService notificationService,  // ← 추가
                      DepositSessionRepository sessionRepository,
                      ... 기존 파라미터들 ...) {
    // 기존 할당...
    this.notificationService = notificationService;  // ← 추가
}
```

#### 변경 2: onTxConfirmed()에 알림 호출 추가

`onTxConfirmed()` 메서드 마지막 부분 (현재 L466 `log.info` 직전)에 추가:

```java
// 파트너 알림 발송 (입금 확정)
if (deposit.getPartnerId() != null) {
    // Webhook 이벤트 데이터
    String webhookEventData = "{\"event\":\"DEPOSIT_CONFIRMED\",\"depositId\":" + depositId
            + ",\"amount\":\"" + deposit.getAmount().toPlainString()
            + "\",\"txHash\":\"" + deposit.getTxHash() + "\"}";

    // Telegram 포맷 메시지
    Partner partner = partnerRepository.findOne(deposit.getPartnerId());
    String currencyName = "USDT";  // TODO: currencyId → 심볼 조회
    String networkName = "BSC";    // TODO: networkId → 이름 조회

    String telegramMsg = TelegramMessageFormatter.depositConfirmed(
            deposit.getDepositCode(), deposit.getAmount(),
            currencyName, networkName,
            deposit.getTxHash(), deposit.getFromAddress());

    notificationService.send(TransactionType.DEPOSIT, deposit.getPartnerId(),
            depositId, webhookEventData, telegramMsg);

    // LARGE_DEPOSIT 판별 (임계값: system_settings 또는 하드코딩)
    BigDecimal largeThreshold = new BigDecimal("10000");  // TODO: system_settings에서 조회
    if (deposit.getAmount().compareTo(largeThreshold) >= 0) {
        String largeTelegramMsg = TelegramMessageFormatter.largeDeposit(
                deposit.getDepositCode(), deposit.getAmount(),
                currencyName, networkName, largeThreshold);
        // LARGE_DEPOSIT는 별도 sendTelegram 직접 호출 (Webhook은 불필요)
        notificationService.sendTelegram(TransactionType.DEPOSIT,
                deposit.getPartnerId(), largeTelegramMsg);
    }
}
```

### 3-6. WithdrawalService 알림 호출 개선

현재 WithdrawalService는 이미 `notificationService.send()`를 호출하고 있으나, Telegram 메시지가 JSON 문자열 그대로 전달되고 있다.

#### onTxConfirmed() (L211~232) 알림 부분 수정

```java
if (withdrawal.getPartnerId() != null) {
    settlementService.debit(withdrawal.getPartnerId(), withdrawal.getCurrencyId(),
            withdrawal.getNetworkId(), withdrawal.getAmount(),
            LedgerReferenceType.WITHDRAWAL, withdrawalId);

    String webhookData = "{\"event\":\"WITHDRAWAL_CONFIRMED\",\"withdrawalId\":" + withdrawalId
            + ",\"txHash\":\"" + txHash + "\"}";

    String telegramMsg = TelegramMessageFormatter.withdrawalConfirmed(
            withdrawal.getWithdrawalCode(), withdrawal.getAmount(),
            "USDT", "BSC",   // TODO: 실제 심볼/네트워크 조회
            txHash, withdrawal.getToAddress());

    notificationService.send(TransactionType.WITHDRAWAL, withdrawal.getPartnerId(),
            withdrawalId, webhookData, telegramMsg);
}
```

#### onTxFailed() (L237~251) 알림 부분 수정

```java
if (withdrawal.getPartnerId() != null) {
    String webhookData = "{\"event\":\"WITHDRAWAL_FAILED\",\"withdrawalId\":" + withdrawalId + "}";

    String telegramMsg = TelegramMessageFormatter.withdrawalFailed(
            withdrawal.getWithdrawalCode(), withdrawal.getAmount(),
            "USDT", "BSC",   // TODO: 실제 심볼/네트워크 조회
            error);

    notificationService.send(TransactionType.WITHDRAWAL, withdrawal.getPartnerId(),
            withdrawalId, webhookData, telegramMsg);
}
```

#### reject() (L135~160) 알림 부분 수정

```java
if (withdrawal.getPartnerId() != null) {
    String webhookData = "{\"event\":\"WITHDRAWAL_REJECTED\",\"withdrawalId\":" + withdrawalId + "}";

    String telegramMsg = TelegramMessageFormatter.withdrawalRejected(
            withdrawal.getWithdrawalCode(), withdrawal.getAmount(),
            "USDT",   // TODO: 실제 심볼 조회
            reason);

    notificationService.send(TransactionType.WITHDRAWAL, withdrawal.getPartnerId(),
            withdrawalId, webhookData, telegramMsg);
}
```

### 3-7. application.yml 설정 추가

**파일**: 각 서비스 모듈의 `application.yml`

```yaml
cryptoments:
  telegram:
    bot-token: ${TELEGRAM_BOT_TOKEN:}   # 환경변수로 주입
    large-deposit-threshold: 10000       # LARGE_DEPOSIT 임계값 (USDT)
    balance-low-threshold: 1000          # BALANCE_LOW 임계값 (USDT)
```

> **보안 주의**: bot-token은 application.yml에 직접 기입하지 말고 환경변수 또는 시크릿 매니저 사용.

---

## 4. 통화/네트워크 심볼 조회 (TODO)

현재 `TelegramMessageFormatter`에 `"USDT"`, `"BSC"` 하드코딩 되어있다.
정확한 구현을 위해 `currencyId → symbol`, `networkId → name` 조회가 필요하다.

**단기 해결**: core 서비스에 헬퍼 메서드 추가

```java
// core 또는 common에 추가
@Service
public class CurrencyLookupService {
    private final CurrencyRepository currencyRepository;
    private final BlockchainNetworkRepository networkRepository;

    public String getCurrencySymbol(Long currencyId) {
        Currency c = currencyRepository.findOne(currencyId);
        return c != null ? c.getSymbol() : "UNKNOWN";
    }

    public String getNetworkName(Long networkId) {
        BlockchainNetwork n = networkRepository.findOne(networkId);
        return n != null ? n.getDisplayName() : "UNKNOWN";
    }
}
```

> 이 서비스는 DepositService / WithdrawalService에 DI하여 Telegram 메시지 생성 시 사용.

---

## 5. 이벤트별 Telegram 메시지 요약

| # | 이벤트 | 아이콘 | 제목 | 포함 정보 |
|---|--------|--------|------|-----------|
| 1 | DEPOSIT_CONFIRMED | ✅ | 입금 확정 | 입금코드, 금액, 통화, 네트워크, 보낸주소, TX해시, 시각 |
| 2 | WITHDRAWAL_CONFIRMED | ✅ | 출금 완료 | 출금코드, 금액, 통화, 네트워크, 받는주소, TX해시, 시각 |
| 3 | WITHDRAWAL_FAILED | ❌ | 출금 실패 | 출금코드, 금액, 통화, 네트워크, 실패사유, 시각, 재시도 안내 |
| 4 | WITHDRAWAL_REJECTED | 🚫 | 출금 거부 | 출금코드, 금액, 통화, 거부사유, 시각 |
| 5 | LARGE_DEPOSIT | 🔔 | 대량 입금 감지 | 입금코드, 금액, 통화, 임계값, 네트워크, 시각, 확인 안내 |
| 6 | BALANCE_LOW | ⚠️ | 잔액 부족 경고 | 지갑유형, 네트워크, 통화, 현재잔액, 임계값, 시각, 충전 안내 |

---

## 6. 파일 변경 체크리스트

### 신규 생성 (2개)

| # | 파일 | 설명 |
|---|------|------|
| 1 | `core/.../notification/TelegramBotClient.java` | Telegram Bot API HTTP 클라이언트 |
| 2 | `core/.../notification/TelegramMessageFormatter.java` | 이벤트별 메시지 포맷터 |

### 수정 (4개)

| # | 파일 | 변경 내용 |
|---|------|-----------|
| 3 | `core/.../notification/NotificationService.java` | TelegramBotClient DI 추가, sendTelegram() 실제 전송 구현, send() 오버로드, mapToTelegramEventType() 개선, sendTestTelegram() 실제 전송 |
| 4 | `core/.../deposit/DepositService.java` | NotificationService DI 추가, onTxConfirmed()에 알림 호출 + LARGE_DEPOSIT 판별 |
| 5 | `core/.../withdrawal/WithdrawalService.java` | 3개 알림 호출 포인트에 TelegramMessageFormatter 적용 |
| 6 | 서비스 모듈 `application.yml` | `cryptoments.telegram.*` 설정 추가 |

### 제거 (subscription 테이블 폐지)

| # | 파일 | 변경 내용 |
|---|------|-----------|
| 7 | `common/.../entity/PartnerTelegramSubscription.java` | **삭제** |
| 8 | `common/.../repository/PartnerTelegramSubscriptionRepository.java` | **삭제** |
| 9 | `core/.../notification/NotificationService.java` | SubscriptionRepository import/필드/생성자 파라미터 제거, sendTelegram()에서 subscription 조회 로직 제거 |
| 10 | `admin-api/.../dto/request/TelegramSubscriptionUpdateRequest.java` | **삭제** |
| 11 | `admin-api/.../service/PartnerManagementService.java` | subscription 관련 메서드 제거 |
| 12 | `admin-api/.../controller/PartnerManagementController.java` | subscription 관련 엔드포인트 제거 |
| 13 | `partner-api/.../service/PartnerIntegrationService.java` | subscription 관련 로직 제거 |
| 14 | `partner-api/.../dto/response/TelegramConfigResponse.java` | subscriptions 필드 제거 (있을 경우) |

### Node.js 제거

| # | 파일 | 변경 내용 |
|---|------|-----------|
| 15 | `node-service/packages/common/src/db/repositories/TelegramSubscriptionRepo.ts` | **삭제** |
| 16 | `node-service/packages/common/src/db/index.ts` | TelegramSubscriptionRepo export 제거 |
| 17 | `node-service/packages/common/src/index.ts` | SUPPORTED_EVENTS export 제거 |
| 18 | `node-service/packages/telegram-bot/src/handlers/index.ts` | `/subscribe`, `/unsubscribe` 핸들러 제거 + SUPPORTED_EVENTS import 제거 |

### DB 적용

```sql
DROP TABLE IF EXISTS partner_telegram_subscriptions;
```

### 선택 사항 (향후)

| # | 파일 | 변경 내용 |
|---|------|-----------|
| 19 | `common/.../enums/TelegramEventType.java` | WITHDRAWAL_REJECTED 추가 검토 |
| 20 | 신규: `core/.../CurrencyLookupService.java` | currencyId/networkId → 심볼/이름 조회 |
| 21 | scheduler 모듈 | BALANCE_LOW 체크 배치 작업 |

---

## 7. 구현 순서 (권장)

```
1. TelegramBotClient.java 생성
2. TelegramMessageFormatter.java 생성
3. NotificationService.java 수정 (DI + sendTelegram + send 오버로드)
4. DepositService.java 수정 (DI + onTxConfirmed 알림 추가)
5. WithdrawalService.java 수정 (3개 포인트 메시지 포맷 적용)
6. application.yml에 telegram 설정 추가
7. 컴파일 검증: ./gradlew :core:compileJava
8. 통합 테스트 (Telegram 테스트 봇으로 실제 발송 확인)
```

---

## 8. 아키텍처 흐름도 (구현 후)

```
[블록체인 이벤트]
     │
     ▼
[open-api: WebhookProcessingService]
     │
     ├── 입금 확정 ───► DepositService.onTxConfirmed()
     │                      ├── 원장 기록 (CREDIT + FEE)
     │                      ├── 집금 enqueue
     │                      └── notificationService.send()  ← NEW
     │                              ├── sendWebhook() → delivery_log PENDING
     │                              └── sendTelegram() → TelegramBotClient.sendMessage()  ← NEW
     │                                                     │
     │                                                     ▼
     │                                              [Telegram Bot API]
     │                                                     │
     │                                                     ▼
     │                                              [파트너 채팅방]
     │
     ├── 출금 확정 ───► WithdrawalService.onTxConfirmed()
     │                      ├── 원장 기록 (DEBIT)
     │                      └── notificationService.send()  (기존 + 포맷 개선)
     │
     └── 출금 실패 ───► WithdrawalService.onTxFailed()
                            └── notificationService.send()  (기존 + 포맷 개선)

[관리 콘솔]
     │
     └── 출금 거부 ───► WithdrawalService.reject()
                            └── notificationService.send()  (기존 + 포맷 개선)
```

---

## 9. Node.js telegram-bot과의 역할 분리

| 역할 | 담당 | 방식 |
|------|------|------|
| **Push 알림 발송** | **Spring Boot** (NotificationService → TelegramBotClient) | HTTP POST → Telegram Bot API |
| **명령 수신 처리** | **Node.js** (telegram-bot 서비스) | Long Polling ← Telegram Bot API |
| **설정 관리** | **양쪽 공유** | DB 테이블 (partner_telegram_configs — is_active 토글) |

> **동일 봇 토큰** 사용. Long Polling(Node.js)과 sendMessage(Spring Boot)는 충돌하지 않음.
> Node.js는 getUpdates로 사용자 명령만 수신하고, Spring Boot는 sendMessage로 알림만 발송.
