feat: 글래스모피즘 디자인 시스템 및 색상 가이드 전면 적용

- @doc/color.md 가이드라인에 따른 색상 시스템 전면 개편
- 딥 블루(#2563EB), 스카이 블루(#60A5FA) 메인 컬러로 변경
- 모든 화면과 위젯에 글래스모피즘 효과 일관성 있게 적용
- darkNavy, navyGray 등 새로운 텍스트 색상 체계 도입
- 공통 스낵바 및 다이얼로그 컴포넌트 추가
- Claude AI 프로젝트 컨텍스트 파일(CLAUDE.md) 추가

영향받은 컴포넌트:
- 10개 스크린 (main, settings, detail, splash 등)
- 30개 이상 위젯 (buttons, cards, forms 등)
- 테마 시스템 (AppColors, AppTheme)

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
JiWoong Sul
2025-07-11 18:41:05 +09:00
parent 83c5e3d64e
commit 2f60ef585a
46 changed files with 1096 additions and 580 deletions

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import '../../../theme/app_colors.dart';
/// 위험한 액션에 사용되는 Danger 버튼
/// 삭제, 취소, 종료 등의 위험한 액션에 사용됩니다.
@@ -39,7 +40,7 @@ class DangerButton extends StatefulWidget {
class _DangerButtonState extends State<DangerButton> {
bool _isHovered = false;
static const Color _dangerColor = Color(0xFFDC2626);
static const Color _dangerColor = AppColors.dangerColor;
Future<void> _handlePress() async {
if (widget.requireConfirmation) {
@@ -62,7 +63,7 @@ class _DangerButtonState extends State<DangerButton> {
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: _dangerColor.withOpacity(0.1),
color: _dangerColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
@@ -98,7 +99,7 @@ class _DangerButtonState extends State<DangerButton> {
),
child: Text(
widget.text,
style: const TextStyle(color: Colors.white),
style: const TextStyle(color: AppColors.pureWhite),
),
),
],
@@ -126,14 +127,14 @@ class _DangerButtonState extends State<DangerButton> {
onPressed: widget.onPressed != null ? _handlePress : null,
style: ElevatedButton.styleFrom(
backgroundColor: _dangerColor,
foregroundColor: Colors.white,
foregroundColor: AppColors.pureWhite,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(widget.borderRadius),
),
padding: widget.padding ?? const EdgeInsets.symmetric(vertical: 16),
elevation: widget.enableHoverEffect && _isHovered ? 8 : 4,
shadowColor: _dangerColor.withOpacity(0.5),
disabledBackgroundColor: _dangerColor.withOpacity(0.6),
elevation: widget.enableHoverEffect && _isHovered ? 2 : 0,
shadowColor: Colors.black.withValues(alpha: 0.08),
disabledBackgroundColor: _dangerColor.withValues(alpha: 0.6),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
@@ -142,7 +143,7 @@ class _DangerButtonState extends State<DangerButton> {
if (widget.icon != null) ...[
Icon(
widget.icon,
color: Colors.white,
color: AppColors.pureWhite,
size: _isHovered ? 24 : 20,
),
const SizedBox(width: 8),
@@ -152,7 +153,7 @@ class _DangerButtonState extends State<DangerButton> {
style: TextStyle(
fontSize: widget.fontSize,
fontWeight: FontWeight.w600,
color: Colors.white,
color: AppColors.pureWhite,
),
),
],

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import '../../../theme/app_colors.dart';
/// 주요 액션에 사용되는 Primary 버튼
/// 저장, 추가, 확인 등의 주요 액션에 사용됩니다.
@@ -43,7 +44,7 @@ class _PrimaryButtonState extends State<PrimaryButton> {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final effectiveBackgroundColor = widget.backgroundColor ?? theme.primaryColor;
final effectiveForegroundColor = widget.foregroundColor ?? Colors.white;
final effectiveForegroundColor = widget.foregroundColor ?? AppColors.pureWhite;
Widget button = AnimatedContainer(
duration: const Duration(milliseconds: 200),
@@ -61,9 +62,9 @@ class _PrimaryButtonState extends State<PrimaryButton> {
borderRadius: BorderRadius.circular(widget.borderRadius),
),
padding: widget.padding ?? const EdgeInsets.symmetric(vertical: 16),
elevation: widget.enableHoverEffect && _isHovered ? 8 : 4,
shadowColor: effectiveBackgroundColor.withOpacity(0.5),
disabledBackgroundColor: effectiveBackgroundColor.withOpacity(0.6),
elevation: widget.enableHoverEffect && _isHovered ? 2 : 0,
shadowColor: Colors.black.withValues(alpha: 0.08),
disabledBackgroundColor: effectiveBackgroundColor.withValues(alpha: 0.6),
),
child: widget.isLoading
? SizedBox(

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import '../../../theme/app_colors.dart';
/// 부차적인 액션에 사용되는 Secondary 버튼
/// 취소, 되돌아가기, 부가 옵션 등에 사용됩니다.
@@ -42,10 +43,8 @@ class _SecondaryButtonState extends State<SecondaryButton> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final effectiveBorderColor = widget.borderColor ??
theme.colorScheme.onSurface.withOpacity(0.2);
final effectiveTextColor = widget.textColor ??
theme.colorScheme.onSurface.withOpacity(0.8);
final effectiveBorderColor = widget.borderColor ?? AppColors.secondaryColor;
final effectiveTextColor = widget.textColor ?? AppColors.primaryColor;
Widget button = AnimatedContainer(
duration: const Duration(milliseconds: 200),
@@ -63,7 +62,7 @@ class _SecondaryButtonState extends State<SecondaryButton> {
),
side: BorderSide(
color: _isHovered
? effectiveBorderColor.withOpacity(0.4)
? effectiveBorderColor.withValues(alpha: 0.4)
: effectiveBorderColor,
width: widget.borderWidth,
),
@@ -72,7 +71,7 @@ class _SecondaryButtonState extends State<SecondaryButton> {
horizontal: 24,
),
backgroundColor: _isHovered
? theme.colorScheme.onSurface.withOpacity(0.05)
? AppColors.glassBackground
: Colors.transparent,
),
child: Row(
@@ -142,13 +141,13 @@ class _TextLinkButtonState extends State<TextLinkButton> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final effectiveColor = widget.color ?? theme.colorScheme.primary;
final effectiveColor = widget.color ?? AppColors.primaryColor;
Widget button = AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
color: _isHovered
? theme.colorScheme.onSurface.withOpacity(0.05)
? theme.colorScheme.onSurface.withValues(alpha: 0.05)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
),

View File

@@ -36,7 +36,7 @@ class SectionCard extends StatelessWidget {
final effectiveBackgroundColor = backgroundColor ?? Colors.white;
final effectiveShadow = boxShadow ?? [
BoxShadow(
color: Colors.black.withOpacity(0.05),
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
@@ -116,7 +116,7 @@ class TransparentSectionCard extends StatelessWidget {
Widget card = Container(
margin: margin,
decoration: BoxDecoration(
color: Colors.white.withOpacity(opacity),
color: Colors.white.withValues(alpha: opacity),
borderRadius: BorderRadius.circular(borderRadius),
border: borderColor != null
? Border.all(color: borderColor!, width: 1)
@@ -134,7 +134,7 @@ class TransparentSectionCard extends StatelessWidget {
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white.withOpacity(0.9),
color: Colors.white.withValues(alpha: 0.9),
),
),
const SizedBox(height: 12),
@@ -207,7 +207,7 @@ class InfoCard extends StatelessWidget {
label,
style: TextStyle(
fontSize: 14,
color: theme.colorScheme.onSurface.withOpacity(0.6),
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
),
const SizedBox(height: 4),

View File

@@ -53,7 +53,7 @@ class ConfirmationDialog extends StatelessWidget {
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: (iconColor ?? effectiveConfirmColor).withOpacity(0.1),
color: (iconColor ?? effectiveConfirmColor).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
@@ -163,7 +163,7 @@ class SuccessDialog extends StatelessWidget {
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
color: Colors.green.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: const Icon(
@@ -271,7 +271,7 @@ class ErrorDialog extends StatelessWidget {
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
color: Colors.red.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: const Icon(

View File

@@ -27,7 +27,7 @@ class LoadingOverlay extends StatelessWidget {
child,
if (isLoading)
Container(
color: (backgroundColor ?? Colors.black).withOpacity(opacity),
color: (backgroundColor ?? Colors.black).withValues(alpha: opacity),
child: Center(
child: Container(
padding: const EdgeInsets.all(24),
@@ -36,7 +36,7 @@ class LoadingOverlay extends StatelessWidget {
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
@@ -193,7 +193,7 @@ class _CustomLoadingIndicatorState extends State<CustomLoadingIndicator>
width: widget.size / 5,
height: widget.size / 5,
decoration: BoxDecoration(
color: effectiveColor.withOpacity(0.3 + value * 0.7),
color: effectiveColor.withValues(alpha: 0.3 + value * 0.7),
shape: BoxShape.circle,
),
);
@@ -212,7 +212,7 @@ class _CustomLoadingIndicatorState extends State<CustomLoadingIndicator>
height: widget.size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: effectiveColor.withOpacity(0.3),
color: effectiveColor.withValues(alpha: 0.3),
),
child: Center(
child: Container(
@@ -220,7 +220,7 @@ class _CustomLoadingIndicatorState extends State<CustomLoadingIndicator>
height: widget.size * (0.3 + _animation.value * 0.5),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: effectiveColor.withOpacity(1 - _animation.value),
color: effectiveColor.withValues(alpha: 1 - _animation.value),
),
),
),

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../theme/app_colors.dart';
/// 공통 텍스트 필드 위젯
/// 프로젝트 전체에서 일관된 스타일의 텍스트 입력 필드를 제공합니다.
@@ -68,7 +69,7 @@ class BaseTextField extends StatelessWidget {
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
color: AppColors.textSecondary,
),
),
const SizedBox(height: 8),
@@ -91,18 +92,18 @@ class BaseTextField extends StatelessWidget {
cursorColor: cursorColor ?? theme.primaryColor,
style: style ?? TextStyle(
fontSize: 16,
color: theme.colorScheme.onSurface,
color: AppColors.textPrimary,
),
decoration: InputDecoration(
hintText: hintText,
hintStyle: TextStyle(
color: theme.colorScheme.onSurface.withOpacity(0.6),
color: AppColors.textMuted,
),
prefixIcon: prefixIcon,
prefixText: prefixText,
suffixIcon: suffixIcon,
filled: true,
fillColor: fillColor ?? Colors.white,
fillColor: fillColor ?? AppColors.glassBackground,
contentPadding: contentPadding ?? const EdgeInsets.all(16),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
@@ -117,7 +118,10 @@ class BaseTextField extends StatelessWidget {
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
borderSide: BorderSide(
color: AppColors.textSecondary,
width: 1,
),
),
disabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),

View File

@@ -68,7 +68,6 @@ class DatePickerField extends StatelessWidget {
surface: Colors.white,
onSurface: Colors.black,
),
dialogBackgroundColor: Colors.white,
),
child: child!,
);
@@ -98,7 +97,7 @@ class DatePickerField extends StatelessWidget {
fontSize: 16,
color: enabled
? theme.colorScheme.onSurface
: theme.colorScheme.onSurface.withOpacity(0.5),
: theme.colorScheme.onSurface.withValues(alpha: 0.5),
),
),
),
@@ -106,8 +105,8 @@ class DatePickerField extends StatelessWidget {
Icons.calendar_today,
size: 20,
color: enabled
? theme.colorScheme.onSurface.withOpacity(0.6)
: theme.colorScheme.onSurface.withOpacity(0.3),
? theme.colorScheme.onSurface.withValues(alpha: 0.6)
: theme.colorScheme.onSurface.withValues(alpha: 0.3),
),
],
),
@@ -214,7 +213,6 @@ class _DateRangeItem extends StatelessWidget {
surface: Colors.white,
onSurface: Colors.black,
),
dialogBackgroundColor: Colors.white,
),
child: child!,
);
@@ -239,7 +237,7 @@ class _DateRangeItem extends StatelessWidget {
label,
style: TextStyle(
fontSize: 12,
color: theme.colorScheme.onSurface.withOpacity(0.6),
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
),
const SizedBox(height: 4),
@@ -252,7 +250,7 @@ class _DateRangeItem extends StatelessWidget {
fontWeight: FontWeight.w500,
color: date != null
? theme.colorScheme.onSurface
: theme.colorScheme.onSurface.withOpacity(0.4),
: theme.colorScheme.onSurface.withValues(alpha: 0.4),
),
),
],

View File

@@ -0,0 +1,272 @@
import 'package:flutter/material.dart';
import '../../../theme/app_colors.dart';
/// 앱 전체에서 사용되는 통합 스낵바
/// 성공, 에러, 정보 등 다양한 타입의 메시지를 표시합니다.
class AppSnackBar {
/// 성공 메시지를 표시합니다.
static void showSuccess({
required BuildContext context,
required String message,
IconData icon = Icons.check_circle_rounded,
Duration duration = const Duration(seconds: 3),
bool showAtTop = true,
}) {
_show(
context: context,
message: message,
icon: icon,
backgroundColor: AppColors.successColor,
iconColor: AppColors.pureWhite,
textColor: AppColors.pureWhite,
duration: duration,
showAtTop: showAtTop,
);
}
/// 에러 메시지를 표시합니다.
static void showError({
required BuildContext context,
required String message,
IconData icon = Icons.error_rounded,
Duration duration = const Duration(seconds: 4),
bool showAtTop = true,
}) {
_show(
context: context,
message: message,
icon: icon,
backgroundColor: AppColors.dangerColor,
iconColor: AppColors.pureWhite,
textColor: AppColors.pureWhite,
duration: duration,
showAtTop: showAtTop,
);
}
/// 정보 메시지를 표시합니다.
static void showInfo({
required BuildContext context,
required String message,
IconData icon = Icons.info_rounded,
Duration duration = const Duration(seconds: 3),
bool showAtTop = true,
}) {
_show(
context: context,
message: message,
icon: icon,
backgroundColor: AppColors.primaryColor,
iconColor: AppColors.pureWhite,
textColor: AppColors.pureWhite,
duration: duration,
showAtTop: showAtTop,
);
}
/// 경고 메시지를 표시합니다.
static void showWarning({
required BuildContext context,
required String message,
IconData icon = Icons.warning_amber_rounded,
Duration duration = const Duration(seconds: 3),
bool showAtTop = true,
}) {
_show(
context: context,
message: message,
icon: icon,
backgroundColor: AppColors.warningColor,
iconColor: AppColors.pureWhite,
textColor: AppColors.pureWhite,
duration: duration,
showAtTop: showAtTop,
);
}
/// 커스텀 스낵바를 표시합니다.
static void showCustom({
required BuildContext context,
required String message,
required IconData icon,
required Color backgroundColor,
Color iconColor = AppColors.pureWhite,
Color textColor = AppColors.pureWhite,
Duration duration = const Duration(seconds: 3),
bool showAtTop = true,
SnackBarAction? action,
}) {
_show(
context: context,
message: message,
icon: icon,
backgroundColor: backgroundColor,
iconColor: iconColor,
textColor: textColor,
duration: duration,
showAtTop: showAtTop,
action: action,
);
}
/// 내부적으로 스낵바를 표시하는 메서드
static void _show({
required BuildContext context,
required String message,
required IconData icon,
required Color backgroundColor,
required Color iconColor,
required Color textColor,
required Duration duration,
required bool showAtTop,
SnackBarAction? action,
}) {
// 기존 스낵바 제거
ScaffoldMessenger.of(context).hideCurrentSnackBar();
// 새 스낵바 표시
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
// 아이콘
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: iconColor.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: iconColor,
size: 20,
),
),
const SizedBox(width: 12),
// 메시지
Expanded(
child: Text(
message,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: textColor,
height: 1.3,
),
),
),
],
),
backgroundColor: backgroundColor,
behavior: SnackBarBehavior.floating,
margin: showAtTop
? EdgeInsets.only(
top: MediaQuery.of(context).padding.top + 16,
left: 16,
right: 16,
bottom: MediaQuery.of(context).size.height - 120,
)
: const EdgeInsets.all(16),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 4,
duration: duration,
dismissDirection: DismissDirection.horizontal,
action: action,
),
);
}
/// 로딩 스낵바를 표시합니다. (자동으로 사라지지 않음)
static ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showLoading({
required BuildContext context,
required String message,
bool showAtTop = true,
}) {
// 기존 스낵바 제거
ScaffoldMessenger.of(context).hideCurrentSnackBar();
return ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
// 로딩 인디케이터
Container(
width: 24,
height: 24,
margin: const EdgeInsets.only(right: 12),
child: CircularProgressIndicator(
strokeWidth: 2.5,
color: AppColors.pureWhite,
),
),
// 메시지
Expanded(
child: Text(
message,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.pureWhite,
),
),
),
],
),
backgroundColor: AppColors.primaryColor,
behavior: SnackBarBehavior.floating,
margin: showAtTop
? EdgeInsets.only(
top: MediaQuery.of(context).padding.top + 16,
left: 16,
right: 16,
bottom: MediaQuery.of(context).size.height - 120,
)
: const EdgeInsets.all(16),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 4,
duration: const Duration(days: 365), // 자동으로 사라지지 않음
dismissDirection: DismissDirection.none, // 스와이프로 닫을 수 없음
),
);
}
/// 액션 버튼이 있는 스낵바를 표시합니다.
static void showWithAction({
required BuildContext context,
required String message,
required String actionLabel,
required VoidCallback onActionPressed,
IconData icon = Icons.info_rounded,
Color backgroundColor = AppColors.primaryColor,
Duration duration = const Duration(seconds: 4),
bool showAtTop = true,
}) {
_show(
context: context,
message: message,
icon: icon,
backgroundColor: backgroundColor,
iconColor: AppColors.pureWhite,
textColor: AppColors.pureWhite,
duration: duration,
showAtTop: showAtTop,
action: SnackBarAction(
label: actionLabel,
textColor: AppColors.pureWhite,
onPressed: onActionPressed,
),
);
}
}