LogoSkills

feature:data

Clean Architecture Data Layer generation

ํ•ญ๋ชฉ๋‚ด์šฉ
Invoke/feature:data
Aliases/data, /data:create
Categorykobic-workflow
Complexitymoderate
MCP Serversserena, context7

/feature:data โ€” ๋ฐ์ดํ„ฐ ์—ฐ๊ฒฐ ๋‹ด๋‹น ์ฝ”๋“œ ๋งŒ๋“ค๊ธฐ#

ํ•œ๋งˆ๋””๋กœ#

์•ฑ ํ™”๋ฉด๊ณผ ์„œ๋ฒ„(๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค) ์‚ฌ์ด๋ฅผ ์˜ค๊ฐ€๋Š” "ํƒ๋ฐฐ ๊ธฐ์‚ฌ" ์—ญํ• ์˜ ์ฝ”๋“œ๋ฅผ ์ž๋™์œผ๋กœ ๋งŒ๋“ค์–ด ์ฃผ๋Š” ๋ช…๋ น์ž…๋‹ˆ๋‹ค. ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ค๊ณ , ์ž์ฃผ ์“ฐ๋Š” ๊ฑด ํœด๋Œ€ํฐ ์•ˆ์— ์ž ๊น ์ €์žฅ(์บ์‹œ)ํ•ด ๋‘์–ด ํ™”๋ฉด์ด ๋น ๋ฅด๊ฒŒ ๋œจ๋„๋ก ํ•ด ์ค๋‹ˆ๋‹ค.

๋ˆ„๊ฐ€ยท์–ธ์ œ ์“ฐ๋‚˜์š”#

  • ์ƒˆ ๊ธฐ๋Šฅ(์˜ˆ: ์ปค๋ฎค๋‹ˆํ‹ฐ, ์ฑ„ํŒ…)์„ ๋งŒ๋“ค๋ฉด์„œ ์„œ๋ฒ„์™€ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›๋Š” ๋ถ€๋ถ„์ด ํ•„์š”ํ•  ๋•Œ
  • ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ์ €์žฅํ•˜๋Š” ๊ทœ์น™(Repository, ์„œ๋ฒ„ ์—ฐ๊ฒฐ, ์บ์‹œ ์ „๋žต)์„ ํ•œ ๋ฒˆ์— ๊ฐ–์ถ”๊ณ  ์‹ถ์„ ๋•Œ
  • ์ „์ฒด ๊ธฐ๋Šฅ์„ ํ†ต์งธ๋กœ ๋งŒ๋“œ๋Š” /feature:create ๊ณผ์ •์˜ 4๋ฒˆ์งธ ๋‹จ๊ณ„๋กœ ์ž๋™ ์‹คํ–‰๋  ๋•Œ

๋ฌด์—‡์„ ํ•ด์ฃผ๋‚˜์š”#

๊ธฐ๋Šฅ ํด๋” ์•ˆ์— ๋ฐ์ดํ„ฐ ๊ด€๋ จ ์ฝ”๋“œ ํŒŒ์ผ ๋ฌถ์Œ์„ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ฃผ์š” ๊ฒฐ๊ณผ๋ฌผ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • Repository ({feature}_repository.dart) โ€” ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ์ €์žฅํ•˜๋Š” ์ผ์„ ์ด๊ด„ํ•˜๋Š” ์ฝ”๋“œ
  • Serverpod Mixin ({feature}_serverpod_mixin.dart) โ€” ์‹ค์ œ ์„œ๋ฒ„ API์™€ ํ†ต์‹ ํ•˜๋Š” ์ฝ”๋“œ
  • Cache / Local ({entity}_cache_repository.dart, {entity}_table.dart, {entity}_dao.dart ๋“ฑ) โ€” ๋ฐ์ดํ„ฐ๋ฅผ ํœด๋Œ€ํฐ ์•ˆ์— ์ž ๊น ์ €์žฅํ•ด ๋‘๋Š” ์ฝ”๋“œ
  • ์ƒ์„ฑ ํ›„ ์ฝ”๋“œ ์ž๋™ ์™„์„ฑ(build_runner) ๋ช…๋ น๊นŒ์ง€ ์•ˆ๋‚ดํ•ฉ๋‹ˆ๋‹ค.

์–ด๋–ป๊ฒŒ ์“ฐ๋‚˜์š”#

# ๊ฒŒ์‹œ๊ธ€(Post) ๋ฐ์ดํ„ฐ ์ฝ”๋“œ ๋งŒ๋“ค๊ธฐ โ€” ๋ชฉ๋ก์€ SWR ๋ฐฉ์‹์œผ๋กœ ์บ์‹œ
/feature:data community Post --location application --caching swr

# ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€(Message) ๋ฐ์ดํ„ฐ ์ฝ”๋“œ ๋งŒ๋“ค๊ธฐ โ€” ์บ์‹œ ์šฐ์„  ๋ฐฉ์‹
/feature:data chat Message --location application --caching cache-first
  • ์ฒซ ๋ฒˆ์งธ ๊ฐ’(community)์€ ๊ธฐ๋Šฅ ์ด๋ฆ„, ๋‘ ๋ฒˆ์งธ ๊ฐ’(Post)์€ ๋‹ค๋ฃฐ ๋ฐ์ดํ„ฐ ์ข…๋ฅ˜์ž…๋‹ˆ๋‹ค.
  • --location ์€ ์ฝ”๋“œ๊ฐ€ ๋“ค์–ด๊ฐˆ ์œ„์น˜(application/common/console, ๊ธฐ๋ณธ๊ฐ’ application).
  • --caching ์€ ์บ์‹œ(์ž„์‹œ ์ €์žฅ) ๋ฐฉ์‹(swr/cache-first/none, ๊ธฐ๋ณธ๊ฐ’ swr)์„ ์ •ํ•ฉ๋‹ˆ๋‹ค.

์•ˆ์—์„œ ๋ฌด์Šจ ์ผ์ด ๋ฒŒ์–ด์ง€๋‚˜์š”#

  1. ๊ธฐ์กด ์ฝ”๋“œ ์‚ดํŽด๋ณด๊ธฐ โ€” ์ด๋ฏธ ๋น„์Šทํ•˜๊ฒŒ ๋งŒ๋“ค์–ด ๋‘” ๋ฐ์ดํ„ฐ ์ฝ”๋“œ๊ฐ€ ์žˆ๋Š”์ง€ ๋จผ์ € ๋ถ„์„ํ•ด ๊ฐ™์€ ๋ชจ์–‘์ƒˆ๋ฅผ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค.
  2. Repository ๋งŒ๋“ค๊ธฐ โ€” ๋ฐ์ดํ„ฐ ์š”์ฒญ์„ ์ด๊ด„ํ•˜๋Š” ๋Œ€ํ‘œ ์ฝ”๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  3. ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์ฝ”๋“œ ๋งŒ๋“ค๊ธฐ โ€” ์„œ๋ฒ„์— ์‹ค์ œ๋กœ ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ , ์„œ๋ฒ„ ํ˜•์‹ โ†” ์•ฑ ํ˜•์‹์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€ํ™˜ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
  4. ์บ์‹œ ์ „๋žต ์ ์šฉ โ€” ๋ชฉ๋ก ๋ฐ์ดํ„ฐ๋Š” SWR(๋น ๋ฅด๊ฒŒ ๋ณด์—ฌ์ฃผ๊ณ  ๋’ค์—์„œ ๊ฐฑ์‹ ), ๋‹จ๊ฑด ๋ฐ์ดํ„ฐ๋Š” ์บ์‹œ ์šฐ์„  ๋ฐฉ์‹์œผ๋กœ ์ž„์‹œ ์ €์žฅ ๊ทœ์น™์„ ๋ถ™์ž…๋‹ˆ๋‹ค.
  5. ๋งˆ๋ฌด๋ฆฌ โ€” ์ƒ์„ฑ๋œ ํŒŒ์ผ ๋ชฉ๋ก์„ ์ •๋ฆฌํ•˜๊ณ , ์ฝ”๋“œ ์ž๋™ ์™„์„ฑ(build_runner) ๋ช…๋ น์„ ์•ˆ๋‚ดํ•ฉ๋‹ˆ๋‹ค.

โš™๏ธ ์ƒ์„ธ ์˜ต์…˜ยท์‹คํ–‰ ๋ช…์„ธ (๊ฐœ๋ฐœ์ž / AI ์—์ด์ „ํŠธ์šฉ)

Triggers#

  • When a new Feature Data Layer is needed
  • When Repository implementation, Serverpod Mixin, and Cache strategy are needed
  • /feature:create orchestration Step 4

Context Trigger Pattern#

/feature:data {feature_name} {entity_name} [--options]

Parameters#

ParameterRequiredDescriptionExample
feature_nameโœ…Feature module name (snake_case)community, chat
entity_nameโœ…Entity name (PascalCase)Post, Message
--locationโŒLocationapplication, common, console (default: application)
--cachingโŒCaching strategyswr, cache-first, none (default: swr)

Behavioral Flow#

1. Existing Pattern Analysis#

Serena MCP๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ธฐ์กด Data Layer ํŒจํ„ด ๋ถ„์„:
- feature/application/community/lib/src/data/repository/community_repository.dart
- feature/application/community/lib/src/data/repository/mixins/community_serverpod_mixin.dart

2. Generate Repository implementation#

import 'package:dependencies/dependencies.dart';
import 'package:serverpod_service/serverpod_service.dart';

import '../../domain/repository/i_{feature}_repository.dart';
import 'mixins/{feature}_serverpod_mixin.dart';

/// {Feature} Repository ๊ตฌํ˜„์ฒด
@LazySingleton(as: I{Feature}Repository)
class {Feature}Repository
    with {Feature}ServerpodMixin
    implements I{Feature}Repository {

  /// [{Feature}Repository]๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  {Feature}Repository(
    this._serverpodService,
    this._database,
  );

  final ServerpodService _serverpodService;
  final {Feature}Database _database;

  @override
  ServerpodClient get client => _serverpodService.client;

  @override
  {Entity}Dao get {entity}Dao => _database.{entity}Dao;
}

3. Generate Serverpod Mixin (Namespace required!)#

import 'package:dependencies/dependencies.dart';
import 'package:serverpod_service/serverpod_service.dart' as pod;  // โœ… Namespace required

import '../../domain/entity/{entity}.dart';
import '../../domain/repository/i_{feature}_repository.dart';

/// {Feature} Serverpod API Mixin
mixin {Feature}ServerpodMixin implements I{Feature}Repository {
  /// Serverpod client
  pod.ServerpodClient get client;  // โœ… Using namespace

  /// {Entity} DAO
  {Entity}Dao get {entity}Dao;

  @override
  Future<Either<Failure, {Entity}>> create{Entity}({
    required {Entity}Category category,
    required String title,
    required String content,
  }) async {
    try {
      // 1. Domain โ†’ Protocol ๋ณ€ํ™˜ (๋„ค์ž„์ŠคํŽ˜์ด์Šค ์‚ฌ์šฉ)
      final request = pod.{Entity}CreateRequest(
        category: _categoryToProtocol(category),
        title: title,
        content: content,
      );

      // 2. API ํ˜ธ์ถœ
      final response = await client.{feature}.create{Entity}(request);

      // 3. Protocol โ†’ Entity ๋ณ€ํ™˜
      final entity = _map{Entity}FromProtocol(response);

      // 4. ์บ์‹œ์— ์ €์žฅ
      await {entity}Dao.save{Entity}(entity);

      return Right(entity);
    } on Exception catch (error, stackTrace) {
      return left(
        RepositoryFailure(
          'Failed to create {entity}',
          error: error,
          stackTrace: stackTrace,
        ),
      );
    }
  }

  // DTO ๋ณ€ํ™˜ ํ—ฌํผ
  {Entity} _map{Entity}FromProtocol(pod.{Entity} response) {
    return {Entity}(
      id: response.id ?? 0,
      title: response.title,
      category: _categoryFromProtocol(response.category),
      // ...
    );
  }

  // Protocol โ†’ Domain ๋ณ€ํ™˜
  {Entity}Category _categoryFromProtocol(pod.{Entity}Category category) {
    switch (category) {
      case pod.{Entity}Category.qna:
        return {Entity}Category.qna;
      // ...
    }
  }

  // Domain โ†’ Protocol ๋ณ€ํ™˜
  pod.{Entity}Category _categoryToProtocol({Entity}Category category) {
    switch (category) {
      case {Entity}Category.qna:
        return pod.{Entity}Category.qna;
      // ...
    }
  }
}

4. SWR Caching Pattern (GET List)#

// Repository Mixin์—์„œ ์ „๋žต ์กฐ๋ฆฝ
@override
Stream<Either<Failure, List<{Entity}>>> get{Entity}s({
  String? categoryId,
}) =>
    SwrStrategyImpl<List<{Entity}>>(
      cacheRepository: {Entity}DataCacheRepository(
        {entity}ItemDao: {entity}ItemDao,
      ),
      networkRepository: {Entity}DataNetworkRepository(
        openApiService: openApiService,
      ),
      policy: CachePolicies.standard,
    ).watchStream(
      {Entity}DataCacheQuery(categoryId: categoryId),
    );

5. CacheFirst Pattern (GET Single)#

@override
Future<Either<Failure, {Entity}?>> get{Entity}Detail({
  required String id,
}) =>
    CacheFirstStrategyImpl<{Entity}?>(
      cacheRepository: {Entity}DetailCacheRepository(
        {entity}DetailDao: {entity}DetailDao,
      ),
      networkRepository: {Entity}DetailNetworkRepository(
        openApiService: openApiService,
      ),
      policy: CachePolicies.standard,
    ).execute(
      {Entity}DetailCacheQuery(id: id),
    );

Output Files#

feature/{location}/{feature_name}/lib/src/data/
โ”œโ”€โ”€ repository/
โ”‚   โ”œโ”€โ”€ {feature}_repository.dart
โ”‚   โ”œโ”€โ”€ mixins/
โ”‚   โ”‚   โ””โ”€โ”€ {feature}_serverpod_mixin.dart
โ”‚   โ””โ”€โ”€ repository.dart       # export
โ”œโ”€โ”€ cache/
โ”‚   โ””โ”€โ”€ {entity}_cache_repository.dart
โ””โ”€โ”€ local/
    โ”œโ”€โ”€ tables/
    โ”‚   โ””โ”€โ”€ {entity}_table.dart
    โ”œโ”€โ”€ dao/
    โ”‚   โ””โ”€โ”€ {entity}_dao.dart
    โ””โ”€โ”€ {feature}_database.dart

Post-Generation Commands#

# Code generation (Drift, Injectable)
melos exec --scope={feature_name} --  " dart run build_runner build --delete-conflicting-outputs "

MCP Integration#

  • Serena: Existing Data Layer Pattern Analysis, Symbol search
  • Context7: Serverpod, Drift Document reference

Examples#

Create Post Data Layer#

/feature:data community Post --location application --caching swr

Create Chat Message Data Layer#

/feature:data chat Message --location application --caching cache-first

Reference Agents#

Detailed implementation rules in ${CLAUDE_PLUGIN_ROOT}/commands/agents/app/data-layer-agent.md reference

Core Rules Summary#

โœ… Critical Import Pattern#

// Repository ๊ตฌํ˜„์ฒด
import 'package:dependencies/dependencies.dart';

// Mixin (๋„ค์ž„์ŠคํŽ˜์ด์Šค ํ•„์ˆ˜!)
import 'package:serverpod_service/serverpod_service.dart' as pod;

mixin {Feature}ServerpodMixin implements I{Feature}Repository {
  pod.ServerpodClient get client;  // โœ… Using namespace

  // Domain Entity ์‚ฌ์šฉ (๋„ค์ž„์ŠคํŽ˜์ด์Šค ์—†์Œ)
  {Entity}Category category = {Entity}Category.qna;

  // Serverpod DTO ์‚ฌ์šฉ (๋„ค์ž„์ŠคํŽ˜์ด์Šค ์‚ฌ์šฉ)
  pod.{Entity}Category apiCategory = pod.{Entity}Category.qna;
}

Caching Strategy Selection (details: /client-cache skill reference)#

StrategyEntry PointReturn TypeSuitable For
SWRwatchStream()Stream<Either<Failure, T>>GET lists, externally mutable
CacheFirstexecute()Future<Either<Failure, T>>GET single, offline first
NetworkFirstexecute()Future<Either<Failure, T>>Payment/auth, always need latest