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,483 @@
import 'package:flutter/material.dart';
import 'package:superport/models/company_model.dart';
import 'package:superport/screens/common/theme_shadcn.dart';
import 'package:superport/screens/common/components/shadcn_components.dart';
import 'package:superport/services/mock_data_service.dart';
import 'package:superport/screens/company/widgets/company_branch_dialog.dart';
/// shadcn/ui 스타일로 재설계된 회사 관리 화면
class CompanyListRedesign extends StatefulWidget {
const CompanyListRedesign({super.key});
@override
State<CompanyListRedesign> createState() => _CompanyListRedesignState();
}
class _CompanyListRedesignState extends State<CompanyListRedesign> {
final MockDataService _dataService = MockDataService();
List<Company> _companies = [];
int _currentPage = 1;
final int _pageSize = 10;
@override
void initState() {
super.initState();
_loadData();
}
/// 데이터 로드
void _loadData() {
setState(() {
_companies = _dataService.getAllCompanies();
_currentPage = 1;
});
}
/// 회사 추가 화면으로 이동
void _navigateToAddScreen() async {
final result = await Navigator.pushNamed(context, '/company/add');
if (result == true) {
_loadData();
}
}
/// 회사 삭제 처리
void _deleteCompany(int id) {
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: const Text('삭제 확인'),
content: const Text('이 회사 정보를 삭제하시겠습니까?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('취소'),
),
TextButton(
onPressed: () {
_dataService.deleteCompany(id);
Navigator.pop(context);
_loadData();
},
child: const Text('삭제'),
),
],
),
);
}
/// 지점 다이얼로그 표시
void _showBranchDialog(Company mainCompany) {
showDialog(
context: context,
builder: (context) => CompanyBranchDialog(mainCompany: mainCompany),
);
}
/// Branch 객체를 Company 객체로 변환
Company _convertBranchToCompany(Branch branch) {
return Company(
id: branch.id,
name: branch.name,
address: branch.address,
contactName: branch.contactName,
contactPosition: branch.contactPosition,
contactPhone: branch.contactPhone,
contactEmail: branch.contactEmail,
companyTypes: [],
remark: branch.remark,
);
}
/// 회사 유형 배지 생성
Widget _buildCompanyTypeChips(List<CompanyType> types) {
return Wrap(
spacing: ShadcnTheme.spacing1,
children:
types.map((type) {
return ShadcnBadge(
text: companyTypeToString(type),
variant:
type == CompanyType.customer
? ShadcnBadgeVariant.primary
: ShadcnBadgeVariant.secondary,
size: ShadcnBadgeSize.small,
);
}).toList(),
);
}
/// 본사/지점 구분 배지 생성
Widget _buildCompanyTypeLabel(bool isBranch) {
return ShadcnBadge(
text: isBranch ? '지점' : '본사',
variant:
isBranch ? ShadcnBadgeVariant.outline : ShadcnBadgeVariant.primary,
size: ShadcnBadgeSize.small,
);
}
/// 회사 이름 표시 (지점인 경우 본사명 포함)
Widget _buildCompanyNameText(
Company company,
bool isBranch, {
String? mainCompanyName,
}) {
if (isBranch && mainCompanyName != null) {
return Text.rich(
TextSpan(
children: [
TextSpan(text: '$mainCompanyName > ', style: ShadcnTheme.bodyMuted),
TextSpan(text: company.name, style: ShadcnTheme.bodyMedium),
],
),
);
} else {
return Text(company.name, style: ShadcnTheme.bodyMedium);
}
}
@override
Widget build(BuildContext context) {
// 본사와 지점 구분하기 위한 데이터 준비
final List<Map<String, dynamic>> displayCompanies = [];
for (final company in _companies) {
displayCompanies.add({
'company': company,
'isBranch': false,
'mainCompanyName': null,
});
if (company.branches != null) {
for (final branch in company.branches!) {
displayCompanies.add({
'branch': branch,
'companyId': company.id,
'isBranch': true,
'mainCompanyName': company.name,
});
}
}
}
// 페이지네이션 처리
final int totalCount = displayCompanies.length;
final int startIndex = (_currentPage - 1) * _pageSize;
final int endIndex =
(startIndex + _pageSize) > totalCount
? totalCount
: (startIndex + _pageSize);
final List<Map<String, dynamic>> pagedCompanies = displayCompanies.sublist(
startIndex,
endIndex,
);
return SingleChildScrollView(
padding: const EdgeInsets.all(ShadcnTheme.spacing6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 헤더 액션 바
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('$totalCount개 회사', style: ShadcnTheme.bodyMuted),
ShadcnButton(
text: '회사 추가',
onPressed: _navigateToAddScreen,
variant: ShadcnButtonVariant.primary,
textColor: Colors.white,
icon: Icon(Icons.add),
),
],
),
const SizedBox(height: ShadcnTheme.spacing4),
// 테이블 카드
Container(
width: double.infinity,
decoration: BoxDecoration(
color: ShadcnTheme.card,
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg),
border: Border.all(color: ShadcnTheme.border),
boxShadow: ShadcnTheme.cardShadow,
),
child:
pagedCompanies.isEmpty
? Container(
padding: const EdgeInsets.all(ShadcnTheme.spacing8),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.business_outlined,
size: 48,
color: ShadcnTheme.muted,
),
const SizedBox(height: ShadcnTheme.spacing4),
Text('등록된 회사가 없습니다', style: ShadcnTheme.bodyMuted),
],
),
),
)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 테이블 헤더
Container(
padding: const EdgeInsets.symmetric(
horizontal: ShadcnTheme.spacing4,
vertical: ShadcnTheme.spacing3,
),
decoration: BoxDecoration(
color: ShadcnTheme.muted.withValues(alpha: 0.3),
border: Border(
bottom: BorderSide(color: ShadcnTheme.border),
),
),
child: Row(
children: [
Expanded(
flex: 1,
child: Text(
'번호',
style: ShadcnTheme.bodyMedium,
),
),
Expanded(
flex: 3,
child: Text(
'회사명',
style: ShadcnTheme.bodyMedium,
),
),
Expanded(
flex: 2,
child: Text(
'구분',
style: ShadcnTheme.bodyMedium,
),
),
Expanded(
flex: 2,
child: Text(
'유형',
style: ShadcnTheme.bodyMedium,
),
),
Expanded(
flex: 2,
child: Text(
'연락처',
style: ShadcnTheme.bodyMedium,
),
),
Expanded(
flex: 2,
child: Text(
'관리',
style: ShadcnTheme.bodyMedium,
),
),
],
),
),
// 테이블 데이터
...pagedCompanies.asMap().entries.map((entry) {
final int index = entry.key;
final companyData = entry.value;
final bool isBranch = companyData['isBranch'] as bool;
final Company company =
isBranch
? _convertBranchToCompany(companyData['branch'] as Branch)
: companyData['company'] as Company;
final String? mainCompanyName =
companyData['mainCompanyName'] as String?;
return Container(
padding: const EdgeInsets.symmetric(
horizontal: ShadcnTheme.spacing4,
vertical: ShadcnTheme.spacing3,
),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: ShadcnTheme.border),
),
),
child: Row(
children: [
// 번호
Expanded(
flex: 1,
child: Text(
'${startIndex + index + 1}',
style: ShadcnTheme.bodySmall,
),
),
// 회사명
Expanded(
flex: 3,
child: _buildCompanyNameText(
company,
isBranch,
mainCompanyName: mainCompanyName,
),
),
// 구분
Expanded(
flex: 2,
child: _buildCompanyTypeLabel(isBranch),
),
// 유형
Expanded(
flex: 2,
child: _buildCompanyTypeChips(
company.companyTypes,
),
),
// 연락처
Expanded(
flex: 2,
child: Text(
company.contactPhone ?? '-',
style: ShadcnTheme.bodySmall,
),
),
// 관리
Expanded(
flex: 2,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (!isBranch &&
company.branches != null &&
company.branches!.isNotEmpty)
ShadcnButton(
text: '지점보기',
onPressed:
() => _showBranchDialog(company),
variant:
ShadcnButtonVariant.secondary,
size: ShadcnButtonSize.small,
),
if (!isBranch &&
company.branches != null &&
company.branches!.isNotEmpty)
const SizedBox(
width: ShadcnTheme.spacing2,
),
ShadcnButton(
text: '수정',
onPressed: company.id != null
? () {
if (isBranch) {
Navigator.pushNamed(
context,
'/company/edit',
arguments: {
'companyId': companyData['companyId'],
'isBranch': true,
'mainCompanyName': mainCompanyName,
'branchId': company.id,
},
).then((result) {
if (result == true) _loadData();
});
} else {
Navigator.pushNamed(
context,
'/company/edit',
arguments: {
'companyId': company.id,
'isBranch': false,
},
).then((result) {
if (result == true) _loadData();
});
}
}
: null,
variant: ShadcnButtonVariant.secondary,
size: ShadcnButtonSize.small,
),
const SizedBox(
width: ShadcnTheme.spacing2,
),
ShadcnButton(
text: '삭제',
onPressed:
(!isBranch && company.id != null)
? () =>
_deleteCompany(company.id!)
: null,
variant:
ShadcnButtonVariant.destructive,
size: ShadcnButtonSize.small,
),
],
),
),
],
),
);
}),
],
),
),
// 페이지네이션
if (totalCount > _pageSize)
Container(
padding: const EdgeInsets.symmetric(
vertical: ShadcnTheme.spacing4,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 이전 페이지 버튼
ShadcnButton(
text: '이전',
onPressed:
_currentPage > 1
? () => setState(() => _currentPage--)
: null,
variant: ShadcnButtonVariant.secondary,
size: ShadcnButtonSize.small,
),
const SizedBox(width: ShadcnTheme.spacing4),
// 페이지 정보
Text(
'$_currentPage / ${((totalCount - 1) ~/ _pageSize) + 1}',
style: ShadcnTheme.bodyMedium,
),
const SizedBox(width: ShadcnTheme.spacing4),
// 다음 페이지 버튼
ShadcnButton(
text: '다음',
onPressed:
_currentPage < ((totalCount - 1) ~/ _pageSize) + 1
? () => setState(() => _currentPage++)
: null,
variant: ShadcnButtonVariant.secondary,
size: ShadcnButtonSize.small,
),
],
),
),
],
),
);
}
}