# Axim REST Doc Generator — JSON Sample 자동 생성 지침서

> **버전**: v1.0
> **작성일**: 2026-04-02
> **기준**: open-api spec-bundle.json (50 APIs, 40 models)
> **관련**: `AXIM_REST_DOC_GENERATOR_IMPROVEMENTS.md` 항목 #8 확장

---

## 1. 목표

spec-bundle.json 생성 시 각 API에 `requestSample`과 `responseSample` 필드를 자동 생성하여,
가이드 UI(api.html)에서 **정적 `API_SAMPLES` 객체 없이** JSON 예시를 렌더링할 수 있게 한다.

### Before (현재)

```
api.html 내부에 수동으로 API_SAMPLES 객체 유지 (200+ lines)
spec-bundle.json 변경 시 수동 동기화 필요
```

### After (목표)

```
Generator가 spec-bundle.json에 requestSample/responseSample 포함
api.html은 spec-bundle.json만 읽어서 렌더링
수동 동기화 불필요
```

---

## 2. spec-bundle.json 현재 구조

### 2.1 API 엔트리

```json
{
  "id": "getAllTokens",
  "name": "전체 토큰 조회",
  "description": "모든 지원 토큰 정보 조회.",
  "group": "그룹없음",
  "parameters": [ ... ],
  "returnClass": "com.cryptoments.openapi.dto.response.TokenResponse",
  "urlMapping": "/widgets/api/tokens",
  "method": "GET",
  "isArrayReturn": true,
  "isPaging": false,
  "isNeedsSession": true
}
```

### 2.2 Model 엔트리

models는 classPath를 키로 사용하며, 각 모델은 `fields[]` 배열을 가진다.

```json
{
  "com.cryptoments.openapi.dto.request.WithdrawalRequest": {
    "name": "com.cryptoments.openapi.dto.request.WithdrawalRequest",
    "type": "Object",
    "description": "출금 요청. (v1 호환)",
    "fields": [
      {
        "name": "partnerUserId",
        "type": "String",
        "classPath": "java.lang.String",
        "description": "파트너 서비스의 사용자 ID",
        "optional": true
      },
      {
        "name": "amount",
        "type": "String",
        "classPath": "java.lang.String",
        "description": "출금 수량",
        "optional": true
      }
    ]
  }
}
```

### 2.3 Parameter 구조

```json
{
  "name": "partnerUserId",
  "type": "String",
  "classPath": "java.lang.String",
  "description": "파트너 서비스의 사용자 ID",
  "isOptional": true,
  "defaultValue": null,
  "parameterKind": "REQUEST_PARAMETER",   // REQUEST_PARAMETER | REQUEST_BODY | PATH_VARIABLE
  "isEnum": false
}
```

---

## 3. 추가할 필드

각 API 엔트리에 2개 필드를 추가한다.

| 필드 | 타입 | 설명 |
|------|------|------|
| `requestSample` | `String` (nullable) | Request Body JSON 예시. GET 또는 body 없는 API는 `null` |
| `responseSample` | `String` (nullable) | Response JSON 예시 |

### 출력 예시

```json
{
  "id": "requestWithdrawal",
  "name": "출금 요청",
  "method": "POST",
  "urlMapping": "/api/v1/withdrawal",
  "returnClass": "com.cryptoments.openapi.dto.response.WithdrawalResponse",
  "isArrayReturn": false,
  "isPaging": false,
  "requestSample": "{\n  \"partnerUserId\": \"user-001\",\n  \"chainType\": \"BSC\",\n  \"currencyType\": \"USDT\",\n  \"toAddress\": \"0x1234567890abcdef1234567890abcdef12345678\",\n  \"amount\": \"100.00\",\n  \"memo\": \"출금 메모\"\n}",
  "responseSample": "{\n  \"withdrawalCode\": \"WD-20260402-001\",\n  \"status\": \"REQUESTED\",\n  \"chainType\": \"BSC\",\n  \"currencyType\": \"USDT\",\n  \"amount\": \"100.00\",\n  \"fee\": \"1.00\",\n  \"toAddress\": \"0x1234567890abcdef1234567890abcdef12345678\",\n  \"createdAt\": \"2026-04-02 14:30:00\"\n}"
}
```

---

## 4. @XSample 어노테이션

### 4.1 어노테이션 정의

DTO 필드에 직접 샘플 값을 지정하는 어노테이션을 추가한다.

```java
package one.axim.framework.rest.annotation;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface XSample {
    /** 샘플 값 (JSON 직렬화 후의 문자열 표현) */
    String value();
}
```

### 4.2 사용 예시

```java
public class WithdrawalRequest {
    /** 파트너 서비스의 사용자 ID */
    @XSample("user-001")
    private String partnerUserId;

    /** 체인 타입 (BSC, ETHEREUM, TRON, POLYGON) */
    @XSample("BSC")
    private String chainType;

    /** 통화 타입 (USDT, USDC) */
    @XSample("USDT")
    private String currencyType;

    /** 수신 주소 */
    @XSample("0x1234567890abcdef1234567890abcdef12345678")
    private String toAddress;

    /** 출금 수량 */
    @XSample("100.00")
    private BigDecimal amount;

    /** 메모/비고 */
    @XSample("출금 메모")
    private String memo;
}
```

```java
public class WithdrawalResponse {
    /** 출금 코드 */
    @XSample("WD-20260402-001")
    private String withdrawalCode;

    /** 출금 상태 */
    @XSample("REQUESTED")
    private WithdrawalStatus status;

    /** 수수료 */
    @XSample("1.00")
    private BigDecimal fee;

    /** 생성 시각 */
    @XSample("2026-04-02 14:30:00")
    private LocalDateTime createdAt;
}
```

### 4.3 값 해석 규칙

`@XSample`의 value는 **JSON 직렬화 후의 문자열 표현**이다. Generator는 필드 타입에 따라 적절히 변환한다.

| 필드 타입 | @XSample value | JSON 출력 | 비고 |
|-----------|---------------|-----------|------|
| String | `"user-001"` | `"user-001"` | 따옴표 감싸기 |
| Long / Integer | `"12345"` | `12345` | 숫자 변환 |
| BigDecimal | `"100.00"` | `"100.00"` | 문자열 유지 (Jackson plain) |
| Boolean | `"true"` | `true` | boolean 변환 |
| Enum | `"REQUESTED"` | `"REQUESTED"` | 따옴표 감싸기 |
| LocalDateTime | `"2026-04-02 14:30:00"` | `"2026-04-02 14:30:00"` | 따옴표 감싸기 |

```java
// Generator 의사 코드
private Object parseXSampleValue(String sampleValue, String classPath) {
    return switch (classPath) {
        case "java.lang.Long", "long" -> Long.parseLong(sampleValue);
        case "java.lang.Integer", "int" -> Integer.parseInt(sampleValue);
        case "java.lang.Boolean", "boolean" -> Boolean.parseBoolean(sampleValue);
        default -> sampleValue;  // String, BigDecimal, Enum, DateTime → 문자열 그대로
    };
}
```

### 4.4 값 결정 우선순위 체인

`@XSample`이 모든 필드에 있을 필요는 없다. 아래 우선순위에 따라 폴백한다.

```
우선순위 1: @XSample("value") → 해당 값 사용 (가장 정확)
우선순위 2: description 내 괄호 힌트 → 첫 번째 값 추출 (기존 규칙)
우선순위 3: 필드명 패턴 매칭 → *Address, *Code 등 (기존 규칙)
우선순위 4: classPath 기반 타입 기본값 → Long→1 등 (기존 규칙)
```

**점진적 적용 전략**: 처음에는 `@XSample` 없이 우선순위 2~4로 자동 생성하고, API 문서 품질이 중요한 DTO부터 `@XSample`을 추가해 나간다.

### 4.5 Generator 리플렉션 구현

```java
// SampleGenerator 내부
private Object resolveDefaultValue(Field modelField, java.lang.reflect.Field javaField, int depth) {
    // 우선순위 1: @XSample
    if (javaField != null) {
        XSample sample = javaField.getAnnotation(XSample.class);
        if (sample != null) {
            return parseXSampleValue(sample.value(), modelField.getClassPath());
        }
    }

    // 우선순위 2: description 괄호 힌트
    String descValue = extractFromDescription(modelField.getDescription());
    if (descValue != null) return descValue;

    // 우선순위 3: 필드명 패턴
    String patternValue = matchFieldNamePattern(modelField.getName(), modelField.getClassPath());
    if (patternValue != null) return patternValue;

    // 우선순위 4: 타입 기본값
    return getTypeDefault(modelField.getClassPath(), depth);
}
```

`javaField`는 models의 classPath로 Class.forName()하여 해당 DTO 클래스를 로드한 뒤, `getDeclaredField(fieldName)`으로 획득한다. 클래스 로드 실패 시 우선순위 2~4로 폴백한다.

---

## 5. Sample 생성 알고리즘

### 5.1 requestSample 생성 조건

> 아래 5.2~5.5절의 필드별 기본값 로직은 **4.4절 우선순위 체인에서 `@XSample`이 없는 필드**에 대한 폴백 규칙이다.

```
IF api.method ∈ {POST, PUT, PATCH}
  AND parameters 중 parameterKind == "REQUEST_BODY"인 파라미터가 있음
  AND 해당 파라미터의 classPath로 models에서 모델을 찾을 수 있음
THEN
  해당 모델의 fields를 순회하여 JSON 객체 생성
ELSE
  requestSample = null
```

**주의**: `parameterKind == "REQUEST_PARAMETER"` (쿼리 파라미터)는 requestSample에 포함하지 않는다. 쿼리 파라미터는 URL에 표시되므로 별도 샘플이 불필요하다.

### 5.2 responseSample 생성

```
IF api.returnClass != null
  AND models[api.returnClass] 존재
THEN
  모델 fields를 순회하여 JSON 객체 생성

  IF api.isArrayReturn == true AND api.isPaging == false
    → 배열로 감싸기: [ {생성된 객체} ]

  IF api.isPaging == true
    → XPage 래퍼로 감싸기 (4.4절 참조)

ELSE IF api.returnClass가 원시 타입 (String, Long 등)
  → 타입에 맞는 단일 값 생성

ELSE
  responseSample = null
```

### 5.3 필드별 기본값 생성 (타입 → 값 매핑, 폴백용)

models의 각 field에서 `classPath`를 기준으로 기본값을 결정한다.

| classPath | 생성 값 | 비고 |
|-----------|---------|------|
| `java.lang.String` | description 기반 또는 필드명 기반 | 아래 세부 규칙 참조 |
| `java.lang.Long` / `long` | `1` | |
| `java.lang.Integer` / `int` | `1` | |
| `java.lang.Boolean` / `boolean` | `true` | |
| `java.math.BigDecimal` | `"100.00"` | 문자열로 출력 (Jackson write-bigdecimal-as-plain) |
| `java.time.LocalDateTime` | `"2026-01-15 14:30:00"` | Jackson format: `yyyy-MM-dd HH:mm:ss`, timezone: Asia/Seoul |
| `java.time.LocalDate` | `"2026-01-15"` | |
| Enum (isEnum: true) | 첫 번째 enum 값 | enum 값 목록이 있으면 첫 번째 값 사용 |
| `java.util.List` / `java.util.Set` | `[]` | 빈 배열 |
| `java.util.Map` | `{}` | 빈 객체 |
| 기타 Object (models에 존재) | 재귀적으로 생성 | 중첩 depth 제한 필요 (최대 2단계) |
| 기타 Object (models에 미존재) | `{}` | |

#### String 필드 세부 규칙

String 필드는 필드명 패턴을 분석하여 의미있는 예시를 생성한다.

| 필드명 패턴 | 생성 값 |
|------------|---------|
| `*Code` (partnerCode, withdrawalCode 등) | `"WD-20260402-001"` 또는 `"PARTNER-001"` |
| `*Id` (partnerUserId 등) | `"user-001"` |
| `*Address` (toAddress 등) | `"0x1234567890abcdef1234567890abcdef12345678"` |
| `*Hash` (txHash 등) | `"0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"` |
| `*Type` (chainType, currencyType 등) | description에서 괄호 내 첫 번째 값 추출 |
| `*Name` | `"샘플 이름"` |
| `*Url` / `*Link` | `"https://example.com/callback"` |
| `memo` / `description` | `"메모"` |
| 기타 | `"sample"` |

#### description 기반 값 추출

description에 괄호로 enum 힌트가 있는 경우 첫 번째 값을 사용한다.

```
description: "체인 타입 (BSC, ETHEREUM, TRON, POLYGON)"
→ 추출: "BSC"

description: "통화 타입 (USDT, USDC)"
→ 추출: "USDT"
```

**정규식**: `\(([^)]+)\)` → 첫 번째 캡처 그룹에서 `,` 분할 → trim → 첫 번째 값

### 5.4 XPage 래퍼 구조

`api.isPaging == true`인 경우 responseSample을 XPage 구조로 감싼다.

```json
{
  "pageRows": [ {생성된 객체} ],
  "page": 1,
  "size": 20,
  "offset": 0,
  "totalCount": 1,
  "hasNext": false,
  "sort": null,
  "orders": []
}
```

### 5.5 배열 반환 처리

`api.isArrayReturn == true && api.isPaging == false`인 경우:

```json
[
  {생성된 객체}
]
```

하나의 요소만 포함하면 충분하다.

---

## 6. 세션 필드 제외 규칙

현재 spec-bundle.json의 parameters에는 세션 데이터 필드가 포함되어 있다. 이들은 Request Body나 Response에 노출되지 않으므로 sample 생성 시 **반드시 제외**해야 한다.

### 제외 대상 필드명

```
DEFAULT_TOKEN_EXPIRE_DAYS, FORMAT, partnerCode, partnerName,
partnerId, sessionId, createDate
```

### 판별 방법

세션 데이터 필드는 다음 특징으로 식별 가능하다.

1. `description`이 `null`인 경우 (대부분의 세션 필드)
2. 아래 클래스의 필드인 경우:
   - `OpenApiSessionData`의 필드: `partnerId`, `partnerCode`, `partnerName`, `sessionId`, `createDate`
   - `XBaseAccessTokenHandler` 상수: `DEFAULT_TOKEN_EXPIRE_DAYS`, `FORMAT`
3. `classPath`가 `java.time.format.DateTimeFormatter`인 경우

### 권장 구현

Generator 레벨에서 세션 데이터 클래스(`OpenApiSessionData`, `WidgetSessionData`)의 필드를 수집하고, API parameters 생성 시 해당 필드들을 제외한다.

```java
// 의사 코드
Set<String> sessionFieldNames = extractFieldNames(OpenApiSessionData.class);
sessionFieldNames.addAll(extractFieldNames(WidgetSessionData.class));
sessionFieldNames.add("DEFAULT_TOKEN_EXPIRE_DAYS");
sessionFieldNames.add("FORMAT");

// API 파라미터 필터링
List<Parameter> filtered = api.parameters.stream()
    .filter(p -> !sessionFieldNames.contains(p.getName()))
    .collect(toList());
```

이 필터링은 sample 생성뿐만 아니라 parameters 배열 자체에도 적용하는 것이 이상적이다. 세션 필드가 API 파라미터로 노출되는 것은 가이드 사용자에게 혼란을 준다.

---

## 7. JSON 포맷 규칙

### 7.1 직렬화 설정 (Jackson 기준)

spec-bundle.json에 저장되는 sample 문자열은 실제 API 응답과 동일한 포맷이어야 한다.

| 설정 | 값 | 비고 |
|------|---|------|
| 날짜 포맷 | `yyyy-MM-dd HH:mm:ss` | Jackson `date-format` |
| 타임존 | `Asia/Seoul` (UTC+9) | Jackson `time-zone` |
| null 필드 | **제외** | `@JsonInclude(NON_NULL)` |
| BigDecimal | plain 문자열 | `write-bigdecimal-as-plain: true` |
| 들여쓰기 | 2 spaces | 가독성 |

### 7.2 null 필드 제외

`@JsonInclude(NON_NULL)` 설정에 따라 null 값 필드는 JSON에 포함하지 않는다. 따라서 sample 생성 시 모든 필드에 기본값을 할당해야 한다 (null 대신).

단, `optional: true`이면서 값을 생성하기 어려운 필드(예: 복잡한 중첩 객체)는 아예 생략 가능하다.

### 7.3 문자열 이스케이프

sample은 JSON 문자열로 저장되므로 내부의 따옴표와 줄바꿈을 이스케이프해야 한다.

```json
"requestSample": "{\n  \"amount\": \"100.00\"\n}"
```

---

## 8. 구현 위치 및 흐름

### 8.1 Generator 내부 흐름

```
1. 기존 로직: Controller 스캔 → API 목록 + Model 목록 생성
                                          ↓
2. [NEW] SampleGenerator 호출
   - 각 API에 대해:
     a. requestSample 생성 (POST/PUT/PATCH + REQUEST_BODY인 경우)
     b. responseSample 생성 (returnClass + models 참조)
   - 생성된 sample을 API 엔트리에 추가
                                          ↓
3. spec-bundle.json 직렬화 출력
```

### 8.2 새로 추가할 클래스

```java
public class SampleGenerator {

    private final Map<String, Model> models;
    private final Set<String> sessionFieldNames;
    private final ObjectMapper objectMapper;  // pretty-print용

    /**
     * API 엔트리에 requestSample, responseSample을 추가한다.
     */
    public void generateSamples(ApiEntry api) {
        api.setRequestSample(buildRequestSample(api));
        api.setResponseSample(buildResponseSample(api));
    }

    /**
     * 모델의 fields를 순회하여 JSON 객체(Map)를 생성한다.
     * depth: 중첩 깊이 (최대 2)
     */
    private Map<String, Object> buildObjectFromModel(Model model, int depth) {
        Map<String, Object> obj = new LinkedHashMap<>();
        for (Field field : model.getFields()) {
            if (sessionFieldNames.contains(field.getName())) continue;
            obj.put(field.getName(), resolveDefaultValue(field, depth));
        }
        return obj;
    }

    /**
     * 4.4절 우선순위 체인에 따라 기본값을 결정한다.
     * 1. @XSample → 2. description 괄호 힌트 → 3. 필드명 패턴 → 4. 타입 기본값
     */
    private Object resolveDefaultValue(Field modelField, java.lang.reflect.Field javaField, int depth) {
        // 4.4~4.5절 참조
    }
}
```

### 8.3 ApiEntry 클래스 변경

```java
public class ApiEntry {
    // ... 기존 필드들 ...

    // 추가
    private String requestSample;   // nullable
    private String responseSample;  // nullable
}
```

---

## 9. api.html 연동 (이미 구현 완료)

api.html에는 이미 sample을 렌더링하는 Vue 템플릿과 매칭 로직이 있다.

### 9.1 현재 매칭 로직 (api.html 1668~1687행)

```javascript
// Match JSON samples
const sampleKey = api.method + ' ' + api.urlMapping;
const sample = API_SAMPLES[sampleKey] || {};

const endpoint = {
  // ...
  requestSample: sample.request || null,
  responseSample: sample.response || null
};
```

### 9.2 마이그레이션: spec-bundle.json 직접 참조로 전환

Generator 업데이트 완료 후 api.html을 수정한다.

**변경 전:**
```javascript
const sampleKey = api.method + ' ' + api.urlMapping;
const sample = API_SAMPLES[sampleKey] || {};
// ...
requestSample: sample.request || null,
responseSample: sample.response || null
```

**변경 후:**
```javascript
// API_SAMPLES 매칭 로직 제거
// ...
requestSample: api.requestSample || null,
responseSample: api.responseSample || null
```

### 9.3 API_SAMPLES 제거

마이그레이션 후 api.html에서 `const API_SAMPLES = { ... }` 블록 (약 200행)을 완전히 삭제한다.

### 9.4 렌더링 템플릿 (변경 불필요)

```html
<template v-if="endpoint.requestSample || endpoint.responseSample">
  <div v-if="endpoint.requestSample" style="margin-top: 16px;">
    <h5>Request Example</h5>
    <pre class="code-block" v-html="endpoint.requestSample"></pre>
  </div>
  <div v-if="endpoint.responseSample" style="margin-top: 16px;">
    <h5>Response Example</h5>
    <pre class="code-block" v-html="endpoint.responseSample"></pre>
  </div>
</template>
```

이 템플릿은 `v-html`로 렌더링하므로, sample 문자열은 **HTML-safe plain text**여야 한다. `<`, `>`, `&` 문자는 이스케이프하거나, Vue의 텍스트 바인딩(`v-text` 또는 `{{ }}`)으로 전환을 고려한다.

> **권장**: `v-html` → `v-text` 또는 `<pre><code>{{ endpoint.requestSample }}</code></pre>` 형태로 변경하면 XSS 방지와 함께 JSON이 그대로 렌더링된다.

---

## 10. 엣지 케이스 처리

### 10.1 순환 참조

모델 A의 필드가 모델 B를 참조하고, 모델 B가 다시 A를 참조하는 경우.

**해결**: depth 제한 (최대 2)과 방문 추적 Set 사용.

```java
private Map<String, Object> buildObjectFromModel(Model model, int depth, Set<String> visited) {
    if (depth > 2 || visited.contains(model.getName())) {
        return Collections.emptyMap();  // {} 반환
    }
    visited.add(model.getName());
    // ... 필드 순회 ...
}
```

### 10.2 제네릭 타입 (List<T>, XPage<T>)

`returnClass`가 `List`나 `XPage`로 래핑된 경우, Generator는 이미 `isArrayReturn`과 `isPaging` 플래그로 이를 표시하고 있다. `returnClass`에는 내부 타입(T)의 classPath가 저장되어 있다.

### 10.3 void 반환

`returnClass`가 null이거나 `void`인 경우 `responseSample = null`.

### 10.4 원시 타입 반환

`returnClass`가 `java.lang.String`, `java.lang.Long` 등인 경우 models에 없을 수 있다. 이 경우 4.3절의 타입→값 매핑을 직접 적용하여 단일 값을 생성한다.

```json
// String 반환
"responseSample": "\"success\""

// Long 반환
"responseSample": "1"
```

### 10.5 models에 없는 returnClass

외부 라이브러리 클래스 등이 returnClass인 경우 `responseSample = null`.

---

## 11. 전체 예시: WithdrawalRequest → WithdrawalResponse

### 입력 (spec-bundle.json의 API 엔트리)

```json
{
  "id": "requestWithdrawal",
  "method": "POST",
  "urlMapping": "/api/v1/withdrawal",
  "returnClass": "com.cryptoments.openapi.dto.response.WithdrawalResponse",
  "isArrayReturn": false,
  "isPaging": false,
  "parameters": [
    { "name": "partnerId", "parameterKind": "REQUEST_PARAMETER", "classPath": "java.lang.Long" },
    { "name": "body", "parameterKind": "REQUEST_BODY",
      "classPath": "com.cryptoments.openapi.dto.request.WithdrawalRequest" }
  ]
}
```

### models 참조

```json
{
  "com.cryptoments.openapi.dto.request.WithdrawalRequest": {
    "fields": [
      { "name": "partnerUserId", "classPath": "java.lang.String", "description": "파트너 서비스의 사용자 ID" },
      { "name": "chainType", "classPath": "java.lang.String", "description": "체인 타입 (BSC, ETHEREUM, TRON, POLYGON)" },
      { "name": "currencyType", "classPath": "java.lang.String", "description": "통화 타입 (USDT, USDC)" },
      { "name": "toAddress", "classPath": "java.lang.String", "description": "수신 주소" },
      { "name": "amount", "classPath": "java.lang.String", "description": "출금 수량" },
      { "name": "memo", "classPath": "java.lang.String", "description": "메모/비고" }
    ]
  },
  "com.cryptoments.openapi.dto.response.WithdrawalResponse": {
    "fields": [
      { "name": "withdrawalCode", "classPath": "java.lang.String", "description": "출금 코드" },
      { "name": "status", "classPath": "java.lang.String", "description": "출금 상태 (REQUESTED, PROCESSING, CONFIRMED)" },
      { "name": "chainType", "classPath": "java.lang.String", "description": "체인 타입" },
      { "name": "currencyType", "classPath": "java.lang.String", "description": "통화 타입" },
      { "name": "amount", "classPath": "java.math.BigDecimal", "description": "출금 수량" },
      { "name": "fee", "classPath": "java.math.BigDecimal", "description": "수수료" },
      { "name": "toAddress", "classPath": "java.lang.String", "description": "수신 주소" },
      { "name": "createdAt", "classPath": "java.time.LocalDateTime", "description": "생성 시각" }
    ]
  }
}
```

### 생성 과정 (우선순위 체인 적용)

1. **requestSample**: method=POST, `body` 파라미터가 REQUEST_BODY → WithdrawalRequest 모델 필드 순회
   - `partnerUserId`: **@XSample("user-001")** → `"user-001"` (우선순위 1)
   - `chainType`: **@XSample("BSC")** → `"BSC"` (우선순위 1)
   - `currencyType`: **@XSample("USDT")** → `"USDT"` (우선순위 1)
   - `toAddress`: **@XSample("0x1234...5678")** → 해당 값 (우선순위 1)
   - `amount`: **@XSample("100.00")** → `"100.00"` (우선순위 1)
   - `memo`: @XSample 없음 → `memo` 패턴 매칭 → `"메모"` (우선순위 3)

2. **responseSample**: returnClass 존재 + isArrayReturn=false + isPaging=false → 단일 객체
   - `withdrawalCode`: **@XSample("WD-20260402-001")** → 해당 값 (우선순위 1)
   - `status`: **@XSample("REQUESTED")** → 해당 값 (우선순위 1)
   - `chainType`: @XSample 없음 → description `(BSC, ...)` → `"BSC"` (우선순위 2)
   - `currencyType`: @XSample 없음 → description `(USDT, USDC)` → `"USDT"` (우선순위 2)
   - `amount`: BigDecimal, @XSample 없음 → 타입 기본값 → `"100.00"` (우선순위 4)
   - `fee`: **@XSample("1.00")** → `"1.00"` (우선순위 1)
   - `toAddress`: @XSample 없음 → `*Address` 패턴 → `"0x1234...5678"` (우선순위 3)
   - `createdAt`: LocalDateTime, @XSample 없음 → 타입 기본값 → `"2026-01-15 14:30:00"` (우선순위 4)

### 출력

```json
{
  "requestSample": "{\n  \"partnerUserId\": \"user-001\",\n  \"chainType\": \"BSC\",\n  \"currencyType\": \"USDT\",\n  \"toAddress\": \"0x1234567890abcdef1234567890abcdef12345678\",\n  \"amount\": \"100.00\",\n  \"memo\": \"메모\"\n}",
  "responseSample": "{\n  \"withdrawalCode\": \"WD-20260402-001\",\n  \"status\": \"REQUESTED\",\n  \"chainType\": \"BSC\",\n  \"currencyType\": \"USDT\",\n  \"amount\": \"100.00\",\n  \"fee\": \"1.00\",\n  \"toAddress\": \"0x1234567890abcdef1234567890abcdef12345678\",\n  \"createdAt\": \"2026-01-15 14:30:00\"\n}"
}
```

---

## 12. 구현 체크리스트

| # | 작업 | 난이도 | 비고 |
|---|------|--------|------|
| 1 | `@XSample` 어노테이션 정의 (rest-framework 모듈) | 하 | `@Target(FIELD)`, `@Retention(RUNTIME)` |
| 2 | `ApiEntry`에 `requestSample`, `responseSample` 필드 추가 | 하 | getter/setter + Jackson 직렬화 |
| 3 | `SampleGenerator` 클래스 신규 생성 | 중 | 핵심 로직, 우선순위 체인 |
| 4 | `@XSample` 리플렉션 읽기 구현 | 하 | `Class.forName()` + `getDeclaredField()` |
| 5 | 타입→값 매핑 테이블 구현 (폴백용) | 하 | switch/map 구조 |
| 6 | String 필드명 패턴 매칭 구현 (폴백용) | 하 | 정규식 기반 |
| 7 | description 내 괄호 값 추출 구현 (폴백용) | 하 | 정규식 `\(([^)]+)\)` |
| 8 | 세션 필드 제외 로직 구현 | 하 | Set 기반 필터링 |
| 9 | XPage 래퍼 생성 로직 | 하 | 고정 구조 |
| 10 | 중첩 객체 + 순환 참조 처리 | 중 | depth 제한 + visited Set |
| 11 | JSON 문자열 직렬화 (pretty-print → escaped string) | 하 | ObjectMapper |
| 12 | 주요 DTO에 `@XSample` 적용 (open-api 모듈) | 중 | 점진적 적용 |
| 13 | api.html 마이그레이션: API_SAMPLES 제거 + spec-bundle 직접 참조 | 하 | Generator 완료 후 |

### 구현 순서 권장

```
Phase 1: 어노테이션 + 기본 동작 (#1 → #2 → #3 → #5 → #11)
  → @XSample 정의 + 타입 기본값으로 sample 생성 가능

Phase 2: @XSample 리플렉션 (#4)
  → @XSample이 있는 필드는 정확한 값 사용

Phase 3: 폴백 품질 향상 (#6 → #7 → #8 → #9)
  → @XSample 없는 필드의 자동 추론 품질 향상

Phase 4: 엣지 케이스 (#10)
  → 중첩 객체, 순환 참조 안전 처리

Phase 5: DTO 적용 + UI 연동 (#12 → #13)
  → 주요 DTO에 @XSample 추가, api.html에서 API_SAMPLES 제거
```

---

## 13. 기존 개선 가이드 연계

이 지침서는 `AXIM_REST_DOC_GENERATOR_IMPROVEMENTS.md`의 **항목 #8 (example 값 지원)**을 확장한 것이다.

| 기존 #8 | 이 지침서 |
|---------|----------|
| 필드별 `example` 값 (Swagger용) | API별 전체 JSON sample (가이드 UI용) |
| `@XApiDoc(example = "...")` 어노테이션 | 타입+패턴 기반 자동 생성 |
| openapi.json 대상 | spec-bundle.json 대상 |

두 개선은 독립적으로 구현 가능하나, 타입→값 매핑 로직은 공유할 수 있다. `SampleGenerator`의 `resolveDefaultValue()` 메서드를 openapi.json 생성에서도 재사용하면 일관된 예시값이 보장된다.

기존 가이드의 #3 (enum 값 목록)이 함께 구현되면 enum 필드의 sample 품질이 크게 향상된다. `isEnum: true`인 필드에서 실제 enum 값 목록을 참조할 수 있기 때문이다.

---

## 부록: 현재 api.html API_SAMPLES 키 목록 (참고용)

Generator 구현 후 아래 키에 해당하는 API들의 sample이 자동 생성되는지 검증한다.

```
GET /widgets/api/tokens
GET /widgets/api/chains
POST /widgets/api/auth/token
GET /widgets/api/deposit/address
POST /widgets/api/deposit/session
GET /widgets/api/deposit/sessions
GET /widgets/api/balances
GET /widgets/api/transactions
GET /widgets/api/withdrawal/policies
POST /widgets/api/withdrawal/request
GET /widgets/api/withdrawal/history
GET /api/v1/tokens
GET /api/v1/chains
GET /api/v1/currencies/price
POST /api/v1/deposit/address
POST /api/v1/deposit/session
GET /api/v1/deposit/sessions
GET /api/v1/balances
GET /api/v1/balances/all
GET /api/v1/transactions
GET /api/v1/withdrawal/policies
POST /api/v1/withdrawal/request
GET /api/v1/withdrawal/status/{withdrawalCode}
GET /api/v1/withdrawal/history
POST /api/v1/axim/connect
DELETE /api/v1/axim/disconnect
GET /api/v1/axim/status
POST /api/v1/axim/deposit
```
