# **NFT 메타데이터 스키마 유효성 검증 시스템 구축 계획**

## **1\. 개요 및 목표**

현재 프로젝트에서 사용하는 NFT는 티켓, 쿠폰, 인증서 등 기능적 용도가 명확한 유틸리티 토큰으로 확장되고 있습니다. 성공적인 DApp 연동을 위해서는 해당 NFT의 메타데이터가 정의된 스키마 구조를 엄격하게 준수해야 합니다.

본 계획은 4가지 타입의 메타데이터 구조를 정의하고, 타입 2, 3, 4에 대해서는 **JSON Schema 기반의 유효성 검증 시스템**을 구축하여 데이터 무결성을 보장하는 것을 목표로 합니다.

## **2\. 메타데이터 타입 정의 및 요구사항**

모든 메타데이터는 **OpenSea 표준 필드**와 \*\*DApp 커스텀 필드 (utility)\*\*를 포함하는 것을 기본 원칙으로 합니다.

| No. | 타입 이름 | 내부 utility.type 값 | 스키마 준수 강제 여부 | 컨트랙트 타입 (참고) |
| :---- | :---- | :---- | :---- | :---- |
| **1** | **자유 서식 (Flexible)** | (제한 없음) | **기본 표준 (OpenSea)만 준수** | 타입 A (거래 가능) |
| **2** | **티켓 (Ticket)** | Ticket | **필수 스키마 검증 강제** | 타입 A (거래 가능) |
| **3** | **쿠폰 (Coupon)** | Coupon | **필수 스키마 검증 강제** | 타입 A (거래 가능) |
| **4** | **인증서 (Certification)** | Certificate | **필수 스키마 검증 강제** | 타입 B (거래 불가능) |

## **3\. 개발 구현 계획**

메타데이터는 Off-chain (IPFS 등)에 저장되지만, **메타데이터 생성/등록 시점과 민팅 시점 모두**에서 유효성 검증을 수행합니다.

### **3.1. 기술 스택 및 표준**

| 항목 | 상세 내용 | 비고 |
| :---- | :---- | :---- |
| **기반 표준** | ERC-721 Metadata JSON Schema, ERC-1155 Metadata URI JSON Schema | OpenSea 호환성 보장 |
| **커스텀 데이터** | utility 필드 사용 | DApp 기능 구현을 위한 커스텀 구조 |
| **유효성 검증** | **JSON Schema Draft 7 이상** | 모든 타입의 구조 검증을 위한 핵심 기술 |
| **권장 구현 위치** | NFT 민팅 API Endpoint (Backend Server) | 블록체인에 등록되기 전, 중앙 서버에서 검증 |

### **3.2. JSON Schema 정의 및 관리**

타입 2, 3, 4의 필수 스키마는 JSON Schema 파일(ticket\_schema.json, coupon\_schema.json, cert\_schema.json)로 정의하고 **repo에 정적 파일로 버전별로 보관**합니다. 스키마는 임의 수정 금지, 변경 시 구버전/신버전 병치 관리(메타데이터가 참조한 스키마 버전으로 검증).

#### **1\) 공통 필수 구조 (Common Base Schema)**

모든 타입이 준수해야 하는 기본 OpenSea 및 Utility 구조를 정의합니다.

{  
  "type": "object",  
  "properties": {  
    "name": { "type": "string" },  
    "image": { "type": "string", "format": "uri" },  
    "attributes": { "type": "array" }, /\* Type 1(자유 서식)은 이 배열이 비어있거나 생략 가능 \*/  
    "utility": {  
      "type": "object",  
      "properties": {  
        "type": { "type": "string", "enum": \["Ticket", "Coupon", "Certificate"\] },  
        "validation\_url": { "type": "string", "format": "uri" },  
        "data": { "type": "object" }  
      },  
      "required": \["type", "data"\]  
    }  
  },  
  "required": \["name", "image", "utility"\]  
}

### **3.3. 타입별 필수 Attributes 정의 (데이터 타입 명시)**

타입 2, 3, 4는 필수 attributes 항목을 포함해야 하며, value는 해당 display\_type에 맞는 형식을 따라야 합니다. **날짜/시간은 Unix Timestamp(Number)만 허용**합니다.

#### **1\) 타입 2: 티켓 (Ticket)**

| trait\_type (특성명) | 필수 value 형식 | display\_type | 비고 |
| :---- | :---- | :---- | :---- |
| Event Name | String | (Text) |  |
| Event Date | Number | date | Unix Timestamp |
| Venue | String | (Text) |  |
| Zone | String | (Text) |  |
| Seat | Number | number |  |
| Status | String | (Text) | DApp에서 수정 가능 |
| Transferable | String | (Text) |  |
| **필수 Utility Data** |  |  |  |
| utility.data | 객체 | N/A | ticket\_id, event.name, event.date, seat.zone, status 필수 |

#### **2\) 타입 3: 쿠폰 (Coupon)**

| trait\_type (특성명) | 필수 value 형식 | display\_type | 비고 |
| :---- | :---- | :---- | :---- |
| Brand | String | (Text) |  |
| Product | String | (Text) |  |
| Price | Number | number |  |
| Expires At | Number | date | Unix Timestamp |
| Status | String | (Text) | DApp에서 수정 가능 |
| **필수 Utility Data** |  |  |  |
| utility.data | 객체 | N/A | token\_id, brand, expired\_at, status 필수 |

#### **3\) 타입 4: 인증서 (Certification) \- 불변성 강조**

| trait\_type (특성명) | 필수 value 형식 | display\_type | 비고 |
| :---- | :---- | :---- | :---- |
| Type | String | (Text) | (예: "인증서") |
| Serial Number | String | (Text) |  |
| Style Name | String | (Text) |  |
| Issuer (Mint by) | String | (Text) |  |
| Creator | String | (Text) |  |
| Issue Date | String 또는 Number | date 또는 (Text) | 날짜 형식 통일 필요 |
| Transferability | String | (Text) | (값: "Non-Transferable (SBT)") |
| **필수 Utility Data** |  |  |  |
| utility.data | 객체 | N/A | valid\_until (보증 만료일) 필수 |

### **3.4. JSON Schema를 통한 유효성 검증**

강제 스키마(타입 2, 3, 4)는 **attributes 배열 자체를 필수로 정의**하며, **contains 로직**을 사용하여 **3.3에 명시된 모든 필수 trait\_type이 배열에 정확히 1회 이상 포함되는지** 검증합니다. 필수 항목 외 추가 attributes는 허용합니다.

- `utility.type`은 값이 존재하면 서버가 허용하는 타입(Ticket, Coupon, Certificate 등) 중 하나여야 하며, 오타/미지정은 오류로 처리합니다(Type 1 포함).

### **3.5. 검증 로직 구현**

메타데이터 생성/등록 또는 민팅 API가 호출될 때 다음 순서로 유효성 검증을 수행합니다.

1. **메타데이터 수신:** 사용자 또는 시스템으로부터 민팅할 메타데이터 JSON을 수신합니다.  
2. **타입 식별:** JSON 내 utility.type 필드의 값을 추출하여 어떤 스키마를 적용할지 결정합니다.  
3. **스키마 선택:**  
   * utility.type이 Ticket, Coupon, Certificate인 경우, 해당 **강제 스키마**를 로드합니다. (이때 attributes 배열의 필수 항목 및 utility.data 구조도 함께 검증됨)  
   * utility.type이 없거나 허용 타입 외인 경우 → 오류 반환(자유 서식도 utility.type을 명시해야 함).  
   * 자유 서식(Type 1)은 OpenSea 기본 스키마만 적용, 필수 필드는 name/image/utility(utility.type/utility.data) 등 기본 필수 항목.
4. **유효성 검증:** JSON Validator 라이브러리(예: Python의 jsonschema, Node.js의 ajv)를 사용하여 메타데이터 JSON과 선택된 스키마를 비교 검증합니다.  
5. **결과 처리:**  
   * **성공 시:** DApp 서버 DB에 메타데이터를 저장하고 NFT 컨트랙트 민팅을 진행합니다.  
   * **실패 시:** 구체적인 오류 메시지(예: "utility.data.ticket\_id 필드가 누락되었습니다" 또는 **"attributes 배열에 'Status' trait\_type이 누락되었습니다"**)를 반환하며 트랜잭션을 중단(Revert)합니다.

### **3.6. 메타데이터 해시 연동 (인증서 Type 4)**

| 항목 | 상세 규정 |
| :--- | :--- |
| 해시 계산 대상 | 인증서(Type 4) 메타데이터 JSON 전체 |
| 해시 알고리즘 | SHA-256 |
| Canonicalization | 1) UTF-8 인코딩된 JSON 문자열<br>2) 객체 키 알파벳 순 정렬<br>3) 공백 없는 compact 형태로 직렬화 |
| 적용 시점 | 민팅 주문 직전, 유효성 검증 직후 표준화된 JSON으로 해시 계산 → `mintCertificate` metadataHash 인자로 전달 |

### **3.7. 검증 도구 및 옵션 (권장)**
- 라이브러리: Node.js는 Ajv v8 이상 권장.  
- 옵션: `allErrors: true`(모든 오류 수집), `removeAdditional: false`(추가 필드 자동 제거 금지), `strict: true`(미정의 키워드/포맷 금지).

### **3.8. 성능 최적화 및 캐싱**
- 메타데이터 서빙 캐싱: `GET /metadata/:contractAddress/:tokenId`를 contractAddress+tokenId 키로 Redis 캐싱. 민팅 성공 후 링크 완료 시 최초 캐싱, Type 2/3 가변 필드 변경 시 캐시 무효화 및 재캐싱.
- JSON 사이즈 제한: 입력 메타데이터 JSON 최대 512KB. 초과 시 요청 거부.

### **3.9. 검증 실패 응답 포맷 (예시)**
```json
{
  "message": "Metadata validation failed",
  "errors": [
    {
      "path": "utility.data.ticket_id",
      "message": "is required",
      "schemaVersion": "ticket_schema@1.0.0",
      "type": "Ticket"
    },
    {
      "path": "attributes[2].value",
      "message": "must be number (unix timestamp)",
      "schemaVersion": "ticket_schema@1.0.0",
      "type": "Ticket"
    }
  ]
}
```
- `errors[*].path`: JSONPath 유사 경로  
- `schemaVersion`: 적용된 스키마 버전  
- `type`: utility.type

### **3.10. 로깅/이벤트**
- 검증 실패/성공 시 스키마 버전, utility.type을 함께 로그에 남김.  
- 필요 시 웹훅/모니터링에 검증 결과(성공/실패)와 스키마 버전 포함.

## **4\. 컨트랙트 및 API 연동 계획**

### **4.1. Non-Transferable 메타데이터 연동 (타입 4 \- 인증서)**

* **불변성 원칙:** 타입 4 (인증서)는 메타데이터가 **최초 생성 후 절대 수정되지 않습니다.**  
* **메타데이터 해시:** 타입 B 컨트랙트(인증서 전용)에 맞춰, 민팅 API는 메타데이터 JSON 전체를 SHA-256 해시화하여 mintCertificate 함수의 인자(metadataHash)로 전달합니다.  
* **API 강제:** 민팅 API는 타입 4 NFT를 민팅할 경우, 반드시 Certificate 스키마 검증을 통과하도록 강제해야 합니다.

### **4.2. 영향 범위 및 테스트 계획**

| 항목 | 내용 |
| :---- | :---- |
| **영향 받는 모듈** | NFT 민팅 API Endpoint, 메타데이터 생성 도구, DApp 클라이언트 |
| **주요 테스트 항목** | 1\. **Happy Path:** Ticket 메타데이터가 Ticket 스키마를 따를 때 민팅 성공 여부 확인. 2\. **Mandatory Attribute Path:** Coupon 메타데이터에서 필수 trait\_type인 'Brand'가 누락될 경우 검증 실패 및 API 오류 발생 여부 확인. 3\. **Certification Immutability:** Certificate 메타데이터는 utility.data에 valid\_until만 포함하고 status 필드를 포함하지 않아도 검증에 성공하는지 확인. 4\. **Type 1 Path:** 자유 서식(Type 1\) 메타데이터가 attributes 배열에 아무것도 포함하지 않거나 배열 자체가 없어도 민팅이 성공하는지 확인. |

