# CODEF 에러 메시지 명확화 구현 지침서

> **작성 배경 (2026-06-29)**: P2P 회원 페이지에서 계좌 등록 시 회원이 보는 에러가
> 모두 **"CODEF 조회 중 오류가 발생했습니다."** 한 덩어리로 뭉개져 오해가 큼.
>
> 운영 로그 실측(open-api, 2026-06-29 02:09~02:18):
> - 회원 `MEMBER/23`, 은행 `081(하나은행)`, 계좌 `12291032877407`
> - CODEF 응답: **`CF-13011`** — `"빠른 조회 서비스 미등록 계좌입니다. 빠른 조회 서비스 등록 후 거래하시기 바랍니다."`
> - 같은 계좌로 4회 재시도, 모두 동일 → 회원은 원인을 몰라 계속 재시도
>
> **근본 원인**: 자격증명 오류가 아니라 "계좌가 은행 빠른조회 서비스에 미가입"이라는
> 사용자 측 선행조건 문제. 그런데 `CF-13011`이 `normalizeResultCode()`에서 별도 분류되지
> 않아 `default → VENDOR_ERROR`로 떨어지고, CODEF가 준 친절한 한글 메시지가 버려진 채
> 자체 generic 메시지로 덮어써짐.

## 설계 방침 — 하이브리드

1. **주요 코드는 전용 매핑**: `CF-13011`(빠른조회 미등록) 등 의미가 분명하고 회원 행동이
   필요한 코드는 우리가 다듬은 명확한 안내 문구로 매핑.
2. **그 외는 벤더 원본 메시지 노출**: 매핑되지 않은 `VENDOR_ERROR`는 CODEF `result.message`를
   그대로 회원에게 전달 → 더 이상 "조회 중 오류"로 뭉개지지 않음.
3. **회원 화면에 "CODEF" 명칭 미노출** ⚠️: 회원이 보는 모든 메시지에서 벤더명 `CODEF`를 제거.
   - 상수명(`CODEF_VENDOR_ERROR` 등)은 내부 식별자라 그대로 두고, **message 문자열만** 수정.
   - 벤더 원본 메시지(`res.getMessage()`)는 계좌/빠른조회 안내 문구라 "CODEF" 단어가 들어있지
     않음(예: "빠른 조회 서비스 미등록 계좌입니다…") → passthrough해도 안전. 만약을 대비해
     방어적으로 한 번 더 치환(아래 2-4 참고).

---

## 변경 파일 (4개)

| 파일 | 변경 |
|------|------|
| `common/exception/ErrorCodes.java` | 신규 에러코드 1개 추가 + 기존 message 문구에서 "CODEF" 제거 |
| `core/p2p/P2pScrapingService.java` | VerifyResult에 vendorMessage 추가, normalizeResultCode 매핑 추가, switch 분기 수정 |

(`CodefFastInquiryClient`/`CodefFastInquiryResponse`는 이미 `message`를 보유 → 수정 불필요)

---

## 1. ErrorCodes.java — 신규 에러코드 추가 + "CODEF" 제거

`// ── 9-5. CODEF 스크래핑 ──` 블록의 message 문자열에서 회원 노출용 "CODEF"를 제거하고,
신규 코드(1005) 1줄을 추가한다. **상수명은 그대로 유지**(내부 식별자).

**기존:**
```java
    // ── 9-5. CODEF 스크래핑 ──
    public static final ErrorCode CODEF_CREDENTIAL_INVALID = new ErrorCode("1001", "CODEF 자격증명이 유효하지 않습니다.");
    public static final ErrorCode CODEF_BANK_MAINTENANCE = new ErrorCode("1002", "은행 점검 시간입니다. 잠시 후 다시 시도해주세요.");
    public static final ErrorCode CODEF_TRANSPORT_ERROR = new ErrorCode("1003", "CODEF 통신 오류가 발생했습니다.");
    public static final ErrorCode CODEF_VENDOR_ERROR = new ErrorCode("1004", "CODEF 조회 중 오류가 발생했습니다.");
```

**변경 (message에서 "CODEF" 제거 + 1005 추가):**
```java
    // ── 9-5. 계좌 빠른조회 스크래핑 ──  (상수명은 내부용이라 CODEF_ 유지, 메시지에서만 제거)
    public static final ErrorCode CODEF_CREDENTIAL_INVALID = new ErrorCode("1001", "계좌 인증 정보가 올바르지 않습니다.");
    public static final ErrorCode CODEF_BANK_MAINTENANCE = new ErrorCode("1002", "은행 점검 시간입니다. 잠시 후 다시 시도해주세요.");
    public static final ErrorCode CODEF_TRANSPORT_ERROR = new ErrorCode("1003", "계좌 조회 서버 통신 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.");
    public static final ErrorCode CODEF_VENDOR_ERROR = new ErrorCode("1004", "계좌 조회 중 오류가 발생했습니다.");
    // ▼ 신규
    public static final ErrorCode CODEF_FAST_INQUIRY_NOT_ENROLLED = new ErrorCode("1005",
            "빠른조회(오픈뱅킹) 서비스에 가입되지 않은 계좌입니다. 해당 은행 앱/인터넷뱅킹에서 빠른조회 서비스 가입 후 다시 등록해주세요.");
```

---

## 2. P2pScrapingService.java

### 2-1. VerifyResult record에 vendorMessage 필드 추가

CODEF 원본 메시지를 호출자(switch 분기)까지 전달하기 위함.

**기존 (132~143행):**
```java
    public record VerifyResult(
            /** call_logs.call_id */
            String callId,
            /** 정규화 결과코드 (OK/INVALID_CREDENTIAL/BANK_MAINTENANCE/TRANSPORT_ERROR/VENDOR_ERROR) */
            String resultCode,
            /** CODEF 원본 코드 (CF-XXXXX) */
            String vendorRawCode,
            /** 거래내역 (성공 시). 실패 시 빈 리스트. */
            List<CodefTransaction> transactions
    ) {
        public boolean isSuccess() { return "OK".equals(resultCode); }
    }
```

**변경:**
```java
    public record VerifyResult(
            /** call_logs.call_id */
            String callId,
            /** 정규화 결과코드 (OK/INVALID_CREDENTIAL/BANK_MAINTENANCE/TRANSPORT_ERROR/FAST_INQUIRY_NOT_ENROLLED/VENDOR_ERROR) */
            String resultCode,
            /** CODEF 원본 코드 (CF-XXXXX) */
            String vendorRawCode,
            /** CODEF 원본 메시지 (회원 안내 fallback용) */
            String vendorMessage,
            /** 거래내역 (성공 시). 실패 시 빈 리스트. */
            List<CodefTransaction> transactions
    ) {
        public boolean isSuccess() { return "OK".equals(resultCode); }
    }
```

> ⚠️ record에 필드를 추가하면 `new VerifyResult(...)` 호출부의 인자 개수가 바뀐다.
> 아래 2-3에서 생성부를 함께 수정한다. 다른 호출부가 있으면 컴파일 에러로 드러나니 모두 맞춰줄 것.

### 2-2. normalizeResultCode() — CF-13011 매핑 추가

**기존 (386~393행):**
```java
    private String normalizeResultCode(String codefCode) {
        if ("CF-00000".equals(codefCode)) return "OK";
        if (codefCode != null && codefCode.startsWith("CF-03")) return "INVALID_CREDENTIAL";
        if ("CF-12100".equals(codefCode)) return "BANK_MAINTENANCE";
        if ("CF-TRANSPORT".equals(codefCode)) return "TRANSPORT_ERROR";
        if ("CF-DECODE".equals(codefCode) || "CF-PARSE".equals(codefCode)) return "TRANSPORT_ERROR";
        return "VENDOR_ERROR";
    }
```

**변경 (CF-13011 한 줄 추가):**
```java
    private String normalizeResultCode(String codefCode) {
        if ("CF-00000".equals(codefCode)) return "OK";
        if (codefCode != null && codefCode.startsWith("CF-03")) return "INVALID_CREDENTIAL";
        if ("CF-12100".equals(codefCode)) return "BANK_MAINTENANCE";
        if ("CF-13011".equals(codefCode)) return "FAST_INQUIRY_NOT_ENROLLED";  // 빠른조회 미등록 계좌
        if ("CF-TRANSPORT".equals(codefCode)) return "TRANSPORT_ERROR";
        if ("CF-DECODE".equals(codefCode) || "CF-PARSE".equals(codefCode)) return "TRANSPORT_ERROR";
        return "VENDOR_ERROR";
    }
```

### 2-3. verify() — VerifyResult 생성 시 vendorMessage 전달

**기존 (286~327행 일부):**
```java
        String resultCode = normalizeResultCode(res.getCode());
        String vendorRawCode = res.getCode();
        ...
        return new VerifyResult(callId, resultCode, vendorRawCode, transactions);
```

**변경:**
```java
        String resultCode = normalizeResultCode(res.getCode());
        String vendorRawCode = res.getCode();
        String vendorMessage = res.getMessage();   // ◀ 추가
        ...
        return new VerifyResult(callId, resultCode, vendorRawCode, vendorMessage, transactions);
```

> 참고: 기존에 `res.getMessage()`는 call_logs의 `lastErrorCode` 조합(314~318행)에만 쓰였다.
> 그 로직은 그대로 두고, VerifyResult에도 함께 실어 보내면 된다.

### 2-4. registerOrReauthAccount() switch — 전용 분기 + 원본 메시지 passthrough

**기존 (114~122행):**
```java
        switch (verifyResult.resultCode()) {
            case "OK" -> { /* 검증 성공 — 연결 진행 */ }
            case "INVALID_CREDENTIAL" -> throw new ConflictException(ErrorCodes.CODEF_CREDENTIAL_INVALID,
                    "인증 정보가 올바르지 않습니다. 확인 후 다시 시도해주세요.");
            case "BANK_MAINTENANCE" -> throw new ConflictException(ErrorCodes.CODEF_BANK_MAINTENANCE);
            case "TRANSPORT_ERROR" -> throw new ConflictException(ErrorCodes.CODEF_TRANSPORT_ERROR);
            default -> throw new ConflictException(ErrorCodes.CODEF_VENDOR_ERROR);
        }
        return linkToAccount(credentialId, account.getId());
```

**변경:**
```java
        switch (verifyResult.resultCode()) {
            case "OK" -> { /* 검증 성공 — 연결 진행 */ }
            case "INVALID_CREDENTIAL" -> throw new ConflictException(ErrorCodes.CODEF_CREDENTIAL_INVALID,
                    "인증 정보가 올바르지 않습니다. 확인 후 다시 시도해주세요.");
            case "FAST_INQUIRY_NOT_ENROLLED" -> throw new ConflictException(
                    ErrorCodes.CODEF_FAST_INQUIRY_NOT_ENROLLED);
            case "BANK_MAINTENANCE" -> throw new ConflictException(ErrorCodes.CODEF_BANK_MAINTENANCE);
            case "TRANSPORT_ERROR" -> throw new ConflictException(ErrorCodes.CODEF_TRANSPORT_ERROR);
            // 그 외: 벤더 원본 메시지를 그대로 노출 (없으면 generic). "조회 중 오류" 뭉개짐 방지.
            //        만약을 대비해 "CODEF" 단어가 들어있으면 회원에게 안 보이도록 방어적 치환.
            default -> throw new ConflictException(ErrorCodes.CODEF_VENDOR_ERROR,
                    sanitizeVendorMessage(verifyResult.vendorMessage()));
        }
        return linkToAccount(credentialId, account.getId());
```

그리고 동일 클래스에 헬퍼 메서드 추가 (내부 헬퍼 섹션 등 적절한 위치):

```java
    /**
     * 벤더 원본 메시지를 회원 노출용으로 정제 — 비어있으면 generic 문구,
     * 벤더명("CODEF")이 포함되면 제거하여 회원에게 노출되지 않게 한다.
     */
    private String sanitizeVendorMessage(String vendorMessage) {
        if (vendorMessage == null || vendorMessage.isBlank()) {
            return ErrorCodes.CODEF_VENDOR_ERROR.messageKey();
        }
        String cleaned = vendorMessage.replaceAll("(?i)codef", "").trim();
        return cleaned.isBlank() ? ErrorCodes.CODEF_VENDOR_ERROR.messageKey() : cleaned;
    }
```

> ⚠️ Axim `ErrorCode`는 `record ErrorCode(String code, String messageKey)` 이므로 메시지 접근자는
> **`.messageKey()`** 다 (`.message()` 아님 — 컴파일 에러). 실측 검증 완료.

> `ConflictException(ErrorCode, String)` 2-인자 생성자는 이미 사용 중(INVALID_CREDENTIAL 분기).
> `XExceptionHandler`가 ApiError로 직렬화하며 이 detail 메시지를 프론트에 전달 → 회원 화면에
> 벤더 원본 문구가 (CODEF 명칭 없이) 그대로 노출된다.

---

## 적용 후 회원이 보게 될 메시지

| CODEF code | 변경 전 (현재) | 변경 후 (회원 화면 — "CODEF" 미노출) |
|---|---|---|
| `CF-13011` | CODEF 조회 중 오류가 발생했습니다. | **빠른조회(오픈뱅킹) 서비스에 가입되지 않은 계좌입니다. 해당 은행 앱/인터넷뱅킹에서 빠른조회 서비스 가입 후 다시 등록해주세요.** |
| `CF-03xxx` | 인증 정보가 올바르지 않습니다… | 계좌 인증 정보가 올바르지 않습니다. 확인 후 다시 시도해주세요. |
| `CF-12100` | 은행 점검 시간입니다… | (변경 없음) 은행 점검 시간입니다… |
| `CF-TRANSPORT` 등 | CODEF 통신 오류가 발생했습니다. | 계좌 조회 서버 통신 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요. |
| 기타 `CF-xxxxx` | CODEF 조회 중 오류가 발생했습니다. | **벤더 원본 메시지** (예: "계좌번호를 다시 확인해주세요."). 비어있으면 "계좌 조회 중 오류가 발생했습니다." |

---

## 검증 체크리스트

1. `./gradlew :common:compileJava :core:compileJava` — VerifyResult 인자 개수 변경에 따른 호출부 컴파일 통과 확인
   - `new VerifyResult(...)` 호출부 전수: `verify()` 외에 다른 곳 없는지 확인 (있으면 vendorMessage 인자 추가)
   - `VerifyResult`를 소비하는 `P2pScrapingVerifyJob` 등이 `transactions()` 접근 시 인자 순서 변경 영향 없는지 확인 (record accessor라 영향 없음)
2. 운영 반영 후: 하나은행(081) 빠른조회 미등록 계좌로 등록 시도 → 화면에 "빠른조회 서비스 가입 후…" 안내가 뜨는지 확인
3. `call_logs`에는 기존처럼 `result_code=VENDOR_ERROR`가 아니라 `FAST_INQUIRY_NOT_ENROLLED`로 기록됨 (정규화 코드 변경 반영). 대시보드/집계에서 이 신규 코드값을 인지하는지 확인
4. **"CODEF" 미노출 확인**: 회원에게 떨어지는 모든 에러 응답(ApiError.message)에 "CODEF" 단어가
   없는지 확인. 프론트(widget-ui)에서 별도로 "CODEF" 하드코딩한 라벨/문구가 없는지도 grep 점검 권장
   (서버 메시지를 그대로 표시하는 구조면 서버 수정만으로 충분)

## 배포

`cryptoments-backend` 레포 → `git push origin main` → GitLab CI/CD 자동 빌드+배포 (open-api 포함).
DDL 변경 없음.

## 후속 검토 (선택)

- CODEF 코드표에서 회원 행동이 필요한 다른 코드(예: 계좌번호 오류, 명의 불일치)도 전용 매핑 후보.
  현재는 hybrid passthrough로 원본 메시지가 노출되므로 우선순위 낮음.
- `CF-13011` 발생 시 자격증명을 굳이 저장할 필요 없음 — 현재 `registerOrReauthAccount`가
  하나의 `@Transactional`이라 예외 시 전체 롤백되어 orphan 자격증명은 남지 않음 (추가 조치 불필요).
