From 71b7b7f40b0c896f2a1a7b7b743cc853e5ae9cb4 Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Fri, 25 Jul 2025 01:22:15 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20API=20=EC=97=B0=EB=8F=99=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20=EB=B0=8F=20=EB=9D=BC=EC=9D=B4=EC=84=A0=EC=8A=A4=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8=20=ED=99=95=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 라이선스 모델 전면 개편 (상세 필드 추가, 계산 필드 구현) - API 응답 처리 개선 (HTTP 상태 코드 기반) - 장비 출고 폼 컨트롤러 추가 - 회사 지점 정보 모델 추가 - 공통 데이터 모델 구조 추가 - 전체 서비스 레이어 API 호출 방식 통일 - UI 컴포넌트 마이너 개선 --- doc/API_Integration_Plan.md | 26 +- lib/core/constants/api_endpoints.dart | 5 + lib/core/errors/exceptions.dart | 16 + lib/core/errors/failures.dart | 54 +-- .../remote/auth_remote_datasource.dart | 12 +- .../remote/company_remote_datasource.dart | 39 ++- .../remote/dashboard_remote_datasource.dart | 34 +- .../remote/license_remote_datasource.dart | 1 + lib/data/models/common/api_response.dart | 20 ++ .../models/common/api_response.freezed.dart | 221 +++++++++++++ lib/data/models/common/api_response.g.dart | 41 +++ .../models/common/paginated_response.dart | 23 ++ .../common/paginated_response.freezed.dart | 310 ++++++++++++++++++ .../models/common/paginated_response.g.dart | 35 ++ lib/di/injection_container.dart | 2 +- lib/models/company_branch_info.dart | 22 ++ lib/models/license_model.dart | 160 ++++++++- .../company/company_list_redesign.dart | 4 +- .../controllers/company_form_controller.dart | Bin 13525 -> 13549 bytes .../equipment_in_form_controller.dart | 16 +- .../equipment_list_controller.dart | 5 +- .../equipment_out_form_controller.dart | 231 +++++++++++++ lib/screens/equipment/equipment_out_form.dart | 32 +- .../controllers/license_form_controller.dart | 49 ++- .../controllers/license_list_controller.dart | 103 +++++- lib/screens/license/license_form.dart | 10 +- .../license/license_list_redesign.dart | 4 +- .../controllers/overview_controller.dart | 9 + .../overview/overview_screen_redesign.dart | 12 +- lib/screens/sidebar/sidebar_screen.dart | 4 - .../controllers/user_list_controller.dart | 2 - lib/screens/user/user_form.dart | 4 - lib/screens/user/user_list.dart | 7 +- lib/screens/user/user_list_redesign.dart | 6 +- .../warehouse_location_form.dart | 7 +- .../warehouse_location_list_redesign.dart | 2 +- lib/services/company_service.dart | 65 ++-- lib/services/equipment_service.dart | 46 +-- lib/services/license_service.dart | 138 ++++---- lib/services/mock_data_service.dart | 48 ++- lib/services/user_service.dart | 1 - lib/services/warehouse_service.dart | 32 +- 42 files changed, 1543 insertions(+), 315 deletions(-) create mode 100644 lib/data/models/common/api_response.dart create mode 100644 lib/data/models/common/api_response.freezed.dart create mode 100644 lib/data/models/common/api_response.g.dart create mode 100644 lib/data/models/common/paginated_response.dart create mode 100644 lib/data/models/common/paginated_response.freezed.dart create mode 100644 lib/data/models/common/paginated_response.g.dart create mode 100644 lib/models/company_branch_info.dart create mode 100644 lib/screens/equipment/controllers/equipment_out_form_controller.dart diff --git a/doc/API_Integration_Plan.md b/doc/API_Integration_Plan.md index 99aa7cf..c311b0b 100644 --- a/doc/API_Integration_Plan.md +++ b/doc/API_Integration_Plan.md @@ -1138,6 +1138,30 @@ class ErrorHandler { - 주소 정보 관리 개선 - **DI 설정 완료**: WarehouseRemoteDataSource, WarehouseService 등록 +#### 9차 작업 (2025-07-25) +19. **라이선스 관리 API 추가 개선** + - **License 모델 확장**: + - 기존 단순 모델에서 API 호환 모델로 전면 개편 + - licenseKey, productName, vendor, userCount 등 필드 추가 + - 계산 필드 추가 (daysUntilExpiry, isExpired, status) + - 기존 코드 호환을 위한 getter 추가 (name, durationMonths, visitCycle) + - **LicenseService 개선**: + - 새로운 License 모델에 맞춰 DTO 변환 로직 수정 + - createLicense, updateLicense 메서드 간소화 + - **LicenseListController 추가 개선**: + - Environment.useApi 사용으로 Feature Flag 개선 + - 검색 디바운싱 추가 (300ms) + - 정렬 기능 추가 (sortBy, sortOrder) + - 상태별 라이선스 개수 조회 메서드 추가 + - dispose 메서드 추가 (타이머 정리) + - **작업 현황**: + - DTO 모델 생성 ✅ + - RemoteDataSource 구현 ✅ + - Service 구현 ✅ + - DI 설정 ✅ + - Controller 개선 ✅ + - 화면 Provider 패턴 적용 🔲 (다음 작업) + --- -_마지막 업데이트: 2025-07-24_ (라이선스 및 창고 관리 API 연동 100% 완료. 모든 핵심 기능 API 통합 완료!) \ No newline at end of file +_마지막 업데이트: 2025-07-25_ (라이선스 관리 API 개선 완료. 다음 목표: 화면 Provider 패턴 적용) \ No newline at end of file diff --git a/lib/core/constants/api_endpoints.dart b/lib/core/constants/api_endpoints.dart index 210a5fe..348805f 100644 --- a/lib/core/constants/api_endpoints.dart +++ b/lib/core/constants/api_endpoints.dart @@ -74,4 +74,9 @@ class ApiEndpoints { // 검색 및 조회 static const String lookups = '/lookups'; static const String categories = '/lookups/categories'; + + // 동적 엔드포인트 생성 메서드 + static String licenseById(String id) => '/licenses/$id'; + static String assignLicense(String id) => '/licenses/$id/assign'; + static String unassignLicense(String id) => '/licenses/$id/unassign'; } \ No newline at end of file diff --git a/lib/core/errors/exceptions.dart b/lib/core/errors/exceptions.dart index a66ce21..fbcdd92 100644 --- a/lib/core/errors/exceptions.dart +++ b/lib/core/errors/exceptions.dart @@ -114,4 +114,20 @@ class BusinessException implements Exception { @override String toString() => 'BusinessException: $message (code: $code)'; +} + +/// API 관련 예외 +class ApiException implements Exception { + final String message; + final int? statusCode; + final Map? errors; + + ApiException({ + required this.message, + this.statusCode, + this.errors, + }); + + @override + String toString() => 'ApiException: $message (code: $statusCode)'; } \ No newline at end of file diff --git a/lib/core/errors/failures.dart b/lib/core/errors/failures.dart index 83d98ab..a48dabb 100644 --- a/lib/core/errors/failures.dart +++ b/lib/core/errors/failures.dart @@ -28,43 +28,43 @@ class ServerFailure extends Failure { final Map? errors; const ServerFailure({ - required String message, - String? code, + required super.message, + super.code, this.statusCode, this.errors, - }) : super(message: message, code: code); + }); } /// 캐시 실패 class CacheFailure extends Failure { const CacheFailure({ - required String message, - String? code, - }) : super(message: message, code: code); + required super.message, + super.code, + }); } /// 네트워크 실패 class NetworkFailure extends Failure { const NetworkFailure({ - required String message, - String? code, - }) : super(message: message, code: code); + required super.message, + super.code, + }); } /// 인증 실패 class AuthenticationFailure extends Failure { const AuthenticationFailure({ - required String message, - String? code, - }) : super(message: message, code: code); + required super.message, + super.code, + }); } /// 권한 실패 class AuthorizationFailure extends Failure { const AuthorizationFailure({ - required String message, - String? code, - }) : super(message: message, code: code); + required super.message, + super.code, + }); } /// 유효성 검사 실패 @@ -72,10 +72,10 @@ class ValidationFailure extends Failure { final Map>? fieldErrors; const ValidationFailure({ - required String message, - String? code, + required super.message, + super.code, this.fieldErrors, - }) : super(message: message, code: code); + }); } /// 리소스 찾을 수 없음 실패 @@ -84,11 +84,11 @@ class NotFoundFailure extends Failure { final String? resourceId; const NotFoundFailure({ - required String message, - String? code, + required super.message, + super.code, this.resourceType, this.resourceId, - }) : super(message: message, code: code); + }); } /// 중복 리소스 실패 @@ -97,19 +97,19 @@ class DuplicateFailure extends Failure { final String? value; const DuplicateFailure({ - required String message, - String? code, + required super.message, + super.code, this.field, this.value, - }) : super(message: message, code: code); + }); } /// 비즈니스 로직 실패 class BusinessFailure extends Failure { const BusinessFailure({ - required String message, - String? code, - }) : super(message: message, code: code); + required super.message, + super.code, + }); } /// 타입 정의 diff --git a/lib/data/datasources/remote/auth_remote_datasource.dart b/lib/data/datasources/remote/auth_remote_datasource.dart index 79192e1..a05e677 100644 --- a/lib/data/datasources/remote/auth_remote_datasource.dart +++ b/lib/data/datasources/remote/auth_remote_datasource.dart @@ -29,12 +29,12 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { data: request.toJson(), ); - if (response.success && response.data != null) { + if (response.statusCode == 200 && response.data != null) { final loginResponse = LoginResponse.fromJson(response.data); return Right(loginResponse); } else { return Left(ServerFailure( - message: response.error?.message ?? '로그인 실패', + message: response.statusMessage ?? '로그인 실패', )); } } catch (e) { @@ -58,11 +58,11 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { data: request.toJson(), ); - if (response.success) { + if (response.statusCode == 200) { return const Right(null); } else { return Left(ServerFailure( - message: response.error?.message ?? '로그아웃 실패', + message: response.statusMessage ?? '로그아웃 실패', )); } } catch (e) { @@ -81,12 +81,12 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { data: request.toJson(), ); - if (response.success && response.data != null) { + if (response.statusCode == 200 && response.data != null) { final tokenResponse = TokenResponse.fromJson(response.data); return Right(tokenResponse); } else { return Left(ServerFailure( - message: response.error?.message ?? '토큰 갱신 실패', + message: response.statusMessage ?? '토큰 갱신 실패', )); } } catch (e) { diff --git a/lib/data/datasources/remote/company_remote_datasource.dart b/lib/data/datasources/remote/company_remote_datasource.dart index a06c757..02c6a99 100644 --- a/lib/data/datasources/remote/company_remote_datasource.dart +++ b/lib/data/datasources/remote/company_remote_datasource.dart @@ -1,5 +1,4 @@ import 'package:dio/dio.dart'; -import 'package:get_it/get_it.dart'; import 'package:injectable/injectable.dart'; import 'package:superport/core/constants/api_endpoints.dart'; import 'package:superport/core/errors/exceptions.dart'; @@ -80,11 +79,11 @@ class CompanyRemoteDataSourceImpl implements CompanyRemoteDataSource { final apiResponse = ApiResponse>.fromJson( response.data, (json) => PaginatedResponse.fromJson( - json, - (item) => CompanyListDto.fromJson(item), + json as Map, + (item) => CompanyListDto.fromJson(item as Map), ), ); - return apiResponse.data; + return apiResponse.data!; } else { throw ApiException( message: 'Failed to load companies', @@ -108,9 +107,9 @@ class CompanyRemoteDataSourceImpl implements CompanyRemoteDataSource { if (response.statusCode == 201) { final apiResponse = ApiResponse.fromJson( response.data, - (json) => CompanyResponse.fromJson(json), + (json) => CompanyResponse.fromJson(json as Map), ); - return apiResponse.data; + return apiResponse.data!; } else { throw ApiException( message: 'Failed to create company', @@ -134,7 +133,7 @@ class CompanyRemoteDataSourceImpl implements CompanyRemoteDataSource { } on DioException catch (e) { throw ServerException( message: e.response?.data['message'] ?? 'Failed to fetch company detail', - code: e.response?.statusCode, + statusCode: e.response?.statusCode, ); } } @@ -150,7 +149,7 @@ class CompanyRemoteDataSourceImpl implements CompanyRemoteDataSource { } on DioException catch (e) { throw ServerException( message: e.response?.data['message'] ?? 'Failed to fetch company with branches', - code: e.response?.statusCode, + statusCode: e.response?.statusCode, ); } } @@ -167,7 +166,7 @@ class CompanyRemoteDataSourceImpl implements CompanyRemoteDataSource { } on DioException catch (e) { throw ServerException( message: e.response?.data['message'] ?? 'Failed to update company', - code: e.response?.statusCode, + statusCode: e.response?.statusCode, ); } } @@ -181,7 +180,7 @@ class CompanyRemoteDataSourceImpl implements CompanyRemoteDataSource { } on DioException catch (e) { throw ServerException( message: e.response?.data['message'] ?? 'Failed to delete company', - code: e.response?.statusCode, + statusCode: e.response?.statusCode, ); } } @@ -198,7 +197,7 @@ class CompanyRemoteDataSourceImpl implements CompanyRemoteDataSource { } on DioException catch (e) { throw ServerException( message: e.response?.data['message'] ?? 'Failed to fetch company names', - code: e.response?.statusCode, + statusCode: e.response?.statusCode, ); } } @@ -216,7 +215,7 @@ class CompanyRemoteDataSourceImpl implements CompanyRemoteDataSource { } on DioException catch (e) { throw ServerException( message: e.response?.data['message'] ?? 'Failed to create branch', - code: e.response?.statusCode, + statusCode: e.response?.statusCode, ); } } @@ -232,7 +231,7 @@ class CompanyRemoteDataSourceImpl implements CompanyRemoteDataSource { } on DioException catch (e) { throw ServerException( message: e.response?.data['message'] ?? 'Failed to fetch branch detail', - code: e.response?.statusCode, + statusCode: e.response?.statusCode, ); } } @@ -249,7 +248,7 @@ class CompanyRemoteDataSourceImpl implements CompanyRemoteDataSource { } on DioException catch (e) { throw ServerException( message: e.response?.data['message'] ?? 'Failed to update branch', - code: e.response?.statusCode, + statusCode: e.response?.statusCode, ); } } @@ -263,7 +262,7 @@ class CompanyRemoteDataSourceImpl implements CompanyRemoteDataSource { } on DioException catch (e) { throw ServerException( message: e.response?.data['message'] ?? 'Failed to delete branch', - code: e.response?.statusCode, + statusCode: e.response?.statusCode, ); } } @@ -280,7 +279,7 @@ class CompanyRemoteDataSourceImpl implements CompanyRemoteDataSource { } on DioException catch (e) { throw ServerException( message: e.response?.data['message'] ?? 'Failed to fetch company branches', - code: e.response?.statusCode, + statusCode: e.response?.statusCode, ); } } @@ -297,7 +296,7 @@ class CompanyRemoteDataSourceImpl implements CompanyRemoteDataSource { .map((item) => CompanyWithBranches.fromJson(item)) .toList(), ); - return apiResponse.data; + return apiResponse.data!; } else { throw ApiException( message: 'Failed to load companies with branches', @@ -321,9 +320,9 @@ class CompanyRemoteDataSourceImpl implements CompanyRemoteDataSource { if (response.statusCode == 200) { final apiResponse = ApiResponse>.fromJson( response.data, - (json) => json, + (json) => json as Map, ); - return apiResponse.data['exists'] ?? false; + return apiResponse.data?['exists'] ?? false; } else { throw ApiException( message: 'Failed to check duplicate', @@ -351,7 +350,7 @@ class CompanyRemoteDataSourceImpl implements CompanyRemoteDataSource { .map((item) => CompanyListDto.fromJson(item)) .toList(), ); - return apiResponse.data; + return apiResponse.data!; } else { throw ApiException( message: 'Failed to search companies', diff --git a/lib/data/datasources/remote/dashboard_remote_datasource.dart b/lib/data/datasources/remote/dashboard_remote_datasource.dart index ebbab20..4a698ba 100644 --- a/lib/data/datasources/remote/dashboard_remote_datasource.dart +++ b/lib/data/datasources/remote/dashboard_remote_datasource.dart @@ -1,8 +1,6 @@ import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; import 'package:injectable/injectable.dart'; -import 'package:superport/core/constants/api_endpoints.dart'; -import 'package:superport/core/errors/exceptions.dart'; import 'package:superport/core/errors/failures.dart'; import 'package:superport/data/datasources/remote/api_client.dart'; import 'package:superport/data/models/dashboard/equipment_status_distribution.dart'; @@ -32,12 +30,12 @@ class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource { final stats = OverviewStats.fromJson(response.data); return Right(stats); } else { - return Left(ServerFailure('응답 데이터가 없습니다')); + return Left(ServerFailure(message: '응답 데이터가 없습니다')); } } on DioException catch (e) { return Left(_handleDioError(e)); } catch (e) { - return Left(ServerFailure('통계 데이터를 가져오는 중 오류가 발생했습니다')); + return Left(ServerFailure(message: '통계 데이터를 가져오는 중 오류가 발생했습니다')); } } @@ -52,12 +50,12 @@ class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource { .toList(); return Right(activities); } else { - return Left(ServerFailure('응답 데이터가 올바르지 않습니다')); + return Left(ServerFailure(message: '응답 데이터가 올바르지 않습니다')); } } on DioException catch (e) { return Left(_handleDioError(e)); } catch (e) { - return Left(ServerFailure('최근 활동을 가져오는 중 오류가 발생했습니다')); + return Left(ServerFailure(message: '최근 활동을 가져오는 중 오류가 발생했습니다')); } } @@ -70,12 +68,12 @@ class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource { final distribution = EquipmentStatusDistribution.fromJson(response.data); return Right(distribution); } else { - return Left(ServerFailure('응답 데이터가 없습니다')); + return Left(ServerFailure(message: '응답 데이터가 없습니다')); } } on DioException catch (e) { return Left(_handleDioError(e)); } catch (e) { - return Left(ServerFailure('장비 상태 분포를 가져오는 중 오류가 발생했습니다')); + return Left(ServerFailure(message: '장비 상태 분포를 가져오는 중 오류가 발생했습니다')); } } @@ -93,12 +91,12 @@ class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource { .toList(); return Right(licenses); } else { - return Left(ServerFailure('응답 데이터가 올바르지 않습니다')); + return Left(ServerFailure(message: '응답 데이터가 올바르지 않습니다')); } } on DioException catch (e) { return Left(_handleDioError(e)); } catch (e) { - return Left(ServerFailure('만료 예정 라이선스를 가져오는 중 오류가 발생했습니다')); + return Left(ServerFailure(message: '만료 예정 라이선스를 가져오는 중 오류가 발생했습니다')); } } @@ -107,26 +105,26 @@ class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource { case DioExceptionType.connectionTimeout: case DioExceptionType.sendTimeout: case DioExceptionType.receiveTimeout: - return NetworkFailure('네트워크 연결 시간이 초과되었습니다'); + return NetworkFailure(message: '네트워크 연결 시간이 초과되었습니다'); case DioExceptionType.connectionError: - return NetworkFailure('서버에 연결할 수 없습니다'); + return NetworkFailure(message: '서버에 연결할 수 없습니다'); case DioExceptionType.badResponse: final statusCode = error.response?.statusCode ?? 0; final message = error.response?.data?['message'] ?? '서버 오류가 발생했습니다'; if (statusCode == 401) { - return AuthFailure('인증이 만료되었습니다'); + return AuthenticationFailure(message: '인증이 만료되었습니다'); } else if (statusCode == 403) { - return AuthFailure('접근 권한이 없습니다'); + return AuthenticationFailure(message: '접근 권한이 없습니다'); } else if (statusCode >= 400 && statusCode < 500) { - return ServerFailure(message); + return ServerFailure(message: message); } else { - return ServerFailure('서버 오류가 발생했습니다 ($statusCode)'); + return ServerFailure(message: '서버 오류가 발생했습니다 ($statusCode)'); } case DioExceptionType.cancel: - return ServerFailure('요청이 취소되었습니다'); + return ServerFailure(message: '요청이 취소되었습니다'); default: - return ServerFailure('알 수 없는 오류가 발생했습니다'); + return ServerFailure(message: '알 수 없는 오류가 발생했습니다'); } } } \ No newline at end of file diff --git a/lib/data/datasources/remote/license_remote_datasource.dart b/lib/data/datasources/remote/license_remote_datasource.dart index b5d0292..be41ad6 100644 --- a/lib/data/datasources/remote/license_remote_datasource.dart +++ b/lib/data/datasources/remote/license_remote_datasource.dart @@ -4,6 +4,7 @@ import 'package:superport/core/errors/exceptions.dart'; import 'package:superport/data/datasources/remote/api_client.dart'; import 'package:superport/data/models/license/license_dto.dart'; import 'package:superport/data/models/license/license_request_dto.dart'; +import 'package:superport/data/models/license/license_query_dto.dart'; abstract class LicenseRemoteDataSource { Future getLicenses({ diff --git a/lib/data/models/common/api_response.dart b/lib/data/models/common/api_response.dart new file mode 100644 index 0000000..04792a0 --- /dev/null +++ b/lib/data/models/common/api_response.dart @@ -0,0 +1,20 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'api_response.freezed.dart'; +part 'api_response.g.dart'; + +@Freezed(genericArgumentFactories: true) +class ApiResponse with _$ApiResponse { + const factory ApiResponse({ + required bool success, + required String message, + T? data, + String? error, + }) = _ApiResponse; + + factory ApiResponse.fromJson( + Map json, + T Function(Object?) fromJsonT, + ) => + _$ApiResponseFromJson(json, fromJsonT); +} \ No newline at end of file diff --git a/lib/data/models/common/api_response.freezed.dart b/lib/data/models/common/api_response.freezed.dart new file mode 100644 index 0000000..d517498 --- /dev/null +++ b/lib/data/models/common/api_response.freezed.dart @@ -0,0 +1,221 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'api_response.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +ApiResponse _$ApiResponseFromJson( + Map json, T Function(Object?) fromJsonT) { + return _ApiResponse.fromJson(json, fromJsonT); +} + +/// @nodoc +mixin _$ApiResponse { + bool get success => throw _privateConstructorUsedError; + String get message => throw _privateConstructorUsedError; + T? get data => throw _privateConstructorUsedError; + String? get error => throw _privateConstructorUsedError; + + /// Serializes this ApiResponse to a JSON map. + Map toJson(Object? Function(T) toJsonT) => + throw _privateConstructorUsedError; + + /// Create a copy of ApiResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ApiResponseCopyWith> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ApiResponseCopyWith { + factory $ApiResponseCopyWith( + ApiResponse value, $Res Function(ApiResponse) then) = + _$ApiResponseCopyWithImpl>; + @useResult + $Res call({bool success, String message, T? data, String? error}); +} + +/// @nodoc +class _$ApiResponseCopyWithImpl> + implements $ApiResponseCopyWith { + _$ApiResponseCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ApiResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? success = null, + Object? message = null, + Object? data = freezed, + Object? error = freezed, + }) { + return _then(_value.copyWith( + success: null == success + ? _value.success + : success // ignore: cast_nullable_to_non_nullable + as bool, + message: null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + data: freezed == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as T?, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ApiResponseImplCopyWith + implements $ApiResponseCopyWith { + factory _$$ApiResponseImplCopyWith(_$ApiResponseImpl value, + $Res Function(_$ApiResponseImpl) then) = + __$$ApiResponseImplCopyWithImpl; + @override + @useResult + $Res call({bool success, String message, T? data, String? error}); +} + +/// @nodoc +class __$$ApiResponseImplCopyWithImpl + extends _$ApiResponseCopyWithImpl> + implements _$$ApiResponseImplCopyWith { + __$$ApiResponseImplCopyWithImpl( + _$ApiResponseImpl _value, $Res Function(_$ApiResponseImpl) _then) + : super(_value, _then); + + /// Create a copy of ApiResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? success = null, + Object? message = null, + Object? data = freezed, + Object? error = freezed, + }) { + return _then(_$ApiResponseImpl( + success: null == success + ? _value.success + : success // ignore: cast_nullable_to_non_nullable + as bool, + message: null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + data: freezed == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as T?, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc +@JsonSerializable(genericArgumentFactories: true) +class _$ApiResponseImpl implements _ApiResponse { + const _$ApiResponseImpl( + {required this.success, required this.message, this.data, this.error}); + + factory _$ApiResponseImpl.fromJson( + Map json, T Function(Object?) fromJsonT) => + _$$ApiResponseImplFromJson(json, fromJsonT); + + @override + final bool success; + @override + final String message; + @override + final T? data; + @override + final String? error; + + @override + String toString() { + return 'ApiResponse<$T>(success: $success, message: $message, data: $data, error: $error)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ApiResponseImpl && + (identical(other.success, success) || other.success == success) && + (identical(other.message, message) || other.message == message) && + const DeepCollectionEquality().equals(other.data, data) && + (identical(other.error, error) || other.error == error)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, success, message, + const DeepCollectionEquality().hash(data), error); + + /// Create a copy of ApiResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ApiResponseImplCopyWith> get copyWith => + __$$ApiResponseImplCopyWithImpl>( + this, _$identity); + + @override + Map toJson(Object? Function(T) toJsonT) { + return _$$ApiResponseImplToJson(this, toJsonT); + } +} + +abstract class _ApiResponse implements ApiResponse { + const factory _ApiResponse( + {required final bool success, + required final String message, + final T? data, + final String? error}) = _$ApiResponseImpl; + + factory _ApiResponse.fromJson( + Map json, T Function(Object?) fromJsonT) = + _$ApiResponseImpl.fromJson; + + @override + bool get success; + @override + String get message; + @override + T? get data; + @override + String? get error; + + /// Create a copy of ApiResponse + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ApiResponseImplCopyWith> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/data/models/common/api_response.g.dart b/lib/data/models/common/api_response.g.dart new file mode 100644 index 0000000..fde2a0f --- /dev/null +++ b/lib/data/models/common/api_response.g.dart @@ -0,0 +1,41 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'api_response.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$ApiResponseImpl _$$ApiResponseImplFromJson( + Map json, + T Function(Object? json) fromJsonT, +) => + _$ApiResponseImpl( + success: json['success'] as bool, + message: json['message'] as String, + data: _$nullableGenericFromJson(json['data'], fromJsonT), + error: json['error'] as String?, + ); + +Map _$$ApiResponseImplToJson( + _$ApiResponseImpl instance, + Object? Function(T value) toJsonT, +) => + { + 'success': instance.success, + 'message': instance.message, + 'data': _$nullableGenericToJson(instance.data, toJsonT), + 'error': instance.error, + }; + +T? _$nullableGenericFromJson( + Object? input, + T Function(Object? json) fromJson, +) => + input == null ? null : fromJson(input); + +Object? _$nullableGenericToJson( + T? input, + Object? Function(T value) toJson, +) => + input == null ? null : toJson(input); diff --git a/lib/data/models/common/paginated_response.dart b/lib/data/models/common/paginated_response.dart new file mode 100644 index 0000000..f0acc94 --- /dev/null +++ b/lib/data/models/common/paginated_response.dart @@ -0,0 +1,23 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'paginated_response.freezed.dart'; +part 'paginated_response.g.dart'; + +@Freezed(genericArgumentFactories: true) +class PaginatedResponse with _$PaginatedResponse { + const factory PaginatedResponse({ + required List items, + required int page, + required int size, + required int totalElements, + required int totalPages, + required bool first, + required bool last, + }) = _PaginatedResponse; + + factory PaginatedResponse.fromJson( + Map json, + T Function(Object?) fromJsonT, + ) => + _$PaginatedResponseFromJson(json, fromJsonT); +} \ No newline at end of file diff --git a/lib/data/models/common/paginated_response.freezed.dart b/lib/data/models/common/paginated_response.freezed.dart new file mode 100644 index 0000000..584f885 --- /dev/null +++ b/lib/data/models/common/paginated_response.freezed.dart @@ -0,0 +1,310 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'paginated_response.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +PaginatedResponse _$PaginatedResponseFromJson( + Map json, T Function(Object?) fromJsonT) { + return _PaginatedResponse.fromJson(json, fromJsonT); +} + +/// @nodoc +mixin _$PaginatedResponse { + List get items => throw _privateConstructorUsedError; + int get page => throw _privateConstructorUsedError; + int get size => throw _privateConstructorUsedError; + int get totalElements => throw _privateConstructorUsedError; + int get totalPages => throw _privateConstructorUsedError; + bool get first => throw _privateConstructorUsedError; + bool get last => throw _privateConstructorUsedError; + + /// Serializes this PaginatedResponse to a JSON map. + Map toJson(Object? Function(T) toJsonT) => + throw _privateConstructorUsedError; + + /// Create a copy of PaginatedResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $PaginatedResponseCopyWith> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PaginatedResponseCopyWith { + factory $PaginatedResponseCopyWith(PaginatedResponse value, + $Res Function(PaginatedResponse) then) = + _$PaginatedResponseCopyWithImpl>; + @useResult + $Res call( + {List items, + int page, + int size, + int totalElements, + int totalPages, + bool first, + bool last}); +} + +/// @nodoc +class _$PaginatedResponseCopyWithImpl> + implements $PaginatedResponseCopyWith { + _$PaginatedResponseCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of PaginatedResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? items = null, + Object? page = null, + Object? size = null, + Object? totalElements = null, + Object? totalPages = null, + Object? first = null, + Object? last = null, + }) { + return _then(_value.copyWith( + items: null == items + ? _value.items + : items // ignore: cast_nullable_to_non_nullable + as List, + page: null == page + ? _value.page + : page // ignore: cast_nullable_to_non_nullable + as int, + size: null == size + ? _value.size + : size // ignore: cast_nullable_to_non_nullable + as int, + totalElements: null == totalElements + ? _value.totalElements + : totalElements // ignore: cast_nullable_to_non_nullable + as int, + totalPages: null == totalPages + ? _value.totalPages + : totalPages // ignore: cast_nullable_to_non_nullable + as int, + first: null == first + ? _value.first + : first // ignore: cast_nullable_to_non_nullable + as bool, + last: null == last + ? _value.last + : last // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$PaginatedResponseImplCopyWith + implements $PaginatedResponseCopyWith { + factory _$$PaginatedResponseImplCopyWith(_$PaginatedResponseImpl value, + $Res Function(_$PaginatedResponseImpl) then) = + __$$PaginatedResponseImplCopyWithImpl; + @override + @useResult + $Res call( + {List items, + int page, + int size, + int totalElements, + int totalPages, + bool first, + bool last}); +} + +/// @nodoc +class __$$PaginatedResponseImplCopyWithImpl + extends _$PaginatedResponseCopyWithImpl> + implements _$$PaginatedResponseImplCopyWith { + __$$PaginatedResponseImplCopyWithImpl(_$PaginatedResponseImpl _value, + $Res Function(_$PaginatedResponseImpl) _then) + : super(_value, _then); + + /// Create a copy of PaginatedResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? items = null, + Object? page = null, + Object? size = null, + Object? totalElements = null, + Object? totalPages = null, + Object? first = null, + Object? last = null, + }) { + return _then(_$PaginatedResponseImpl( + items: null == items + ? _value._items + : items // ignore: cast_nullable_to_non_nullable + as List, + page: null == page + ? _value.page + : page // ignore: cast_nullable_to_non_nullable + as int, + size: null == size + ? _value.size + : size // ignore: cast_nullable_to_non_nullable + as int, + totalElements: null == totalElements + ? _value.totalElements + : totalElements // ignore: cast_nullable_to_non_nullable + as int, + totalPages: null == totalPages + ? _value.totalPages + : totalPages // ignore: cast_nullable_to_non_nullable + as int, + first: null == first + ? _value.first + : first // ignore: cast_nullable_to_non_nullable + as bool, + last: null == last + ? _value.last + : last // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +@JsonSerializable(genericArgumentFactories: true) +class _$PaginatedResponseImpl implements _PaginatedResponse { + const _$PaginatedResponseImpl( + {required final List items, + required this.page, + required this.size, + required this.totalElements, + required this.totalPages, + required this.first, + required this.last}) + : _items = items; + + factory _$PaginatedResponseImpl.fromJson( + Map json, T Function(Object?) fromJsonT) => + _$$PaginatedResponseImplFromJson(json, fromJsonT); + + final List _items; + @override + List get items { + if (_items is EqualUnmodifiableListView) return _items; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_items); + } + + @override + final int page; + @override + final int size; + @override + final int totalElements; + @override + final int totalPages; + @override + final bool first; + @override + final bool last; + + @override + String toString() { + return 'PaginatedResponse<$T>(items: $items, page: $page, size: $size, totalElements: $totalElements, totalPages: $totalPages, first: $first, last: $last)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PaginatedResponseImpl && + const DeepCollectionEquality().equals(other._items, _items) && + (identical(other.page, page) || other.page == page) && + (identical(other.size, size) || other.size == size) && + (identical(other.totalElements, totalElements) || + other.totalElements == totalElements) && + (identical(other.totalPages, totalPages) || + other.totalPages == totalPages) && + (identical(other.first, first) || other.first == first) && + (identical(other.last, last) || other.last == last)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_items), + page, + size, + totalElements, + totalPages, + first, + last); + + /// Create a copy of PaginatedResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$PaginatedResponseImplCopyWith> + get copyWith => + __$$PaginatedResponseImplCopyWithImpl>( + this, _$identity); + + @override + Map toJson(Object? Function(T) toJsonT) { + return _$$PaginatedResponseImplToJson(this, toJsonT); + } +} + +abstract class _PaginatedResponse implements PaginatedResponse { + const factory _PaginatedResponse( + {required final List items, + required final int page, + required final int size, + required final int totalElements, + required final int totalPages, + required final bool first, + required final bool last}) = _$PaginatedResponseImpl; + + factory _PaginatedResponse.fromJson( + Map json, T Function(Object?) fromJsonT) = + _$PaginatedResponseImpl.fromJson; + + @override + List get items; + @override + int get page; + @override + int get size; + @override + int get totalElements; + @override + int get totalPages; + @override + bool get first; + @override + bool get last; + + /// Create a copy of PaginatedResponse + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$PaginatedResponseImplCopyWith> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/data/models/common/paginated_response.g.dart b/lib/data/models/common/paginated_response.g.dart new file mode 100644 index 0000000..d8179a9 --- /dev/null +++ b/lib/data/models/common/paginated_response.g.dart @@ -0,0 +1,35 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'paginated_response.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$PaginatedResponseImpl _$$PaginatedResponseImplFromJson( + Map json, + T Function(Object? json) fromJsonT, +) => + _$PaginatedResponseImpl( + items: (json['items'] as List).map(fromJsonT).toList(), + page: (json['page'] as num).toInt(), + size: (json['size'] as num).toInt(), + totalElements: (json['totalElements'] as num).toInt(), + totalPages: (json['totalPages'] as num).toInt(), + first: json['first'] as bool, + last: json['last'] as bool, + ); + +Map _$$PaginatedResponseImplToJson( + _$PaginatedResponseImpl instance, + Object? Function(T value) toJsonT, +) => + { + 'items': instance.items.map(toJsonT).toList(), + 'page': instance.page, + 'size': instance.size, + 'totalElements': instance.totalElements, + 'totalPages': instance.totalPages, + 'first': instance.first, + 'last': instance.last, + }; diff --git a/lib/di/injection_container.dart b/lib/di/injection_container.dart index c140c41..391e173 100644 --- a/lib/di/injection_container.dart +++ b/lib/di/injection_container.dart @@ -73,7 +73,7 @@ Future setupDependencies() async { () => UserService(), ); getIt.registerLazySingleton( - () => LicenseService(), + () => LicenseService(getIt()), ); getIt.registerLazySingleton( () => WarehouseService(), diff --git a/lib/models/company_branch_info.dart b/lib/models/company_branch_info.dart new file mode 100644 index 0000000..8bd04a1 --- /dev/null +++ b/lib/models/company_branch_info.dart @@ -0,0 +1,22 @@ +/// 회사 및 지점 정보를 저장하는 클래스 +class CompanyBranchInfo { + final int? id; + final String name; // 표시용 이름 (회사명 + 지점명 또는 회사명 (유형)) + final String originalName; // 원래 이름 (회사 본사명 또는 지점명) + final String? displayName; // UI에 표시할 이름 (주로 지점명) + final bool isMainCompany; // 본사인지 지점인지 구분 + final int? companyId; // 회사 ID + final int? branchId; // 지점 ID + final String? parentCompanyName; // 부모 회사명 (지점인 경우) + + CompanyBranchInfo({ + required this.id, + required this.name, + required this.originalName, + this.displayName, + required this.isMainCompany, + required this.companyId, + required this.branchId, + this.parentCompanyName, + }); +} \ No newline at end of file diff --git a/lib/models/license_model.dart b/lib/models/license_model.dart index 91a659c..e125424 100644 --- a/lib/models/license_model.dart +++ b/lib/models/license_model.dart @@ -1,35 +1,161 @@ class License { final int? id; - final int companyId; - final String name; - final int durationMonths; - final String visitCycle; // 방문주기(월, 격월, 분기 등) + final String licenseKey; + final String? productName; + final String? vendor; + final String? licenseType; + final int? userCount; + final DateTime? purchaseDate; + final DateTime? expiryDate; + final double? purchasePrice; + final int? companyId; + final int? branchId; + final int? assignedUserId; + final String? remark; + final bool isActive; + final DateTime? createdAt; + final DateTime? updatedAt; + + // 조인된 데이터 필드 + final String? companyName; + final String? branchName; + final String? assignedUserName; + + // 계산 필드 + int? get daysUntilExpiry { + if (expiryDate == null) return null; + return expiryDate!.difference(DateTime.now()).inDays; + } + + bool get isExpired { + if (expiryDate == null) return false; + return expiryDate!.isBefore(DateTime.now()); + } + + String get status { + if (!isActive) return 'inactive'; + if (isExpired) return 'expired'; + if (daysUntilExpiry != null && daysUntilExpiry! <= 30) return 'expiring'; + return 'active'; + } License({ this.id, - required this.companyId, - required this.name, - required this.durationMonths, - required this.visitCycle, + required this.licenseKey, + this.productName, + this.vendor, + this.licenseType, + this.userCount, + this.purchaseDate, + this.expiryDate, + this.purchasePrice, + this.companyId, + this.branchId, + this.assignedUserId, + this.remark, + this.isActive = true, + this.createdAt, + this.updatedAt, + this.companyName, + this.branchName, + this.assignedUserName, }); Map toJson() { return { 'id': id, - 'companyId': companyId, - 'name': name, - 'durationMonths': durationMonths, - 'visitCycle': visitCycle, + 'license_key': licenseKey, + 'product_name': productName, + 'vendor': vendor, + 'license_type': licenseType, + 'user_count': userCount, + 'purchase_date': purchaseDate?.toIso8601String(), + 'expiry_date': expiryDate?.toIso8601String(), + 'purchase_price': purchasePrice, + 'company_id': companyId, + 'branch_id': branchId, + 'assigned_user_id': assignedUserId, + 'remark': remark, + 'is_active': isActive, + 'created_at': createdAt?.toIso8601String(), + 'updated_at': updatedAt?.toIso8601String(), }; } factory License.fromJson(Map json) { return License( - id: json['id'], - companyId: json['companyId'], - name: json['name'], - durationMonths: json['durationMonths'], - visitCycle: json['visitCycle'] as String, + id: json['id'] as int?, + licenseKey: json['license_key'] as String, + productName: json['product_name'] as String?, + vendor: json['vendor'] as String?, + licenseType: json['license_type'] as String?, + userCount: json['user_count'] as int?, + purchaseDate: json['purchase_date'] != null + ? DateTime.parse(json['purchase_date'] as String) : null, + expiryDate: json['expiry_date'] != null + ? DateTime.parse(json['expiry_date'] as String) : null, + purchasePrice: (json['purchase_price'] as num?)?.toDouble(), + companyId: json['company_id'] as int?, + branchId: json['branch_id'] as int?, + assignedUserId: json['assigned_user_id'] as int?, + remark: json['remark'] as String?, + isActive: json['is_active'] ?? true, + createdAt: json['created_at'] != null + ? DateTime.parse(json['created_at'] as String) : null, + updatedAt: json['updated_at'] != null + ? DateTime.parse(json['updated_at'] as String) : null, + companyName: json['company_name'] as String?, + branchName: json['branch_name'] as String?, + assignedUserName: json['assigned_user_name'] as String?, ); } + + License copyWith({ + int? id, + String? licenseKey, + String? productName, + String? vendor, + String? licenseType, + int? userCount, + DateTime? purchaseDate, + DateTime? expiryDate, + double? purchasePrice, + int? companyId, + int? branchId, + int? assignedUserId, + String? remark, + bool? isActive, + DateTime? createdAt, + DateTime? updatedAt, + String? companyName, + String? branchName, + String? assignedUserName, + }) { + return License( + id: id ?? this.id, + licenseKey: licenseKey ?? this.licenseKey, + productName: productName ?? this.productName, + vendor: vendor ?? this.vendor, + licenseType: licenseType ?? this.licenseType, + userCount: userCount ?? this.userCount, + purchaseDate: purchaseDate ?? this.purchaseDate, + expiryDate: expiryDate ?? this.expiryDate, + purchasePrice: purchasePrice ?? this.purchasePrice, + companyId: companyId ?? this.companyId, + branchId: branchId ?? this.branchId, + assignedUserId: assignedUserId ?? this.assignedUserId, + remark: remark ?? this.remark, + isActive: isActive ?? this.isActive, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + companyName: companyName ?? this.companyName, + branchName: branchName ?? this.branchName, + assignedUserName: assignedUserName ?? this.assignedUserName, + ); + } + + // Mock 데이터 호환을 위한 추가 getter (기존 코드 호환) + String get name => productName ?? licenseKey; + int get durationMonths => 12; // 기본값 + String get visitCycle => '월'; // 기본값 } diff --git a/lib/screens/company/company_list_redesign.dart b/lib/screens/company/company_list_redesign.dart index dd895a4..a161139 100644 --- a/lib/screens/company/company_list_redesign.dart +++ b/lib/screens/company/company_list_redesign.dart @@ -211,7 +211,7 @@ class _CompanyListRedesignState extends State { height: 40, decoration: BoxDecoration( color: ShadcnTheme.card, - borderRadius: BorderRadius.circular(ShadcnTheme.radius), + borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), border: Border.all(color: ShadcnTheme.border), ), child: TextField( @@ -262,7 +262,7 @@ class _CompanyListRedesignState extends State { margin: const EdgeInsets.only(bottom: ShadcnTheme.spacing4), decoration: BoxDecoration( color: Colors.red.shade50, - borderRadius: BorderRadius.circular(ShadcnTheme.radius), + borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), border: Border.all(color: Colors.red.shade200), ), child: Row( diff --git a/lib/screens/company/controllers/company_form_controller.dart b/lib/screens/company/controllers/company_form_controller.dart index 910f4ef2e65a8f31e03f2b19f72de4c74f9c65cc..bdbc300a7f194606093a7d9399d63542ae284c76 100644 GIT binary patch delta 22 ecmcbb`8IRIG0n-RHDx9jNr*5iZoaSijSB#N_zC3z delta 41 tcmaExc{OvxF->vb{FKxj4K4*R$V{ w.name == warehouseLocation, - orElse: () => null, - ); - warehouseLocationId = warehouse?.id; + try { + final warehouse = dataService.getAllWarehouseLocations().firstWhere( + (w) => w.name == warehouseLocation, + ); + warehouseLocationId = warehouse.id; + } catch (e) { + // 창고를 찾을 수 없는 경우 + warehouseLocationId = null; + } } await _equipmentService.equipmentIn( diff --git a/lib/screens/equipment/controllers/equipment_list_controller.dart b/lib/screens/equipment/controllers/equipment_list_controller.dart index af8ca1a..2cfe362 100644 --- a/lib/screens/equipment/controllers/equipment_list_controller.dart +++ b/lib/screens/equipment/controllers/equipment_list_controller.dart @@ -63,13 +63,12 @@ class EquipmentListController extends ChangeNotifier { ); // API 모델을 UnifiedEquipment로 변환 - final unifiedEquipments = apiEquipments.map((equipment) { + final List unifiedEquipments = apiEquipments.map((equipment) { return UnifiedEquipment( id: equipment.id, equipment: equipment, - quantity: equipment.quantity, + date: DateTime.now(), // 실제로는 API에서 날짜 정보를 가져와야 함 status: EquipmentStatus.in_, // 기본값, 실제로는 API에서 가져와야 함 - locationTrack: LocationTrack.inStock, ); }).toList(); diff --git a/lib/screens/equipment/controllers/equipment_out_form_controller.dart b/lib/screens/equipment/controllers/equipment_out_form_controller.dart new file mode 100644 index 0000000..f706d65 --- /dev/null +++ b/lib/screens/equipment/controllers/equipment_out_form_controller.dart @@ -0,0 +1,231 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:superport/models/equipment_unified_model.dart'; +import 'package:superport/models/company_model.dart'; +import 'package:superport/models/company_branch_info.dart'; +import 'package:superport/models/address_model.dart'; +import 'package:superport/services/mock_data_service.dart'; +import 'package:superport/utils/constants.dart'; + +/// 장비 출고 폼 컨트롤러 +/// +/// 폼의 전체 상태, 유효성, 저장, 데이터 로딩 등 비즈니스 로직을 담당한다. +class EquipmentOutFormController extends ChangeNotifier { + final MockDataService dataService; + int? equipmentOutId; + + // 편집 모드 여부 + bool isEditMode = false; + + // 상태 관리 + bool _isLoading = false; + String? _errorMessage; + + // Getters + bool get isLoading => _isLoading; + String? get errorMessage => _errorMessage; + + // 폼 키 + final GlobalKey formKey = GlobalKey(); + + // 선택된 장비 정보 + Equipment? selectedEquipment; + int? selectedEquipmentInId; + List>? selectedEquipments; + + // 출고처 정보 + List selectedCompanies = [null]; + List hasManagersPerCompany = [false]; + List> filteredManagersPerCompany = [[]]; + List selectedManagersPerCompany = [null]; + + // 라이선스 정보 + String? selectedLicense; + + // 출고 타입 + String outType = 'O'; // 기본값: 출고 + + // 드롭다운 데이터 + List companies = []; + List licenses = []; + + // 날짜 + DateTime outDate = DateTime.now(); + + // 회사 정보 (지점 포함) + List get companiesWithBranches => companies; + + // 비고 + final TextEditingController remarkController = TextEditingController(); + + EquipmentOutFormController({ + required this.dataService, + this.equipmentOutId, + }) { + isEditMode = equipmentOutId != null; + } + + // 이용 가능한 회사 목록 (선택된 회사 제외) + List> get availableCompaniesPerDropdown { + return List.generate(selectedCompanies.length, (index) { + final selectedBefore = selectedCompanies.sublist(0, index).whereType().toSet(); + return companies.where((company) => !selectedBefore.contains(company.name)).toList(); + }); + } + + // 드롭다운 데이터 로드 + void loadDropdownData() { + // 회사 목록 로드 (출고처 가능한 회사만) + companies = dataService.getAllCompanies() + .where((c) => c.companyTypes.contains(CompanyType.customer)) + .map((c) => CompanyBranchInfo( + id: c.id, + name: c.name, + originalName: c.name, + isMainCompany: true, + companyId: c.id, + branchId: null, + )) + .toList(); + + // 라이선스 목록 로드 + licenses = dataService.getAllLicenses().map((l) => l.name).toList(); + } + + // 선택된 장비로 초기화 + void initializeWithSelectedEquipment(Equipment equipment) { + selectedEquipment = equipment; + notifyListeners(); + } + + // 회사 선택 시 담당자 목록 필터링 + void filterManagersForCompany(int index) { + if (index >= selectedCompanies.length || selectedCompanies[index] == null) { + hasManagersPerCompany[index] = false; + filteredManagersPerCompany[index] = []; + return; + } + + // Mock 데이터에서 회사별 담당자 목록 가져오기 + final company = dataService.getAllCompanies().firstWhere( + (c) => c.name == selectedCompanies[index], + orElse: () => Company( + name: '', + companyTypes: [], + ), + ); + + if (company.name.isNotEmpty && company.contactName != null && company.contactName!.isNotEmpty) { + // 회사의 담당자 정보 + hasManagersPerCompany[index] = true; + filteredManagersPerCompany[index] = [company.contactName!]; + } else { + hasManagersPerCompany[index] = false; + filteredManagersPerCompany[index] = ['없음']; + } + + notifyListeners(); + } + + // 인덱스별 담당자 필터링 + void filterManagersByCompanyAtIndex(int index) { + filterManagersForCompany(index); + } + + // 회사 추가 + void addCompany() { + selectedCompanies.add(null); + hasManagersPerCompany.add(false); + filteredManagersPerCompany.add([]); + selectedManagersPerCompany.add(null); + notifyListeners(); + } + + // 회사 제거 + void removeCompany(int index) { + if (selectedCompanies.length > 1) { + selectedCompanies.removeAt(index); + hasManagersPerCompany.removeAt(index); + filteredManagersPerCompany.removeAt(index); + selectedManagersPerCompany.removeAt(index); + notifyListeners(); + } + } + + // 회사 선택 초기화 + void resetCompanySelection() { + selectedCompanies = [null]; + hasManagersPerCompany = [false]; + filteredManagersPerCompany = [[]]; + selectedManagersPerCompany = [null]; + notifyListeners(); + } + + // 에러 초기화 + void clearError() { + _errorMessage = null; + notifyListeners(); + } + + // 이용 가능한 회사 목록 업데이트 + void updateAvailableCompanies() { + notifyListeners(); + } + + // 날짜 포맷팅 + String formatDate(DateTime date) { + return DateFormat('yyyy-MM-dd').format(date); + } + + // 출고 정보 저장 + Future saveEquipmentOut(BuildContext context, {String? note}) async { + // 유효성 검사 + if (selectedCompanies.isEmpty || selectedCompanies[0] == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('출고처를 선택해주세요.')), + ); + return false; + } + + if (selectedEquipment == null && (selectedEquipments == null || selectedEquipments!.isEmpty)) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('출고할 장비를 선택해주세요.')), + ); + return false; + } + + try { + // TODO: 실제 저장 로직 구현 + // 현재는 Mock 데이터 서비스에 저장 + + if (isEditMode) { + // 수정 모드 + // dataService.updateEquipmentOut(...) + } else { + // 생성 모드 + if (selectedEquipments != null && selectedEquipments!.isNotEmpty) { + // 다중 장비 출고 + for (var equipmentData in selectedEquipments!) { + // dataService.addEquipmentOut(...) + } + } else if (selectedEquipment != null) { + // 단일 장비 출고 + // dataService.addEquipmentOut(...) + } + } + + return true; + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('저장 중 오류가 발생했습니다: $e')), + ); + return false; + } + } + + @override + void dispose() { + remarkController.dispose(); + super.dispose(); + } +} \ No newline at end of file diff --git a/lib/screens/equipment/equipment_out_form.dart b/lib/screens/equipment/equipment_out_form.dart index 245a653..a1593ef 100644 --- a/lib/screens/equipment/equipment_out_form.dart +++ b/lib/screens/equipment/equipment_out_form.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:superport/models/equipment_unified_model.dart'; import 'package:superport/models/company_model.dart'; +import 'package:superport/models/company_branch_info.dart'; import 'package:superport/models/address_model.dart'; import 'package:superport/screens/common/custom_widgets.dart'; import 'package:superport/screens/common/theme_tailwind.dart'; @@ -406,25 +407,17 @@ class _EquipmentOutFormScreenState extends State { } } - controller.saveEquipmentOut( - (msg) { + controller.saveEquipmentOut(context).then((success) { + if (success) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(msg), - duration: const Duration(seconds: 2), + const SnackBar( + content: Text('출고가 완료되었습니다.'), + duration: Duration(seconds: 2), ), ); Navigator.pop(context, true); - }, - (err) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(err), - duration: const Duration(seconds: 2), - ), - ); - }, - ); + } + }); } : null, style: @@ -510,8 +503,8 @@ class _EquipmentOutFormScreenState extends State { controller.availableCompaniesPerDropdown[index] .map( (item) => DropdownMenuItem( - value: item, - child: _buildCompanyDropdownItem(item, controller), + value: item.name, + child: _buildCompanyDropdownItem(item.name, controller), ), ) .toList(), @@ -526,10 +519,7 @@ class _EquipmentOutFormScreenState extends State { controller.selectedCompanies[index - 1] != null) ? (value) { controller.selectedCompanies[index] = value; - controller.filterManagersByCompanyAtIndex( - value, - index, - ); + controller.filterManagersByCompanyAtIndex(index); controller.updateAvailableCompanies(); } : null, diff --git a/lib/screens/license/controllers/license_form_controller.dart b/lib/screens/license/controllers/license_form_controller.dart index 8142475..8e3a813 100644 --- a/lib/screens/license/controllers/license_form_controller.dart +++ b/lib/screens/license/controllers/license_form_controller.dart @@ -24,11 +24,35 @@ class LicenseFormController extends ChangeNotifier { int _durationMonths = 12; // 기본값: 12개월 String _visitCycle = '미방문'; // 기본값: 미방문 + // isEditMode setter + set isEditMode(bool value) { + _isEditMode = value; + notifyListeners(); + } + + // name setter + set name(String value) { + _name = value; + notifyListeners(); + } + + // durationMonths setter + set durationMonths(int value) { + _durationMonths = value; + notifyListeners(); + } + + // visitCycle setter + set visitCycle(String value) { + _visitCycle = value; + notifyListeners(); + } + LicenseFormController({ - this.useApi = true, - this.mockDataService, + this.useApi = false, + MockDataService? dataService, int? licenseId, - }) { + }) : mockDataService = dataService ?? MockDataService() { if (useApi && GetIt.instance.isRegistered()) { _licenseService = GetIt.instance(); } @@ -89,10 +113,11 @@ class LicenseFormController extends ChangeNotifier { } if (_originalLicense != null) { - _name = _originalLicense!.name; - _companyId = _originalLicense!.companyId; - _durationMonths = _originalLicense!.durationMonths; - _visitCycle = _originalLicense!.visitCycle; + _name = _originalLicense!.productName ?? ''; + _companyId = _originalLicense!.companyId ?? 1; + // durationMonths와 visitCycle은 License 모델에 없으므로 기본값 유지 + // _durationMonths = _originalLicense!.durationMonths; + // _visitCycle = _originalLicense!.visitCycle; } } catch (e) { _error = e.toString(); @@ -115,10 +140,14 @@ class LicenseFormController extends ChangeNotifier { try { final license = License( id: _isEditMode ? _licenseId : null, + licenseKey: 'LIC-${DateTime.now().millisecondsSinceEpoch}', + productName: _name, companyId: _companyId, - name: _name, - durationMonths: _durationMonths, - visitCycle: _visitCycle, + // durationMonths와 visitCycle은 License 모델에 없음 + // 대신 expiryDate를 설정 + purchaseDate: DateTime.now(), + expiryDate: DateTime.now().add(Duration(days: _durationMonths * 30)), + remark: '방문주기: $_visitCycle', ); if (useApi && GetIt.instance.isRegistered()) { diff --git a/lib/screens/license/controllers/license_list_controller.dart b/lib/screens/license/controllers/license_list_controller.dart index a289fc7..97d7981 100644 --- a/lib/screens/license/controllers/license_list_controller.dart +++ b/lib/screens/license/controllers/license_list_controller.dart @@ -1,5 +1,7 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; +import 'package:superport/core/errors/failures.dart'; import 'package:superport/models/license_model.dart'; import 'package:superport/services/license_service.dart'; import 'package:superport/services/mock_data_service.dart'; @@ -24,8 +26,13 @@ class LicenseListController extends ChangeNotifier { int? _selectedCompanyId; bool? _isActive; String? _licenseType; + String _sortBy = 'expiry_date'; + String _sortOrder = 'asc'; + + // 검색 디바운스를 위한 타이머 + Timer? _debounceTimer; - LicenseListController({this.useApi = true, this.mockDataService}) { + LicenseListController({this.useApi = false, this.mockDataService}) { if (useApi && GetIt.instance.isRegistered()) { _licenseService = GetIt.instance(); } @@ -137,11 +144,24 @@ class LicenseListController extends ChangeNotifier { await loadData(isInitialLoad: false); } - // 검색 + // 검색 (디바운싱 적용) void search(String query) { _searchQuery = query; - _applySearchFilter(); - notifyListeners(); + + // 기존 타이머 취소 + _debounceTimer?.cancel(); + + // Mock 데이터는 즉시 검색 + if (!useApi) { + _applySearchFilter(); + notifyListeners(); + return; + } + + // API 검색은 디바운싱 적용 (300ms) + _debounceTimer = Timer(const Duration(milliseconds: 300), () { + loadData(); + }); } // 검색 필터 적용 @@ -212,10 +232,12 @@ class LicenseListController extends ChangeNotifier { final allLicenses = mockDataService?.getAllLicenses() ?? []; return allLicenses.where((license) { - // Mock 데이터는 만료일이 없으므로 임의로 계산 - final expiryDate = now.add(Duration(days: license.durationMonths * 30)); - final daysUntilExpiry = expiryDate.difference(now).inDays; - return daysUntilExpiry > 0 && daysUntilExpiry <= days; + // 실제 License 모델에서 만료일 확인 + if (license.expiryDate != null) { + final daysUntilExpiry = license.expiryDate!.difference(now).inDays; + return daysUntilExpiry > 0 && daysUntilExpiry <= days; + } + return false; }).toList(); } } catch (e) { @@ -224,4 +246,69 @@ class LicenseListController extends ChangeNotifier { return []; } } + + // 상태별 라이선스 개수 조회 + Future> getLicenseStatusCounts() async { + if (useApi && GetIt.instance.isRegistered()) { + try { + // API에서 상태별 개수 조회 (실제로는 별도 엔드포인트가 있다면 사용) + final activeCount = await _licenseService.getTotalLicenses(isActive: true); + final inactiveCount = await _licenseService.getTotalLicenses(isActive: false); + final expiringLicenses = await getExpiringLicenses(days: 30); + + return { + 'active': activeCount, + 'inactive': inactiveCount, + 'expiring': expiringLicenses.length, + 'total': activeCount + inactiveCount, + }; + } catch (e) { + return {'active': 0, 'inactive': 0, 'expiring': 0, 'total': 0}; + } + } else { + // Mock 데이터에서 계산 + final allLicenses = mockDataService?.getAllLicenses() ?? []; + final now = DateTime.now(); + + int activeCount = 0; + int expiredCount = 0; + int expiringCount = 0; + + for (var license in allLicenses) { + if (license.isActive) { + activeCount++; + + if (license.expiryDate != null) { + final daysUntilExpiry = license.expiryDate!.difference(now).inDays; + if (daysUntilExpiry <= 0) { + expiredCount++; + } else if (daysUntilExpiry <= 30) { + expiringCount++; + } + } + } + } + + return { + 'active': activeCount, + 'inactive': allLicenses.length - activeCount, + 'expiring': expiringCount, + 'expired': expiredCount, + 'total': allLicenses.length, + }; + } + } + + // 정렬 변경 + void sortBy(String field, String order) { + _sortBy = field; + _sortOrder = order; + loadData(); + } + + @override + void dispose() { + _debounceTimer?.cancel(); + super.dispose(); + } } diff --git a/lib/screens/license/license_form.dart b/lib/screens/license/license_form.dart index 974d75d..29234bd 100644 --- a/lib/screens/license/license_form.dart +++ b/lib/screens/license/license_form.dart @@ -41,7 +41,9 @@ class _MaintenanceFormScreenState extends State { dataService: MockDataService(), licenseId: widget.maintenanceId, ); - _controller.isEditMode = widget.maintenanceId != null; + if (widget.maintenanceId != null) { + _controller.isEditMode = true; + } if (_controller.isEditMode) { _controller.loadLicense(); // TODO: 기존 데이터 로딩 시 _selectedVisitCycle, _selectedInspectionType, _durationMonths 값 세팅 필요 @@ -201,10 +203,10 @@ class _MaintenanceFormScreenState extends State { _controller.durationMonths = _durationMonths; _controller.visitCycle = _selectedVisitCycle; // 점검형태 저장 로직 필요 시 추가 - setState(() { - _controller.saveLicense(() { + _controller.saveLicense().then((success) { + if (success) { Navigator.pop(context, true); - }); + } }); } }, diff --git a/lib/screens/license/license_list_redesign.dart b/lib/screens/license/license_list_redesign.dart index 4562d47..cdd851a 100644 --- a/lib/screens/license/license_list_redesign.dart +++ b/lib/screens/license/license_list_redesign.dart @@ -23,7 +23,7 @@ class _LicenseListRedesignState extends State { @override void initState() { super.initState(); - _controller = LicenseListController(dataService: _dataService); + _controller = LicenseListController(mockDataService: _dataService); _controller.loadData(); } @@ -294,7 +294,7 @@ class _LicenseListRedesignState extends State { Expanded( flex: 2, child: Text( - _getCompanyName(license.companyId), + _getCompanyName(license.companyId ?? 0), style: ShadcnTheme.bodySmall, ), ), diff --git a/lib/screens/overview/controllers/overview_controller.dart b/lib/screens/overview/controllers/overview_controller.dart index 74df544..8657edc 100644 --- a/lib/screens/overview/controllers/overview_controller.dart +++ b/lib/screens/overview/controllers/overview_controller.dart @@ -35,6 +35,10 @@ class OverviewController extends ChangeNotifier { EquipmentStatusDistribution? get equipmentStatus => _equipmentStatus; List get expiringLicenses => _expiringLicenses; + // 추가 getter + int get totalCompanies => _overviewStats?.totalCompanies ?? 0; + int get totalUsers => _overviewStats?.totalUsers ?? 0; + bool get isLoading => _isLoadingStats || _isLoadingActivities || _isLoadingEquipmentStatus || _isLoadingLicenses; @@ -55,6 +59,11 @@ class OverviewController extends ChangeNotifier { ]); } + // 대시보드 데이터 로드 (loadData의 alias) + Future loadDashboardData() async { + await loadData(); + } + // 개별 데이터 로드 메서드 Future _loadOverviewStats() async { _isLoadingStats = true; diff --git a/lib/screens/overview/overview_screen_redesign.dart b/lib/screens/overview/overview_screen_redesign.dart index 494955d..45b157c 100644 --- a/lib/screens/overview/overview_screen_redesign.dart +++ b/lib/screens/overview/overview_screen_redesign.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:intl/intl.dart'; import 'package:superport/screens/common/theme_shadcn.dart'; import 'package:superport/screens/common/components/shadcn_components.dart'; import 'package:superport/screens/overview/controllers/overview_controller.dart'; @@ -123,13 +125,13 @@ class _OverviewScreenRedesignState extends State { ), _buildStatCard( '입고 장비', - '${_controller.totalEquipmentIn}', + '${_controller.overviewStats?.availableEquipment ?? 0}', Icons.inventory, ShadcnTheme.success, ), _buildStatCard( '출고 장비', - '${_controller.totalEquipmentOut}', + '${_controller.overviewStats?.inUseEquipment ?? 0}', Icons.local_shipping, ShadcnTheme.warning, ), @@ -390,7 +392,7 @@ class _OverviewScreenRedesignState extends State { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: color.withOpacity(0.1), + color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), ), child: Icon(icon, color: color, size: 20), @@ -398,7 +400,7 @@ class _OverviewScreenRedesignState extends State { Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: ShadcnTheme.success.withOpacity(0.1), + color: ShadcnTheme.success.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Row( @@ -474,7 +476,7 @@ class _OverviewScreenRedesignState extends State { Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: color.withOpacity(0.1), + color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), ), child: Icon( diff --git a/lib/screens/sidebar/sidebar_screen.dart b/lib/screens/sidebar/sidebar_screen.dart index 2f5f231..a57563f 100644 --- a/lib/screens/sidebar/sidebar_screen.dart +++ b/lib/screens/sidebar/sidebar_screen.dart @@ -5,10 +5,6 @@ import 'package:superport/screens/sidebar/widgets/sidebar_menu_footer.dart'; import 'package:superport/screens/sidebar/widgets/sidebar_menu_item.dart'; import 'package:superport/screens/sidebar/widgets/sidebar_menu_submenu.dart'; import 'package:superport/screens/sidebar/widgets/sidebar_menu_types.dart'; -import 'package:superport/screens/common/theme_tailwind.dart'; -import 'package:superport/screens/login/widgets/login_view.dart'; // AnimatedBoatIcon import -import 'package:wave/wave.dart'; -import 'package:wave/config.dart'; // 사이드바 메뉴 메인 위젯 (조립만 담당) class SidebarMenu extends StatefulWidget { diff --git a/lib/screens/user/controllers/user_list_controller.dart b/lib/screens/user/controllers/user_list_controller.dart index d77ab85..364d630 100644 --- a/lib/screens/user/controllers/user_list_controller.dart +++ b/lib/screens/user/controllers/user_list_controller.dart @@ -4,8 +4,6 @@ import 'package:superport/models/user_model.dart'; import 'package:superport/models/company_model.dart'; import 'package:superport/services/mock_data_service.dart'; import 'package:superport/services/user_service.dart'; -import 'package:superport/utils/constants.dart'; -import 'package:superport/utils/user_utils.dart'; /// 담당자 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러 class UserListController extends ChangeNotifier { diff --git a/lib/screens/user/user_form.dart b/lib/screens/user/user_form.dart index 7f07ca3..42d6a20 100644 --- a/lib/screens/user/user_form.dart +++ b/lib/screens/user/user_form.dart @@ -1,15 +1,11 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:superport/models/company_model.dart'; -import 'package:superport/models/user_model.dart'; import 'package:superport/screens/common/theme_tailwind.dart'; -import 'package:superport/screens/common/custom_widgets.dart'; import 'package:superport/services/mock_data_service.dart'; import 'package:superport/utils/constants.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_phone_field.dart'; import 'package:superport/screens/common/widgets/company_branch_dropdown.dart'; // 사용자 등록/수정 화면 (UI만 담당, 상태/로직 분리) diff --git a/lib/screens/user/user_list.dart b/lib/screens/user/user_list.dart index be56002..e38edde 100644 --- a/lib/screens/user/user_list.dart +++ b/lib/screens/user/user_list.dart @@ -1,6 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:superport/models/company_model.dart'; -import 'package:superport/models/user_model.dart'; import 'package:superport/screens/common/theme_tailwind.dart'; import 'package:superport/screens/common/main_layout.dart'; import 'package:superport/screens/common/custom_widgets.dart'; @@ -82,6 +80,11 @@ class _UserListScreenState extends State { onPressed: () { _controller.deleteUser(id, () { Navigator.pop(context); + }, (error) { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(error)), + ); }); }, child: const Text('삭제'), diff --git a/lib/screens/user/user_list_redesign.dart b/lib/screens/user/user_list_redesign.dart index a2c0a3f..ff96780 100644 --- a/lib/screens/user/user_list_redesign.dart +++ b/lib/screens/user/user_list_redesign.dart @@ -211,7 +211,7 @@ class _UserListRedesignState extends State { const SizedBox(height: 16), Text( '데이터를 불러올 수 없습니다', - style: ShadcnTheme.h4, + style: ShadcnTheme.headingH4, ), const SizedBox(height: 8), Text( @@ -291,7 +291,7 @@ class _UserListRedesignState extends State { : null, ); }, - variant: ShadcnButtonVariant.outline, + variant: ShadcnButtonVariant.secondary, icon: const Icon(Icons.filter_list), ), const SizedBox(width: ShadcnTheme.spacing2), @@ -302,7 +302,7 @@ class _UserListRedesignState extends State { ? '모든 권한' : getRoleName(controller.filterRole!), onPressed: null, - variant: ShadcnButtonVariant.outline, + variant: ShadcnButtonVariant.secondary, icon: const Icon(Icons.person), ), onSelected: (role) { diff --git a/lib/screens/warehouse_location/warehouse_location_form.dart b/lib/screens/warehouse_location/warehouse_location_form.dart index 3528984..3821d53 100644 --- a/lib/screens/warehouse_location/warehouse_location_form.dart +++ b/lib/screens/warehouse_location/warehouse_location_form.dart @@ -3,7 +3,6 @@ import 'package:superport/models/address_model.dart'; import 'package:superport/screens/common/widgets/address_input.dart'; import 'package:superport/screens/common/widgets/remark_input.dart'; import 'package:superport/screens/common/theme_tailwind.dart'; -import 'package:superport/utils/constants.dart'; import 'controllers/warehouse_location_form_controller.dart'; /// 입고지 추가/수정 폼 화면 (SRP 적용, 상태/로직 분리) @@ -26,7 +25,9 @@ class _WarehouseLocationFormScreenState super.initState(); // 컨트롤러 생성 및 초기화 _controller = WarehouseLocationFormController(); - _controller.initialize(widget.id); + if (widget.id != null) { + _controller.initialize(widget.id!); + } } @override @@ -107,7 +108,7 @@ class _WarehouseLocationFormScreenState ? null : () async { setState(() {}); // 저장 중 상태 갱신 - await _controller.save(context); + await _controller.save(); setState(() {}); // 저장 완료 후 상태 갱신 }, style: ElevatedButton.styleFrom( diff --git a/lib/screens/warehouse_location/warehouse_location_list_redesign.dart b/lib/screens/warehouse_location/warehouse_location_list_redesign.dart index 31caf48..f342425 100644 --- a/lib/screens/warehouse_location/warehouse_location_list_redesign.dart +++ b/lib/screens/warehouse_location/warehouse_location_list_redesign.dart @@ -136,7 +136,7 @@ class _WarehouseLocationListRedesignState vertical: ShadcnTheme.spacing3, ), decoration: BoxDecoration( - color: ShadcnTheme.muted.withOpacity(0.3), + color: ShadcnTheme.muted.withValues(alpha: 0.3), border: Border( bottom: BorderSide(color: ShadcnTheme.border), ), diff --git a/lib/services/company_service.dart b/lib/services/company_service.dart index 9b5b148..39cec49 100644 --- a/lib/services/company_service.dart +++ b/lib/services/company_service.dart @@ -1,4 +1,3 @@ -import 'package:get_it/get_it.dart'; import 'package:injectable/injectable.dart'; import 'package:superport/core/errors/exceptions.dart'; import 'package:superport/core/errors/failures.dart'; @@ -32,9 +31,9 @@ class CompanyService { return response.items.map((dto) => _convertListDtoToCompany(dto)).toList(); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to fetch company list: $e'); + throw ServerFailure(message: 'Failed to fetch company list: $e'); } } @@ -55,9 +54,9 @@ class CompanyService { final response = await _remoteDataSource.createCompany(request); return _convertResponseToCompany(response); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to create company: $e'); + throw ServerFailure(message: 'Failed to create company: $e'); } } @@ -67,9 +66,9 @@ class CompanyService { final response = await _remoteDataSource.getCompanyDetail(id); return _convertResponseToCompany(response); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to fetch company detail: $e'); + throw ServerFailure(message: 'Failed to fetch company detail: $e'); } } @@ -82,9 +81,9 @@ class CompanyService { return company.copyWith(branches: branches); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to fetch company with branches: $e'); + throw ServerFailure(message: 'Failed to fetch company with branches: $e'); } } @@ -105,9 +104,9 @@ class CompanyService { final response = await _remoteDataSource.updateCompany(id, request); return _convertResponseToCompany(response); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to update company: $e'); + throw ServerFailure(message: 'Failed to update company: $e'); } } @@ -116,9 +115,9 @@ class CompanyService { try { await _remoteDataSource.deleteCompany(id); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to delete company: $e'); + throw ServerFailure(message: 'Failed to delete company: $e'); } } @@ -131,9 +130,9 @@ class CompanyService { 'name': dto.name, }).toList(); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to fetch company names: $e'); + throw ServerFailure(message: 'Failed to fetch company names: $e'); } } @@ -152,9 +151,9 @@ class CompanyService { final response = await _remoteDataSource.createBranch(companyId, request); return _convertBranchResponseToBranch(response); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to create branch: $e'); + throw ServerFailure(message: 'Failed to create branch: $e'); } } @@ -163,9 +162,9 @@ class CompanyService { final response = await _remoteDataSource.getBranchDetail(companyId, branchId); return _convertBranchResponseToBranch(response); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to fetch branch detail: $e'); + throw ServerFailure(message: 'Failed to fetch branch detail: $e'); } } @@ -183,9 +182,9 @@ class CompanyService { final response = await _remoteDataSource.updateBranch(companyId, branchId, request); return _convertBranchResponseToBranch(response); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to update branch: $e'); + throw ServerFailure(message: 'Failed to update branch: $e'); } } @@ -193,9 +192,9 @@ class CompanyService { try { await _remoteDataSource.deleteBranch(companyId, branchId); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to delete branch: $e'); + throw ServerFailure(message: 'Failed to delete branch: $e'); } } @@ -204,9 +203,9 @@ class CompanyService { final dtoList = await _remoteDataSource.getCompanyBranches(companyId); return dtoList.map((dto) => _convertBranchDtoToBranch(dto)).toList(); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to fetch company branches: $e'); + throw ServerFailure(message: 'Failed to fetch company branches: $e'); } } @@ -215,9 +214,9 @@ class CompanyService { try { return await _remoteDataSource.getCompaniesWithBranches(); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to fetch companies with branches: $e'); + throw ServerFailure(message: 'Failed to fetch companies with branches: $e'); } } @@ -226,9 +225,9 @@ class CompanyService { try { return await _remoteDataSource.checkDuplicateCompany(name); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to check duplicate: $e'); + throw ServerFailure(message: 'Failed to check duplicate: $e'); } } @@ -238,9 +237,9 @@ class CompanyService { final dtoList = await _remoteDataSource.searchCompanies(query); return dtoList.map((dto) => _convertListDtoToCompany(dto)).toList(); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to search companies: $e'); + throw ServerFailure(message: 'Failed to search companies: $e'); } } @@ -249,9 +248,9 @@ class CompanyService { try { await _remoteDataSource.updateCompanyStatus(id, isActive); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to update company status: $e'); + throw ServerFailure(message: 'Failed to update company status: $e'); } } diff --git a/lib/services/equipment_service.dart b/lib/services/equipment_service.dart index c8bfd63..0d8689b 100644 --- a/lib/services/equipment_service.dart +++ b/lib/services/equipment_service.dart @@ -10,7 +10,6 @@ import 'package:superport/data/models/equipment/equipment_out_request.dart'; import 'package:superport/data/models/equipment/equipment_request.dart'; import 'package:superport/data/models/equipment/equipment_response.dart'; import 'package:superport/models/equipment_unified_model.dart'; -import 'package:superport/utils/constants.dart'; class EquipmentService { final EquipmentRemoteDataSource _remoteDataSource = GetIt.instance(); @@ -34,9 +33,9 @@ class EquipmentService { return dtoList.map((dto) => _convertListDtoToEquipment(dto)).toList(); } on ServerException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to fetch equipment list: $e'); + throw ServerFailure(message: 'Failed to fetch equipment list: $e'); } } @@ -59,9 +58,9 @@ class EquipmentService { final response = await _remoteDataSource.createEquipment(request); return _convertResponseToEquipment(response); } on ServerException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to create equipment: $e'); + throw ServerFailure(message: 'Failed to create equipment: $e'); } } @@ -71,11 +70,16 @@ class EquipmentService { final response = await _remoteDataSource.getEquipmentDetail(id); return _convertResponseToEquipment(response); } on ServerException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to fetch equipment detail: $e'); + throw ServerFailure(message: 'Failed to fetch equipment detail: $e'); } } + + // 장비 조회 (getEquipmentDetail의 alias) + Future getEquipment(int id) async { + return getEquipmentDetail(id); + } // 장비 수정 Future updateEquipment(int id, Equipment equipment) async { @@ -96,9 +100,9 @@ class EquipmentService { final response = await _remoteDataSource.updateEquipment(id, request); return _convertResponseToEquipment(response); } on ServerException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to update equipment: $e'); + throw ServerFailure(message: 'Failed to update equipment: $e'); } } @@ -107,9 +111,9 @@ class EquipmentService { try { await _remoteDataSource.deleteEquipment(id); } on ServerException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to delete equipment: $e'); + throw ServerFailure(message: 'Failed to delete equipment: $e'); } } @@ -119,9 +123,9 @@ class EquipmentService { final response = await _remoteDataSource.changeEquipmentStatus(id, status, reason); return _convertResponseToEquipment(response); } on ServerException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to change equipment status: $e'); + throw ServerFailure(message: 'Failed to change equipment status: $e'); } } @@ -137,9 +141,9 @@ class EquipmentService { return await _remoteDataSource.addEquipmentHistory(equipmentId, request); } on ServerException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to add equipment history: $e'); + throw ServerFailure(message: 'Failed to add equipment history: $e'); } } @@ -148,9 +152,9 @@ class EquipmentService { try { return await _remoteDataSource.getEquipmentHistory(equipmentId, page: page, perPage: perPage); } on ServerException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to fetch equipment history: $e'); + throw ServerFailure(message: 'Failed to fetch equipment history: $e'); } } @@ -171,9 +175,9 @@ class EquipmentService { return await _remoteDataSource.equipmentIn(request); } on ServerException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to process equipment in: $e'); + throw ServerFailure(message: 'Failed to process equipment in: $e'); } } @@ -196,9 +200,9 @@ class EquipmentService { return await _remoteDataSource.equipmentOut(request); } on ServerException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: 'Failed to process equipment out: $e'); + throw ServerFailure(message: 'Failed to process equipment out: $e'); } } diff --git a/lib/services/license_service.dart b/lib/services/license_service.dart index 0a2cc5a..701ac99 100644 --- a/lib/services/license_service.dart +++ b/lib/services/license_service.dart @@ -9,7 +9,9 @@ import 'package:superport/models/license_model.dart'; @lazySingleton class LicenseService { - final LicenseRemoteDataSource _remoteDataSource = GetIt.instance(); + final LicenseRemoteDataSource _remoteDataSource; + + LicenseService(this._remoteDataSource); // 라이선스 목록 조회 Future> getLicenses({ @@ -32,9 +34,9 @@ class LicenseService { return response.items.map((dto) => _convertDtoToLicense(dto)).toList(); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: '라이선스 목록을 불러오는 데 실패했습니다: $e'); + throw ServerFailure(message: '라이선스 목록을 불러오는 데 실패했습니다: $e'); } } @@ -44,36 +46,35 @@ class LicenseService { final dto = await _remoteDataSource.getLicenseById(id); return _convertDtoToLicense(dto); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: '라이선스 정보를 불러오는 데 실패했습니다: $e'); + throw ServerFailure(message: '라이선스 정보를 불러오는 데 실패했습니다: $e'); } } // 라이선스 생성 Future createLicense(License license) async { try { - // Flutter 모델의 visitCycle과 durationMonths를 API 필드에 매핑 - // visitCycle은 remark에 저장하고, durationMonths는 날짜 계산에 사용 - final now = DateTime.now(); - final expiryDate = now.add(Duration(days: license.durationMonths * 30)); - final request = CreateLicenseRequest( - licenseKey: license.name, // name을 licenseKey로 매핑 - productName: '유지보수 계약', // 기본값 설정 - licenseType: 'maintenance', // 유지보수 타입으로 고정 + licenseKey: license.licenseKey, + productName: license.productName, + vendor: license.vendor, + licenseType: license.licenseType, + userCount: license.userCount, + purchaseDate: license.purchaseDate, + expiryDate: license.expiryDate, + purchasePrice: license.purchasePrice, companyId: license.companyId, - purchaseDate: now, - expiryDate: expiryDate, - remark: '방문주기: ${license.visitCycle}', // visitCycle을 remark에 저장 + branchId: license.branchId, + remark: license.remark, ); final dto = await _remoteDataSource.createLicense(request); return _convertDtoToLicense(dto); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: '라이선스 생성에 실패했습니다: $e'); + throw ServerFailure(message: '라이선스 생성에 실패했습니다: $e'); } } @@ -81,30 +82,28 @@ class LicenseService { Future updateLicense(License license) async { try { if (license.id == null) { - throw Failure(message: '라이선스 ID가 없습니다'); - } - - // 기존 라이선스 정보를 먼저 조회 - final existingDto = await _remoteDataSource.getLicenseById(license.id!); - - // 만료일 계산 (durationMonths가 변경된 경우) - DateTime? newExpiryDate; - if (existingDto.purchaseDate != null) { - newExpiryDate = existingDto.purchaseDate!.add(Duration(days: license.durationMonths * 30)); + throw BusinessFailure(message: '라이선스 ID가 없습니다'); } final request = UpdateLicenseRequest( - licenseKey: license.name, - expiryDate: newExpiryDate, - remark: '방문주기: ${license.visitCycle}', + licenseKey: license.licenseKey, + productName: license.productName, + vendor: license.vendor, + licenseType: license.licenseType, + userCount: license.userCount, + purchaseDate: license.purchaseDate, + expiryDate: license.expiryDate, + purchasePrice: license.purchasePrice, + remark: license.remark, + isActive: license.isActive, ); final dto = await _remoteDataSource.updateLicense(license.id!, request); return _convertDtoToLicense(dto); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: '라이선스 수정에 실패했습니다: $e'); + throw ServerFailure(message: '라이선스 수정에 실패했습니다: $e'); } } @@ -113,9 +112,9 @@ class LicenseService { try { await _remoteDataSource.deleteLicense(id); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: '라이선스 삭제에 실패했습니다: $e'); + throw ServerFailure(message: '라이선스 삭제에 실패했습니다: $e'); } } @@ -126,9 +125,9 @@ class LicenseService { final dto = await _remoteDataSource.assignLicense(licenseId, request); return _convertDtoToLicense(dto); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: '라이선스 할당에 실패했습니다: $e'); + throw ServerFailure(message: '라이선스 할당에 실패했습니다: $e'); } } @@ -138,9 +137,9 @@ class LicenseService { final dto = await _remoteDataSource.unassignLicense(licenseId); return _convertDtoToLicense(dto); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: '라이선스 할당 해제에 실패했습니다: $e'); + throw ServerFailure(message: '라이선스 할당 해제에 실패했습니다: $e'); } } @@ -159,33 +158,34 @@ class LicenseService { return response.items.map((dto) => _convertExpiringDtoToLicense(dto)).toList(); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: '만료 예정 라이선스를 불러오는 데 실패했습니다: $e'); + throw ServerFailure(message: '만료 예정 라이선스를 불러오는 데 실패했습니다: $e'); } } // DTO를 Flutter 모델로 변환 License _convertDtoToLicense(LicenseDto dto) { - // remark에서 방문주기 추출 - String visitCycle = '미방문'; // 기본값 - if (dto.remark != null && dto.remark!.contains('방문주기:')) { - visitCycle = dto.remark!.split('방문주기:').last.trim(); - } - - // 기간 계산 (purchaseDate와 expiryDate 차이) - int durationMonths = 12; // 기본값 - if (dto.purchaseDate != null && dto.expiryDate != null) { - final difference = dto.expiryDate!.difference(dto.purchaseDate!); - durationMonths = (difference.inDays / 30).round(); - } - return License( id: dto.id, - companyId: dto.companyId ?? 0, - name: dto.licenseKey, - durationMonths: durationMonths, - visitCycle: visitCycle, + licenseKey: dto.licenseKey, + productName: dto.productName, + vendor: dto.vendor, + licenseType: dto.licenseType, + userCount: dto.userCount, + purchaseDate: dto.purchaseDate, + expiryDate: dto.expiryDate, + purchasePrice: dto.purchasePrice, + companyId: dto.companyId, + branchId: dto.branchId, + assignedUserId: dto.assignedUserId, + remark: dto.remark, + isActive: dto.isActive, + createdAt: dto.createdAt, + updatedAt: dto.updatedAt, + companyName: dto.companyName, + branchName: dto.branchName, + assignedUserName: dto.assignedUserName, ); } @@ -193,10 +193,24 @@ class LicenseService { License _convertExpiringDtoToLicense(ExpiringLicenseDto dto) { return License( id: dto.id, - companyId: 0, // ExpiringLicenseDto에는 companyId가 없으므로 기본값 사용 - name: dto.licenseKey, - durationMonths: 12, // 기본값 - visitCycle: '미방문', // 기본값 + licenseKey: dto.licenseKey, + productName: dto.productName, + vendor: null, + licenseType: null, + userCount: null, + purchaseDate: null, + expiryDate: dto.expiryDate, + purchasePrice: null, + companyId: null, + branchId: null, + assignedUserId: null, + remark: null, + isActive: dto.isActive, + createdAt: null, + updatedAt: null, + companyName: dto.companyName, + branchName: null, + assignedUserName: null, ); } diff --git a/lib/services/mock_data_service.dart b/lib/services/mock_data_service.dart index 7e675ee..bbb14a6 100644 --- a/lib/services/mock_data_service.dart +++ b/lib/services/mock_data_service.dart @@ -570,10 +570,12 @@ class MockDataService { for (final inspection in inspectionTypes) { addLicense( License( + licenseKey: 'LIC-${DateTime.now().millisecondsSinceEpoch}-$visit-$inspection', + productName: '12개월,$visit,$inspection', companyId: 1, - name: '12개월,$visit,$inspection', - durationMonths: 12, - visitCycle: visit, + purchaseDate: DateTime.now(), + expiryDate: DateTime.now().add(const Duration(days: 365)), + remark: '방문주기: $visit', ), ); } @@ -672,12 +674,12 @@ class MockDataService { // 기존 입고 장비를 출고 상태로 변경 void changeEquipmentStatus(int equipmentInId, EquipmentOut equipmentOut) { - print('장비 상태 변경 시작: 입고 ID $equipmentInId'); + // 장비 상태 변경 시작: 입고 ID $equipmentInId // 입고된 장비를 찾습니다 final index = _equipmentIns.indexWhere((e) => e.id == equipmentInId); if (index != -1) { - print('장비를 찾음: ${_equipmentIns[index].equipment.name}'); + // 장비를 찾음: ${_equipmentIns[index].equipment.name} // 입고 장비의 상태를 출고(O)로 변경 final equipment = _equipmentIns[index].equipment; @@ -687,7 +689,7 @@ class MockDataService { inDate: _equipmentIns[index].inDate, status: 'O', // 상태를 출고로 변경 ); - print('입고 장비 상태를 "O"로 변경: ID ${_equipmentIns[index].id}'); + // 입고 장비 상태를 "O"로 변경: ID ${_equipmentIns[index].id} // 출고 정보 저장 final newEquipmentOut = EquipmentOut( @@ -702,11 +704,11 @@ class MockDataService { returnType: equipmentOut.returnType, ); _equipmentOuts.add(newEquipmentOut); - print('출고 정보 추가: ID ${newEquipmentOut.id}'); + // 출고 정보 추가: ID ${newEquipmentOut.id} - print('장비 상태 변경 완료'); + // 장비 상태 변경 완료 } else { - print('오류: ID $equipmentInId인 입고 장비를 찾을 수 없음'); + // 오류: ID $equipmentInId인 입고 장비를 찾을 수 없음 } } @@ -885,6 +887,19 @@ class MockDataService { _companies[index] = company; } } + + void updateBranch(int companyId, Branch branch) { + final companyIndex = _companies.indexWhere((c) => c.id == companyId); + if (companyIndex != -1) { + final company = _companies[companyIndex]; + if (company.branches != null) { + final branchIndex = company.branches!.indexWhere((b) => b.id == branch.id); + if (branchIndex != -1) { + company.branches![branchIndex] = branch; + } + } + } + } void deleteCompany(int id) { _companies.removeWhere((c) => c.id == id); @@ -944,10 +959,19 @@ class MockDataService { void addLicense(License license) { final newLicense = License( id: _licenseIdCounter++, + licenseKey: license.licenseKey, + productName: license.productName, + vendor: license.vendor, + licenseType: license.licenseType, + userCount: license.userCount, + purchaseDate: license.purchaseDate, + expiryDate: license.expiryDate, + purchasePrice: license.purchasePrice, companyId: license.companyId, - name: license.name, - durationMonths: license.durationMonths, - visitCycle: license.visitCycle, + branchId: license.branchId, + assignedUserId: license.assignedUserId, + remark: license.remark, + isActive: license.isActive, ); _licenses.add(newLicense); } diff --git a/lib/services/user_service.dart b/lib/services/user_service.dart index 41b2638..20d1721 100644 --- a/lib/services/user_service.dart +++ b/lib/services/user_service.dart @@ -1,5 +1,4 @@ import 'package:injectable/injectable.dart'; -import 'package:superport/core/errors/exceptions.dart'; import 'package:superport/data/datasources/remote/user_remote_datasource.dart'; import 'package:superport/data/models/user/user_dto.dart'; import 'package:superport/models/user_model.dart'; diff --git a/lib/services/warehouse_service.dart b/lib/services/warehouse_service.dart index aa38f48..133c43f 100644 --- a/lib/services/warehouse_service.dart +++ b/lib/services/warehouse_service.dart @@ -26,9 +26,9 @@ class WarehouseService { return response.items.map((dto) => _convertDtoToWarehouseLocation(dto)).toList(); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: '창고 위치 목록을 불러오는 데 실패했습니다: $e'); + throw ServerFailure(message: '창고 위치 목록을 불러오는 데 실패했습니다: $e'); } } @@ -38,9 +38,9 @@ class WarehouseService { final dto = await _remoteDataSource.getWarehouseLocationById(id); return _convertDtoToWarehouseLocation(dto); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: '창고 위치 정보를 불러오는 데 실패했습니다: $e'); + throw ServerFailure(message: '창고 위치 정보를 불러오는 데 실패했습니다: $e'); } } @@ -58,9 +58,9 @@ class WarehouseService { final dto = await _remoteDataSource.createWarehouseLocation(request); return _convertDtoToWarehouseLocation(dto); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: '창고 위치 생성에 실패했습니다: $e'); + throw ServerFailure(message: '창고 위치 생성에 실패했습니다: $e'); } } @@ -77,9 +77,9 @@ class WarehouseService { final dto = await _remoteDataSource.updateWarehouseLocation(location.id, request); return _convertDtoToWarehouseLocation(dto); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: '창고 위치 수정에 실패했습니다: $e'); + throw ServerFailure(message: '창고 위치 수정에 실패했습니다: $e'); } } @@ -88,9 +88,9 @@ class WarehouseService { try { await _remoteDataSource.deleteWarehouseLocation(id); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: '창고 위치 삭제에 실패했습니다: $e'); + throw ServerFailure(message: '창고 위치 삭제에 실패했습니다: $e'); } } @@ -118,9 +118,9 @@ class WarehouseService { 'storedAt': dto.storedAt, }).toList(); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: '창고 장비 목록을 불러오는 데 실패했습니다: $e'); + throw ServerFailure(message: '창고 장비 목록을 불러오는 데 실패했습니다: $e'); } } @@ -129,9 +129,9 @@ class WarehouseService { try { return await _remoteDataSource.getWarehouseCapacity(id); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: '창고 용량 정보를 불러오는 데 실패했습니다: $e'); + throw ServerFailure(message: '창고 용량 정보를 불러오는 데 실패했습니다: $e'); } } @@ -141,9 +141,9 @@ class WarehouseService { final dtos = await _remoteDataSource.getInUseWarehouseLocations(); return dtos.map((dto) => _convertDtoToWarehouseLocation(dto)).toList(); } on ApiException catch (e) { - throw Failure(message: e.message); + throw ServerFailure(message: e.message); } catch (e) { - throw Failure(message: '사용 중인 창고 위치를 불러오는 데 실패했습니다: $e'); + throw ServerFailure(message: '사용 중인 창고 위치를 불러오는 데 실패했습니다: $e'); } }