LogoSkills

openapi-mapper

OpenAPI Response를 Domain Entity Mapper로 자동 생성합니다.

/cc-dev:openapi:mapper — API 응답을 우리 앱 데이터로 바꿔주는 "변환기" 만들기#

항목내용
실행 명령/cc-dev:openapi:mapper
분류가이드
난이도●●○ 보통

한마디로#

서버(API)가 보내주는 데이터는 우리 앱이 바로 쓰기엔 모양이 다릅니다. 이 명령은 그 둘 사이를 이어주는 번역기(Mapper) 코드를 자동으로 만들어 줍니다. 외국에서 온 서류를 우리 양식에 맞게 옮겨 적어주는 번역가를 두는 것과 같아요.

누가·언제 쓰나요#

  • 새 API 화면(엔드포인트)을 붙이면서, 서버 응답을 앱 데이터 형태로 옮기는 코드가 필요할 때
  • 데이터 계층을 만드는 /cc-flutter:feature:data 작업을 시작하기 직전 준비 단계로 미리 변환기를 만들어 둘 때

무엇을 해주나요#

{feature_name}_mapper.dart 라는 변환기 파일 하나를 만들어 줍니다. 위치는 보통:

feature/{location}/{feature_name}/lib/src/data/mappers/{feature}_mapper.dart

이 파일이 알아서 처리해 주는 것들:

  • 빈 값 안전 처리 — 서버 값이 비어 있어도 앱이 멈추지 않도록 기본값을 채워 줍니다 (예: 비어 있으면 빈 문자열, 빈 목록).
  • 목록·중첩 데이터 변환 — 학생 목록, 선생님 정보처럼 안에 또 들어 있는 데이터까지 변환합니다.
  • 에러 정리 — 서버 오류(권한 없음, 못 찾음, 네트워크 문제 등)를 앱이 이해하는 형태로 깔끔하게 바꿔 줍니다.

어떻게 쓰나요#

# 기본형: 기능 이름, 서버 응답 타입, 앱 데이터 타입 순서로 적습니다
/cc-dev:openapi:mapper {feature_name} {response_type} {entity_type}

# 예시 1 — 학급(classroom) 변환기 만들기
/cc-dev:openapi:mapper classroom ClassResponse ClassroomClassInfo

# 예시 2 — 학생(student) 변환기 만들기
/cc-dev:openapi:mapper student StudentResponse StudentInfo
  • feature_name: 기능 모듈 이름 (예: classroom, student)
  • response_type: 서버가 주는 응답 타입 이름 (예: ClassResponse)
  • entity_type: 앱에서 쓰는 데이터 타입 이름 (예: ClassroomClassInfo)

세 가지 모두 꼭 적어야 합니다.

안에서 무슨 일이 벌어지나요#

  1. 서버 응답 살펴보기 — 서버가 어떤 항목들을 어떤 형태로 주는지(빈 값이 올 수 있는지 등) 먼저 확인합니다.
  2. 앱 데이터 살펴보기 — 우리 앱이 기대하는 항목과 형태, 꼭 필요한 값이 무엇인지 확인합니다.
  3. 변환기 코드 만들기 — 둘을 맞춰서, 빈 값까지 안전하게 처리하는 변환기 코드를 생성합니다.
  4. 다음 단계로 연결 — 이렇게 만든 변환기는 곧바로 /cc-flutter:feature:data 작업에서 그대로 쓰입니다.

⚙️ 상세 옵션·실행 명세 (개발자 / AI 에이전트용)

Triggers#

  • When a Mapper to convert OpenAPI Response to Domain Entity is needed
  • When mapping logic for a new API endpoint is needed
  • When creating a Mapper before /cc-flutter:feature:data

Context Trigger Pattern#

/cc-dev:openapi:mapper {feature_name} {response_type} {entity_type}

Parameters#

ParameterRequiredDescriptionExample
feature_nameFeature module name (snake_case)classroom, student
response_typeOpenAPI Response type nameClassResponse, StudentResponse
entity_typeDomain Entity type nameClassroomClassInfo, StudentInfo

Behavioral Flow#

1. OpenAPI Response Analysis#

# Check Response type in OpenAPI package
find package/openapi -name  " *.dart "   | xargs grep -l  " {response_type} "

Analysis targets:

  • Response field list
  • Field types (nullable or not)
  • Nested object types

2. Domain Entity Analysis#

# Check Domain Entity
find feature -path  " */domain/entity/* "   -name  " *.dart "   | xargs grep -l  " {entity_type} "

Analysis targets:

  • Entity field list
  • Field types and defaults
  • Required/optional fields

3. Mapper Class Generation#

import 'package:core/core.dart';
import 'package:dependencies/dependencies.dart';
import 'package:openapi/api.dart';

/// Mapper that converts {Feature} API Response to Domain Entity
abstract final class {Feature}Mapper {
  /// Convert {Response}Response to {Entity}
  static {Entity} from{Response}Response({Response}Response response) {
    return {Entity}(
      // Required fields: null-safe conversion
      id: response.id?.toString() ?? '',
      name: response.name ?? '',

      // Date fields
      createdAt: response.createdAt ?? DateTime.now(),

      // Nested object fields
      teacher: response.teacher != null
          ? fromTeacherResponse(response.teacher!)
          : null,

      // List fields
      students: response.students
              ?.map(fromStudentResponse)
              .toList() ??
          [],

      // Enum fields
      status: _mapStatus(response.status),
    );
  }

  /// Convert {Response}Response list
  static List<{Entity}> from{Response}ResponseList(
    List<{Response}Response>? responses,
  ) {
    return responses?.map(from{Response}Response).toList() ?? [];
  }

  /// Convert nested object (TeacherResponse → TeacherInfo)
  static TeacherInfo fromTeacherResponse(TeacherResponse response) {
    return TeacherInfo(
      id: response.id?.toString() ?? '',
      name: response.name ?? '',
      // ...
    );
  }

  /// Enum conversion helper
  static {Entity}Status _mapStatus({Response}Status? status) {
    return switch (status) {
      {Response}Status.active => {Entity}Status.active,
      {Response}Status.inactive => {Entity}Status.inactive,
      _ => {Entity}Status.unknown,
    };
  }

  /// Convert API error to Failure
  static Failure mapException(Object error, StackTrace stackTrace) {
    Log.e('API Error: $error', stackTrace: stackTrace);
    if (error is DioException) {
      final statusCode = error.response?.statusCode;
      final message = error.message ?? 'Network error occurred';

      return switch (statusCode) {
        400 => ValidationFailure(message, error: error, stackTrace: stackTrace),
        401 => AuthFailure(message, error: error, stackTrace: stackTrace),
        403 => PermissionFailure(message, error: error, stackTrace: stackTrace),
        404 => NotFoundFailure(message, error: error, stackTrace: stackTrace),
        _ => NetworkFailure(message, error: error, stackTrace: stackTrace),
      };
    }
    return UnexpectedFailure(
      error.toString(),
      error: error is Exception ? error : null,
      stackTrace: stackTrace,
    );
  }
}

Output File#

feature/{location}/{feature_name}/lib/src/data/mappers/{feature}_mapper.dart

Mapping Rules#

Conversion Patterns by Field Type#

Response TypeEntity TypeConversion Pattern
int?Stringresponse.id?.toString() ?? ''
String?Stringresponse.name ?? ''
DateTime?DateTimeresponse.createdAt ?? DateTime.now()
List<T>?List<T>responses?.map(fromT).toList() ?? []
Nested?Entity?response.nested != null ? fromNested(response.nested!) : null
Enum?Enum_mapEnum(response.status)

Null Safety Principles#

  1. Required fields: Provide default values

    id: response.id?.toString() ?? '',
    name: response.name ?? '',
    
  2. Optional fields: Keep nullable

    description: response.description,
    avatarUrl: response.avatarUrl,
    
  3. List fields: Default to empty list

    items: response.items?.map(fromItem).toList() ?? [],
    
  4. Date fields: Use current time or nullable

    createdAt: response.createdAt ?? DateTime.now(),
    updatedAt: response.updatedAt,  // When nullable allowed
    

Examples#

Classroom Mapper Generation#

/cc-dev:openapi:mapper classroom ClassResponse ClassroomClassInfo

Generated result:

abstract final class ClassroomMapper {
  static ClassroomClassInfo fromClassResponse(ClassResponse response) {
    return ClassroomClassInfo(
      classId: response.classId?.toString() ?? '',
      name: response.name ?? '',
      description: response.description ?? '',
      teacherId: response.teacherId?.toString() ?? '',
      joinCode: response.joinCode ?? '',
      memberCount: response.memberCount ?? 0,
      createdAt: response.createdAt ?? DateTime.now(),
    );
  }

  static Failure mapException(Object error, StackTrace stackTrace) { ... }
}

Student Mapper Generation#

/cc-dev:openapi:mapper student StudentResponse StudentInfo

Integration with /cc-flutter:feature:data#

/cc-dev:openapi:mapper is used as a preprocessing step for /cc-flutter:feature:data:

1. /cc-dev:openapi:mapper classroom ClassResponse ClassroomClassInfo
   → data/mappers/classroom_mapper.dart generated

2. /cc-flutter:feature:data classroom ClassroomClassInfoRepository, Mixin use ClassroomMapper

Checklist#

  • OpenAPI Response type analysis
  • Domain Entity type analysis
  • Field mapping table creation
  • Mapper class generation (abstract final class)
  • from{Response}Response() method implementation
  • from{Response}ResponseList() method implementation
  • Nested object conversion methods implementation
  • Enum conversion helper implementation
  • mapException() method implementation
  • Null-safe field mapping verification
  • Add export to data.dart

Reference Documents#

  • .claude/references/patterns/repository-patterns.md - Repository patterns
  • feature/application/classroom/lib/src/data/mappers/classroom_mapper.dart - Reference implementation