사용하지 않는 파일 정리 전 백업 (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

@@ -227,14 +227,12 @@ class UserFormController extends ChangeNotifier {
(_) => onResult(null),
);
} else {
// 사용자 생성
// 사용자 생성 (백엔드 API v1 준수)
final params = CreateUserParams(
username: username,
email: email,
password: password,
name: name,
email: email.isEmpty ? null : email,
phone: phoneNumber.isEmpty ? null : phoneNumber,
role: role,
companiesId: 1, // TODO: 실제 회사 선택 기능 필요
);
final result = await _createUserUseCase(params);

View File

@@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:superport/models/user_model.dart';
import 'package:superport/core/controllers/base_list_controller.dart';
@@ -7,6 +6,8 @@ import 'package:superport/domain/usecases/user/get_users_usecase.dart';
import 'package:superport/domain/usecases/user/create_user_usecase.dart';
import 'package:superport/domain/usecases/user/check_username_availability_usecase.dart';
import 'package:superport/domain/repositories/user_repository.dart';
import 'package:superport/domain/repositories/company_repository.dart';
import 'package:superport/models/company_item_model.dart';
import 'package:superport/core/errors/failures.dart';
/// 사용자 목록 화면 컨트롤러 (서버 API v0.2.1 대응)
@@ -16,6 +17,7 @@ class UserListController extends BaseListController<User> {
late final CreateUserUseCase _createUserUseCase;
late final CheckUsernameAvailabilityUseCase _checkUsernameUseCase;
late final UserRepository _userRepository;
late final CompanyRepository _companyRepository;
// 필터 옵션 (서버 API v0.2.1 지원 필드만)
UserRole? _filterRole;
@@ -31,12 +33,25 @@ class UserListController extends BaseListController<User> {
// 사용자명 중복 체크 상태
bool _usernameCheckInProgress = false;
bool get isCheckingUsername => _usernameCheckInProgress;
// 회사 선택 관련 상태
List<CompanyItem> _companies = [];
int? _selectedCompanyId;
bool _isLoadingCompanies = false;
List<CompanyItem> get companies => _companies;
int? get selectedCompanyId => _selectedCompanyId;
bool get isLoadingCompanies => _isLoadingCompanies;
UserListController() {
_getUsersUseCase = GetIt.instance<GetUsersUseCase>();
_createUserUseCase = GetIt.instance<CreateUserUseCase>();
_checkUsernameUseCase = GetIt.instance<CheckUsernameAvailabilityUseCase>();
_userRepository = GetIt.instance<UserRepository>();
_companyRepository = GetIt.instance<CompanyRepository>();
// 초기화 시 회사 목록 로드
loadCompanies();
}
@override
@@ -118,6 +133,42 @@ class UserListController extends BaseListController<User> {
loadData(isRefresh: true);
}
/// 회사 목록 로드
Future<void> loadCompanies() async {
_isLoadingCompanies = true;
notifyListeners();
try {
final result = await _companyRepository.getCompanies();
result.fold(
(failure) {
// 에러 처리는 로그만 남기고 계속 진행
print('회사 목록 로드 실패: ${failure.message}');
},
(paginatedResponse) {
_companies = paginatedResponse.items
.map((company) => CompanyItem.headquarters(company))
.toList();
// 첫 번째 회사를 기본 선택
if (_companies.isNotEmpty && _selectedCompanyId == null) {
_selectedCompanyId = _companies.first.company.id;
}
},
);
} catch (e) {
print('회사 목록 로드 중 예외 발생: $e');
} finally {
_isLoadingCompanies = false;
notifyListeners();
}
}
/// 회사 선택
void selectCompany(int? companyId) {
_selectedCompanyId = companyId;
notifyListeners();
}
/// 사용자 생성
Future<void> createUser({
required String username,
@@ -126,14 +177,19 @@ class UserListController extends BaseListController<User> {
required String name,
String? phone,
required UserRole role,
int? companiesId,
}) async {
final effectiveCompanyId = companiesId ?? _selectedCompanyId;
if (effectiveCompanyId == null) {
throw Exception('회사를 선택해주세요.');
}
final params = CreateUserParams(
username: username,
email: email,
password: password,
name: name,
email: email,
phone: phone,
role: role,
companiesId: effectiveCompanyId,
);
final result = await _createUserUseCase(params);

View File

@@ -1,11 +1,11 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:superport/screens/common/theme_shadcn.dart';
import 'package:superport/utils/constants.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:superport/utils/validators.dart';
import 'package:flutter/services.dart';
import 'package:superport/screens/user/controllers/user_form_controller.dart';
import 'package:superport/models/user_model.dart';
import 'package:superport/utils/formatters/korean_phone_formatter.dart';
// 사용자 등록/수정 화면 (UI만 담당, 상태/로직 분리)
class UserFormScreen extends StatefulWidget {
@@ -42,7 +42,7 @@ class _UserFormScreenState extends State<UserFormScreen> {
title: Text(controller.isEditMode ? '사용자 수정' : '사용자 등록'),
),
body: controller.isLoading
? const Center(child: CircularProgressIndicator())
? const Center(child: ShadProgress())
: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
@@ -89,9 +89,7 @@ class _UserFormScreenState extends State<UserFormScreen> {
height: 20,
child: Padding(
padding: EdgeInsets.all(12.0),
child: CircularProgressIndicator(
strokeWidth: 2,
),
child: ShadProgress(),
),
)
: controller.isUsernameAvailable != null
@@ -153,45 +151,52 @@ class _UserFormScreenState extends State<UserFormScreen> {
],
// 수정 모드에서 비밀번호 변경 (선택사항)
if (controller.isEditMode) ...[
ExpansionTile(
title: const Text('비밀번호 변경'),
if (controller.isEditMode) ...[
ShadAccordion<int>(
children: [
_buildPasswordField(
label: '새 비밀번호',
controller: _passwordController,
hintText: '변경할 경우만 입력하세요',
obscureText: !_showPassword,
onToggleVisibility: () {
setState(() {
_showPassword = !_showPassword;
});
},
validator: (value) {
if (value != null && value.isNotEmpty && value.length < 6) {
return '비밀번호는 6자 이상이어야 합니다';
}
return null;
},
onSaved: (value) => controller.password = value ?? '',
),
_buildPasswordField(
label: '새 비밀번호 확인',
controller: _confirmPasswordController,
hintText: '비밀번호를 다시 입력하세요',
obscureText: !_showConfirmPassword,
onToggleVisibility: () {
setState(() {
_showConfirmPassword = !_showConfirmPassword;
});
},
validator: (value) {
if (_passwordController.text.isNotEmpty && value != _passwordController.text) {
return '비밀번호가 일치하지 않습니다';
}
return null;
},
ShadAccordionItem(
value: 1,
title: const Text('비밀번호 변경'),
child: Column(
children: [
_buildPasswordField(
label: '새 비밀번호',
controller: _passwordController,
hintText: '변경할 경우만 입력하세요',
obscureText: !_showPassword,
onToggleVisibility: () {
setState(() {
_showPassword = !_showPassword;
});
},
validator: (value) {
if (value != null && value.isNotEmpty && value.length < 6) {
return '비밀번호는 6자 이상이어야 합니다';
}
return null;
},
onSaved: (value) => controller.password = value ?? '',
),
_buildPasswordField(
label: '새 비밀번호 확인',
controller: _confirmPasswordController,
hintText: '비밀번호를 다시 입력하세요',
obscureText: !_showConfirmPassword,
onToggleVisibility: () {
setState(() {
_showConfirmPassword = !_showConfirmPassword;
});
},
validator: (value) {
if (_passwordController.text.isNotEmpty && value != _passwordController.text) {
return '비밀번호가 일치하지 않습니다';
}
return null;
},
),
],
),
),
],
),
@@ -221,54 +226,22 @@ class _UserFormScreenState extends State<UserFormScreen> {
const SizedBox(height: 24),
// 오류 메시지 표시
if (controller.error != null)
Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red.shade200),
),
child: Row(
children: [
Icon(Icons.error_outline, color: Colors.red.shade700),
const SizedBox(width: 8),
Expanded(
child: Text(
controller.error!,
style: TextStyle(color: Colors.red.shade700),
),
),
],
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: ShadAlert.destructive(
title: const Text('오류'),
description: Text(controller.error!),
),
),
// 저장 버튼
SizedBox(
width: double.infinity,
child: ElevatedButton(
child: ShadButton(
onPressed: controller.isLoading
? null
: () => _onSaveUser(controller),
style: ElevatedButton.styleFrom(
backgroundColor: ShadcnTheme.primary,
foregroundColor: Colors.white,
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: controller.isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text(
controller.isEditMode ? '수정하기' : '등록하기',
style: const TextStyle(fontSize: 16),
),
),
size: ShadButtonSize.lg,
child: Text(controller.isEditMode ? '수정하기' : '등록하기'),
),
),
],
@@ -294,6 +267,7 @@ class _UserFormScreenState extends State<UserFormScreen> {
void Function(String)? onChanged,
Widget? suffixIcon,
}) {
final controller = TextEditingController(text: initialValue);
return Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Column(
@@ -301,12 +275,9 @@ class _UserFormScreenState extends State<UserFormScreen> {
children: [
Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
TextFormField(
initialValue: initialValue,
decoration: InputDecoration(
hintText: hintText,
suffixIcon: suffixIcon,
),
ShadInputFormField(
controller: controller,
placeholder: Text(hintText),
keyboardType: keyboardType,
inputFormatters: inputFormatters,
validator: validator,
@@ -335,18 +306,10 @@ class _UserFormScreenState extends State<UserFormScreen> {
children: [
Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
TextFormField(
ShadInputFormField(
controller: controller,
obscureText: obscureText,
decoration: InputDecoration(
hintText: hintText,
suffixIcon: IconButton(
icon: Icon(
obscureText ? Icons.visibility : Icons.visibility_off,
),
onPressed: onToggleVisibility,
),
),
placeholder: Text(hintText),
validator: validator,
onSaved: onSaved,
),
@@ -355,7 +318,7 @@ class _UserFormScreenState extends State<UserFormScreen> {
);
}
// 전화번호 입력 섹션 (드롭다운 + 텍스트 필드)
// 전화번호 입력 섹션 (통합 입력 필드)
Widget _buildPhoneNumberSection(UserFormController controller) {
return Padding(
padding: const EdgeInsets.only(bottom: 16.0),
@@ -364,78 +327,28 @@ class _UserFormScreenState extends State<UserFormScreen> {
children: [
const Text('전화번호', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Row(
children: [
// 접두사 드롭다운 (010, 02, 031 등)
Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(4),
),
child: DropdownButton<String>(
value: controller.phonePrefix,
items: controller.phonePrefixes.map((prefix) {
return DropdownMenuItem(
value: prefix,
child: Text(prefix),
);
}).toList(),
onChanged: (value) {
if (value != null) {
controller.updatePhonePrefix(value);
}
},
underline: Container(), // 밑줄 제거
),
),
const SizedBox(width: 8),
const Text('-', style: TextStyle(fontSize: 16)),
const SizedBox(width: 8),
// 전화번호 입력 (7-8자리)
Expanded(
child: TextFormField(
initialValue: controller.phoneNumber,
decoration: const InputDecoration(
hintText: '1234567 또는 12345678',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.phone,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(8),
],
validator: (value) {
if (value != null && value.isNotEmpty) {
if (value.length < 7 || value.length > 8) {
return '전화번호는 7-8자리 숫자를 입력해주세요';
}
}
return null;
},
onChanged: (value) {
controller.updatePhoneNumber(value);
},
onSaved: (value) {
if (value != null) {
controller.updatePhoneNumber(value);
}
},
),
),
ShadInputFormField(
controller: TextEditingController(text: controller.combinedPhoneNumber),
placeholder: const Text('010-1234-5678'),
keyboardType: TextInputType.phone,
inputFormatters: [
KoreanPhoneFormatter(), // 한국식 전화번호 자동 포맷팅
],
validator: (value) {
if (value.isNotEmpty) {
return PhoneValidator.validate(value);
}
return null; // 선택 필드이므로 비어있어도 OK
},
onChanged: (value) {
controller.updatePhoneNumber(value);
},
onSaved: (value) {
if (value != null) {
controller.updatePhoneNumber(value);
}
},
),
if (controller.combinedPhoneNumber.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'전화번호: ${controller.combinedPhoneNumber}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
),
],
),
);
@@ -450,14 +363,11 @@ class _UserFormScreenState extends State<UserFormScreen> {
children: [
const Text('권한 *', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
DropdownButtonFormField<UserRole>(
value: controller.role,
decoration: const InputDecoration(
hintText: '권한을 선택하세요',
border: OutlineInputBorder(),
),
items: UserRole.values.map((role) {
return DropdownMenuItem<UserRole>(
ShadSelect<UserRole>(
selectedOptionBuilder: (context, value) => Text(value.displayName ?? ''),
placeholder: const Text('권한을 선택하세요'),
options: UserRole.values.map((role) {
return ShadOption(
value: role,
child: Text(role.displayName),
);
@@ -467,12 +377,6 @@ class _UserFormScreenState extends State<UserFormScreen> {
controller.role = value;
}
},
validator: (value) {
if (value == null) {
return '권한을 선택해주세요';
}
return null;
},
),
const SizedBox(height: 4),
Text(
@@ -494,17 +398,19 @@ class _UserFormScreenState extends State<UserFormScreen> {
void _onSaveUser(UserFormController controller) async {
await controller.saveUser((error) {
if (error != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(error),
backgroundColor: Colors.red,
ShadToaster.of(context).show(
ShadToast.destructive(
title: const Text('오류'),
description: Text(error),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(controller.isEditMode ? '사용자 정보가 수정되었습니다' : '사용자가 등록되었습니다'),
backgroundColor: Colors.green,
ShadToaster.of(context).show(
ShadToast(
title: const Text('성공'),
description: Text(
controller.isEditMode ? '사용자 정보가 수정되었습니다' : '사용자가 등록되었습니다',
),
),
);
Navigator.pop(context, true);

View File

@@ -1,13 +1,12 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:superport/models/user_model.dart';
import 'package:superport/screens/common/theme_shadcn.dart';
import 'package:superport/screens/common/components/shadcn_components.dart';
import 'package:superport/screens/common/widgets/pagination.dart';
import 'package:superport/screens/user/controllers/user_list_controller.dart';
import 'package:superport/utils/constants.dart';
import 'package:superport/utils/user_utils.dart';
/// shadcn/ui 스타일로 재설계된 사용자 관리 화면
class UserList extends StatefulWidget {
@@ -55,11 +54,6 @@ class _UserListState extends State<UserList> {
});
}
/// 회사명 반환 함수
String _getCompanyName(int companyId) {
// TODO: CompanyService를 통해 회사 정보 가져오기
return '회사 $companyId'; // 임시 처리
}
/// 상태별 색상 반환
Color _getStatusColor(bool isActive) {
@@ -112,26 +106,29 @@ class _UserListState extends State<UserList> {
/// 사용자 삭제 다이얼로그
void _showDeleteDialog(int userId, String userName) {
showDialog(
showShadDialog(
context: context,
builder: (context) => AlertDialog(
builder: (context) => ShadDialog(
title: const Text('사용자 삭제'),
content: Text('"$userName" 사용자를 정말로 삭제하시겠습니까?'),
description: Text('"$userName" 사용자를 정말로 삭제하시겠습니까?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
ShadButton.outline(
child: const Text('취소'),
onPressed: () => Navigator.of(context).pop(),
),
TextButton(
ShadButton.destructive(
child: const Text('삭제'),
onPressed: () async {
Navigator.of(context).pop();
await _controller.deleteUser(userId);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('사용자가 삭제되었습니다')),
ShadToaster.of(context).show(
ShadToast(
title: const Text('삭제 완료'),
description: const Text('사용자가 삭제되었습니다'),
),
);
},
child: const Text('삭제', style: TextStyle(color: Colors.red)),
),
],
),
@@ -143,23 +140,23 @@ class _UserListState extends State<UserList> {
final newStatus = !user.isActive;
final statusText = newStatus ? '활성화' : '비활성화';
showDialog(
showShadDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('사용자 상태 변경'),
content: Text('"${user.name}" 사용자를 $statusText 하시겠습니까?'),
builder: (context) => ShadDialog(
title: const Text('사용자 상태 변경'),
description: Text('"${user.name}" 사용자를 $statusText 하시겠습니까?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
ShadButton.outline(
child: const Text('취소'),
onPressed: () => Navigator.of(context).pop(),
),
TextButton(
ShadButton(
child: Text(statusText),
onPressed: () async {
Navigator.of(context).pop();
await _controller.changeUserStatus(user, newStatus);
},
child: Text(statusText),
),
],
),
@@ -173,7 +170,7 @@ class _UserListState extends State<UserList> {
builder: (context, child) {
if (_controller.isLoading && _controller.users.isEmpty) {
return const Center(
child: CircularProgressIndicator(),
child: ShadProgress(),
);
}
@@ -226,28 +223,9 @@ class _UserListState extends State<UserList> {
child: Column(
children: [
// 검색 바
TextField(
ShadInputFormField(
controller: _searchController,
decoration: InputDecoration(
hintText: '이름, 이메일, 사용자명으로 검색...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
_controller.setSearchQuery('');
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: ShadcnTheme.spacing4,
vertical: ShadcnTheme.spacing3,
),
),
placeholder: const Text('이름, 이메일, 사용자명으로 검색...'),
),
const SizedBox(height: ShadcnTheme.spacing3),
// 필터 버튼들
@@ -309,12 +287,13 @@ class _UserListState extends State<UserList> {
// 관리자용 비활성 포함 체크박스
Row(
children: [
Checkbox(
ShadCheckbox(
value: _controller.includeInactive,
onChanged: (_) => setState(() {
_controller.toggleIncludeInactive();
}),
),
const SizedBox(width: 8),
const Text('비활성 포함'),
],
),