# PARTNER_SUSPEND_ENFORCEMENT_GUIDE — 파트너 SUSPENDED 실효성 보강 지침서

> 작성: 2026-07-27 (오케스트레이터). 구현: 서브에이전트.
> 배경: SUSPENDED 차단 범위 실측 결과 갭 3건 발견 — knowledge repo `domains/partner/README.md` 참조.
> DDL 변경 없음. 코드만 수정.

## 배경 (실측 요약)

파트너 SUSPENDED 시 현재 차단되는 것은 콘솔 **로그인 시점** 검사와 open-api **HMAC 서버 API**(`OpenApiAccessTokenHandler`) 뿐. 아래 3개 경로는 그대로 뚫려 있다:

| # | 갭 | 파일 |
|---|---|---|
| A | 위젯 토큰 발급/세션이 status 미검사 | `open-api/security/ApiSignatureService.java`, `open-api/config/OpenApiAccessTokenHandler.java` |
| B | 파트너 콘솔 기존 세션 — 요청별 재검사 없음 | `partner-api/config/PartnerSecurityFilter.java` |
| C | P2P 매칭 쿼리가 `partners.status` 미필터 | `common/mapper/P2pMatchingMapper.java` |

**정책**: SUSPENDED = 모든 신규 거래·접근 차단. 단, 진행 중 출금/매칭/입금 크레딧은 계속(자금 안전 — 중간에 끊으면 자금이 갇힘). 온체인 입금 크레딧도 계속(의도된 동작).

---

## Fix A — open-api 위젯 경로 status 검사

### A-1. 위젯 토큰 발급 차단

`open-api/src/main/java/com/cryptoments/openapi/security/ApiSignatureService.java`
`validateApiSignature()`에서 partner null 체크 직후 추가:

```java
if (partner.getStatus() != PartnerStatus.ACTIVE) {
    throw new ApiSignatureException("Partner not active: " + apiKey);
}
```

- import: `com.cryptoments.common.enums.PartnerStatus`
- 호출처는 `WidgetAuthController` 1곳 — `ApiSignatureException` catch → 401 반환 (기존 흐름 그대로).

### A-2. 기발급 위젯/세션 토큰 무력화 (요청별 재검사)

위젯 토큰은 24h 유효 — A-1만으로는 기발급 토큰이 최대 24h 살아있음.
`OpenApiAccessTokenHandler.parseAccessTokenAndSession()`의 **Axim 세션 토큰 fallback 경로**(2번, `baseTokenHandler.parseAccessTokenAndSession(...)` 반환 직전)에서:

```java
R parsed = baseTokenHandler.parseAccessTokenAndSession(request, cls);
if (parsed instanceof WidgetSessionData ws) {
    Partner p = partnerRepository.findOne(ws.getPartnerId());
    if (p == null || p.getStatus() != PartnerStatus.ACTIVE) {
        throw new UnAuthorizedException(UnAuthorizedException.INVALID_ACCESS_TOKEN);
    }
}
return parsed;
```

- `PartnerRepository`는 이미 핸들러 필드에 있음. `findOne(Long)`은 IXRepository 기본 제공.
- 성능: 요청당 PK 단건 조회 — 허용. 캐시 도입 금지(불필요한 복잡도).
- `OpenApiSessionData`는 HMAC 경로에서 이미 매 요청 검사되므로 추가 불필요.

## Fix B — partner-api 요청별 status 재검사

`partner-api/src/main/java/com/cryptoments/partnerapi/config/PartnerSecurityFilter.java`
세션 파싱 성공 블록(`session != null && !session.isExpire()`) 안에서 SecurityContext 설정 **전에**:

```java
Partner partner = partnerRepository.findOne(session.getPartnerId());
if (partner == null || partner.getStatus() != PartnerStatus.ACTIVE) {
    log.warn("Suspended/invalid partner request blocked: partnerId={}", session.getPartnerId());
    // 인증 미설정 → SecurityConfig가 401/403 처리
} else {
    // 기존 authorities + SecurityContext 설정 코드
}
```

- `PartnerRepository` 생성자 주입 추가 (`common` 의존성은 partner-api에 이미 있음).
- 효과: SUSPENDED 즉시 기존 로그인 세션도 전 API 401 — 재로그인 시도는 기존 `PartnerAuthService`가 1002로 차단.
- 로그인/비번재설정 등 permitAll 경로는 필터에서 인증만 안 실릴 뿐 접근 자체는 SecurityConfig 규칙 그대로 — 동작 변화 없어야 함(확인 필수).

## Fix C — P2P 매칭 쿼리 status 필터

`common/src/main/java/com/cryptoments/common/mapper/P2pMatchingMapper.java`

1. **출금측 3개 쿼리** — `findMatchableWithdrawOrdersForUpdate`, `findAvailableWithdrawOrders`, `findExactMatchesForUpdate`의 `JOIN partners wp` 조건에 추가:

```sql
AND wp.status = 'ACTIVE'
```

(기존 `wp.krw_enabled = 1 AND wp.p2p_withdraw_enabled = 1` 뒤에 나란히)

2. **입금측 재매칭 쿼리** — `findWaitingDepositOrdersForUpdate`에 구매자 파트너 게이트 추가:

```sql
SELECT o.* FROM p2p_deposit_orders o
JOIN partners dp ON dp.id = o.partner_id AND dp.status = 'ACTIVE'
WHERE o.status IN (...)  -- 기존 조건에 alias o. 부여
...
FOR UPDATE OF o
```

- ⚠️ FOR UPDATE → **`FOR UPDATE OF o`** 로 변경 필수 (partners 행 잠금 금지 — v2.5 결정과 동일 이유: 어드민 토글 경합).
- ⚠️ `<script>` 내부 XML 파싱 규칙 준수 — `<` 직접 사용 금지 (`&gt;` 기존 패턴 유지). CLAUDE.md 참조.
- 효과: 정지 파트너의 대기 출금주문·입금주문이 매칭 풀에서 제외. 이미 CREATED/BANK_PENDING인 매칭은 계속 진행(정책).

---

## 완료 기준

1. `./gradlew :common:compileJava :open-api:compileJava :partner-api:compileJava` 성공 (로컬 Mac, Desktop Commander).
2. 동작 체크리스트:
   - SUSPENDED 파트너: 위젯 토큰 발급 401 / 기발급 위젯 토큰으로 위젯 API 401 / 콘솔 기존 세션 API 401 / 매칭 풀에서 출금·입금 주문 제외
   - ACTIVE 파트너: 전 경로 기존과 동일 (회귀 없음)
   - 진행 중 매칭(CREATED/BANK_PENDING)·출금 상태머신은 영향 없음
3. `<script>` 수정 후 기동 검증 — SAXParseException 함정 (2026-06-11 장애 전례).
4. 커밋 메시지: `partner: SUSPENDED 실효성 보강 — 위젯/세션/P2P매칭 status 게이트 (Fix A/B/C)`

## 리뷰 포인트 (오케스트레이터용)

- Fix B에서 SecurityContext 미설정 시 permitAll 경로 회귀 여부
- Fix C의 FOR UPDATE OF 절 정확성 + XML escape
- ACTIVE 문자열 리터럴 == enum name 일치 (VARCHAR enum)
