Banner#
페이지 상단에 중요한 정보, 경고, 오류 등을 표시하는 배너 컴포넌트입니다. Banner는 Flutter와 Web에서 동일한 named properties API를 제공하며,
default/info/success/warning/destructive
5개 variant를 지원합니다.
Live Preview#
class BannerInfoExample extends StatefulComponent {
const BannerInfoExample({super.key});
@override
State<BannerInfoExample> createState() => _BannerInfoExampleState();
}
class _BannerInfoExampleState extends State<BannerInfoExample> {
bool _show = true;
@override
Component build(BuildContext context) {
if (_show) {
return Banner.info(
message: const Text('새로운 업데이트가 준비되었습니다.'),
onDismiss: () => setState(() => _show = false),
);
}
return Button(
variant: CoreButtonVariant.outline,
onPressed: () => setState(() => _show = true),
child: const Text('Show banner'),
);
}
}
class BannerInfoExample extends StatefulWidget {
const BannerInfoExample({super.key});
@override
State<BannerInfoExample> createState() => _BannerInfoExampleState();
}
class _BannerInfoExampleState extends State<BannerInfoExample> {
bool _show = true;
@override
Widget build(BuildContext context) {
if (_show) {
return Banner.info(
message: const Text('새로운 업데이트가 준비되었습니다.'),
onDismiss: () => setState(() => _show = false),
);
}
return Button(
variant: CoreButtonVariant.outline,
onPressed: () => setState(() => _show = true),
child: const Text('Show banner'),
);
}
}
class BannerWarningExample extends StatefulComponent {
const BannerWarningExample({super.key});
@override
State<BannerWarningExample> createState() => _BannerWarningExampleState();
}
class _BannerWarningExampleState extends State<BannerWarningExample> {
bool _show = true;
@override
Component build(BuildContext context) {
if (_show) {
return Banner.warning(
message: const Text('세션이 5분 후에 만료됩니다.'),
onDismiss: () => setState(() => _show = false),
);
}
return Button(
variant: CoreButtonVariant.outline,
onPressed: () => setState(() => _show = true),
child: const Text('Show banner'),
);
}
}
class BannerWarningExample extends StatefulWidget {
const BannerWarningExample({super.key});
@override
State<BannerWarningExample> createState() => _BannerWarningExampleState();
}
class _BannerWarningExampleState extends State<BannerWarningExample> {
bool _show = true;
@override
Widget build(BuildContext context) {
if (_show) {
return Banner.warning(
message: const Text('세션이 5분 후에 만료됩니다.'),
onDismiss: () => setState(() => _show = false),
);
}
return Button(
variant: CoreButtonVariant.outline,
onPressed: () => setState(() => _show = true),
child: const Text('Show banner'),
);
}
}
class BannerDestructiveExample extends StatefulComponent {
const BannerDestructiveExample({super.key});
@override
State<BannerDestructiveExample> createState() =>
_BannerDestructiveExampleState();
}
class _BannerDestructiveExampleState extends State<BannerDestructiveExample> {
bool _show = true;
@override
Component build(BuildContext context) {
if (_show) {
return Banner.destructive(
message: const Text('요청을 처리하는 중 문제가 발생했습니다.'),
onDismiss: () => setState(() => _show = false),
);
}
return Button(
variant: CoreButtonVariant.outline,
onPressed: () => setState(() => _show = true),
child: const Text('Show banner'),
);
}
}
class BannerDestructiveExample extends StatefulWidget {
const BannerDestructiveExample({super.key});
@override
State<BannerDestructiveExample> createState() =>
_BannerDestructiveExampleState();
}
class _BannerDestructiveExampleState extends State<BannerDestructiveExample> {
bool _show = true;
@override
Widget build(BuildContext context) {
if (_show) {
return Banner.destructive(
message: const Text('요청을 처리하는 중 문제가 발생했습니다.'),
onDismiss: () => setState(() => _show = false),
);
}
return Button(
variant: CoreButtonVariant.outline,
onPressed: () => setState(() => _show = true),
child: const Text('Show banner'),
);
}
}
class BannerSuccessExample extends StatefulComponent {
const BannerSuccessExample({super.key});
@override
State<BannerSuccessExample> createState() => _BannerSuccessExampleState();
}
class _BannerSuccessExampleState extends State<BannerSuccessExample> {
bool _show = true;
@override
Component build(BuildContext context) {
if (_show) {
return Banner.success(
message: const Text('변경 사항이 성공적으로 저장되었습니다.'),
onDismiss: () => setState(() => _show = false),
);
}
return Button(
variant: CoreButtonVariant.outline,
onPressed: () => setState(() => _show = true),
child: const Text('Show banner'),
);
}
}
class BannerSuccessExample extends StatefulWidget {
const BannerSuccessExample({super.key});
@override
State<BannerSuccessExample> createState() => _BannerSuccessExampleState();
}
class _BannerSuccessExampleState extends State<BannerSuccessExample> {
bool _show = true;
@override
Widget build(BuildContext context) {
if (_show) {
return Banner.success(
message: const Text('변경 사항이 성공적으로 저장되었습니다.'),
onDismiss: () => setState(() => _show = false),
);
}
return Button(
variant: CoreButtonVariant.outline,
onPressed: () => setState(() => _show = true),
child: const Text('Show banner'),
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 페이지 전체에 영향을 미치는 시스템 상태 메시지를 표시할 때 (점검 공지, 서비스 알림)
- 사용자가 취해야 할 중요한 액션을 안내할 때 (세션 만료 경고, 구독 만료 알림)
- 사용자가 방금 완료한 작업의 결과를 페이지 상단에 지속적으로 표시할 때
대신 다른 컴포넌트를 사용하세요:
Toast: 일시적으로 나타났다가 자동으로 사라지는 알림이 필요할 때Alert: 특정 콘텐츠 영역 내에 인라인으로 표시하는 경고 메시지가 필요할 때Dialog: 사용자의 즉각적인 응답이 필요한 중요한 알림에는 모달 다이얼로그 사용
기본 사용법 (Basic Usage)#
CoUI는 Flutter와 Web에서 동일한 API를 제공합니다. 아래 예제 코드는 양쪽 플랫폼에서 그대로 사용할 수 있습니다.
// 정보 배너
Banner.info(
message: Text('시스템 점검이 예정되어 있습니다.'),
)
// 성공 배너 (해제 버튼 포함)
Banner.success(
message: Text('저장이 완료되었습니다.'),
onDismiss: handleDismiss,
)
// 경고 배너 (액션 포함)
Banner.warning(
message: Text('세션이 곧 만료됩니다.'),
description: Text('지금 연장하면 작업을 이어서 할 수 있습니다.'),
action: Button(
variant: CoreButtonVariant.outline,
onPressed: handleExtendSession,
child: Text('연장하기'),
),
onDismiss: handleDismiss,
)
// 오류 배너
Banner.destructive(
message: Text('요청을 처리하는 중 오류가 발생했습니다.'),
onDismiss: handleDismiss,
)
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
message |
Widget / Component |
필수 | 배너에 표시할 메시지 |
description |
Widget? / Component? |
null |
메시지 아래에 표시되는 선택적 보조 설명 |
variant |
CoreBannerVariant |
info |
배너 변형 (
default_
,
info
,
success
,
warning
,
destructive
)
|
onDismiss |
VoidCallback? / CoreVoidCallback? |
null |
닫기 버튼 클릭 핸들러. null이면 닫기 버튼 미표시 |
action |
Widget? / Component? |
null |
배너 우측에 표시할 액션 위젯 |
showIcon |
bool |
true |
variant 별 기본 아이콘 표시 여부 |
bannerStyle |
CoreBannerStyle? |
null |
인스턴스 스타일 (Style 시스템 참조) |
스타일 시스템 (Style System)#
Banner 의 모든 chrome / dimensional / nested-slot 오버라이드는 CoreBannerStyle 단일 슬롯으로 흐릅니다 (Epic #1302 원칙 6/7/8, Phase 6f 팬텀 generic 제거). 시맨틱 enum (variant) 과 behaviour (message
/ description / action / onDismiss / showIcon) 는 위젯 파라미터로 직접 전달합니다.
시맨틱 vs 스타일#
-
시맨틱 enum / behaviour: 위젯/컴포넌트 파라미터로 직접 (
variant,message,description,action,onDismiss,showIcon) -
chrome / dimensional / 슬롯 스타일:
CoreBannerStyle한 곳으로 (backgroundColor/foregroundColor/borderColor/borderWidth/borderRadius/padding/iconContentGapStyle/contentDismissGapStyle/titleDescriptionGapStyle/titleStyle/descriptionStyle/iconStyle/actionButtonStyle/closeButtonStyle— 닫기 버튼의 패딩·모서리·아이콘·hover 전환은 전부 nestedcloseButtonStyle슬롯)
Resolve chain#
design system default for banner
→ CoreBannerTheme.style // 프로젝트 공통
→ parent component slot override
→ widget.bannerStyle // 인스턴스별
각 nested 슬롯 스타일 (titleStyle / descriptionStyle / iconStyle / actionButtonStyle
/ closeButtonStyle) 은 자기 컴포넌트의 자체 resolve chain 으로 다시 한 번 머지됩니다.
슬롯 매핑 (CoreBannerStyle 필드)#
Epic #1696 Phase 3 에서 옛 CoreBannerTokens (raw double 보관 holder) 가 폐기되고, 모든 chrome 이 CoreBannerStyle
슬롯 + static const default* 로 흡수됨.
| 필드 | 타입 | Default | 적용 영역 |
|---|---|---|---|
backgroundColor |
CoreColor? |
defaultsByVariant[v].background |
배너 배경 색 (variant 기본값 위에 오버라이드) |
foregroundColor |
CoreColor? |
defaultsByVariant[v].textColor |
메시지 / 설명 / 아이콘 전경 색 |
borderColor |
CoreColor? |
defaultsByVariant[v].borderColor |
좌측 액센트 보더 색 |
borderWidth |
double? |
defaultBorderWidth (4px) |
좌측 보더 두께 (px) |
borderRadius |
CoreBorderRadius? |
defaultBorderRadius (16px all) |
모서리 반지름 |
padding |
CoreEdgeInsets? |
defaultPadding (h=16, v=12) |
패딩 |
iconContentGapStyle |
CoreGapStyle? |
defaultIconContentGapStyle (12px) |
아이콘 / 콘텐츠 / 액션 간격 |
contentDismissGapStyle |
CoreGapStyle? |
defaultContentDismissGapStyle (8px) |
닫기 버튼 앞 gap |
titleDescriptionGapStyle |
CoreGapStyle? |
defaultTitleDescriptionGapStyle (4px) |
메시지 ↔ 설명 수직 gap |
titleStyle |
CoreTextStyle? |
labelLarge | 메시지 텍스트 스타일 |
descriptionStyle |
CoreTextStyle? |
bodySmall | 설명 텍스트 스타일 |
iconStyle |
CoreIconStyle? |
(variant icon color, 24px) | variant 아이콘 스타일 |
actionButtonStyle |
CoreButtonStyle? |
— | 액션 버튼 chrome 슬롯 |
closeButtonStyle |
CoreButtonStyle? |
defaultCloseButtonStyle (padding 4px / full round / 150ms / 20px glyph) |
닫기 버튼 chrome 슬롯 —
Button(variant: .plain)
으로 raw-forward. 옛 평면
dismissIconStyle
/
dismissButtonPadding
/
dismissBorderRadius
/
dismissButtonOpacity
/
dismissButtonHoverOpacity
/
transitionDuration
는 이 슬롯으로 흡수 (
padding
/
borderRadius
/
leadingIconStyle
/
foregroundColor
·
hoverForegroundColor
/
animationDuration
)
|
Migration — 옛 평면 chrome → 새 위치 매핑#
기존 CoreBannerTheme 의 평면 chrome (backgroundColor / borderColor /
borderRadius) 은 호환을 위해 유지되지만, 새 코드는 bannerStyle 슬롯을 사용하세요:
| 기존 (legacy theme 필드) | 새 위치 |
|---|---|
CoreBannerTheme.backgroundColor |
bannerStyle.backgroundColor 또는 CoreBannerTheme.style.backgroundColor |
CoreBannerTheme.borderColor |
bannerStyle.borderColor 또는 CoreBannerTheme.style.borderColor |
CoreBannerTheme.borderRadius |
bannerStyle.borderRadius 또는 CoreBannerTheme.style.borderRadius |
| 인스턴스 padding 오버라이드 (불가능) | bannerStyle.padding |
| 인스턴스 메시지 텍스트 스타일 (불가능) | bannerStyle.titleStyle |
| 인스턴스 설명 텍스트 스타일 (불가능) | bannerStyle.descriptionStyle |
| 인스턴스 아이콘 사이즈/색 (불가능) | bannerStyle.iconStyle |
옛 평면 닫기 버튼 chrome (
dismissIconStyle
/
dismissButtonPadding
/
dismissBorderRadius
/
dismissButtonOpacity
/
dismissButtonHoverOpacity
/
transitionDuration
)
|
bannerStyle.closeButtonStyle
(
leadingIconStyle
/
padding
/
borderRadius
/
foregroundColor
·
hoverForegroundColor
/
animationDuration
)
|
사용 예 (Flutter)#
Banner.warning(
message: Text('세션이 곧 만료됩니다.'),
description: Text('지금 연장하면 작업을 이어서 할 수 있습니다.'),
bannerStyle: CoreBannerStyle(
padding: CoreEdgeInsets.symmetric(horizontal: 24, vertical: 16),
borderRadius: CoreBorderRadius.all(12),
titleStyle: CoreTextStyle(fontSize: 14, fontWeight: 600),
descriptionStyle: CoreTextStyle(fontSize: 12),
iconStyle: CoreIconStyle(size: 20),
),
onDismiss: handleDismiss,
)
사용 예 (Web)#
Banner.warning(
message: text('세션이 곧 만료됩니다.'),
description: text('지금 연장하면 작업을 이어서 할 수 있습니다.'),
bannerStyle: CoreBannerStyle(
padding: CoreEdgeInsets.symmetric(horizontal: 24, vertical: 16),
borderRadius: CoreBorderRadius.all(12),
),
onDismiss: handleDismiss,
)
변형 (Variants)#
Info#
일반적인 정보 전달에 사용합니다.
Banner.info(
message: Text('새로운 업데이트가 있습니다.'),
)
Success#
성공적인 작업 완료를 알릴 때 사용합니다.
Banner.success(
message: Text('변경 사항이 저장되었습니다.'),
)
Warning#
사용자의 주의가 필요한 상황에 사용합니다.
Banner.warning(
message: Text('구독이 7일 후 만료됩니다.'),
)
Destructive (Error)#
오류 또는 실패 상태를 알릴 때 사용합니다.
Banner.destructive(
message: Text('네트워크 연결에 실패했습니다.'),
)
동작 스펙 (Behavior)#
인터랙션#
- 닫기 버튼 클릭:
onDismiss콜백 실행. 배너 숨김 처리는 부모 위젯에서 상태로 관리 - 액션 버튼 클릭:
action위젯에 정의된 콜백 실행 - 호버: 닫기 버튼 opacity 상승
상태 전환#
- 배너 표시: 슬라이드 다운 또는 페이드인 (부모에서
AnimatedSwitcher사용 권장) - 배너 닫기:
onDismiss호출 후 부모에서 상태를 변경하여 제거
테마 오버라이드#
프로젝트 레벨에서 CoreComponentTheme.banner로 기본값을 재정의할 수 있습니다.
CoreComponentTheme(
banner: CoreBannerTheme(
style: CoreBannerStyle(
backgroundColor: CoreColor(0xFFF8F9FA),
borderColor: CoreColor(0xFF6366F1),
borderRadius: CoreBorderRadius.all(12),
),
),
)
사용 가이드라인 (Usage Guidelines)#
✅ Do#
중요도에 맞는 variant 사용
// 시스템 점검 공지: info
Banner.info(
message: Text('2026-03-15 02:00~04:00 시스템 점검이 예정되어 있습니다.'),
)
// 보안 경고: warning
Banner.warning(
message: Text('비밀번호를 90일 이상 변경하지 않았습니다.'),
action: TextButton(
onPressed: handleChangePassword,
child: Text('변경하기'),
),
)
올바른 variant를 사용하면 사용자가 메시지의 중요도를 즉시 파악할 수 있습니다.
❌ Don't#
여러 배너를 동시에 표시 금지
// ❌ 여러 배너 동시 표시
Column(
children: [
Banner.info(message: Text('점검 예정')),
Banner.success(message: Text('저장 완료')),
Banner.warning(message: Text('세션 만료')),
],
)
여러 배너가 동시에 표시되면 사용자에게 압도적인 느낌을 주고 콘텐츠 영역을 지나치게 차지합니다. 가장 중요한 하나만 표시하세요.
✅ Do#
해제 가능한 배너에는 항상 onDismiss 제공
Banner.info(
message: Text('새로운 기능이 추가되었습니다. 지금 확인해보세요!'),
onDismiss: handleDismissBanner,
action: TextButton(
onPressed: handleLearnMore,
child: Text('자세히 보기'),
),
)
사용자가 불필요한 배너를 닫을 수 있어야 콘텐츠에 집중할 수 있습니다.
❌ Don't#
destructive variant를 일반 정보 표시에 남용 금지
// ❌ 단순 안내에 destructive variant 사용
Banner.destructive(
message: Text('이 기능은 프리미엄 회원 전용입니다.'),
)
destructive variant는 실제 오류 상황에만 사용해야 합니다. 남용하면 사용자가 진짜 오류를 놓칠 수 있습니다.
✅ Do#
액션이 필요한 배너에는 action 버튼을 추가하세요.
Banner.warning(
message: Text('구독이 3일 후 만료됩니다.'),
action: TextButton(
onPressed: handleRenewSubscription,
child: Text('갱신하기'),
),
onDismiss: handleDismiss,
)
사용자가 즉각적인 조치를 취할 수 있도록 명확한 액션 버튼을 제공하면 전환율이 높아집니다.
접근성 (Accessibility)#
키보드 인터랙션#
| 키 | 동작 |
|---|---|
Tab | 배너 내 인터랙티브 요소(액션 버튼, 닫기 버튼)로 이동 |
Enter / Space | 포커스된 버튼 활성화 |
Escape | 닫기 버튼이 있는 경우 배너 닫기 |
스크린 리더#
- Flutter:
Semantics(container: true, label: 'Banner')적용 - Web:
role="banner"속성 자동 적용
크로스 플랫폼 차이점 (Platform Differences)#
Banner는 Flutter와 Web에서 동일한 named properties API를 사용합니다. 플랫폼별로 타입이 다른 부분만 차이가 있습니다.
| 항목 | Flutter | Web |
|---|---|---|
message / description / action 타입 |
Widget |
Component |
onDismiss 콜백 타입 |
VoidCallback? |
CoreVoidCallback? |
| 배치 방식 | Column 최상단 또는 Scaffold 내 삽입 |
페이지 상단 고정 (position: sticky) |
| 애니메이션 | AnimatedSwitcher 등 Flutter 애니메이션 | CSS transition |