사용하지 않는 파일 정리 전 백업 (Phase 10 완료 후 상태)

This commit is contained in:
JiWoong Sul
2025-08-29 15:11:59 +09:00
parent a740ff10c8
commit d916b281a7
333 changed files with 53617 additions and 22574 deletions

View File

@@ -0,0 +1,596 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:superport/data/models/administrator_dto.dart';
import 'package:superport/screens/administrator/controllers/administrator_controller.dart';
import 'package:superport/screens/common/widgets/pagination.dart';
/// 관리자 목록 화면 (shadcn/ui 스타일)
class AdministratorList extends StatefulWidget {
const AdministratorList({super.key});
@override
State<AdministratorList> createState() => _AdministratorListState();
}
class _AdministratorListState extends State<AdministratorList> {
final TextEditingController _searchController = TextEditingController();
late AdministratorController _controller;
Timer? _debounce;
@override
void initState() {
super.initState();
_controller = Provider.of<AdministratorController>(context, listen: false);
// 초기 데이터 로드
WidgetsBinding.instance.addPostFrameCallback((_) {
_controller.initialize();
});
// 검색 디바운싱
_searchController.addListener(() {
_onSearchChanged(_searchController.text);
});
}
@override
void dispose() {
_searchController.dispose();
_debounce?.cancel();
super.dispose();
}
/// 검색어 변경 처리 (디바운싱)
void _onSearchChanged(String query) {
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 300), () {
_controller.setSearchQuery(query);
});
}
/// 관리자 추가 다이얼로그
void _showAddDialog() {
showShadDialog(
context: context,
builder: (context) => _AdministratorFormDialog(
title: '관리자 추가',
onSubmit: (name, phone, mobile, email, password) async {
final success = await _controller.createAdministrator(
name: name,
phone: phone,
mobile: mobile,
email: email,
password: password ?? '',
);
if (success && mounted) {
Navigator.of(context).pop();
ShadToaster.of(context).show(
const ShadToast(
title: Text('생성 완료'),
description: Text('관리자가 생성되었습니다'),
),
);
}
return success;
},
),
);
}
/// 관리자 수정 다이얼로그
void _showEditDialog(AdministratorDto administrator) {
showShadDialog(
context: context,
builder: (context) => _AdministratorFormDialog(
title: '관리자 수정',
administrator: administrator,
onSubmit: (name, phone, mobile, email, password) async {
final success = await _controller.updateAdministrator(
administrator.id!,
name: name,
phone: phone,
mobile: mobile,
email: email,
password: password ?? '',
);
if (success && mounted) {
Navigator.of(context).pop();
ShadToaster.of(context).show(
const ShadToast(
title: Text('수정 완료'),
description: Text('관리자 정보가 수정되었습니다'),
),
);
}
return success;
},
),
);
}
/// 관리자 삭제 확인 다이얼로그
void _showDeleteDialog(AdministratorDto administrator) {
showShadDialog(
context: context,
builder: (context) => ShadDialog(
title: const Text('관리자 삭제'),
description: Text('"${administrator.name}" 관리자를 정말로 삭제하시겠습니까?'),
actions: [
ShadButton.outline(
child: const Text('취소'),
onPressed: () => Navigator.of(context).pop(),
),
ShadButton.destructive(
child: const Text('삭제'),
onPressed: () async {
Navigator.of(context).pop();
final success = await _controller.deleteAdministrator(administrator.id!);
if (success && mounted) {
ShadToaster.of(context).show(
const ShadToast(
title: Text('삭제 완료'),
description: Text('관리자가 삭제되었습니다'),
),
);
} else if (mounted && _controller.errorMessage != null) {
ShadToaster.of(context).show(
ShadToast.destructive(
title: const Text('삭제 실패'),
description: Text(_controller.errorMessage!),
),
);
}
},
),
],
),
);
}
/// 데이터 테이블 컬럼 정의
List<DataColumn> get _columns => [
const DataColumn(label: Text('이름')),
const DataColumn(label: Text('이메일')),
const DataColumn(label: Text('전화번호')),
const DataColumn(label: Text('휴대폰')),
const DataColumn(label: Text('작업')),
];
/// 데이터 테이블 행 생성
List<DataRow> _buildRows(List<AdministratorDto> administrators) {
return administrators.map((admin) {
return DataRow(
cells: [
DataCell(Text(admin.name)),
DataCell(Text(admin.email)),
DataCell(Text(admin.phone)),
DataCell(Text(admin.mobile)),
DataCell(
Row(
mainAxisSize: MainAxisSize.min,
children: [
ShadButton.ghost(
size: ShadButtonSize.sm,
onPressed: () => _showEditDialog(admin),
child: const Icon(Icons.edit, size: 16),
),
const SizedBox(width: 4),
ShadButton.ghost(
size: ShadButtonSize.sm,
onPressed: () => _showDeleteDialog(admin),
child: const Icon(Icons.delete, size: 16, color: Colors.red),
),
],
),
),
],
);
}).toList();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
// Action Bar
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// 제목
const Text(
'관리자 관리',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 16),
// 검색창
Expanded(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
const Padding(
padding: EdgeInsets.all(12),
child: Icon(Icons.search, size: 16, color: Colors.grey),
),
Expanded(
child: TextField(
controller: _searchController,
decoration: const InputDecoration(
hintText: '이름 또는 이메일로 검색',
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 12),
),
),
),
],
),
),
),
const SizedBox(width: 16),
// 추가 버튼
ShadButton(
onPressed: _showAddDialog,
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.add, size: 16),
SizedBox(width: 4),
Text('관리자 추가'),
],
),
),
const SizedBox(width: 8),
// 새로고침 버튼
ShadButton.outline(
onPressed: () => _controller.refresh(),
child: const Icon(Icons.refresh, size: 16),
),
],
),
),
// Content
Expanded(
child: Consumer<AdministratorController>(
builder: (context, controller, child) {
if (controller.isLoading && controller.administrators.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
if (controller.errorMessage != null && controller.administrators.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text(
controller.errorMessage!,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.red),
),
const SizedBox(height: 16),
ShadButton(
child: const Text('다시 시도'),
onPressed: () => controller.refresh(),
),
],
),
);
}
return Column(
children: [
// Summary Card
Padding(
padding: const EdgeInsets.all(16),
child: ShadCard(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
const Icon(Icons.admin_panel_settings, size: 32),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'전체 관리자',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
Text(
'${controller.totalCount}',
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
],
),
const Spacer(),
if (controller.isLoading)
const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
],
),
),
),
),
// Data Table
Expanded(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ShadCard(
child: controller.administrators.isEmpty
? const Padding(
padding: EdgeInsets.all(64),
child: Center(
child: Column(
children: [
Icon(Icons.inbox, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'관리자가 없습니다',
style: TextStyle(fontSize: 18, color: Colors.grey),
),
],
),
),
)
: DataTable(
columns: _columns,
rows: _buildRows(controller.administrators),
columnSpacing: 24,
headingRowHeight: 56,
dataRowMinHeight: 48,
dataRowMaxHeight: 56,
),
),
),
),
),
// Pagination
if (controller.totalPages > 1)
Padding(
padding: const EdgeInsets.all(16),
child: Pagination(
totalCount: controller.totalCount,
currentPage: controller.currentPage,
pageSize: controller.pageSize,
onPageChanged: (page) => controller.goToPage(page),
),
),
],
);
},
),
),
],
),
);
}
}
/// 관리자 추가/수정 다이얼로그
class _AdministratorFormDialog extends StatefulWidget {
final String title;
final AdministratorDto? administrator;
final Future<bool> Function(String name, String phone, String mobile, String email, String? password) onSubmit;
const _AdministratorFormDialog({
required this.title,
this.administrator,
required this.onSubmit,
});
@override
State<_AdministratorFormDialog> createState() => _AdministratorFormDialogState();
}
class _AdministratorFormDialogState extends State<_AdministratorFormDialog> {
final _formKey = GlobalKey<FormState>();
late final TextEditingController _nameController;
late final TextEditingController _phoneController;
late final TextEditingController _mobileController;
late final TextEditingController _emailController;
late final TextEditingController _passwordController;
bool _isSubmitting = false;
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.administrator?.name ?? '');
_phoneController = TextEditingController(text: widget.administrator?.phone ?? '');
_mobileController = TextEditingController(text: widget.administrator?.mobile ?? '');
_emailController = TextEditingController(text: widget.administrator?.email ?? '');
_passwordController = TextEditingController();
}
@override
void dispose() {
_nameController.dispose();
_phoneController.dispose();
_mobileController.dispose();
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
bool get _isEditMode => widget.administrator != null;
Future<void> _handleSubmit() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_isSubmitting = true;
});
try {
final success = await widget.onSubmit(
_nameController.text.trim(),
_phoneController.text.trim(),
_mobileController.text.trim(),
_emailController.text.trim(),
_passwordController.text.isNotEmpty ? _passwordController.text : null,
);
if (!success) {
// 에러는 Controller에서 처리되므로 여기서는 로딩 상태만 해제
setState(() {
_isSubmitting = false;
});
}
} catch (e) {
setState(() {
_isSubmitting = false;
});
}
}
@override
Widget build(BuildContext context) {
return ShadDialog(
title: Text(widget.title),
child: SizedBox(
width: 500,
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 이름
ShadInputFormField(
controller: _nameController,
label: const Text('이름'),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '이름을 입력해주세요';
}
if (value.length > 100) {
return '이름은 100자 이내로 입력해주세요';
}
return null;
},
),
const SizedBox(height: 16),
// 이메일
ShadInputFormField(
controller: _emailController,
label: const Text('이메일'),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '이메일을 입력해주세요';
}
if (!RegExp(r'^[^\s@]+@[^\s@]+\.[^\s@]+$').hasMatch(value.trim())) {
return '유효한 이메일을 입력해주세요';
}
return null;
},
),
const SizedBox(height: 16),
// 전화번호
ShadInputFormField(
controller: _phoneController,
label: const Text('전화번호'),
keyboardType: TextInputType.phone,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '전화번호를 입력해주세요';
}
final cleaned = value.replaceAll(RegExp(r'[\s\-\(\)]'), '');
if (!RegExp(r'^\d{8,11}$').hasMatch(cleaned)) {
return '유효한 전화번호를 입력해주세요 (8-11자리 숫자)';
}
return null;
},
),
const SizedBox(height: 16),
// 휴대폰
ShadInputFormField(
controller: _mobileController,
label: const Text('휴대폰'),
keyboardType: TextInputType.phone,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '휴대폰 번호를 입력해주세요';
}
final cleaned = value.replaceAll(RegExp(r'[\s\-\(\)]'), '');
if (!RegExp(r'^\d{8,11}$').hasMatch(cleaned)) {
return '유효한 휴대폰 번호를 입력해주세요 (8-11자리 숫자)';
}
return null;
},
),
const SizedBox(height: 16),
// 비밀번호
ShadInputFormField(
controller: _passwordController,
label: Text(_isEditMode ? '새 비밀번호 (선택사항)' : '비밀번호'),
obscureText: true,
validator: (value) {
// 수정 모드에서는 비밀번호 선택사항
if (_isEditMode && (value == null || value.isEmpty)) {
return null;
}
// 생성 모드에서는 비밀번호 필수
if (!_isEditMode && (value == null || value.trim().isEmpty)) {
return '비밀번호를 입력해주세요';
}
// 입력된 경우 길이 검증
if (value != null && value.isNotEmpty && value.length < 6) {
return '비밀번호는 최소 6자 이상이어야 합니다';
}
return null;
},
),
],
),
),
),
actions: [
ShadButton.outline(
child: const Text('취소'),
onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(),
),
ShadButton(
onPressed: _isSubmitting ? null : _handleSubmit,
child: _isSubmitting
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(_isEditMode ? '수정' : '생성'),
),
],
);
}
}

View File

@@ -0,0 +1,340 @@
import 'package:flutter/foundation.dart';
import 'package:injectable/injectable.dart';
import 'package:superport/data/models/administrator_dto.dart';
import 'package:superport/domain/usecases/administrator_usecase.dart';
import 'package:superport/utils/constants.dart';
/// 관리자 화면 컨트롤러 (Provider 패턴)
/// 관리자 목록 조회, CRUD, 검색 등의 상태 관리
@injectable
class AdministratorController extends ChangeNotifier {
final AdministratorUseCase _administratorUseCase;
AdministratorController(this._administratorUseCase);
// 상태 변수들
List<AdministratorDto> _administrators = [];
AdministratorDto? _selectedAdministrator;
bool _isLoading = false;
String? _errorMessage;
// 페이지네이션
int _currentPage = 1;
int _totalPages = 1;
int _totalCount = 0;
final int _pageSize = PaginationConstants.defaultPageSize;
// 검색
String _searchQuery = '';
// Form 관련 상태
bool _isFormSubmitting = false;
String? _formErrorMessage;
// Getters
List<AdministratorDto> get administrators => _administrators;
AdministratorDto? get selectedAdministrator => _selectedAdministrator;
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;
int get currentPage => _currentPage;
int get totalPages => _totalPages;
int get totalCount => _totalCount;
int get pageSize => _pageSize;
String get searchQuery => _searchQuery;
bool get hasNextPage => _currentPage < _totalPages;
bool get hasPreviousPage => _currentPage > 1;
// Form 관련 Getters
bool get isFormSubmitting => _isFormSubmitting;
String? get formErrorMessage => _formErrorMessage;
/// 컨트롤러 초기화
Future<void> initialize() async {
_isLoading = true;
notifyListeners();
await loadAdministrators();
}
/// 관리자 목록 로드
Future<void> loadAdministrators({bool refresh = false}) async {
if (refresh) {
_currentPage = 1;
}
_setLoading(true);
_clearError();
try {
final result = await _administratorUseCase.getAdministrators(
page: _currentPage,
pageSize: _pageSize,
search: _searchQuery.isNotEmpty ? _searchQuery : null,
);
result.fold(
(failure) {
_setError('관리자 목록을 불러오는데 실패했습니다: ${failure.message}');
},
(response) {
_administrators = List.from(response.items);
_totalCount = response.totalCount;
_totalPages = response.totalPages;
_currentPage = response.currentPage;
notifyListeners();
},
);
} catch (e) {
_setError('관리자 목록을 불러오는데 실패했습니다: ${e.toString()}');
} finally {
_setLoading(false);
}
}
/// 특정 관리자 선택/조회
Future<void> selectAdministrator(int id) async {
_setLoading(true);
_clearError();
try {
final result = await _administratorUseCase.getAdministratorById(id);
result.fold(
(failure) {
_setError('관리자 정보를 불러오는데 실패했습니다: ${failure.message}');
},
(administrator) {
_selectedAdministrator = administrator;
notifyListeners();
},
);
} catch (e) {
_setError('관리자 정보를 불러오는데 실패했습니다: ${e.toString()}');
} finally {
_setLoading(false);
}
}
/// 관리자 생성
Future<bool> createAdministrator({
required String name,
required String phone,
required String mobile,
required String email,
required String password,
}) async {
_setFormSubmitting(true);
_clearFormError();
try {
final request = AdministratorRequestDto(
name: name.trim(),
phone: phone.trim(),
mobile: mobile.trim(),
email: email.trim(),
passwd: password,
);
final result = await _administratorUseCase.createAdministrator(request);
return result.fold(
(failure) {
_setFormError('관리자 생성에 실패했습니다: ${failure.message}');
return false;
},
(administrator) {
// 목록 새로고침
loadAdministrators(refresh: true);
return true;
},
);
} catch (e) {
_setFormError('관리자 생성에 실패했습니다: ${e.toString()}');
return false;
} finally {
_setFormSubmitting(false);
}
}
/// 관리자 정보 수정
Future<bool> updateAdministrator(
int id, {
String? name,
String? phone,
String? mobile,
String? email,
String? password,
}) async {
_setFormSubmitting(true);
_clearFormError();
try {
final request = AdministratorUpdateRequestDto(
name: name?.trim(),
phone: phone?.trim(),
mobile: mobile?.trim(),
email: email?.trim(),
passwd: password,
);
final result = await _administratorUseCase.updateAdministrator(id, request);
return result.fold(
(failure) {
_setFormError('관리자 정보 수정에 실패했습니다: ${failure.message}');
return false;
},
(administrator) {
// 목록 새로고침
loadAdministrators(refresh: true);
// 선택된 관리자 업데이트
if (_selectedAdministrator?.id == id) {
_selectedAdministrator = administrator;
}
notifyListeners();
return true;
},
);
} catch (e) {
_setFormError('관리자 정보 수정에 실패했습니다: ${e.toString()}');
return false;
} finally {
_setFormSubmitting(false);
}
}
/// 관리자 삭제
Future<bool> deleteAdministrator(int id) async {
_setLoading(true);
_clearError();
try {
final result = await _administratorUseCase.deleteAdministrator(id);
return result.fold(
(failure) {
_setError('관리자 삭제에 실패했습니다: ${failure.message}');
return false;
},
(_) {
// 목록에서 제거
_administrators.removeWhere((admin) => admin.id == id);
// 선택된 관리자가 삭제된 경우 클리어
if (_selectedAdministrator?.id == id) {
_selectedAdministrator = null;
}
// 총 개수 업데이트
_totalCount--;
notifyListeners();
return true;
},
);
} catch (e) {
_setError('관리자 삭제에 실패했습니다: ${e.toString()}');
return false;
} finally {
_setLoading(false);
}
}
/// 검색 쿼리 설정
void setSearchQuery(String query) {
_searchQuery = query.trim();
loadAdministrators(refresh: true);
}
/// 검색 초기화
void clearSearch() {
if (_searchQuery.isNotEmpty) {
_searchQuery = '';
loadAdministrators(refresh: true);
}
}
/// 다음 페이지 로드
Future<void> loadNextPage() async {
if (hasNextPage && !_isLoading) {
_currentPage++;
await loadAdministrators();
}
}
/// 이전 페이지 로드
Future<void> loadPreviousPage() async {
if (hasPreviousPage && !_isLoading) {
_currentPage--;
await loadAdministrators();
}
}
/// 특정 페이지로 이동
Future<void> goToPage(int page) async {
if (page > 0 && page <= _totalPages && page != _currentPage && !_isLoading) {
_currentPage = page;
await loadAdministrators();
}
}
/// 이메일 중복 확인
Future<bool> checkEmailDuplicate(String email, {int? excludeId}) async {
try {
final result = await _administratorUseCase.checkEmailDuplicate(
email.trim(),
excludeId: excludeId,
);
return result.fold(
(failure) => true, // 에러 시 안전하게 중복으로 처리
(isDuplicate) => isDuplicate,
);
} catch (e) {
return true; // 에러 시 안전하게 중복으로 처리
}
}
/// 선택된 관리자 클리어
void clearSelectedAdministrator() {
_selectedAdministrator = null;
notifyListeners();
}
/// 목록 새로고침
Future<void> refresh() async {
await loadAdministrators(refresh: true);
}
// 내부 상태 관리 메서드들
void _setLoading(bool loading) {
_isLoading = loading;
notifyListeners();
}
void _setError(String error) {
_errorMessage = error;
notifyListeners();
}
void _clearError() {
_errorMessage = null;
}
void _setFormSubmitting(bool submitting) {
_isFormSubmitting = submitting;
notifyListeners();
}
void _setFormError(String error) {
_formErrorMessage = error;
notifyListeners();
}
void _clearFormError() {
_formErrorMessage = null;
}
@override
void dispose() {
// 추가적인 정리 작업이 필요한 경우 여기서 수행
super.dispose();
}
}