문서 목차
이 페이지에서목차

resources-agent

UI 컴포넌트, 테마, 확장 메서드 생성 전문가입니다. CoUI 커스터마이징과 공통 위젯 구현에 사용합니다.

항목내용
실행 명령/shared:resources
별칭/ui:theme, /widget:create
모델sonnet
사용 도구 Read, Edit, Write, Glob, Grep
연계 스킬coui-getting-started

UI 부품, 테마(색·글꼴), 공통 위젯을 만들고 관리하는 전문 에이전트

한마디로#

앱 전체에서 똑같이 쓰이는 공통 디자인 부품을 만들어 주는 담당자입니다. 인테리어에 비유하면, 집집마다 따로 사지 않고 한 번 정해 두면 모든 방에 똑같이 적용되는 "표준 가구·색상·조명 세트"를 만드는 일이에요. 버튼 모양, 밝은/어두운 테마, 표(Table)나 팝업창 같은 부품을 한곳에서 챙깁니다.

누가·언제 쓰나요#

  • 앱의 색상·글꼴·전체 테마(밝은 모드 / 어두운 모드)를 정하거나 바꿀 때
  • 여러 화면에서 반복해서 쓰는 공통 위젯(배너, 카드, 팝업창, 표 등)이 필요할 때
  • CoUI 디자인 시스템의 버튼·뱃지 같은 부품을 우리 앱에 맞게 커스터마이즈할 때
  • /shared:resources 명령을 부르면 이 담당자가 작동합니다. (/ui:theme, /widget:create 로도 호출 가능)

무엇을 해주나요#

package/resources 패키지 안에 공통 자원을 만들어 정리합니다. 실제로 만들어지는 것들:

  • AppTheme — 브랜드 한 곳(app_theme.dart): 브랜드 시드 색에서 CoUI 가 밝은 · 어두운 · 고대비 테마를 만들고, 제목 서체가 있으면 그 역할에만 입힙니다. 색상표 · 글꼴 표를 따로 만들지 않습니다
  • 공통 위젯 — 표(table_builder.dart), 확인 팝업(confirm_dialog.dart), 배너·카드 등
  • 편의 단축 코드 — CoUI 가 이미 주는 context.theme · context.colorScheme · context.typography 밖의 앱 고유 값에만, CoUI 와 겹치지 않는 이름의 BuildContext 확장 메서드
  • 커스텀 SVG 아이콘 — Figma에서 export한, CoUI 표준 아이콘(Lucide)에 없는 브랜드/커스텀 아이콘을 assets/svg/에 보관하고 flutter_gen으로 타입 세이프하게 접근 (Assets.svg.xxx)

어떻게 쓰나요#

# 테마(색·글꼴) 만들기
/shared:resources component_type=theme component_name=AppTheme

# 공통 위젯 만들기 (예: 표 부품)
/shared:resources component_type=widget component_name=TableBuilder category=table

# 편의 단축 코드(확장 메서드) 만들기
/shared:resources component_type=extension component_name=AppSpacingContext
  • component_type (필수): theme(테마) / widget(공통 위젯) / extension(확장 메서드) 중 하나
  • component_name (필수): 만들 부품 이름 (PascalCase, 예: TableBuilder)
  • category (선택): 위젯 종류 — banner, card, dialog, table 등

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

요청한 부품 종류에 맞춰 정해진 패턴대로 코드를 만들고, 마지막에 빠진 게 없는지 점검합니다.

  1. 테마 만들기 — 브랜드 시드를 정하면 CoUI 가 밝은 · 어두운 · 고대비 색을 만들고, 제목 서체가 있으면 그 역할에만 입힌 뒤, 앱 루트(CoUIApp)에 넘겨 앱 전체 테마(AppTheme)로 완성합니다.
  2. 공통 위젯 만들기 — 표·팝업창 같은 재사용 부품을 정해진 구조로 만듭니다. (예: 정렬·컬럼 크기조절·순서변경이 되는 범용 표)
  3. 편의 단축 코드 만들기 — CoUI 에 없는 앱 고유 값만 짧게 접근하도록, CoUI 와 겹치지 않는 이름의 확장 메서드를 추가합니다(테마 · 색 · 글꼴은 CoUI 의 context.theme · context.colorScheme · context.typography).
  4. CoUI 표준 확인 — 버튼·뱃지·아이콘 같은 부품은 CoUI 의 현행 방식(Button(variant: ...) · Badge(tone: ...) · Icon(LucideIcons.x), 접두어 없음)에 맞는지 확인하고 적용합니다.
  5. 마무리 점검 — 밝은/어두운 테마가 모두 정의됐는지, 주석이 달렸는지 등 체크리스트로 빠진 부분을 확인합니다.

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

Role#

Creates and manages UI elements in the Resources package.

  • AppTheme (Light/Dark mode)
  • CoUI Flutter component customization
  • Common widgets (Banner, Card, Dialog, Table)
  • BuildContext extension methods

Activation Conditions#

  • /shared:resources Activated when command is invoked
  • Invoked during UI component, theme, and extension method work

Parameters#

ParameterRequiredDescription
component_type✅theme, widget, extension
component_name✅Component name (PascalCase)
category❌Widget category (banner, card, dialog, table etc.)

Package Structure#

package/resources/
├── assets/
│   └── svg/                          # 커스텀/브랜드 SVG 아이콘 원본 (Lucide에 없는 것만)
│       ├── action_bell.svg
│       └── bottom_navi_home_off.svg
├── pubspec.yaml                       # flutter_gen_runner + flutter_gen 설정
└── lib/
    ├── resources.dart                # Export 파일
    └── src/
        ├── core/
        │   ├── extensions/           # BuildContext 확장 — CoUI 와 겹치지 않는 앱 고유 값만
        │   │   └── app_spacing_context_extension.dart
        │   ├── locale/               # 로케일 유틸리티
        │   ├── size/                 # 사이즈 상수
        │   ├── theme/                # 테마 정의 — 브랜드 · 테마 함수 · 컴포넌트 테마
        │   │   └── app_theme.dart
        │   ├── util/                 # 유틸리티
        │   └── generated/
        │       └── assets.gen.dart   # flutter_gen 자동 생성 → Assets.svg.xxx
        └── widgets/                  # 공통 위젯
            ├── banner/
            ├── card/
            ├── console/
            ├── dialog/
            ├── navigation/
            └── table/

Custom SVG Icon Workflow#

CoUI's Icon(LucideIcons.x) covers the standard icon set. When a Figma design uses a brand/custom icon with no Lucide equivalent:

  1. Export the icon from Figma as SVG
  2. Add it to package/resources/assets/svg/ (check assets.gen.dart first — don't duplicate an existing entry)
  3. Regenerate: melos run generate:assets (re-runs flutter_gen_runner)
  4. Reference via the generated accessor: Assets.svg.iconBestseller.svg(width: 24, height: 24)

Never hardcode an SvgPicture.asset('assets/svg/...') path directly — always go through the generated Assets.svg.* accessor so renames/moves are caught at compile time.


Import Order (Required)#

// 1. CoUI — 위젯 · 토큰 · 테마. UI 파일은 material.dart 를 import 하지 않는다
//    (CoUI 의 bare 이름 Card · Checkbox · Icon … 이 Material 과 겹친다)
import 'package:coui_flutter/coui_flutter.dart';

// 2. 내부 모듈
import '../theme/app_theme.dart';

Core Patterns#

1. 테마 모듈 — 브랜드 한 곳 (CoreBrand → ThemeData.fromCore → CoUIApp)#

색은 앱이 상수로 들고 있지 않는다. 브랜드 시드에서 CoUI 가 원시 사다리와 시맨틱 매핑을 만들고(라이트 · 다크 · 고대비 두 벌), 앱은 그 결과를 CoUIApp 에 넘긴다. 7축 결정(색 · 타이포 · 형태 · 재질 · 밀도 · 효과 · 모션)과 그 근거는 cc-flutter:demo-brand-theme 이 정본이다.

import 'package:coui_flutter/coui_flutter.dart';

/// 브랜드 결정이 존재하는 유일한 자리 — 시드 HSL 에서 CoUI 가 사다리를 만든다
final CoreBrand appBrand = CoreBrand(
  id: 'app',
  displayName: '앱 이름',
  palette: PrimitiveColors.generate(
    primaryHSL: (h: 212.0, s: 0.70, l: 0.45),
    secondaryHSL: (h: 28.0, s: 0.70, l: 0.55),
    tertiaryHSL: (h: 176.0, s: 0.45, l: 0.40),
    successHSL: (h: 140.0, s: 0.60, l: 0.32),
    infoHSL: (h: 200.0, s: 0.70, l: 0.40),
    warningHSL: (h: 42.0, s: 0.85, l: 0.50),
    errorHSL: (h: 2.0, s: 0.72, l: 0.48),
    neutralHue: 212.0,
  ),
);

/// 컴포넌트 하나의 모양 결정만 — 크기와 무관한 값(반경 · 패딩 · 아이콘 크기를 테마로 다시 고르지 않는다)
const CoreComponentTheme appComponents = CoreComponentTheme(
  badge: CoreBadgeTheme(style: CoreBadgeStyle(borderRadius: CoreBorderRadius.all(CoreRadius.radius9999))),
);

/// 밝기 · 고대비마다 같은 함수 — 앱 · 위젯북 · 골든이 이 함수 하나를 쓴다
ThemeData appTheme(CoreBrightness brightness, {bool highContrast = false}) {
  final source = appBrand.source;
  final scheme = switch ((brightness, highContrast)) {
    (CoreBrightness.light, false) => source.light(),
    (CoreBrightness.dark, false) => source.dark(),
    (CoreBrightness.light, true) => source.highContrastLight(),
    (CoreBrightness.dark, true) => source.highContrastDark(),
  };
  return ThemeData.fromCore(
    CoreThemeData(
      colorScheme: scheme,
      brightness: brightness,
      designSystem: DesignSystem.coui,
      // 제목 서체가 따로 있는 제품만 — display · headline 역할에만 닿는다(본문 서체는 CoUI 기본)
      typography: CoreTypography.coui.withFamily('Brand Display'),
    ),
    coreComponentTheme: appComponents,
  );
}

/// 앱 루트 — 서페이스 스타일 · 밀도는 테마 축이라 CoUIApp 이 받는다(Figma 프레임 모드와 같은 값)
Widget appRoot(Widget home) => CoUIApp(
  theme: appTheme(CoreBrightness.light),
  darkTheme: appTheme(CoreBrightness.dark),
  highContrastTheme: appTheme(CoreBrightness.light, highContrast: true),
  highContrastDarkTheme: appTheme(CoreBrightness.dark, highContrast: true),
  surfaceStyle: CoreSurfaceStyleId.defaultStyle,
  density: CoreDensity.standard,
  home: home,
);

2. 색 — 평행 팔레트를 만들지 않는다#

AppColorScheme.light = ColorScheme(primary: Color(0xFF…), …) 처럼 색을 상수로 다시 적지 않는다. 그 상수에는 다크 · 고대비 · 브랜드 교체가 닿지 않고, 시맨틱 슬롯(surfaceContainer · outlineVariant · onPrimaryContainer …)을 빠뜨린 채 일부만 적게 된다. 화면은 토큰 이름만 읽는다 — context.colorScheme.<슬롯> · Text 체인(Text('…').onSurfaceVariant) · 부품의 스타일 슬롯(CoreColor.token(CoreColors.x)).

3. 타이포그래피 — 역할을 만들지 않고 고른다#

Typography(displayLarge: TextStyle(fontSize: 57, …)) 처럼 역할을 다시 정의하지 않는다. CoUI 의 CoreTypography.coui 가 22역할(크기 · 굵기 · 행간 · 자간 한 묶음)을 갖고, 앱이 바꾸는 것은 서체뿐이다 — 제목 서체는 CoreTypography.coui.withFamily('<서체>')(위). 화면은 Text('…').titleMedium 처럼 역할 체인으로 쓴다.

4. BuildContext 확장 — CoUI 것을 쓴다#

CoUI 의 ThemeContext 가 이미 context.theme · context.colorScheme · context.typography 를 준다. 같은 이름의 확장을 resources 에 다시 만들지 않는다 — 두 확장이 같은 이름을 주면 호출이 모호해져 컴파일되지 않는다. 앱 고유 값(브레이크포인트 별 여백 류)이 필요하면 CoUI 와 겹치지 않는 이름으로 만든다.

5. Common Widget Pattern (TableBuilder)#

표 자체는 CoUI 가 그린다 — 셀 · 머리 · 선택 · 호버는 Table, 열 크기 조절 · 고정 셀은 ResizableTable · Table(frozenCells:). 이 빌더는 정렬 상태 · 열 정의 · 로딩 같은 앱 쪽 상태만 들고 CoUI 표에 넘긴다.

import 'package:coui_flutter/coui_flutter.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

/// 범용 테이블 빌더
///
/// 정렬, 컬럼 리사이즈, 리오더, 가시성 제어 기능을 제공합니다.
class TableBuilder<T, TSortField> extends HookWidget {
  /// TableBuilder 생성자
  const TableBuilder({
    required this.items,
    required this.columns,
    this.isLoading = false,
    this.sortState,
    this.onSortChanged,
    this.enableColumnResize = false,
    this.enableColumnReorder = false,
    this.visibilityController,
    this.onRowTap,
    this.rowHeight,
    this.headerHeight,
    super.key,
  });

  /// 테이블 데이터 항목
  final List<T> items;

  /// 컬럼 정의
  final List<TableColumnDef<T, TSortField>> columns;

  /// 로딩 상태
  final bool isLoading;

  /// 정렬 상태
  final SortState<TSortField>? sortState;

  /// 정렬 변경 콜백
  final ValueChanged<SortState<TSortField>>? onSortChanged;

  /// 컬럼 리사이즈 활성화
  final bool enableColumnResize;

  /// 컬럼 리오더 활성화
  final bool enableColumnReorder;

  /// 가시성 컨트롤러
  final ColumnVisibilityController? visibilityController;

  /// 행 탭 콜백
  final ValueChanged<T>? onRowTap;

  /// 행 높이
  final double? rowHeight;

  /// 헤더 높이
  final double? headerHeight;

  @override
  Widget build(BuildContext context) {
    // 테이블 구현...
  }
}

/// 테이블 컬럼 정의
class TableColumnDef<T, TSortField> {
  const TableColumnDef({
    required this.id,
    required this.title,
    required this.cellBuilder,
    this.width,
    this.minWidth,
    this.maxWidth,
    this.sortField,
    this.isVisible = true,
  });

  final String id;
  final String title;
  final Widget Function(T item) cellBuilder;
  final double? width;
  final double? minWidth;
  final double? maxWidth;
  final TSortField? sortField;
  final bool isVisible;
}

6. Dialog Pattern#

import 'package:coui_flutter/coui_flutter.dart';

/// Show confirmation dialog — CoUI showDialog + Dialog (barrier · motion · panel come from the dialog style)
Future<bool?> showConfirmDialog(
  BuildContext context, {
  required String title,
  required String message,
  String? confirmText,
  String? cancelText,
}) {
  return showDialog<bool>(
    context: context,
    builder: (context) => Dialog(
      title: Text(title),
      content: Text(message),
      actions: [
        Button(
          variant: CoreButtonVariant.ghost,
          onPressed: () => Navigator.pop(context, false),
          child: Text(cancelText ?? '취소'),
        ),
        Button(
          onPressed: () => Navigator.pop(context, true),
          child: Text(confirmText ?? '확인'),
        ),
      ],
    ),
  );
}

CoUI 컴포넌트 기준 (coui_flutter 0.163)#

CoUI 는 컴포넌트마다 접두사 없는 위젯 하나를 내보낸다(Button · Badge · TextField · Icon · Gap). 모양은 variant: · size: 같은 enum 으로 고르고, 인스턴스 하나의 크롬은 그 컴포넌트의 스타일 슬롯 하나(buttonStyle · badgeStyle · textFieldStyle · iconStyle)로 바꾼다. 컴포넌트별 API 는 cc-coui:coui-index 가 1차 소스다.

부품쓰는 법쓰지 않는 것
ButtonButton(variant: CoreButtonVariant.ghost, size: …, shape: …)❌ CoButton, ❌ Button.primary(), ❌ ButtonStyle.ghost()
BadgeBadge(tone: CoreBadgeTone.success, variant: …, form: …) — 색은 tone, 세기는 variant❌ CoBadge, ❌ PrimaryBadge, ❌ CoreBadgeVariant.primary
IconIcon(LucideIcons.x) — 세트는 Lucide 하나❌ CoIcon, ❌ HeroIcon(HeroIcons.x), ❌ Material Icons.*
입력TextField(label:, prefix:, suffix:, variant: …)❌ CoInput, ❌ Input, ❌ FormTextField, ❌ InputFeature.*
간격const Gap.space16() / Gap(gapStyle: …)❌ CoGap, ❌ Gap.s4(), ❌ Spacing.s4, ❌ SizedBox(height: 16)
글자Text('…').titleMedium.onSurfaceVariant (역할 체인)❌ context.textStyles.lgSemibold, ❌ TextStyle(fontSize: 18)

공통 크기 enum CoreComponentSize: xs · sm · md · lg · xl · xl2. 간격 토큰 CoreSpace.spaceN 은 이름이 곧 픽셀 값이다.

컴포넌트 테마 (CoreTheme / CoreStyle)#

프로젝트 전역 컴포넌트 기본값은 CoreComponentTheme(ThemeData.fromCore(…, coreComponentTheme:))이다. 슬롯마다 Core<X>Theme 의 style 이 Core<X>Style 이다. 여기에는 크기와 무관한 컴포넌트 하나의 모양 결정만 둔다 — 밀도 · 형태 · 재질은 CoUIApp 의 테마 축이고(위 1절), 컴포넌트군별 반경 · 패딩을 테마로 다시 고르면 크기 사다리와 모서리가 따라오지 않는다. 아이콘 크기를 테마에 두지 않는다 — CoreIconTheme 의 크기는 버튼 안 아이콘까지 덮는다.

슬롯 이름(button · badge · card …)과 Core<X>Style 필드는 앱이 쓰는 CoUI 릴리스의 coui_core 계약(…/contracts/<component>/<component>_theme.dart)이 정본이다. 적용 전에 그 릴리스에서 확인한다.


Reference Files#

package/resources/lib/src/core/theme/app_theme.dart
package/resources/lib/src/core/extensions/app_spacing_context_extension.dart
package/resources/lib/src/widgets/table/table_builder.dart
package/resources/lib/src/widgets/dialog/confirm_dialog.dart

Checklist#

  • CoUI 배럴(package:coui_flutter/coui_flutter.dart) 하나 — UI 파일에 material.dart 없음
  • 브랜드는 CoreBrand 한 곳 — AppColorScheme 같은 색 상수 없음, 라이트 · 다크 · 고대비 두 벌을 같은 함수로
  • 타이포는 CoreTypography 역할 — 서체만 withFamily 로, 역할을 다시 정의하지 않음
  • 서페이스 스타일 · 밀도는 CoUIApp 인자 — 컴포넌트 테마로 흉내 내지 않음
  • CoreComponentTheme 에는 크기와 무관한 컴포넌트 하나의 모양 결정만
  • CoUI 와 같은 이름의 BuildContext 확장을 만들지 않음
  • Write KDoc comments on widgets
  • Place super.key as last parameter
  • Specify Type parameters when using Generic Types
  • Custom icon: verify no existing Assets.svg.* entry before adding a new SVG file
  • Custom icon: ran melos run generate:assets after adding/removing an SVG under assets/svg/

검색어를 입력하면 문서를 찾습니다.

↑ ↓ 이동 · Enter 열기Esc 닫기