# LP 공급자를 DB 로 관리 (v2.10) — TORQ2 → BARO

작성 2026-08-20 · repo `cryptoments` + `cryptoments-admin`
DDL 은 **이미 운영에 적용됨** (§0). 이 문서는 코드 구현 지침이다.

---

## 무엇을 바꾸는가

지금 LP 공급자는 **enum + env** 다. LP 하나 붙이려면 코드 수정 + 배포 + 6개 모듈 env 편집이 필요하다.
이걸 **DB 행 하나**로 바꾼다.

```
현재   enum LpProvider {TORQ, TORQ2}
       torq.providers.TORQ2.api-key  ← env, 모듈마다 따로

이후   lp_providers 테이블 행
       provider_code = 'TORQ' | 'BARO'
       자격증명 · 네트워크 · 리베이트 여부가 전부 컬럼
       (콜백 URL 은 컬럼이 아니다 — provider_code 파생)
```

**`TORQ2` 라는 이름은 사라진다.** 서비스명이 BARO 로 정해졌으므로 코드값도 `BARO` 다.
운영 데이터에 `TORQ2` 는 **0건**이라(파트너 0 · 거래 0) 이관 비용이 없다 — 지금이 유일하게 공짜인 시점이다.

## 이 변경이 노리는 것

| | 지금 | 이후 |
|---|---|---|
| LP 추가 | 코드 + 배포 + env×6 | 어드민에서 행 추가 |
| 자격증명 위치 | 모듈별 env (불일치가 조용한 장애) | 한 곳 |
| 콜백 URL | 아무 데도 기록 없음 | provider 코드에서 파생 + 화면에 읽기 전용 표시(복사용) |

> **범위의 한계 — 과장하지 마라.** 이건 "**TORQ API 방언을 쓰는 LP**"만 무배포로 붙는다는 뜻이다.
> API 계약이 다른 LP 는 어차피 클라이언트 코드가 필요하다. 문서·주석에 "어떤 LP 든 무배포"라고 쓰지 마라.

---

## §0. DDL — 이미 적용 완료 (다시 하지 마라)

```sql
-- 2026-08-20 운영 적용 완료
CREATE TABLE lp_providers (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  provider_code  VARCHAR(20)  NOT NULL,   -- partners.lp_provider / torq_trades.lp_provider 에 각인
  display_name   VARCHAR(50)  NOT NULL,
  base_url       VARCHAR(255) NULL,       -- 비면 TORQ 는 env 폴백
  api_key        VARCHAR(255) NULL,
  api_secret     VARCHAR(255) NULL,       -- 평문 — 어드민 응답에서 반드시 마스킹
  webhook_secret VARCHAR(255) NULL,       -- 비면 서명검증 생략
  -- callback_url 은 없다 (2026-08-21 제거) — provider_code 파생 값이라 저장하지 않는다
  network_id     BIGINT       NULL,       -- NULL 이면 주문 네트워크(없으면 TRON)
  rebate_enabled TINYINT(1) NOT NULL DEFAULT 0,
  is_enabled     TINYINT(1) NOT NULL DEFAULT 0,
  sort_order     INT NOT NULL DEFAULT 0,
  created_at, updated_at,
  UNIQUE KEY uk_lp_providers_code (provider_code)
);

INSERT: ('TORQ','TORQ', network NULL, rebate 1, enabled 1, sort 1)
        ('BARO','BARO', network 3(TRON — 2026-08-21 BSC 에서 변경), rebate 0, enabled 0, sort 2)
```

**TORQ 행의 자격증명은 일부러 비워 뒀다.** §2 의 env 폴백으로 계속 살아 있어야 한다.
정본 DDL 파일(`CRYPTOMENTS_V2_DDL.sql`)에 문자열로 반영하라 — **DDL 실행은 금지**.

---

## §1. ☠️ 가장 큰 위험 — TORQ 를 죽이지 마라

TORQ 는 **원화 물량의 99%** 다. 자격증명 출처를 env → DB 로 옮기는 순간
TORQ 행이 비어 있거나 잘못 읽히면 **전 원화 거래가 멈춘다.**

그래서 이번 변경의 제1 규칙:

> **TORQ 는 DB 행이 없거나 비어 있어도 env 로 동작해야 한다.**

- 로컬·스테이징 DB 에 `lp_providers` 가 없을 수도 있다 → 테이블 조회 실패도 폴백으로 흡수
- 폴백 제거는 **DB 자격증명으로 실거래가 증명된 뒤**의 별도 작업이다. 이번에 하지 마라
- BARO 는 폴백이 없다 — 행이 없으면 그냥 실패한다 (정상)

---

## §2. `LpProviderRegistry` — 새 해석기 (common)

`TorqProviderProperties` 를 **대체한다** (삭제). 같은 역할을 DB 기반으로 한다.

```
LpProviderConfig require(String code)
    행 조회 → is_enabled 확인 → base_url/api_key/api_secret 확인
    TORQ 인데 비었으면 → flat env 폴백으로 채운다 (§1)
    그래도 없으면 TorqApiException (기존과 동일한 예외 — 호출부가 파트너 레그로 폴백한다)

String webhookSecret(String code)
    예외를 던지지 않는다. 없으면 "" — 빈 값이면 서명검증을 건너뛰는 기존 동작 유지

List<LpProviderConfig> enabled()      어드민 드롭다운용
String orDefault(String code)         null → "TORQ"
```

- 엔티티 이름은 **`LpProviderConfig`** 로 하라 (`@XEntity("lp_providers")`).
  삭제되는 enum 과 이름이 겹치면 이관 중 헷갈린다
- **캐시를 넣지 마라.** 물량이 시간당 1건 미만이고, 캐시는 "어드민에서 고쳤는데 안 바뀐다"는
  새 장애 유형을 만든다. 필요해지면 그때 넣는다
- flat env 폴백 `@Value` 4개(`torq.base-url` / `api-key` / `api-secret` / `webhook-secret`)는
  `TorqProviderProperties` 에서 **그대로 옮겨 온다**

## §3. enum 삭제 — 라우팅 키는 `String`

`LpProvider` enum 을 지우고 **provider code 문자열**을 쓴다.
DB 컬럼이 이미 `VARCHAR(20)` 이라 저장 형태는 바뀌지 않는다.

```
Partner.lpProvider          LpProvider → String
TorqTrade.lpProvider        LpProvider → String
TorqTradeRepository         findBy...(String lpProvider, Long escrowId)
TorqClient  32곳            LpProvider → String providerCode
TorqService 16곳
P2pGroupResolver 6곳
```

- 기본값 `"TORQ"` 는 **상수 하나**로 두라 (`LpProviderCodes.TORQ`). 문자열 리터럴을 흩뿌리지 마라
- **대문자 정규화**: 외부에서 들어오는 code(웹훅 경로·어드민 요청)는 `trim().toUpperCase()`
- 유효성은 **DB 존재 여부**로 판정한다. 코드에 화이트리스트를 만들지 마라 — 그러면 DB 로 옮긴 의미가 없다

### ⚠️ enum 을 지우면 컴파일러가 지켜주던 것이 사라진다

`switch (provider)` 의 누락 경고가 없어진다. 그 자리를 **DB 값**이 메워야 한다 —
아래 §4·§5 가 정확히 그 작업이다. 코드에 `if ("BARO".equals(code))` 같은 분기를
**하나라도 남기면 이번 작업은 실패다.** 남겨야 한다면 멈추고 보고하라.

## §4. 네트워크 매핑 — `lpNetworkOf` 를 행에서 읽는다

```java
// 현재 (P2pMatchingService:1048)
if (provider == LpProvider.TORQ2) return BSC_NETWORK_ID;   // ← 당시 초안. 실제로는 행 값
return orderNetworkId != null ? orderNetworkId : DEFAULT_NETWORK_ID;

// 이후
Long fixed = cfg.getNetworkId();
return fixed != null ? fixed : (orderNetworkId != null ? orderNetworkId : DEFAULT_NETWORK_ID);
```

`BSC_NETWORK_ID` 상수는 더 이상 쓰이지 않으면 지운다.

> ⚠️ 기존 경고 주석(`:787` 부근)을 **살려 둬라** — LP 도입 시 그 그룹 파트너 전원에게
> 해당 체인 MASTER 지갑이 없으면 LP 레그가 **에러 없이 조용히 스킵**된다.
> DB 로 바꾸면 네트워크를 화면에서 쉽게 바꿀 수 있게 되므로 이 함정은 오히려 더 밟기 쉬워진다.

## §5. 리베이트 — `rebate_enabled` 로

`TorqRebateService` 의 TORQ 하드코딩을 행 값으로 바꾼다.
`TorqWebhookController` 가 `lp != TORQ` 일 때 `REBATE_PAID` 를 무시하는 분기도 마찬가지다.

**리베이트 대상을 넓히지 마라** — BARO 행은 `rebate_enabled = 0` 이다.

## §6. 웹훅 라우팅 — 콜백 (2026-08-21 갱신)

```
POST /webhooks/{provider}         provider = lp_providers.provider_code (대소문자 무시)
                                  /webhooks/torq 는 이 매핑의 한 경우 — 기존 등록 URL 그대로 산다
```

- `LpProvider.valueOf` → **DB 조회**로 교체. 없는 code 면 기존과 같은 4xx
- `is_enabled = 0` 인 provider 의 웹훅은 **거부하지 마라.** 비활성화 직후 도착하는
  정상 웹훅을 잃는다. 행이 존재하면 처리한다
- ⚠️ `/webhooks` 아래에 **2세그먼트 리터럴 경로를 새로 만들지 마라** — 이 매핑에 먹힌다
  (`/webhooks/axim/{partnerId}` · `/webhooks/banqpipe/{partnerId}` 는 3세그먼트라 무사)
- **콜백 URL 은 저장하지 않는다 (컬럼 제거됨).** provider 코드로 완전히 파생된다:
  `{cryptoments.webhook.base-url}/webhooks/{소문자 provider_code}`
  (예: BARO → `https://api.cryptoments.cc/webhooks/baro`)
  어드민 응답(`LpProviderResponse.callbackUrl`)이 이 값을 계산해 내려주고, 화면은
  **읽기 전용 + 복사 버튼**으로만 보여준다 (운영자가 LP 콘솔에 등록). 라우팅이 바뀌면
  파생 규칙 한 곳만 고치면 된다 — 데이터를 같이 고칠 필요가 없다

> ☠️ `webhook_secret` — BARO 는 **서명을 보내지 않는다.** 값을 넣으면 전 웹훅이 401 이 된다
> (TORQ 에서 실제로 겪은 사고). 화면에 이 경고를 문구로 띄워라.

## §7. 어드민 저장 검증 — 파트너의 LP 선택

`PartnerManagementService` 의 `assertLpProviderSelectable` 를 DB 기준으로.

```
행 없음 · is_enabled=0 · 자격증명 미해석  →  거부 (기존 2009)
메시지에 "어느 provider 가 왜" 가 드러나야 한다
```

`LpProvider.isConfigurable()` 은 enum 과 함께 사라진다.

---

# §8. 어드민 화면 — LP 공급자 관리 (`cryptoments-admin/admin-ui`)

**이게 없으면 DB 로 옮긴 의미가 없다** (운영자가 DML 을 쳐야 하므로).

```
목록   provider_code · 표시명 · 활성 · 네트워크 · 리베이트 · 자격증명 설정됨 여부
편집   base_url · api_key · api_secret · webhook_secret
       · network_id · rebate_enabled · is_enabled
표시   callback_url — 입력 불가(자동 생성). 읽기 전용 + 복사 버튼
```

## §8-1. ★ 검증 우선 저장 (verify-first)

**W9 에서 Axim 설정에 적용한 패턴을 그대로 따른다.** 그때 문제가 정확히 이거였다 —
저장부터 하고 검증해서, 실패해도 행이 남고 화면엔 오류가 안 떴다.

```
1  입력값으로 LP 를 실제 호출한다 (fx-info 등 부작용 없는 GET)
2  실패하면 → DB 에 아무것도 쓰지 않는다 + 실패 사유를 화면에 그대로
3  성공해야 저장한다
```

`is_enabled = true` 로 켜는 저장에서는 **반드시** 검증한다.
끄는 저장(`is_enabled=false`)은 검증 없이 통과시킨다 — LP 가 죽었을 때 끌 수 없으면 안 된다.

## §8-2. ★ 시크릿 취급

- **응답에 `api_secret` · `webhook_secret` 원문을 절대 싣지 마라.** `"설정됨/미설정"` 또는 마스킹
- 입력이 비어 있으면 **기존 값 유지** (빈 문자열로 덮어쓰지 마라)
- 로그·에러 메시지에 값이 새지 않게 하라

> 참고로 발견한 사실: `partner_axim_settings.api_secret_enc` 는 **이름과 달리 암호화되어 있지 않다**
> (어드민이 평문을 그대로 넣고 클라이언트가 그대로 쓴다). 이번 표는 그 관례를 따르되
> **컬럼명으로 거짓말하지 않는다**(`api_secret`). 암호화는 별도 과제로 남긴다 — **이번에 하지 마라.**

## §8-3. 파트너 상세의 LP 드롭다운

`LP_PROVIDER_OPTIONS` **하드코딩 배열을 지우고** `is_enabled` 행을 API 로 받아 그린다.
`LpProvider` TS 유니온 타입도 `string` 으로.

---

## 절대 규칙

- **DDL·DML 금지. DB 접속 금지. 운영 서버 접속 금지** — DDL 은 §0 으로 이미 끝났다
- **자격증명 값을 코드·문서·로그·시드에 넣지 마라**
- §1 의 TORQ env 폴백을 **제거하지 마라**
- provider code 분기(`if ("BARO".equals(...))`)를 코드에 남기지 마라 (§3)
- 리베이트를 BARO 로 넓히지 마라
- MyBatis `<script>` 안에 `<` `<=` `<>` 금지 — SAXParseException 으로 전 서비스 다운
- `IXRepository.modify()` 는 non-null 필드만 갱신 — NULL 로 지우려면 전용 `@Update`
- `IXRepository.save()` 는 PK(Long) 반환 — 엔티티 아님
- 미배포 작업(시간 모델·매칭 대기·T5·T4·파트너 콘솔)을 건드리지 마라
- 새 라이브러리 금지 · **git commit / push 하지 마라**

## 완료 기준

```
 1  lp_providers 엔티티·리포지토리·LpProviderRegistry 존재. TorqProviderProperties 삭제
 2  ★ TORQ 행이 비어 있어도 env 로 동작한다 — 코드 경로로 증명 (§1)
 3  ★ 테이블 조회가 실패해도 TORQ 가 동작한다 (로컬 DB 무테이블 상황)
 4  enum LpProvider 삭제. 잔존 참조 0 (grep 으로 증명)
 5  ★ provider code 하드코딩 분기 0곳 — "TORQ" 기본값 상수 1개만 허용 (grep 으로 증명)
 6  네트워크·리베이트·웹훅시크릿이 전부 행에서 나온다 (콜백은 코드 파생 — 컬럼 없음)
 7  웹훅 /webhooks/baro 가 라우팅된다. /webhooks/torq 는 그대로 TORQ
 8  is_enabled=0 인 provider 의 웹훅도 처리된다 (§6)
 9  ★ 어드민 저장이 검증 우선이다 — 실패 시 DB 무기록 (§8-1)
10  ★ 응답 어디에도 api_secret·webhook_secret 원문이 없다 (§8-2)
11  파트너 LP 드롭다운이 API 기반 (하드코딩 배열 삭제)
12  ./gradlew compileJava 통과 · admin-ui 빌드 통과
13  git commit·push 하지 않았다
```

## 보고 형식

- 수정/생성 파일:라인 + 한 줄
- **2·3·5·9·10 을 코드 경로로 증명하라** — "했다"가 아니라 어디서 어떻게
- 지침이 실제 코드와 어긋난 지점 — **고치지 말고 먼저 보고**
- 빌드 결과

## 착수 전 필수 확인

```
common/.../enums/LpProvider.java                       삭제 대상
common/.../client/TorqProviderProperties.java          대체 대상 — env 폴백 로직을 옮긴다
common/.../client/TorqClient.java                      cfg(LpProvider) 32곳
common/.../entity/Partner.java · TorqTrade.java
common/.../repository/TorqTradeRepository.java
common/.../entity/PartnerAximSettings.java             표 설계의 선례 (읽기만)
core/.../p2p/P2pMatchingService.java                   :1048 lpNetworkOf · :775-793
core/.../p2p/P2pGroupResolver.java
core/.../torq/TorqService.java · TorqRebateService.java
open-api/.../webhook/controller/TorqWebhookController.java
admin-api/.../service/PartnerManagementService.java    assertLpProviderSelectable · Axim 검증우선 선례
admin-ui/src/api/types/partner.ts · views/partners/PartnerDetailView.vue
```

지침과 다르면 **멈추고 보고하라.**

---

## 오케스트레이터가 할 일 (서브에이전트 범위 밖)

```
1  배포 후 어드민에서 BARO 자격증명 입력 (base_url · api_key · api_secret)
   ⚠️ webhook_secret 은 비워 둔다
2  BARO 콘솔에 콜백 등록: https://api.cryptoments.cc/webhooks/baro
   (어드민 LP 공급자 화면의 "콜백 URL (자동 생성)" 값을 복사 — 손으로 만들지 마라)
3  BARO 그룹 파트너 전원에게 TRON MASTER 지갑 확보 확인 (§4 함정)
   → 저장 시점에 자동 검증된다(F2). 미보유 파트너가 있으면 거부 + 명단 표시
4  대상 총판의 lp_provider = BARO
5  소액 실거래 1건 — torq_trades.lp_provider = 'BARO' 각인 확인
6  (별도) TORQ 자격증명을 DB 로 이관 → 증명된 뒤에야 env 폴백 제거
```

**⚠️ 확인 필요**: BARO 가 TORQ 와 **같은 API 계약**(`/api/widget/fx-info` · `quote` · `trades` …)인지.
다르면 `TorqClient` 재사용 전제가 깨지고 별도 클라이언트가 필요하다 — 이 지침의 범위 밖이다.
