# 파트너 텔레그램 알림에 회원 ID 채우기 — 구현 지침서

**작성일:** 2026-08-25
**대상 모듈:** `core`, `open-api`
**DDL 변경:** 없음 (필요한 컬럼·엔티티 필드가 모두 이미 있다)

---

## 0. 왜 하는가

파트너가 받는 알림에 **어느 회원의 거래인지가 없다.** 그래서 파트너 쪽에서 자동 충전 처리를
구현할 수 없다 — 금액과 주문코드만으로는 회원을 특정할 수 없기 때문이다.

오너 제보(2026-08-25):

```
✅ P2P 주문 종결 — 전액 확정
주문코드: pdo_b78f1066f2ca
확정: 49.238578000000000000 USDT (70000원)
...
```
→ `partner_user_id` 가 없다.

**이 지침서의 범위는 "이미 가진 값을 메시지에 싣는 것"이다.** 새 조회·새 컬럼·새 웹훅은 없다.

---

## 1. 원칙 — 넣는 것과 넣지 않는 것

| 구분 | 판단 |
|---|---|
| **파트너 채널** 알림에 **그 파트너 자신의** 회원 ID | ✅ 넣는다 |
| **회원 개인** 텔레그램(`p2pMember*`) | ❌ 넣지 않는다. 본인에게 본인 ID를 알리는 건 잡음이다 |
| **상대 파트너의** 회원 ID | ☠️ **절대 금지.** P2P 분쟁 알림은 "상대 파트너명·회원 실명을 담지 않는다"가 명문 규칙이다 |
| `systemAlert` (운영자용) | ❌ 대상 회원이라는 개념이 없다 |

**값이 없으면(null/blank) 그 줄 자체를 생략한다.** `-` 나 `null` 을 찍지 않는다 —
파트너가 파싱해서 자동 처리하는 값이라 빈 값이 문자열로 들어가면 그쪽에서 사고가 난다.

**표기:** 주문/거래 코드 **바로 다음 줄**에 `회원 ID: <code>{partnerUserId}</code>`.
`escapeHtml` 을 반드시 통과시킨다(파트너가 정한 문자열이라 `<`, `&` 가 들어올 수 있다).

---

## 2. 대상 — 파트너 채널 메시지 전건

`core/src/main/java/com/cryptoments/core/notification/TelegramMessageFormatter.java`

### 2-1. P2P (호출부에 `P2pDepositOrder` 가 이미 있다)

| 메서드 | 호출부 | 값 출처 |
|---|---|---|
| `p2pOrderComplete` | `P2pSettlementService.sendP2pOrderCompleteTelegram:1313` | `dpo.getPartnerUserId()` |
| `p2pLegDepositConfirmed` | `P2pSettlementService:495`(원화) / `:890`(TORQ) / `:1756`(USDT) | `dpo.getPartnerUserId()` |

> ⚠️ `sendP2pOrderCompleteTelegram` 은 **웹훅 페이로드 JSON 에서 값을 읽어 쓴다**(재계산 금지 규칙).
> 회원 ID 는 페이로드에 없으므로 **인자로 받은 `dpo` 에서 직접 꺼낸다.** 페이로드에 필드를
> 새로 추가하지 말 것 — 웹훅 계약을 건드리는 일이고 이번 범위가 아니다.

### 2-2. 일반 입금 (`Deposit.getPartnerUserId()` — 엔티티에 이미 매핑돼 있다)

| 메서드 | 호출부 |
|---|---|
| `depositDetected` | `DepositService:245` |
| `depositConfirmed` | `WebhookProcessingService` |
| `largeDeposit` | **호출부 없음** — 시그니처만 맞춰 두고 넘어간다 |

### 2-3. 출금 (`Withdrawal.getPartnerUserId()` — 엔티티에 이미 매핑돼 있다)

`withdrawalRequested` / `withdrawalApproved` / `withdrawalConfirmed` / `withdrawalFailed` /
`withdrawalRejected` / `withdrawalCancelled` / `withdrawalCompleted` / `withdrawalP2pPending`

호출부: `WithdrawalService`(9곳), `WebhookProcessingService`(2곳).
각 호출부에 `Withdrawal` 엔티티가 있는지 확인하고, 없으면 **이미 로드된 객체에서** 꺼낸다.
회원 ID 하나 때문에 새 조회를 추가하지 말 것 — 없으면 그 호출부는 null 을 넘겨 줄을 생략시킨다.

### 2-4. P2P 분쟁 — ☠️ 여기만 주의가 필요하다

`p2pPartnerDisputeOpened` / `p2pPartnerDisputeResolved`
(`P2pDisputeService:729`, `:761`)

수신자가 **입금측·출금측 두 파트너**다. `Recipient` 레코드가
`(Long partnerId, String orderCode)` 로 "그 파트너 **자신의** 주문 코드"만 담는 것이
바로 상대 정보 노출을 막기 위한 장치다.

**같은 방식으로 회원 ID도 자기 쪽 것만 담는다.**

```java
private record Recipient(Long partnerId, String orderCode, String partnerUserId) {}
```

- 입금측 수신자 → `P2pDepositOrder.getPartnerUserId()`
- 출금측 수신자 → `P2pWithdrawOrder.getPartnerUserId()`

두 파트너가 **같은 파트너**일 때(자기 그룹 내 매칭) 수신자가 1건으로 합쳐지는 기존 동작이 있다.
그 경우 어느 쪽 회원 ID를 쓸지 정해야 한다 — **입금측(구매자)** 을 쓴다. 알림의 주어가
"입금 건"이기 때문이다. 합치는 로직을 확인하고 그에 맞춰 처리할 것.

---

## 3. 시그니처 변경 방식

기존 호출부가 깨지지 않도록 **인자를 뒤에 추가**한다. 오버로드를 남기지 말고
**전 호출부를 함께 고친다** — 오버로드를 두면 회원 ID 없는 옛 시그니처가 조용히 계속 쓰인다.

```java
public static String p2pOrderComplete(boolean full, String orderCode, String partnerUserId,
                                       String settledAmount, ...)
```

공통 헬퍼를 하나 두면 표기가 흔들리지 않는다:

```java
/** 회원 ID 줄 — 값이 없으면 빈 문자열(줄 자체를 만들지 않는다). */
private static String memberLine(String partnerUserId) {
    return (partnerUserId == null || partnerUserId.isBlank())
            ? ""
            : "회원 ID: <code>" + escapeHtml(partnerUserId) + "</code>\n";
}
```

텍스트 블록(`"""`)으로 작성된 메서드가 여럿이다. 그 경우 블록 안에 `%s` 자리를 만들고
`memberLine(...)` 결과를 그대로 끼워 넣는다(값이 없으면 빈 줄도 남지 않아야 한다).

---

## 4. 코딩 규칙 (프로젝트 공통)

- Java 17. `@Data` 금지, Lombok 은 `@Getter/@Setter/@Builder(toBuilder=true)/@NoArgsConstructor/@AllArgsConstructor`
- **새 DB 조회를 추가하지 말 것.** 호출부에 이미 있는 엔티티에서만 값을 꺼낸다
- 알림 발송 실패는 기존대로 삼킨다 — 트랜잭션을 되돌리지 않는다
- 텔레그램은 HTML parse mode 다. 파트너가 정한 문자열은 **반드시** `escapeHtml`

## 5. 완료 기준

- [ ] 파트너 채널 메시지에서 회원 ID를 실을 수 있는 곳이 **전부** 실린다
- [ ] `p2pMember*` (회원 개인 알림)에는 **추가하지 않았다**
- [ ] 분쟁 알림에서 각 파트너가 **자기 쪽 회원 ID만** 받는다 (상대 것이 새지 않는다)
- [ ] 값이 null/blank 면 줄 자체가 없다 (`-`·`null` 문자열이 찍히지 않는다)
- [ ] 회원 ID가 `escapeHtml` 을 통과한다
- [ ] 옛 시그니처 오버로드가 남아 있지 않다
- [ ] 새로 추가된 DB 조회가 없다
- [ ] `./gradlew :core:compileJava :open-api:compileJava` 통과 (로컬 Mac, Desktop Commander)

## 6. 하지 말 것

- ☠️ 웹훅 페이로드(`WebhookPayloadBuilder`)를 건드리지 말 것 — 파트너 연동 계약이다
- ☠️ 회원 **실명**(`buyer_name`, `account_holder`)을 넣지 말 것. 넣는 것은 `partner_user_id` 뿐이다
- ☠️ 상대 파트너의 회원 ID를 넣지 말 것
- ☠️ git commit / push 하지 말 것 — 변경만 남기고 보고한다
