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등
안에서 무슨 일이 벌어지나요#
요청한 부품 종류에 맞춰 정해진 패턴대로 코드를 만들고, 마지막에 빠진 게 없는지 점검합니다.
-
테마 만들기 — 브랜드 시드를 정하면 CoUI 가 밝은 · 어두운 · 고대비 색을 만들고, 제목 서체가 있으면 그 역할에만 입힌 뒤, 앱 루트(
CoUIApp)에 넘겨 앱 전체 테마(AppTheme)로 완성합니다. - 공통 위젯 만들기 — 표·팝업창 같은 재사용 부품을 정해진 구조로 만듭니다. (예: 정렬·컬럼 크기조절·순서변경이 되는 범용 표)
-
편의 단축 코드 만들기 — CoUI 에 없는 앱 고유 값만 짧게 접근하도록, CoUI 와 겹치지 않는 이름의 확장 메서드를 추가합니다(테마 · 색 · 글꼴은 CoUI 의
context.theme·context.colorScheme·context.typography). -
CoUI 표준 확인 — 버튼·뱃지·아이콘 같은 부품은 CoUI 의 현행 방식(
Button(variant: ...)·Badge(tone: ...)·Icon(LucideIcons.x), 접두어 없음)에 맞는지 확인하고 적용합니다. - 마무리 점검 — 밝은/어두운 테마가 모두 정의됐는지, 주석이 달렸는지 등 체크리스트로 빠진 부분을 확인합니다.
⚙️ 상세 옵션·실행 명세 (개발자 / 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:resourcesActivated when command is invoked- Invoked during UI component, theme, and extension method work
Parameters#
| Parameter | Required | Description |
|---|---|---|
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:
- Export the icon from Figma as SVG
- Add it to
package/resources/assets/svg/(checkassets.gen.dartfirst — don't duplicate an existing entry) - Regenerate:
melos run generate:assets(re-runsflutter_gen_runner) - 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차 소스다.
| 부품 | 쓰는 법 | 쓰지 않는 것 |
|---|---|---|
| Button | Button(variant: CoreButtonVariant.ghost, size: …, shape: …) | ❌ CoButton, ❌ Button.primary(), ❌ ButtonStyle.ghost() |
| Badge | Badge(tone: CoreBadgeTone.success, variant: …, form: …) — 색은 tone, 세기는 variant | ❌ CoBadge, ❌ PrimaryBadge, ❌ CoreBadgeVariant.primary |
| Icon | Icon(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.dartChecklist#
- 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:assetsafter adding/removing an SVG underassets/svg/