feat: 회사 관리 API 연동 완료
- CompanyListController 생성 (ChangeNotifier 패턴) - CompanyListRedesign 화면 Provider 패턴으로 변경 - 무한 스크롤 및 실시간 검색 기능 구현 (디바운싱 적용) - 중복 회사명 체크 API 연동 - 지점 저장 로직 API 연동 (saveBranch 메서드 추가) - 에러 처리 및 로딩 상태 UI 구현 - API 통합 계획 문서 업데이트 (회사 관리 100% 완료)
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'dart:async';
|
||||
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';
|
||||
import 'package:superport/screens/company/controllers/company_list_controller.dart';
|
||||
|
||||
/// shadcn/ui 스타일로 재설계된 회사 관리 화면
|
||||
class CompanyListRedesign extends StatefulWidget {
|
||||
@@ -14,22 +17,43 @@ class CompanyListRedesign extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
||||
final MockDataService _dataService = MockDataService();
|
||||
List<Company> _companies = [];
|
||||
int _currentPage = 1;
|
||||
final int _pageSize = 10;
|
||||
late CompanyListController _controller;
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
Timer? _debounceTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadData();
|
||||
_controller = CompanyListController(dataService: MockDataService());
|
||||
_controller.initialize();
|
||||
_setupScrollListener();
|
||||
}
|
||||
|
||||
/// 데이터 로드
|
||||
void _loadData() {
|
||||
setState(() {
|
||||
_companies = _dataService.getAllCompanies();
|
||||
_currentPage = 1;
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_scrollController.dispose();
|
||||
_searchController.dispose();
|
||||
_debounceTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 스크롤 리스너 설정 (무한 스크롤)
|
||||
void _setupScrollListener() {
|
||||
_scrollController.addListener(() {
|
||||
if (_scrollController.position.pixels ==
|
||||
_scrollController.position.maxScrollExtent) {
|
||||
_controller.loadMore();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 검색어 입력 처리 (디바운싱)
|
||||
void _onSearchChanged(String value) {
|
||||
_debounceTimer?.cancel();
|
||||
_debounceTimer = Timer(const Duration(milliseconds: 500), () {
|
||||
_controller.updateSearchKeyword(value);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -37,7 +61,7 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
||||
void _navigateToAddScreen() async {
|
||||
final result = await Navigator.pushNamed(context, '/company/add');
|
||||
if (result == true) {
|
||||
_loadData();
|
||||
_controller.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,10 +79,17 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
||||
child: const Text('취소'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_dataService.deleteCompany(id);
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
_loadData();
|
||||
final success = await _controller.deleteCompany(id);
|
||||
if (!success && mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(_controller.error ?? '삭제에 실패했습니다'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('삭제'),
|
||||
),
|
||||
@@ -140,88 +171,159 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
||||
|
||||
@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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return ChangeNotifierProvider.value(
|
||||
value: _controller,
|
||||
child: Consumer<CompanyListController>(
|
||||
builder: (context, controller, child) {
|
||||
// 본사와 지점 구분하기 위한 데이터 준비
|
||||
final List<Map<String, dynamic>> displayCompanies = [];
|
||||
for (final company in controller.filteredCompanies) {
|
||||
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,
|
||||
);
|
||||
final int totalCount = displayCompanies.length;
|
||||
|
||||
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),
|
||||
],
|
||||
return SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.all(ShadcnTheme.spacing6),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 헤더 및 검색 바
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.card,
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radius),
|
||||
border: Border.all(color: ShadcnTheme.border),
|
||||
),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
onChanged: _onSearchChanged,
|
||||
decoration: InputDecoration(
|
||||
hintText: '회사명, 담당자명, 연락처로 검색',
|
||||
hintStyle: TextStyle(color: ShadcnTheme.muted),
|
||||
prefixIcon: Icon(Icons.search, color: ShadcnTheme.muted),
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
const SizedBox(width: ShadcnTheme.spacing4),
|
||||
ShadcnButton(
|
||||
text: '회사 추가',
|
||||
onPressed: _navigateToAddScreen,
|
||||
variant: ShadcnButtonVariant.primary,
|
||||
textColor: Colors.white,
|
||||
icon: Icon(Icons.add),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: ShadcnTheme.spacing4),
|
||||
|
||||
// 결과 정보
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('총 $totalCount개 회사', style: ShadcnTheme.bodyMuted),
|
||||
if (controller.searchKeyword.isNotEmpty)
|
||||
Text(
|
||||
'"${controller.searchKeyword}" 검색 결과',
|
||||
style: ShadcnTheme.bodyMuted,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: ShadcnTheme.spacing4),
|
||||
|
||||
// 에러 메시지
|
||||
if (controller.error != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(ShadcnTheme.spacing4),
|
||||
margin: const EdgeInsets.only(bottom: ShadcnTheme.spacing4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radius),
|
||||
border: Border.all(color: Colors.red.shade200),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.error_outline, color: Colors.red),
|
||||
const SizedBox(width: ShadcnTheme.spacing2),
|
||||
Expanded(
|
||||
child: Text(
|
||||
controller.error!,
|
||||
style: TextStyle(color: Colors.red.shade700),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.close, size: 16),
|
||||
onPressed: controller.clearError,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: BoxConstraints(maxHeight: 24, maxWidth: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 테이블 카드
|
||||
Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.card,
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusLg),
|
||||
border: Border.all(color: ShadcnTheme.border),
|
||||
boxShadow: ShadcnTheme.cardShadow,
|
||||
),
|
||||
child: controller.isLoading && controller.companies.isEmpty
|
||||
? Container(
|
||||
padding: const EdgeInsets.all(ShadcnTheme.spacing8),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
: displayCompanies.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(
|
||||
controller.searchKeyword.isNotEmpty
|
||||
? '검색 결과가 없습니다'
|
||||
: '등록된 회사가 없습니다',
|
||||
style: ShadcnTheme.bodyMuted,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -286,7 +388,7 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
||||
),
|
||||
|
||||
// 테이블 데이터
|
||||
...pagedCompanies.asMap().entries.map((entry) {
|
||||
...displayCompanies.asMap().entries.map((entry) {
|
||||
final int index = entry.key;
|
||||
final companyData = entry.value;
|
||||
final bool isBranch = companyData['isBranch'] as bool;
|
||||
@@ -313,7 +415,7 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
'${startIndex + index + 1}',
|
||||
'${index + 1}',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
@@ -389,7 +491,7 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
||||
'branchId': company.id,
|
||||
},
|
||||
).then((result) {
|
||||
if (result == true) _loadData();
|
||||
if (result == true) controller.refresh();
|
||||
});
|
||||
} else {
|
||||
Navigator.pushNamed(
|
||||
@@ -400,7 +502,7 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
||||
'isBranch': false,
|
||||
},
|
||||
).then((result) {
|
||||
if (result == true) _loadData();
|
||||
if (result == true) controller.refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -431,52 +533,32 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 페이지네이션
|
||||
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,
|
||||
// 무한 스크롤 로딩 인디케이터
|
||||
if (controller.isLoading && controller.companies.isNotEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(ShadcnTheme.spacing4),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: ShadcnTheme.spacing4),
|
||||
|
||||
// 페이지 정보
|
||||
Text(
|
||||
'$_currentPage / ${((totalCount - 1) ~/ _pageSize) + 1}',
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
// 더 이상 로드할 데이터가 없을 때 메시지
|
||||
if (!controller.hasMore && controller.companies.isNotEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(ShadcnTheme.spacing4),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'모든 회사를 불러왔습니다',
|
||||
style: ShadcnTheme.bodyMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: ShadcnTheme.spacing4),
|
||||
|
||||
// 다음 페이지 버튼
|
||||
ShadcnButton(
|
||||
text: '다음',
|
||||
onPressed:
|
||||
_currentPage < ((totalCount - 1) ~/ _pageSize) + 1
|
||||
? () => setState(() => _currentPage++)
|
||||
: null,
|
||||
variant: ShadcnButtonVariant.secondary,
|
||||
size: ShadcnButtonSize.small,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user