UI 전체 리디자인 및 개선사항 적용
## 주요 변경사항: ### UI/UX 개선 - shadcn/ui 스타일 기반의 새로운 디자인 시스템 도입 - 모든 주요 화면에 대한 리디자인 구현 완료 - 로그인 화면: 모던한 카드 스타일 적용 - 대시보드: 통계 카드와 차트를 활용한 개요 화면 - 리스트 화면들: 일관된 테이블 디자인과 검색/필터 기능 - 다크모드 지원을 위한 테마 시스템 구축 ### 기능 개선 - Equipment List: 고급 필터링 (상태, 담당자별) - Company List: 검색 및 정렬 기능 강화 - User List: 역할별 필터링 추가 - License List: 만료일 기반 상태 표시 - Warehouse Location: 재고 수준 시각화 ### 기술적 개선 - 재사용 가능한 컴포넌트 라이브러리 구축 - 일관된 코드 패턴 가이드라인 작성 - 프로젝트 구조 분석 및 문서화 ### 문서화 - 프로젝트 분석 문서 추가 - UI 리디자인 진행 상황 문서 - 코드 패턴 가이드 작성 - Equipment 기능 격차 분석 및 구현 계획 ### 삭제/리팩토링 - goods_list.dart 제거 (equipment_list로 통합) - 불필요한 import 및 코드 정리 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
509
lib/screens/common/components/shadcn_components.dart
Normal file
509
lib/screens/common/components/shadcn_components.dart
Normal file
@@ -0,0 +1,509 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
|
||||
/// shadcn/ui 스타일 기본 컴포넌트들
|
||||
|
||||
// 카드 컴포넌트
|
||||
class ShadcnCard extends StatelessWidget {
|
||||
final Widget child;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final EdgeInsetsGeometry? margin;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const ShadcnCard({
|
||||
Key? key,
|
||||
required this.child,
|
||||
this.padding,
|
||||
this.margin,
|
||||
this.width,
|
||||
this.height,
|
||||
this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cardContent = Container(
|
||||
width: width,
|
||||
height: height,
|
||||
padding: padding ?? const EdgeInsets.all(ShadcnTheme.spacing6),
|
||||
margin: margin,
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.card,
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg),
|
||||
border: Border.all(color: ShadcnTheme.border),
|
||||
boxShadow: ShadcnTheme.cardShadow,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
|
||||
if (onTap != null) {
|
||||
return GestureDetector(onTap: onTap, child: cardContent);
|
||||
}
|
||||
|
||||
return cardContent;
|
||||
}
|
||||
}
|
||||
|
||||
// 버튼 컴포넌트
|
||||
class ShadcnButton extends StatelessWidget {
|
||||
final String text;
|
||||
final VoidCallback? onPressed;
|
||||
final ShadcnButtonVariant variant;
|
||||
final ShadcnButtonSize size;
|
||||
final Widget? icon;
|
||||
final bool fullWidth;
|
||||
final bool loading;
|
||||
final Color? backgroundColor;
|
||||
final Color? textColor;
|
||||
|
||||
const ShadcnButton({
|
||||
Key? key,
|
||||
required this.text,
|
||||
this.onPressed,
|
||||
this.variant = ShadcnButtonVariant.primary,
|
||||
this.size = ShadcnButtonSize.medium,
|
||||
this.icon,
|
||||
this.fullWidth = false,
|
||||
this.loading = false,
|
||||
this.backgroundColor,
|
||||
this.textColor,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ButtonStyle style = _getButtonStyle();
|
||||
final EdgeInsetsGeometry padding = _getPadding();
|
||||
|
||||
Widget buttonChild = Row(
|
||||
mainAxisSize: fullWidth ? MainAxisSize.max : MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (loading)
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
textColor ?? _getDefaultTextColor(),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (icon != null)
|
||||
icon!,
|
||||
if ((loading || icon != null) && text.isNotEmpty)
|
||||
const SizedBox(width: ShadcnTheme.spacing2),
|
||||
if (text.isNotEmpty) Text(text, style: _getTextStyle()),
|
||||
],
|
||||
);
|
||||
|
||||
if (variant == ShadcnButtonVariant.primary) {
|
||||
return SizedBox(
|
||||
width: fullWidth ? double.infinity : null,
|
||||
child: ElevatedButton(
|
||||
onPressed: loading ? null : onPressed,
|
||||
style: style.copyWith(padding: WidgetStateProperty.all(padding)),
|
||||
child: buttonChild,
|
||||
),
|
||||
);
|
||||
} else if (variant == ShadcnButtonVariant.secondary) {
|
||||
return SizedBox(
|
||||
width: fullWidth ? double.infinity : null,
|
||||
child: OutlinedButton(
|
||||
onPressed: loading ? null : onPressed,
|
||||
style: style.copyWith(padding: WidgetStateProperty.all(padding)),
|
||||
child: buttonChild,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return SizedBox(
|
||||
width: fullWidth ? double.infinity : null,
|
||||
child: TextButton(
|
||||
onPressed: loading ? null : onPressed,
|
||||
style: style.copyWith(padding: WidgetStateProperty.all(padding)),
|
||||
child: buttonChild,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ButtonStyle _getButtonStyle() {
|
||||
switch (variant) {
|
||||
case ShadcnButtonVariant.primary:
|
||||
return ElevatedButton.styleFrom(
|
||||
backgroundColor: backgroundColor ?? ShadcnTheme.primary,
|
||||
foregroundColor: textColor ?? ShadcnTheme.primaryForeground,
|
||||
elevation: 0,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
);
|
||||
case ShadcnButtonVariant.secondary:
|
||||
return OutlinedButton.styleFrom(
|
||||
backgroundColor: backgroundColor ?? ShadcnTheme.secondary,
|
||||
foregroundColor: textColor ?? ShadcnTheme.secondaryForeground,
|
||||
side: const BorderSide(color: ShadcnTheme.border),
|
||||
elevation: 0,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
);
|
||||
case ShadcnButtonVariant.destructive:
|
||||
return ElevatedButton.styleFrom(
|
||||
backgroundColor: backgroundColor ?? ShadcnTheme.destructive,
|
||||
foregroundColor: textColor ?? ShadcnTheme.destructiveForeground,
|
||||
elevation: 0,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
);
|
||||
case ShadcnButtonVariant.ghost:
|
||||
return TextButton.styleFrom(
|
||||
backgroundColor: backgroundColor ?? Colors.transparent,
|
||||
foregroundColor: textColor ?? ShadcnTheme.foreground,
|
||||
elevation: 0,
|
||||
shadowColor: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EdgeInsetsGeometry _getPadding() {
|
||||
switch (size) {
|
||||
case ShadcnButtonSize.small:
|
||||
return const EdgeInsets.symmetric(
|
||||
horizontal: ShadcnTheme.spacing3,
|
||||
vertical: ShadcnTheme.spacing1,
|
||||
);
|
||||
case ShadcnButtonSize.medium:
|
||||
return const EdgeInsets.symmetric(
|
||||
horizontal: ShadcnTheme.spacing4,
|
||||
vertical: ShadcnTheme.spacing2,
|
||||
);
|
||||
case ShadcnButtonSize.large:
|
||||
return const EdgeInsets.symmetric(
|
||||
horizontal: ShadcnTheme.spacing8,
|
||||
vertical: ShadcnTheme.spacing3,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TextStyle _getTextStyle() {
|
||||
TextStyle baseStyle;
|
||||
switch (size) {
|
||||
case ShadcnButtonSize.small:
|
||||
baseStyle = ShadcnTheme.labelSmall;
|
||||
break;
|
||||
case ShadcnButtonSize.medium:
|
||||
baseStyle = ShadcnTheme.labelMedium;
|
||||
break;
|
||||
case ShadcnButtonSize.large:
|
||||
baseStyle = ShadcnTheme.labelLarge;
|
||||
break;
|
||||
}
|
||||
return textColor != null ? baseStyle.copyWith(color: textColor) : baseStyle;
|
||||
}
|
||||
|
||||
Color _getDefaultTextColor() {
|
||||
switch (variant) {
|
||||
case ShadcnButtonVariant.primary:
|
||||
return ShadcnTheme.primaryForeground;
|
||||
case ShadcnButtonVariant.secondary:
|
||||
return ShadcnTheme.secondaryForeground;
|
||||
case ShadcnButtonVariant.destructive:
|
||||
return ShadcnTheme.destructiveForeground;
|
||||
case ShadcnButtonVariant.ghost:
|
||||
return ShadcnTheme.foreground;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 버튼 variants
|
||||
enum ShadcnButtonVariant { primary, secondary, destructive, ghost }
|
||||
|
||||
// 버튼 사이즈
|
||||
enum ShadcnButtonSize { small, medium, large }
|
||||
|
||||
// 입력 필드 컴포넌트
|
||||
class ShadcnInput extends StatelessWidget {
|
||||
final String? label;
|
||||
final String? placeholder;
|
||||
final String? errorText;
|
||||
final TextEditingController? controller;
|
||||
final bool obscureText;
|
||||
final TextInputType? keyboardType;
|
||||
final ValueChanged<String>? onChanged;
|
||||
final VoidCallback? onTap;
|
||||
final Widget? prefixIcon;
|
||||
final Widget? suffixIcon;
|
||||
final bool readOnly;
|
||||
final bool enabled;
|
||||
final int? maxLines;
|
||||
|
||||
const ShadcnInput({
|
||||
Key? key,
|
||||
this.label,
|
||||
this.placeholder,
|
||||
this.errorText,
|
||||
this.controller,
|
||||
this.obscureText = false,
|
||||
this.keyboardType,
|
||||
this.onChanged,
|
||||
this.onTap,
|
||||
this.prefixIcon,
|
||||
this.suffixIcon,
|
||||
this.readOnly = false,
|
||||
this.enabled = true,
|
||||
this.maxLines = 1,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (label != null) ...[
|
||||
Text(label!, style: ShadcnTheme.labelMedium),
|
||||
const SizedBox(height: ShadcnTheme.spacing1),
|
||||
],
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
obscureText: obscureText,
|
||||
keyboardType: keyboardType,
|
||||
onChanged: onChanged,
|
||||
onTap: onTap,
|
||||
readOnly: readOnly,
|
||||
enabled: enabled,
|
||||
maxLines: maxLines,
|
||||
decoration: InputDecoration(
|
||||
hintText: placeholder,
|
||||
prefixIcon: prefixIcon,
|
||||
suffixIcon: suffixIcon,
|
||||
errorText: errorText,
|
||||
filled: true,
|
||||
fillColor: ShadcnTheme.background,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: ShadcnTheme.spacing3,
|
||||
vertical: ShadcnTheme.spacing2,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
borderSide: const BorderSide(color: ShadcnTheme.input),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
borderSide: const BorderSide(color: ShadcnTheme.input),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
borderSide: const BorderSide(color: ShadcnTheme.ring, width: 2),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
borderSide: const BorderSide(color: ShadcnTheme.destructive),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
borderSide: const BorderSide(
|
||||
color: ShadcnTheme.destructive,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
hintStyle: ShadcnTheme.bodyMedium.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 배지 컴포넌트
|
||||
class ShadcnBadge extends StatelessWidget {
|
||||
final String text;
|
||||
final ShadcnBadgeVariant variant;
|
||||
final ShadcnBadgeSize size;
|
||||
|
||||
const ShadcnBadge({
|
||||
Key? key,
|
||||
required this.text,
|
||||
this.variant = ShadcnBadgeVariant.primary,
|
||||
this.size = ShadcnBadgeSize.medium,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: _getPadding(),
|
||||
decoration: BoxDecoration(
|
||||
color: _getBackgroundColor(),
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusXl),
|
||||
border: Border.all(color: _getBorderColor()),
|
||||
),
|
||||
child: Text(text, style: _getTextStyle()),
|
||||
);
|
||||
}
|
||||
|
||||
EdgeInsetsGeometry _getPadding() {
|
||||
switch (size) {
|
||||
case ShadcnBadgeSize.small:
|
||||
return const EdgeInsets.symmetric(
|
||||
horizontal: ShadcnTheme.spacing1,
|
||||
vertical: ShadcnTheme.spacing1 / 2,
|
||||
);
|
||||
case ShadcnBadgeSize.medium:
|
||||
return const EdgeInsets.symmetric(
|
||||
horizontal: ShadcnTheme.spacing2,
|
||||
vertical: ShadcnTheme.spacing1,
|
||||
);
|
||||
case ShadcnBadgeSize.large:
|
||||
return const EdgeInsets.symmetric(
|
||||
horizontal: ShadcnTheme.spacing3,
|
||||
vertical: ShadcnTheme.spacing1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Color _getBackgroundColor() {
|
||||
switch (variant) {
|
||||
case ShadcnBadgeVariant.primary:
|
||||
return ShadcnTheme.primary;
|
||||
case ShadcnBadgeVariant.secondary:
|
||||
return ShadcnTheme.secondary;
|
||||
case ShadcnBadgeVariant.destructive:
|
||||
return ShadcnTheme.destructive;
|
||||
case ShadcnBadgeVariant.success:
|
||||
return ShadcnTheme.success;
|
||||
case ShadcnBadgeVariant.warning:
|
||||
return ShadcnTheme.warning;
|
||||
case ShadcnBadgeVariant.outline:
|
||||
return Colors.transparent;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getBorderColor() {
|
||||
switch (variant) {
|
||||
case ShadcnBadgeVariant.outline:
|
||||
return ShadcnTheme.border;
|
||||
default:
|
||||
return Colors.transparent;
|
||||
}
|
||||
}
|
||||
|
||||
TextStyle _getTextStyle() {
|
||||
final Color textColor =
|
||||
variant == ShadcnBadgeVariant.outline
|
||||
? ShadcnTheme.foreground
|
||||
: variant == ShadcnBadgeVariant.secondary
|
||||
? ShadcnTheme.secondaryForeground
|
||||
: ShadcnTheme.primaryForeground;
|
||||
|
||||
switch (size) {
|
||||
case ShadcnBadgeSize.small:
|
||||
return ShadcnTheme.labelSmall.copyWith(color: textColor);
|
||||
case ShadcnBadgeSize.medium:
|
||||
return ShadcnTheme.labelMedium.copyWith(color: textColor);
|
||||
case ShadcnBadgeSize.large:
|
||||
return ShadcnTheme.labelLarge.copyWith(color: textColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 배지 variants
|
||||
enum ShadcnBadgeVariant {
|
||||
primary,
|
||||
secondary,
|
||||
destructive,
|
||||
success,
|
||||
warning,
|
||||
outline,
|
||||
}
|
||||
|
||||
// 배지 사이즈
|
||||
enum ShadcnBadgeSize { small, medium, large }
|
||||
|
||||
// 구분선 컴포넌트
|
||||
class ShadcnSeparator extends StatelessWidget {
|
||||
final Axis direction;
|
||||
final double thickness;
|
||||
final Color? color;
|
||||
|
||||
const ShadcnSeparator({
|
||||
Key? key,
|
||||
this.direction = Axis.horizontal,
|
||||
this.thickness = 1.0,
|
||||
this.color,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: direction == Axis.horizontal ? double.infinity : thickness,
|
||||
height: direction == Axis.vertical ? double.infinity : thickness,
|
||||
color: color ?? ShadcnTheme.border,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 아바타 컴포넌트
|
||||
class ShadcnAvatar extends StatelessWidget {
|
||||
final String? imageUrl;
|
||||
final String? initials;
|
||||
final double size;
|
||||
final Color? backgroundColor;
|
||||
|
||||
const ShadcnAvatar({
|
||||
Key? key,
|
||||
this.imageUrl,
|
||||
this.initials,
|
||||
this.size = 40,
|
||||
this.backgroundColor,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor ?? ShadcnTheme.muted,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: ShadcnTheme.border),
|
||||
),
|
||||
child: ClipOval(
|
||||
child:
|
||||
imageUrl != null
|
||||
? Image.network(
|
||||
imageUrl!,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) => _buildFallback(),
|
||||
)
|
||||
: _buildFallback(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFallback() {
|
||||
return Container(
|
||||
color: backgroundColor ?? ShadcnTheme.muted,
|
||||
child: Center(
|
||||
child: Text(
|
||||
initials ?? '?',
|
||||
style: ShadcnTheme.labelMedium.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
fontSize: size * 0.4,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user