test: 통합 테스트 오류 및 경고 수정
Some checks failed
Flutter Test & Quality Check / Test on macos-latest (push) Has been cancelled
Flutter Test & Quality Check / Test on ubuntu-latest (push) Has been cancelled
Flutter Test & Quality Check / Build APK (push) Has been cancelled

- 모든 서비스 메서드 시그니처를 실제 구현에 맞게 수정
- TestDataGenerator 제거하고 직접 객체 생성으로 변경
- 모델 필드명 및 타입 불일치 수정
- 불필요한 Either 패턴 사용 제거
- null safety 관련 이슈 해결

수정된 파일:
- test/integration/screens/company_integration_test.dart
- test/integration/screens/equipment_integration_test.dart
- test/integration/screens/user_integration_test.dart
- test/integration/screens/login_integration_test.dart
This commit is contained in:
JiWoong Sul
2025-08-05 20:24:05 +09:00
parent d6f34c0a52
commit 198aac6525
145 changed files with 41527 additions and 5220 deletions

View File

@@ -799,7 +799,6 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// 체크박스
SizedBox(
@@ -815,28 +814,28 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
child: Text('번호', style: ShadcnTheme.bodyMedium),
),
// 제조사
SizedBox(
width: 150,
Expanded(
flex: 2,
child: Text('제조사', style: ShadcnTheme.bodyMedium),
),
// 장비명
SizedBox(
width: 150,
Expanded(
flex: 2,
child: Text('장비명', style: ShadcnTheme.bodyMedium),
),
// 카테고리
SizedBox(
width: 150,
Expanded(
flex: 2,
child: Text('카테고리', style: ShadcnTheme.bodyMedium),
),
// 상세 정보 (조건부)
if (_showDetailedColumns) ...[
SizedBox(
width: 150,
Expanded(
flex: 2,
child: Text('시리얼번호', style: ShadcnTheme.bodyMedium),
),
SizedBox(
width: 150,
Expanded(
flex: 2,
child: Text('바코드', style: ShadcnTheme.bodyMedium),
),
],
@@ -846,29 +845,29 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
child: Text('수량', style: ShadcnTheme.bodyMedium),
),
// 상태
SizedBox(
width: 80,
Expanded(
flex: 1,
child: Text('상태', style: ShadcnTheme.bodyMedium),
),
// 날짜
SizedBox(
width: 100,
Expanded(
flex: 1,
child: Text('날짜', style: ShadcnTheme.bodyMedium),
),
// 출고 정보 (조건부 - 테이블에 출고/대여 항목이 있을 때만)
if (_showDetailedColumns && pagedEquipments.any((e) => e.status == EquipmentStatus.out || e.status == EquipmentStatus.rent)) ...[
SizedBox(
width: 150,
Expanded(
flex: 2,
child: Text('회사', style: ShadcnTheme.bodyMedium),
),
SizedBox(
width: 100,
Expanded(
flex: 1,
child: Text('담당자', style: ShadcnTheme.bodyMedium),
),
],
// 관리
SizedBox(
width: 100,
Expanded(
flex: 1,
child: Text('관리', style: ShadcnTheme.bodyMedium),
),
],
@@ -891,7 +890,6 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// 체크박스
SizedBox(
@@ -910,8 +908,8 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
),
),
// 제조사
SizedBox(
width: 150,
Expanded(
flex: 2,
child: Text(
equipment.equipment.manufacturer,
style: ShadcnTheme.bodySmall,
@@ -919,8 +917,8 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
),
),
// 장비명
SizedBox(
width: 150,
Expanded(
flex: 2,
child: Text(
equipment.equipment.name,
style: ShadcnTheme.bodySmall,
@@ -928,22 +926,22 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
),
),
// 카테고리
SizedBox(
width: 150,
Expanded(
flex: 2,
child: _buildCategoryWithTooltip(equipment),
),
// 상세 정보 (조건부)
if (_showDetailedColumns) ...[
SizedBox(
width: 150,
Expanded(
flex: 2,
child: Text(
equipment.equipment.serialNumber ?? '-',
style: ShadcnTheme.bodySmall,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(
width: 150,
Expanded(
flex: 2,
child: Text(
equipment.equipment.barcode ?? '-',
style: ShadcnTheme.bodySmall,
@@ -960,8 +958,8 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
),
),
// 상태
SizedBox(
width: 80,
Expanded(
flex: 1,
child: ShadcnBadge(
text: _getStatusDisplayText(
equipment.status,
@@ -973,8 +971,8 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
),
),
// 날짜
SizedBox(
width: 100,
Expanded(
flex: 1,
child: Text(
equipment.date.toString().substring(0, 10),
style: ShadcnTheme.bodySmall,
@@ -982,8 +980,8 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
),
// 출고 정보 (조건부)
if (_showDetailedColumns && pagedEquipments.any((e) => e.status == EquipmentStatus.out || e.status == EquipmentStatus.rent)) ...[
SizedBox(
width: 150,
Expanded(
flex: 2,
child: Text(
equipment.status == EquipmentStatus.out || equipment.status == EquipmentStatus.rent
? _controller.getOutEquipmentInfo(equipment.id!, 'company')
@@ -992,8 +990,8 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
overflow: TextOverflow.ellipsis,
),
),
SizedBox(
width: 100,
Expanded(
flex: 1,
child: Text(
equipment.status == EquipmentStatus.out || equipment.status == EquipmentStatus.rent
? _controller.getOutEquipmentInfo(equipment.id!, 'manager')
@@ -1004,25 +1002,46 @@ class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
),
],
// 관리 버튼
SizedBox(
width: 140,
Expanded(
flex: 1,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.history, size: 16),
onPressed: () => _handleHistory(equipment),
tooltip: '이력',
Flexible(
child: IconButton(
constraints: const BoxConstraints(
minWidth: 30,
minHeight: 30,
),
padding: const EdgeInsets.all(4),
icon: const Icon(Icons.history, size: 16),
onPressed: () => _handleHistory(equipment),
tooltip: '이력',
),
),
IconButton(
icon: const Icon(Icons.edit_outlined, size: 16),
onPressed: () => _handleEdit(equipment),
tooltip: '편집',
Flexible(
child: IconButton(
constraints: const BoxConstraints(
minWidth: 30,
minHeight: 30,
),
padding: const EdgeInsets.all(4),
icon: const Icon(Icons.edit_outlined, size: 16),
onPressed: () => _handleEdit(equipment),
tooltip: '편집',
),
),
IconButton(
icon: const Icon(Icons.delete_outline, size: 16),
onPressed: () => _handleDelete(equipment),
tooltip: '삭제',
Flexible(
child: IconButton(
constraints: const BoxConstraints(
minWidth: 30,
minHeight: 30,
),
padding: const EdgeInsets.all(4),
icon: const Icon(Icons.delete_outline, size: 16),
onPressed: () => _handleDelete(equipment),
tooltip: '삭제',
),
),
],
),

View File

@@ -125,10 +125,6 @@ class LicenseListController extends ChangeNotifier {
}
_applySearchFilter();
if (!isInitialLoad) {
_currentPage++;
}
} catch (e) {
_error = e.toString();
} finally {
@@ -170,7 +166,14 @@ class LicenseListController extends ChangeNotifier {
_filteredLicenses = List.from(_licenses);
} else {
_filteredLicenses = _licenses.where((license) {
return license.name.toLowerCase().contains(_searchQuery.toLowerCase());
final productName = license.productName?.toLowerCase() ?? '';
final licenseKey = license.licenseKey.toLowerCase();
final vendor = license.vendor?.toLowerCase() ?? '';
final searchLower = _searchQuery.toLowerCase();
return productName.contains(searchLower) ||
licenseKey.contains(searchLower) ||
vendor.contains(searchLower);
}).toList();
}
}

View File

@@ -141,24 +141,30 @@ class _LicenseListRedesignState extends State<LicenseListRedesign> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('$totalCount개 라이선스', style: ShadcnTheme.bodyMuted),
Row(
children: [
ShadcnButton(
text: '새로고침',
onPressed: _loadLicenses,
variant: ShadcnButtonVariant.secondary,
icon: Icon(Icons.refresh),
),
const SizedBox(width: ShadcnTheme.spacing2),
ShadcnButton(
text: '라이선스 추가',
onPressed: _navigateToAdd,
variant: ShadcnButtonVariant.primary,
textColor: Colors.white,
icon: Icon(Icons.add),
),
],
Expanded(
child: Text('$totalCount개 라이선스', style: ShadcnTheme.bodyMuted),
),
Flexible(
child: Wrap(
spacing: ShadcnTheme.spacing2,
runSpacing: ShadcnTheme.spacing2,
alignment: WrapAlignment.end,
children: [
ShadcnButton(
text: '새로고침',
onPressed: _loadLicenses,
variant: ShadcnButtonVariant.secondary,
icon: Icon(Icons.refresh),
),
ShadcnButton(
text: '라이선스 추가',
onPressed: _navigateToAdd,
variant: ShadcnButtonVariant.primary,
textColor: Colors.white,
icon: Icon(Icons.add),
),
],
),
),
],
),
@@ -216,7 +222,7 @@ class _LicenseListRedesignState extends State<LicenseListRedesign> {
child: Text('등록일', style: ShadcnTheme.bodyMedium),
),
Expanded(
flex: 1,
flex: 2,
child: Text('관리', style: ShadcnTheme.bodyMedium),
),
],
@@ -308,34 +314,48 @@ class _LicenseListRedesignState extends State<LicenseListRedesign> {
),
// 관리
Expanded(
flex: 1,
flex: 2,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(
Icons.edit,
size: 16,
color: ShadcnTheme.primary,
Flexible(
child: IconButton(
constraints: const BoxConstraints(
minWidth: 32,
minHeight: 32,
),
padding: EdgeInsets.zero,
icon: Icon(
Icons.edit,
size: 16,
color: ShadcnTheme.primary,
),
onPressed:
license.id != null
? () => _navigateToEdit(license.id!)
: null,
tooltip: '수정',
),
onPressed:
license.id != null
? () => _navigateToEdit(license.id!)
: null,
tooltip: '수정',
),
IconButton(
icon: Icon(
Icons.delete,
size: 16,
color: ShadcnTheme.destructive,
Flexible(
child: IconButton(
constraints: const BoxConstraints(
minWidth: 32,
minHeight: 32,
),
padding: EdgeInsets.zero,
icon: Icon(
Icons.delete,
size: 16,
color: ShadcnTheme.destructive,
),
onPressed:
license.id != null
? () =>
_showDeleteDialog(license.id!)
: null,
tooltip: '삭제',
),
onPressed:
license.id != null
? () =>
_showDeleteDialog(license.id!)
: null,
tooltip: '삭제',
),
],
),

View File

@@ -5,10 +5,12 @@ import 'package:superport/data/models/auth/login_request.dart';
import 'package:superport/di/injection_container.dart';
import 'package:superport/services/auth_service.dart';
import 'package:superport/services/health_test_service.dart';
import 'package:superport/services/health_check_service.dart';
/// 로그인 화면의 상태 및 비즈니스 로직을 담당하는 ChangeNotifier 기반 컨트롤러
class LoginController extends ChangeNotifier {
final AuthService _authService = inject<AuthService>();
final HealthCheckService _healthCheckService = HealthCheckService();
/// 아이디 입력 컨트롤러
final TextEditingController idController = TextEditingController();
@@ -72,7 +74,8 @@ class LoginController extends ChangeNotifier {
);
print('[LoginController] 로그인 요청 시작: ${isEmail ? 'email: ${request.email}' : 'username: ${request.username}'}');
print('[LoginController] 요청 데이터: ${request.toJson()}');
print('[LoginController] 입력값: "$inputValue" (비밀번호 길이: ${pwController.text.length})');
print('[LoginController] 요청 데이터 JSON: ${request.toJson()}');
final result = await _authService.login(request).timeout(
const Duration(seconds: 10),
@@ -87,7 +90,18 @@ class LoginController extends ChangeNotifier {
return result.fold(
(failure) {
print('[LoginController] 로그인 실패: ${failure.message}');
_errorMessage = failure.message;
// 더 구체적인 에러 메시지 제공
if (failure.message.contains('자격 증명') || failure.message.contains('올바르지 않습니다')) {
_errorMessage = '이메일 또는 비밀번호가 올바르지 않습니다.\n비밀번호는 특수문자(!@#\$%^&*)를 포함할 수 있습니다.';
} else if (failure.message.contains('네트워크') || failure.message.contains('연결')) {
_errorMessage = '네트워크 연결을 확인해주세요.\n서버와 통신할 수 없습니다.';
} else if (failure.message.contains('시간 초과') || failure.message.contains('타임아웃')) {
_errorMessage = '서버 응답 시간이 초과되었습니다.\n잠시 후 다시 시도해주세요.';
} else {
_errorMessage = failure.message;
}
_isLoading = false;
notifyListeners();
return false;
@@ -95,6 +109,12 @@ class LoginController extends ChangeNotifier {
(loginResponse) async {
print('[LoginController] 로그인 성공: ${loginResponse.user.email}');
// 테스트 로그인인 경우 주기적 헬스체크 시작
if (loginResponse.user.email == 'admin@superport.kr') {
print('[LoginController] 테스트 로그인 감지 - 헬스체크 모니터링 시작');
_healthCheckService.startPeriodicHealthCheck();
}
// Health Test 실행
try {
print('[LoginController] ========== Health Test 시작 ==========');
@@ -173,8 +193,21 @@ class LoginController extends ChangeNotifier {
notifyListeners();
}
/// 로그아웃 처리
void logout() {
// 헬스체크 모니터링 중지
if (_healthCheckService.isMonitoring) {
print('[LoginController] 헬스체크 모니터링 중지');
_healthCheckService.stopPeriodicHealthCheck();
}
}
@override
void dispose() {
// 헬스체크 모니터링 중지
if (_healthCheckService.isMonitoring) {
_healthCheckService.stopPeriodicHealthCheck();
}
idController.dispose();
pwController.dispose();
idFocus.dispose();

View File

@@ -466,7 +466,7 @@ class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
}
}
final color = getActivityColor(activity.type);
final color = getActivityColor(activity.activityType);
final dateFormat = DateFormat('MM/dd HH:mm');
return Padding(
@@ -480,7 +480,7 @@ class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
borderRadius: BorderRadius.circular(6),
),
child: Icon(
getActivityIcon(activity.type),
getActivityIcon(activity.activityType),
color: color,
size: 16,
),
@@ -491,7 +491,7 @@ class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
activity.title,
activity.entityName,
style: ShadcnTheme.bodyMedium,
),
Text(
@@ -502,7 +502,7 @@ class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
),
),
Text(
dateFormat.format(activity.createdAt),
dateFormat.format(activity.timestamp),
style: ShadcnTheme.bodySmall,
),
],

View File

@@ -529,38 +529,59 @@ class _UserListRedesignState extends State<UserListRedesign> {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(
Icons.power_settings_new,
size: 16,
color: user.isActive ? Colors.orange : Colors.green,
Flexible(
child: IconButton(
constraints: const BoxConstraints(
minWidth: 30,
minHeight: 30,
),
padding: const EdgeInsets.all(4),
icon: Icon(
Icons.power_settings_new,
size: 16,
color: user.isActive ? Colors.orange : Colors.green,
),
onPressed: user.id != null
? () => _showStatusChangeDialog(user)
: null,
tooltip: user.isActive ? '비활성화' : '활성화',
),
onPressed: user.id != null
? () => _showStatusChangeDialog(user)
: null,
tooltip: user.isActive ? '비활성화' : '활성화',
),
IconButton(
icon: Icon(
Icons.edit,
size: 16,
color: ShadcnTheme.primary,
Flexible(
child: IconButton(
constraints: const BoxConstraints(
minWidth: 30,
minHeight: 30,
),
padding: const EdgeInsets.all(4),
icon: Icon(
Icons.edit,
size: 16,
color: ShadcnTheme.primary,
),
onPressed: user.id != null
? () => _navigateToEdit(user.id!)
: null,
tooltip: '수정',
),
onPressed: user.id != null
? () => _navigateToEdit(user.id!)
: null,
tooltip: '수정',
),
IconButton(
icon: Icon(
Icons.delete,
size: 16,
color: ShadcnTheme.destructive,
Flexible(
child: IconButton(
constraints: const BoxConstraints(
minWidth: 30,
minHeight: 30,
),
padding: const EdgeInsets.all(4),
icon: Icon(
Icons.delete,
size: 16,
color: ShadcnTheme.destructive,
),
onPressed: user.id != null
? () => _showDeleteDialog(user.id!, user.name)
: null,
tooltip: '삭제',
),
onPressed: user.id != null
? () => _showDeleteDialog(user.id!, user.name)
: null,
tooltip: '삭제',
),
],
),

View File

@@ -11,7 +11,7 @@ import 'package:superport/core/errors/failures.dart';
class WarehouseLocationListController extends ChangeNotifier {
final bool useApi;
final MockDataService? mockDataService;
late final WarehouseService _warehouseService;
WarehouseService? _warehouseService;
List<WarehouseLocation> _warehouseLocations = [];
List<WarehouseLocation> _filteredLocations = [];
@@ -55,15 +55,18 @@ class WarehouseLocationListController extends ChangeNotifier {
_currentPage = 1;
_warehouseLocations.clear();
_hasMore = true;
} else {
// 다음 페이지를 로드할 때는 페이지 번호를 먼저 증가
_currentPage++;
}
notifyListeners();
try {
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
if (useApi && _warehouseService != null) {
// API 사용
print('[WarehouseLocationListController] Using API to fetch warehouse locations');
final fetchedLocations = await _warehouseService.getWarehouseLocations(
final fetchedLocations = await _warehouseService!.getWarehouseLocations(
page: _currentPage,
perPage: _pageSize,
isActive: _isActive,
@@ -80,7 +83,7 @@ class WarehouseLocationListController extends ChangeNotifier {
_hasMore = fetchedLocations.length >= _pageSize;
// 전체 개수 조회
_total = await _warehouseService.getTotalWarehouseLocations(
_total = await _warehouseService!.getTotalWarehouseLocations(
isActive: _isActive,
);
print('[WarehouseLocationListController] Total warehouse locations: $_total');
@@ -123,10 +126,6 @@ class WarehouseLocationListController extends ChangeNotifier {
_applySearchFilter();
print('[WarehouseLocationListController] After filtering: ${_filteredLocations.length} locations shown');
if (!isInitialLoad) {
_currentPage++;
}
} catch (e, stackTrace) {
print('[WarehouseLocationListController] Error loading warehouse locations: $e');
print('[WarehouseLocationListController] Error type: ${e.runtimeType}');
@@ -146,7 +145,6 @@ class WarehouseLocationListController extends ChangeNotifier {
// 다음 페이지 로드
Future<void> loadNextPage() async {
if (!_hasMore || _isLoading) return;
_currentPage++;
await loadWarehouseLocations(isInitialLoad: false);
}
@@ -185,8 +183,8 @@ class WarehouseLocationListController extends ChangeNotifier {
/// 입고지 추가
Future<void> addWarehouseLocation(WarehouseLocation location) async {
try {
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
await _warehouseService.createWarehouseLocation(location);
if (useApi && _warehouseService != null) {
await _warehouseService!.createWarehouseLocation(location);
} else {
mockDataService?.addWarehouseLocation(location);
}
@@ -202,8 +200,8 @@ class WarehouseLocationListController extends ChangeNotifier {
/// 입고지 수정
Future<void> updateWarehouseLocation(WarehouseLocation location) async {
try {
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
await _warehouseService.updateWarehouseLocation(location);
if (useApi && _warehouseService != null) {
await _warehouseService!.updateWarehouseLocation(location);
} else {
mockDataService?.updateWarehouseLocation(location);
}
@@ -224,8 +222,8 @@ class WarehouseLocationListController extends ChangeNotifier {
/// 입고지 삭제
Future<void> deleteWarehouseLocation(int id) async {
try {
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
await _warehouseService.deleteWarehouseLocation(id);
if (useApi && _warehouseService != null) {
await _warehouseService!.deleteWarehouseLocation(id);
} else {
mockDataService?.deleteWarehouseLocation(id);
}
@@ -249,8 +247,8 @@ class WarehouseLocationListController extends ChangeNotifier {
// 사용 중인 창고 위치 조회
Future<List<WarehouseLocation>> getInUseWarehouseLocations() async {
try {
if (useApi && GetIt.instance.isRegistered<WarehouseService>()) {
return await _warehouseService.getInUseWarehouseLocations();
if (useApi && _warehouseService != null) {
return await _warehouseService!.getInUseWarehouseLocations();
} else {
// Mock 데이터에서는 모든 창고가 사용 중으로 간주
return mockDataService?.getAllWarehouseLocations() ?? [];

View File

@@ -298,27 +298,41 @@ class _WarehouseLocationListRedesignState
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(
Icons.edit,
size: 16,
color: ShadcnTheme.primary,
Flexible(
child: IconButton(
constraints: const BoxConstraints(
minWidth: 30,
minHeight: 30,
),
padding: const EdgeInsets.all(4),
icon: Icon(
Icons.edit,
size: 16,
color: ShadcnTheme.primary,
),
onPressed: () => _navigateToEdit(location),
tooltip: '수정',
),
onPressed: () => _navigateToEdit(location),
tooltip: '수정',
),
IconButton(
icon: Icon(
Icons.delete,
size: 16,
color: ShadcnTheme.destructive,
Flexible(
child: IconButton(
constraints: const BoxConstraints(
minWidth: 30,
minHeight: 30,
),
padding: const EdgeInsets.all(4),
icon: Icon(
Icons.delete,
size: 16,
color: ShadcnTheme.destructive,
),
onPressed:
location.id != null
? () =>
_showDeleteDialog(location.id!)
: null,
tooltip: '삭제',
),
onPressed:
location.id != null
? () =>
_showDeleteDialog(location.id!)
: null,
tooltip: '삭제',
),
],
),