LogoCocode Skills

feature:data

Clean Architecture Data Layer ์ƒ์„ฑ

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

/feature:data#

Context Framework Note: This behavioral instruction activates when Claude Code users type /feature:data patterns.

Triggers#

  • ์ƒˆ๋กœ์šด Feature์˜ Data Layer๊ฐ€ ํ•„์š”ํ•  ๋•Œ
  • Repository ๊ตฌํ˜„์ฒด, Serverpod Mixin, Cache ์ „๋žต ๊ตฌํ˜„์ด ํ•„์š”ํ•  ๋•Œ
  • /feature:create ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜์˜ Step 4์—์„œ ํ˜ธ์ถœ๋  ๋•Œ

Context Trigger Pattern#

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

Parameters#

ํŒŒ๋ผ๋ฏธํ„ฐํ•„์ˆ˜์„ค๋ช…์˜ˆ์‹œ
feature_name โœ… Feature ๋ชจ๋“ˆ๋ช… (snake_case) community, chat
entity_name โœ… Entity๋ช… (PascalCase) Post, Message
--location โŒ ์œ„์น˜ application, common, console (๊ธฐ๋ณธ: application)
--caching โŒ ์บ์‹ฑ ์ „๋žต swr, cache-first, none (๊ธฐ๋ณธ: swr)

Behavioral Flow#

1. ๊ธฐ์กด ํŒจํ„ด ๋ถ„์„#

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. Repository ๊ตฌํ˜„์ฒด ์ƒ์„ฑ#

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. Serverpod Mixin ์ƒ์„ฑ (๋„ค์ž„์ŠคํŽ˜์ด์Šค ํ•„์ˆ˜!)#

import 'package:dependencies/dependencies.dart';
import 'package:serverpod_service/serverpod_service.dart' as pod;  // โœ… ๋„ค์ž„์ŠคํŽ˜์ด์Šค ํ•„์ˆ˜

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

/// {Feature} Serverpod API Mixin
mixin {Feature}ServerpodMixin implements I{Feature}Repository {
  /// Serverpod ํด๋ผ์ด์–ธํŠธ
  pod.ServerpodClient get client;  // โœ… ๋„ค์ž„์ŠคํŽ˜์ด์Šค ์‚ฌ์šฉ

  /// {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 ์บ์‹ฑ ํŒจํ„ด#

@override
Future<Either<Failure, {Entity}ListResult>> get{Entity}s({...}) async {
  try {
    // 1. ์บ์‹œ ํ™•์ธ
    final cachedData = await {entity}Dao.getAll{Entity}s(limit: limit, offset: offset);

    if (cachedData.isNotEmpty) {
      // 2. ์บ์‹œ ๋ฐ์ดํ„ฐ ์ฆ‰์‹œ ๋ฐ˜ํ™˜
      final entities = cachedData.map((data) => {entity}Dao.dataTo{Entity}(data)).toList();
      final cachedResult = {Entity}ListResult(items: entities, ...);

      // 3. ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๊ฐฑ์‹  (SWR)
      unawaited(_refresh{Entity}sInBackground(...));

      return Right(cachedResult);
    }

    // 4. ์บ์‹œ ์—†์œผ๋ฉด API ํ˜ธ์ถœ
    return _fetchAndCache{Entity}s(...);
  } on Exception catch (error, stackTrace) {
    return left(RepositoryFailure(...));
  }
}

5. Cache-First Stream ํŒจํ„ด#

@override
Stream<CacheFirstResult<{Entity}ListResult>> get{Entity}sAsStream({...}) async* {
  // 1. ์บ์‹œ ์กฐํšŒ
  final cachedData = await {entity}Dao.getAll{Entity}s(...);

  // 2. ์บ์‹œ๊ฐ€ ์žˆ์œผ๋ฉด ์ฆ‰์‹œ emit
  if (cachedData.isNotEmpty) {
    yield CacheFirstResult(data: cachedResult, fromCache: true, isRefreshing: true);
  }

  // 3. ๋„คํŠธ์›Œํฌ์—์„œ ์ตœ์‹  ๋ฐ์ดํ„ฐ
  final result = await get{Entity}s(...);

  // 4. ๋„คํŠธ์›Œํฌ ๊ฒฐ๊ณผ emit
  yield* result.fold(
    (failure) async* { if (cachedData.isEmpty) throw Exception(...); },
    (networkResult) async* {
      yield CacheFirstResult(data: networkResult, fromCache: false, isRefreshing: false);
    },
  );
}

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#

# ์ฝ”๋“œ ์ƒ์„ฑ (Drift, Injectable)
melos exec --scope={feature_name} -- "dart run build_runner build --delete-conflicting-outputs"

MCP Integration#

  • Serena: ๊ธฐ์กด Data Layer ํŒจํ„ด ๋ถ„์„, ์‹ฌ๋ณผ ๊ฒ€์ƒ‰
  • Context7: Serverpod, Drift ๋ฌธ์„œ ์ฐธ์กฐ

Examples#

๊ฒŒ์‹œ๊ธ€ Data Layer ์ƒ์„ฑ#

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

์ฑ„ํŒ… ๋ฉ”์‹œ์ง€ Data Layer ์ƒ์„ฑ#

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

์ฐธ์กฐ ์—์ด์ „ํŠธ#

์ƒ์„ธ ๊ตฌํ˜„ ๊ทœ์น™์€ ~/.claude/commands/agents/data-layer-agent.md ์ฐธ์กฐ

ํ•ต์‹ฌ ๊ทœ์น™ ์š”์•ฝ#

โœ… Critical Import ํŒจํ„ด#

// 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;  // โœ… ๋„ค์ž„์ŠคํŽ˜์ด์Šค ์‚ฌ์šฉ

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

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

์บ์‹ฑ ์ „๋žต ์„ ํƒ#

์ „๋žต์‚ฌ์šฉ ์‹œ์ ํŠน์ง•
SWR์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ์บ์‹œ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ + ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๊ฐฑ์‹ 
Cache-First์ •์  ๋ฐ์ดํ„ฐ์บ์‹œ ์žˆ์œผ๋ฉด ๋„คํŠธ์›Œํฌ ํ˜ธ์ถœ ์•ˆ ํ•จ
Noneํ•ญ์ƒ ์ตœ์‹  ํ•„์š”๋งค๋ฒˆ ๋„คํŠธ์›Œํฌ ํ˜ธ์ถœ