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:
JiWoong Sul
2025-07-07 19:45:32 +09:00
parent e346f83c97
commit e0bc5894b2
34 changed files with 7764 additions and 571 deletions

View 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,
),
),
),
);
}
}