feature-domain
기능의 Clean Architecture 도메인 레이어를 만듭니다 — Entity, Repository 인터페이스, CRUD UseCase 와 각 UseCase 테스트, 실패·예외 파일과 묶음 export 를 함께 생성합니다.
| 항목 | 내용 |
|---|---|
| 실행 명령 | /cc-flutter:feature:domain |
| 분류 | 워크플로우 |
| 난이도 | ●●○ 보통 |
| MCP 서버 | serena, context7 |
먼저 feature-verification을 읽는다. unit, widget, golden, Widgetbook, BDD 5종은 전체 feature의 필수 작성 계약이다. 이 단계는 소유 역할의 test/src/domain/usecase에 의미 있는 unit을 작성하고 AC·후속 UI/BDD 검증 매핑을 전달한다. domain 생성만으로 전체 기능 완료를 보고하지 않는다.
한마디로#
앱 기능의 가장 안쪽 뼈대(핵심 규칙·데이터 모양·동작 정의) 를 자동으로 만들어 줍니다. 건물로 치면 인테리어나 외관 전에 먼저 세우는 철골 구조와 설계 도면에 해당하는 부분이에요.
누가·언제 쓰나요#
- 새로운 기능을 만들기 시작하면서 그 기능의 데이터 구조와 핵심 동작 규칙을 정의해야 할 때
- 게시글(Post), 메시지(Message)처럼 다룰 대상(Entity) 과 "목록 조회·생성·수정·삭제" 같은 동작(UseCase) 이 필요할 때
- 전체 기능을 한 번에 만드는
/cc-flutter:feature:create작업의 3번째 단계로 자동 호출됩니다
무엇을 해주나요#
기능의 도메인(핵심 규칙) 폴더 안에 필요한 파일들을 한 번에 만들어 줍니다.
- Entity — 다룰 대상의 데이터 모양 정의 (예:
post.dart) -
Repository Interface — 데이터를 가져오고 저장하는 "약속(규격)" 정의 (예:
i_community_repository.dart) - UseCase — "목록 조회·단건 조회·생성·수정·삭제" 같은 개별 동작 (예:
get_posts_usecase.dart) - 테스트 코드 — 각 동작이 제대로 작동하는지 검증하는 자동 테스트
- 그 외 실패 메시지·예외 처리용 파일과 묶음 export 파일까지 함께 정리됩니다
어떻게 쓰나요#
# 게시글(Post) 도메인 만들기
/cc-flutter:feature:domain community Post --location application
# 채팅 메시지(Message) 도메인을, 원하는 동작만 골라서 만들기
/cc-flutter:feature:domain chat Message --location application
--usecases "getMessages, sendMessage, markAsRead"
- 첫 번째 값은 기능 이름(community, chat 등), 두 번째 값은 대상 이름(Post, Message 등)입니다.
-
--location은 UI 역할:application→ app(기본),console→ console. common은 legacy 판정에서만 사용한다. --usecases로 필요한 동작만 콤마로 나열해 골라서 만들 수 있습니다.
안에서 무슨 일이 벌어지나요#
- 기존 패턴 살펴보기 — 이미 만들어진 비슷한 코드를 분석해 같은 스타일로 맞춥니다.
- 데이터 모양(Entity) 만들기 — 다룰 대상의 항목들(제목, 작성자, 작성일 등)을 정의합니다.
- 데이터 약속(Repository Interface) 만들기 — 조회·생성·수정·삭제 같은 규격을 정합니다.
- 동작(UseCase) 만들기 — 각 기능 동작을 하나씩 코드로 작성합니다.
- 테스트 만들기 — 각 동작이 잘 작동하는지 검증하는 코드를 함께 생성합니다.
- 마무리 정리 — 코드 생성 명령을 돌려 자동 생성 파일들을 완성합니다.
⚙️ 상세 옵션·실행 명세 (개발자 / AI 에이전트용)
Triggers#
- When a new Feature Domain Layer is needed
- When Entity, Repository Interface, and UseCase generation are needed
/cc-flutter:feature:createorchestration Step 3
Context Trigger Pattern#
/cc-flutter:feature:domain {feature_name} {entity_name} [--options]Parameters#
| Parameter | Required | Description | Example |
|---|---|---|---|
feature_name | ✅ | Feature module name (snake_case) | community, chat |
entity_name | ✅ | Entity name (PascalCase) | Post, Message |
--location | ❌ | UI role | application → app, console → console (default: application); common은 legacy 전용 |
--usecases | ❌ | To generate UseCase | "getList, get, create, update, delete" |
Behavioral Flow#
먼저 .cob/feature-layout.json을 읽고, 없으면 cob의 기존 legacy 트리 판정 → 나머지는 bundle을 따른다. marker를 보존하고 생성 배치 계약을 적용한다. 생성 루트는 feature/{feature_name}/{app,console}/lib/src/domain/, 패키지는 {feature_name}·console_{feature_name}이다. 단위 테스트는 같은 역할의 test/에 둔다.
1. Existing Pattern Analysis#
Analyze existing Domain Layer patterns using Serena MCP:
- feature/community/app/lib/src/domain/entity/post.dart
- feature/community/app/lib/src/domain/repository/i_community_repository.dart
- feature/community/app/lib/src/domain/usecase/get_posts_usecase.dart2. Entity Generation#
import 'package:dependencies/dependencies.dart';
/// {엔티티} 한글 설명
class {Entity} extends Equatable {
/// [{Entity}]를 생성합니다.
const {Entity}({
required this.id,
required this.title,
required this.authorId,
required this.createdAt,
this.updatedAt,
});
final int id;
final String title;
final int authorId;
final DateTime createdAt;
final DateTime? updatedAt;
@override
List<Object?> get props => [id, title, authorId, createdAt, updatedAt];
}
3. Generate Repository Interface#
import 'package:dependencies/dependencies.dart';
/// {Feature} Repository Interface
abstract interface class I{Feature}Repository {
/// {엔티티} 목록을 SWR Stream으로 조회합니다.
Stream<Either<Failure, List<{Entity}>>> get{Entity}s({
{Entity}Category? category,
});
/// {엔티티} 단건 조회
Future<Either<Failure, {Entity}>> get{Entity}(int {entity}Id);
/// {엔티티} 생성
Future<Either<Failure, {Entity}>> create{Entity}({...});
/// {엔티티} 수정
Future<Either<Failure, {Entity}>> update{Entity}({...});
/// {엔티티} 삭제
Future<Either<Failure, void>> delete{Entity}(int {entity}Id);
}
4. Generate UseCase (Optional Constructor Injection)#
import 'package:core/core.dart';
import 'package:dependencies/dependencies.dart';
/// {엔티티} 목록 조회 Params
class Get{Entity}sParams {
const Get{Entity}sParams({
this.limit = 20,
this.offset = 0,
this.category,
});
final int limit;
final int offset;
final {Entity}Category? category;
}
/// {엔티티} 목록을 조회하는 UseCase
class Get{Entity}sUsecase {
/// [Get{Entity}sUsecase]를 생성합니다.
const Get{Entity}sUsecase([I{Feature}Repository? repo])
: _repo = repo ?? const {Feature}Repository();
final I{Feature}Repository _repo;
Future<Either<Failure, {Entity}ListResult>> call(Get{Entity}sParams params) {
return _repo.get{Entity}s(
limit: params.limit,
offset: params.offset,
category: params.category,
);
}
}
5. Generate UseCase tests#
void main() {
late Get{Entity}sUsecase usecase;
late MockI{Feature}Repository mockRepository;
setUpAll(registerFallbackValues);
setUp(() {
mockRepository = MockI{Feature}Repository();
usecase = Get{Entity}sUsecase(mockRepository); // 직접 주입
});
group('Get{Entity}sUsecase', () {
test('성공 시 Right({Entity}ListResult) 반환', () async {
// arrange
when(() => mockRepository.get{Entity}s(...))
.thenAnswer((_) async => Right(testResult));
// act
final result = await usecase(testParams);
// assert
expect(result.isRight(), true);
verify(() => mockRepository.get{Entity}s(...)).called(1);
});
});
}
Output Files#
feature/{feature_name}/{app,console}/lib/src/domain/
├── entity/
│ ├── {entity}.dart
│ ├── {entity}_list_result.dart
│ └── entity.dart # export
├── repository/
│ ├── i_{feature}_repository.dart
│ └── repository.dart # export
├── usecase/
│ ├── get_{entity}s_usecase.dart
│ ├── get_{entity}_usecase.dart
│ ├── create_{entity}_usecase.dart
│ ├── update_{entity}_usecase.dart
│ ├── delete_{entity}_usecase.dart
│ └── usecase.dart # export
├── failure/
│ └── {feature}_failure_messages.dart
└── exception/
└── {feature}_exception.dart
feature/{feature_name}/{app,console}/test/domain/usecase/
├── get_{entity}s_usecase_test.dart
├── get_{entity}_usecase_test.dart
├── create_{entity}_usecase_test.dart
├── update_{entity}_usecase_test.dart
└── delete_{entity}_usecase_test.dartPost-Generation Commands#
# Code generation (Freezed 등)
melos exec --scope={feature_name} -- "dart run build_runner build --delete-conflicting-outputs"MCP Integration#
- Serena: Existing Domain Layer Pattern Analysis, Symbol search
- Context7: Clean Architecture, Either Pattern Document reference
Examples#
Create Post Domain#
/cc-flutter:feature:domain community Post --location applicationCreate Chat Message Domain#
/cc-flutter:feature:domain chat Message --location application
--usecases "getMessages, sendMessage, markAsRead"Reference Agents#
Reference ${CLAUDE_PLUGIN_ROOT}/agents/app/domain-layer-agent.md for detailed implementation rules
Core Rules Summary#
✅ Required Patterns#
- Entity extends Equatable
- Repository Interface uses
Iprefix - UseCase uses Optional Constructor Injection (
const GetPostsUsecase([IRepo? repo])) - UseCase creates ConcreteRepo directly as default value (
repo ?? const ConcreteRepo()) - Write unit tests for all UseCases
❌ Prohibited Patterns#
// ❌ @injectable 어노테이션 금지
// @injectable
// class GetPostsUseCase { ... }
// ❌ Getting Repository via getIt prohibited
// IRepo get repo => getIt<IRepo>();
// ❌ Relative path import prohibited
// import '../entity/post.dart';
Test Patterns#
setUp(() {
mockRepository = MockI{Feature}Repository();
usecase = Get{Entity}sUsecase(mockRepository); // 직접 주입
});
// getIt.reset() 불필요!