feat: API 연동 개선 및 라이선스 모델 확장

- 라이선스 모델 전면 개편 (상세 필드 추가, 계산 필드 구현)
- API 응답 처리 개선 (HTTP 상태 코드 기반)
- 장비 출고 폼 컨트롤러 추가
- 회사 지점 정보 모델 추가
- 공통 데이터 모델 구조 추가
- 전체 서비스 레이어 API 호출 방식 통일
- UI 컴포넌트 마이너 개선
This commit is contained in:
JiWoong Sul
2025-07-25 01:22:15 +09:00
parent 8384423cf2
commit 71b7b7f40b
42 changed files with 1543 additions and 315 deletions

View File

@@ -1138,6 +1138,30 @@ class ErrorHandler {
- 주소 정보 관리 개선 - 주소 정보 관리 개선
- **DI 설정 완료**: WarehouseRemoteDataSource, WarehouseService 등록 - **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 통합 완료!) _마지막 업데이트: 2025-07-25_ (라이선스 관리 API 개선 완료. 다음 목표: 화면 Provider 패턴 적용)

View File

@@ -74,4 +74,9 @@ class ApiEndpoints {
// 검색 및 조회 // 검색 및 조회
static const String lookups = '/lookups'; static const String lookups = '/lookups';
static const String categories = '/lookups/categories'; 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';
} }

View File

@@ -115,3 +115,19 @@ class BusinessException implements Exception {
@override @override
String toString() => 'BusinessException: $message (code: $code)'; String toString() => 'BusinessException: $message (code: $code)';
} }
/// API 관련 예외
class ApiException implements Exception {
final String message;
final int? statusCode;
final Map<String, dynamic>? errors;
ApiException({
required this.message,
this.statusCode,
this.errors,
});
@override
String toString() => 'ApiException: $message (code: $statusCode)';
}

View File

@@ -28,43 +28,43 @@ class ServerFailure extends Failure {
final Map<String, dynamic>? errors; final Map<String, dynamic>? errors;
const ServerFailure({ const ServerFailure({
required String message, required super.message,
String? code, super.code,
this.statusCode, this.statusCode,
this.errors, this.errors,
}) : super(message: message, code: code); });
} }
/// 캐시 실패 /// 캐시 실패
class CacheFailure extends Failure { class CacheFailure extends Failure {
const CacheFailure({ const CacheFailure({
required String message, required super.message,
String? code, super.code,
}) : super(message: message, code: code); });
} }
/// 네트워크 실패 /// 네트워크 실패
class NetworkFailure extends Failure { class NetworkFailure extends Failure {
const NetworkFailure({ const NetworkFailure({
required String message, required super.message,
String? code, super.code,
}) : super(message: message, code: code); });
} }
/// 인증 실패 /// 인증 실패
class AuthenticationFailure extends Failure { class AuthenticationFailure extends Failure {
const AuthenticationFailure({ const AuthenticationFailure({
required String message, required super.message,
String? code, super.code,
}) : super(message: message, code: code); });
} }
/// 권한 실패 /// 권한 실패
class AuthorizationFailure extends Failure { class AuthorizationFailure extends Failure {
const AuthorizationFailure({ const AuthorizationFailure({
required String message, required super.message,
String? code, super.code,
}) : super(message: message, code: code); });
} }
/// 유효성 검사 실패 /// 유효성 검사 실패
@@ -72,10 +72,10 @@ class ValidationFailure extends Failure {
final Map<String, List<String>>? fieldErrors; final Map<String, List<String>>? fieldErrors;
const ValidationFailure({ const ValidationFailure({
required String message, required super.message,
String? code, super.code,
this.fieldErrors, this.fieldErrors,
}) : super(message: message, code: code); });
} }
/// 리소스 찾을 수 없음 실패 /// 리소스 찾을 수 없음 실패
@@ -84,11 +84,11 @@ class NotFoundFailure extends Failure {
final String? resourceId; final String? resourceId;
const NotFoundFailure({ const NotFoundFailure({
required String message, required super.message,
String? code, super.code,
this.resourceType, this.resourceType,
this.resourceId, this.resourceId,
}) : super(message: message, code: code); });
} }
/// 중복 리소스 실패 /// 중복 리소스 실패
@@ -97,19 +97,19 @@ class DuplicateFailure extends Failure {
final String? value; final String? value;
const DuplicateFailure({ const DuplicateFailure({
required String message, required super.message,
String? code, super.code,
this.field, this.field,
this.value, this.value,
}) : super(message: message, code: code); });
} }
/// 비즈니스 로직 실패 /// 비즈니스 로직 실패
class BusinessFailure extends Failure { class BusinessFailure extends Failure {
const BusinessFailure({ const BusinessFailure({
required String message, required super.message,
String? code, super.code,
}) : super(message: message, code: code); });
} }
/// 타입 정의 /// 타입 정의

View File

@@ -29,12 +29,12 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
data: request.toJson(), data: request.toJson(),
); );
if (response.success && response.data != null) { if (response.statusCode == 200 && response.data != null) {
final loginResponse = LoginResponse.fromJson(response.data); final loginResponse = LoginResponse.fromJson(response.data);
return Right(loginResponse); return Right(loginResponse);
} else { } else {
return Left(ServerFailure( return Left(ServerFailure(
message: response.error?.message ?? '로그인 실패', message: response.statusMessage ?? '로그인 실패',
)); ));
} }
} catch (e) { } catch (e) {
@@ -58,11 +58,11 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
data: request.toJson(), data: request.toJson(),
); );
if (response.success) { if (response.statusCode == 200) {
return const Right(null); return const Right(null);
} else { } else {
return Left(ServerFailure( return Left(ServerFailure(
message: response.error?.message ?? '로그아웃 실패', message: response.statusMessage ?? '로그아웃 실패',
)); ));
} }
} catch (e) { } catch (e) {
@@ -81,12 +81,12 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
data: request.toJson(), data: request.toJson(),
); );
if (response.success && response.data != null) { if (response.statusCode == 200 && response.data != null) {
final tokenResponse = TokenResponse.fromJson(response.data); final tokenResponse = TokenResponse.fromJson(response.data);
return Right(tokenResponse); return Right(tokenResponse);
} else { } else {
return Left(ServerFailure( return Left(ServerFailure(
message: response.error?.message ?? '토큰 갱신 실패', message: response.statusMessage ?? '토큰 갱신 실패',
)); ));
} }
} catch (e) { } catch (e) {

View File

@@ -1,5 +1,4 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:superport/core/constants/api_endpoints.dart'; import 'package:superport/core/constants/api_endpoints.dart';
import 'package:superport/core/errors/exceptions.dart'; import 'package:superport/core/errors/exceptions.dart';
@@ -80,11 +79,11 @@ class CompanyRemoteDataSourceImpl implements CompanyRemoteDataSource {
final apiResponse = ApiResponse<PaginatedResponse<CompanyListDto>>.fromJson( final apiResponse = ApiResponse<PaginatedResponse<CompanyListDto>>.fromJson(
response.data, response.data,
(json) => PaginatedResponse<CompanyListDto>.fromJson( (json) => PaginatedResponse<CompanyListDto>.fromJson(
json, json as Map<String, dynamic>,
(item) => CompanyListDto.fromJson(item), (item) => CompanyListDto.fromJson(item as Map<String, dynamic>),
), ),
); );
return apiResponse.data; return apiResponse.data!;
} else { } else {
throw ApiException( throw ApiException(
message: 'Failed to load companies', message: 'Failed to load companies',
@@ -108,9 +107,9 @@ class CompanyRemoteDataSourceImpl implements CompanyRemoteDataSource {
if (response.statusCode == 201) { if (response.statusCode == 201) {
final apiResponse = ApiResponse<CompanyResponse>.fromJson( final apiResponse = ApiResponse<CompanyResponse>.fromJson(
response.data, response.data,
(json) => CompanyResponse.fromJson(json), (json) => CompanyResponse.fromJson(json as Map<String, dynamic>),
); );
return apiResponse.data; return apiResponse.data!;
} else { } else {
throw ApiException( throw ApiException(
message: 'Failed to create company', message: 'Failed to create company',
@@ -134,7 +133,7 @@ class CompanyRemoteDataSourceImpl implements CompanyRemoteDataSource {
} on DioException catch (e) { } on DioException catch (e) {
throw ServerException( throw ServerException(
message: e.response?.data['message'] ?? 'Failed to fetch company detail', 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) { } on DioException catch (e) {
throw ServerException( throw ServerException(
message: e.response?.data['message'] ?? 'Failed to fetch company with branches', 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) { } on DioException catch (e) {
throw ServerException( throw ServerException(
message: e.response?.data['message'] ?? 'Failed to update company', 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) { } on DioException catch (e) {
throw ServerException( throw ServerException(
message: e.response?.data['message'] ?? 'Failed to delete company', 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) { } on DioException catch (e) {
throw ServerException( throw ServerException(
message: e.response?.data['message'] ?? 'Failed to fetch company names', 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) { } on DioException catch (e) {
throw ServerException( throw ServerException(
message: e.response?.data['message'] ?? 'Failed to create branch', 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) { } on DioException catch (e) {
throw ServerException( throw ServerException(
message: e.response?.data['message'] ?? 'Failed to fetch branch detail', 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) { } on DioException catch (e) {
throw ServerException( throw ServerException(
message: e.response?.data['message'] ?? 'Failed to update branch', 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) { } on DioException catch (e) {
throw ServerException( throw ServerException(
message: e.response?.data['message'] ?? 'Failed to delete branch', 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) { } on DioException catch (e) {
throw ServerException( throw ServerException(
message: e.response?.data['message'] ?? 'Failed to fetch company branches', 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)) .map((item) => CompanyWithBranches.fromJson(item))
.toList(), .toList(),
); );
return apiResponse.data; return apiResponse.data!;
} else { } else {
throw ApiException( throw ApiException(
message: 'Failed to load companies with branches', message: 'Failed to load companies with branches',
@@ -321,9 +320,9 @@ class CompanyRemoteDataSourceImpl implements CompanyRemoteDataSource {
if (response.statusCode == 200) { if (response.statusCode == 200) {
final apiResponse = ApiResponse<Map<String, dynamic>>.fromJson( final apiResponse = ApiResponse<Map<String, dynamic>>.fromJson(
response.data, response.data,
(json) => json, (json) => json as Map<String, dynamic>,
); );
return apiResponse.data['exists'] ?? false; return apiResponse.data?['exists'] ?? false;
} else { } else {
throw ApiException( throw ApiException(
message: 'Failed to check duplicate', message: 'Failed to check duplicate',
@@ -351,7 +350,7 @@ class CompanyRemoteDataSourceImpl implements CompanyRemoteDataSource {
.map((item) => CompanyListDto.fromJson(item)) .map((item) => CompanyListDto.fromJson(item))
.toList(), .toList(),
); );
return apiResponse.data; return apiResponse.data!;
} else { } else {
throw ApiException( throw ApiException(
message: 'Failed to search companies', message: 'Failed to search companies',

View File

@@ -1,8 +1,6 @@
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:injectable/injectable.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/core/errors/failures.dart';
import 'package:superport/data/datasources/remote/api_client.dart'; import 'package:superport/data/datasources/remote/api_client.dart';
import 'package:superport/data/models/dashboard/equipment_status_distribution.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); final stats = OverviewStats.fromJson(response.data);
return Right(stats); return Right(stats);
} else { } else {
return Left(ServerFailure('응답 데이터가 없습니다')); return Left(ServerFailure(message: '응답 데이터가 없습니다'));
} }
} on DioException catch (e) { } on DioException catch (e) {
return Left(_handleDioError(e)); return Left(_handleDioError(e));
} catch (e) { } catch (e) {
return Left(ServerFailure('통계 데이터를 가져오는 중 오류가 발생했습니다')); return Left(ServerFailure(message: '통계 데이터를 가져오는 중 오류가 발생했습니다'));
} }
} }
@@ -52,12 +50,12 @@ class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource {
.toList(); .toList();
return Right(activities); return Right(activities);
} else { } else {
return Left(ServerFailure('응답 데이터가 올바르지 않습니다')); return Left(ServerFailure(message: '응답 데이터가 올바르지 않습니다'));
} }
} on DioException catch (e) { } on DioException catch (e) {
return Left(_handleDioError(e)); return Left(_handleDioError(e));
} catch (e) { } catch (e) {
return Left(ServerFailure('최근 활동을 가져오는 중 오류가 발생했습니다')); return Left(ServerFailure(message: '최근 활동을 가져오는 중 오류가 발생했습니다'));
} }
} }
@@ -70,12 +68,12 @@ class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource {
final distribution = EquipmentStatusDistribution.fromJson(response.data); final distribution = EquipmentStatusDistribution.fromJson(response.data);
return Right(distribution); return Right(distribution);
} else { } else {
return Left(ServerFailure('응답 데이터가 없습니다')); return Left(ServerFailure(message: '응답 데이터가 없습니다'));
} }
} on DioException catch (e) { } on DioException catch (e) {
return Left(_handleDioError(e)); return Left(_handleDioError(e));
} catch (e) { } catch (e) {
return Left(ServerFailure('장비 상태 분포를 가져오는 중 오류가 발생했습니다')); return Left(ServerFailure(message: '장비 상태 분포를 가져오는 중 오류가 발생했습니다'));
} }
} }
@@ -93,12 +91,12 @@ class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource {
.toList(); .toList();
return Right(licenses); return Right(licenses);
} else { } else {
return Left(ServerFailure('응답 데이터가 올바르지 않습니다')); return Left(ServerFailure(message: '응답 데이터가 올바르지 않습니다'));
} }
} on DioException catch (e) { } on DioException catch (e) {
return Left(_handleDioError(e)); return Left(_handleDioError(e));
} catch (e) { } catch (e) {
return Left(ServerFailure('만료 예정 라이선스를 가져오는 중 오류가 발생했습니다')); return Left(ServerFailure(message: '만료 예정 라이선스를 가져오는 중 오류가 발생했습니다'));
} }
} }
@@ -107,26 +105,26 @@ class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource {
case DioExceptionType.connectionTimeout: case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout: case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout: case DioExceptionType.receiveTimeout:
return NetworkFailure('네트워크 연결 시간이 초과되었습니다'); return NetworkFailure(message: '네트워크 연결 시간이 초과되었습니다');
case DioExceptionType.connectionError: case DioExceptionType.connectionError:
return NetworkFailure('서버에 연결할 수 없습니다'); return NetworkFailure(message: '서버에 연결할 수 없습니다');
case DioExceptionType.badResponse: case DioExceptionType.badResponse:
final statusCode = error.response?.statusCode ?? 0; final statusCode = error.response?.statusCode ?? 0;
final message = error.response?.data?['message'] ?? '서버 오류가 발생했습니다'; final message = error.response?.data?['message'] ?? '서버 오류가 발생했습니다';
if (statusCode == 401) { if (statusCode == 401) {
return AuthFailure('인증이 만료되었습니다'); return AuthenticationFailure(message: '인증이 만료되었습니다');
} else if (statusCode == 403) { } else if (statusCode == 403) {
return AuthFailure('접근 권한이 없습니다'); return AuthenticationFailure(message: '접근 권한이 없습니다');
} else if (statusCode >= 400 && statusCode < 500) { } else if (statusCode >= 400 && statusCode < 500) {
return ServerFailure(message); return ServerFailure(message: message);
} else { } else {
return ServerFailure('서버 오류가 발생했습니다 ($statusCode)'); return ServerFailure(message: '서버 오류가 발생했습니다 ($statusCode)');
} }
case DioExceptionType.cancel: case DioExceptionType.cancel:
return ServerFailure('요청이 취소되었습니다'); return ServerFailure(message: '요청이 취소되었습니다');
default: default:
return ServerFailure('알 수 없는 오류가 발생했습니다'); return ServerFailure(message: '알 수 없는 오류가 발생했습니다');
} }
} }
} }

View File

@@ -4,6 +4,7 @@ import 'package:superport/core/errors/exceptions.dart';
import 'package:superport/data/datasources/remote/api_client.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_dto.dart';
import 'package:superport/data/models/license/license_request_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 { abstract class LicenseRemoteDataSource {
Future<LicenseListResponseDto> getLicenses({ Future<LicenseListResponseDto> getLicenses({

View File

@@ -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<T> with _$ApiResponse<T> {
const factory ApiResponse({
required bool success,
required String message,
T? data,
String? error,
}) = _ApiResponse<T>;
factory ApiResponse.fromJson(
Map<String, dynamic> json,
T Function(Object?) fromJsonT,
) =>
_$ApiResponseFromJson<T>(json, fromJsonT);
}

View File

@@ -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>(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<T> _$ApiResponseFromJson<T>(
Map<String, dynamic> json, T Function(Object?) fromJsonT) {
return _ApiResponse<T>.fromJson(json, fromJsonT);
}
/// @nodoc
mixin _$ApiResponse<T> {
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<String, dynamic> 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<T, ApiResponse<T>> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ApiResponseCopyWith<T, $Res> {
factory $ApiResponseCopyWith(
ApiResponse<T> value, $Res Function(ApiResponse<T>) then) =
_$ApiResponseCopyWithImpl<T, $Res, ApiResponse<T>>;
@useResult
$Res call({bool success, String message, T? data, String? error});
}
/// @nodoc
class _$ApiResponseCopyWithImpl<T, $Res, $Val extends ApiResponse<T>>
implements $ApiResponseCopyWith<T, $Res> {
_$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<T, $Res>
implements $ApiResponseCopyWith<T, $Res> {
factory _$$ApiResponseImplCopyWith(_$ApiResponseImpl<T> value,
$Res Function(_$ApiResponseImpl<T>) then) =
__$$ApiResponseImplCopyWithImpl<T, $Res>;
@override
@useResult
$Res call({bool success, String message, T? data, String? error});
}
/// @nodoc
class __$$ApiResponseImplCopyWithImpl<T, $Res>
extends _$ApiResponseCopyWithImpl<T, $Res, _$ApiResponseImpl<T>>
implements _$$ApiResponseImplCopyWith<T, $Res> {
__$$ApiResponseImplCopyWithImpl(
_$ApiResponseImpl<T> _value, $Res Function(_$ApiResponseImpl<T>) _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<T>(
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<T> implements _ApiResponse<T> {
const _$ApiResponseImpl(
{required this.success, required this.message, this.data, this.error});
factory _$ApiResponseImpl.fromJson(
Map<String, dynamic> 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<T> &&
(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<T, _$ApiResponseImpl<T>> get copyWith =>
__$$ApiResponseImplCopyWithImpl<T, _$ApiResponseImpl<T>>(
this, _$identity);
@override
Map<String, dynamic> toJson(Object? Function(T) toJsonT) {
return _$$ApiResponseImplToJson<T>(this, toJsonT);
}
}
abstract class _ApiResponse<T> implements ApiResponse<T> {
const factory _ApiResponse(
{required final bool success,
required final String message,
final T? data,
final String? error}) = _$ApiResponseImpl<T>;
factory _ApiResponse.fromJson(
Map<String, dynamic> json, T Function(Object?) fromJsonT) =
_$ApiResponseImpl<T>.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<T, _$ApiResponseImpl<T>> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,41 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'api_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$ApiResponseImpl<T> _$$ApiResponseImplFromJson<T>(
Map<String, dynamic> json,
T Function(Object? json) fromJsonT,
) =>
_$ApiResponseImpl<T>(
success: json['success'] as bool,
message: json['message'] as String,
data: _$nullableGenericFromJson(json['data'], fromJsonT),
error: json['error'] as String?,
);
Map<String, dynamic> _$$ApiResponseImplToJson<T>(
_$ApiResponseImpl<T> instance,
Object? Function(T value) toJsonT,
) =>
<String, dynamic>{
'success': instance.success,
'message': instance.message,
'data': _$nullableGenericToJson(instance.data, toJsonT),
'error': instance.error,
};
T? _$nullableGenericFromJson<T>(
Object? input,
T Function(Object? json) fromJson,
) =>
input == null ? null : fromJson(input);
Object? _$nullableGenericToJson<T>(
T? input,
Object? Function(T value) toJson,
) =>
input == null ? null : toJson(input);

View File

@@ -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<T> with _$PaginatedResponse<T> {
const factory PaginatedResponse({
required List<T> items,
required int page,
required int size,
required int totalElements,
required int totalPages,
required bool first,
required bool last,
}) = _PaginatedResponse<T>;
factory PaginatedResponse.fromJson(
Map<String, dynamic> json,
T Function(Object?) fromJsonT,
) =>
_$PaginatedResponseFromJson<T>(json, fromJsonT);
}

View File

@@ -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>(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<T> _$PaginatedResponseFromJson<T>(
Map<String, dynamic> json, T Function(Object?) fromJsonT) {
return _PaginatedResponse<T>.fromJson(json, fromJsonT);
}
/// @nodoc
mixin _$PaginatedResponse<T> {
List<T> 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<String, dynamic> 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<T, PaginatedResponse<T>> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $PaginatedResponseCopyWith<T, $Res> {
factory $PaginatedResponseCopyWith(PaginatedResponse<T> value,
$Res Function(PaginatedResponse<T>) then) =
_$PaginatedResponseCopyWithImpl<T, $Res, PaginatedResponse<T>>;
@useResult
$Res call(
{List<T> items,
int page,
int size,
int totalElements,
int totalPages,
bool first,
bool last});
}
/// @nodoc
class _$PaginatedResponseCopyWithImpl<T, $Res,
$Val extends PaginatedResponse<T>>
implements $PaginatedResponseCopyWith<T, $Res> {
_$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<T>,
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<T, $Res>
implements $PaginatedResponseCopyWith<T, $Res> {
factory _$$PaginatedResponseImplCopyWith(_$PaginatedResponseImpl<T> value,
$Res Function(_$PaginatedResponseImpl<T>) then) =
__$$PaginatedResponseImplCopyWithImpl<T, $Res>;
@override
@useResult
$Res call(
{List<T> items,
int page,
int size,
int totalElements,
int totalPages,
bool first,
bool last});
}
/// @nodoc
class __$$PaginatedResponseImplCopyWithImpl<T, $Res>
extends _$PaginatedResponseCopyWithImpl<T, $Res, _$PaginatedResponseImpl<T>>
implements _$$PaginatedResponseImplCopyWith<T, $Res> {
__$$PaginatedResponseImplCopyWithImpl(_$PaginatedResponseImpl<T> _value,
$Res Function(_$PaginatedResponseImpl<T>) _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<T>(
items: null == items
? _value._items
: items // ignore: cast_nullable_to_non_nullable
as List<T>,
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<T> implements _PaginatedResponse<T> {
const _$PaginatedResponseImpl(
{required final List<T> 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<String, dynamic> json, T Function(Object?) fromJsonT) =>
_$$PaginatedResponseImplFromJson(json, fromJsonT);
final List<T> _items;
@override
List<T> 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<T> &&
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<T, _$PaginatedResponseImpl<T>>
get copyWith =>
__$$PaginatedResponseImplCopyWithImpl<T, _$PaginatedResponseImpl<T>>(
this, _$identity);
@override
Map<String, dynamic> toJson(Object? Function(T) toJsonT) {
return _$$PaginatedResponseImplToJson<T>(this, toJsonT);
}
}
abstract class _PaginatedResponse<T> implements PaginatedResponse<T> {
const factory _PaginatedResponse(
{required final List<T> 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<T>;
factory _PaginatedResponse.fromJson(
Map<String, dynamic> json, T Function(Object?) fromJsonT) =
_$PaginatedResponseImpl<T>.fromJson;
@override
List<T> 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<T, _$PaginatedResponseImpl<T>>
get copyWith => throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,35 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'paginated_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$PaginatedResponseImpl<T> _$$PaginatedResponseImplFromJson<T>(
Map<String, dynamic> json,
T Function(Object? json) fromJsonT,
) =>
_$PaginatedResponseImpl<T>(
items: (json['items'] as List<dynamic>).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<String, dynamic> _$$PaginatedResponseImplToJson<T>(
_$PaginatedResponseImpl<T> instance,
Object? Function(T value) toJsonT,
) =>
<String, dynamic>{
'items': instance.items.map(toJsonT).toList(),
'page': instance.page,
'size': instance.size,
'totalElements': instance.totalElements,
'totalPages': instance.totalPages,
'first': instance.first,
'last': instance.last,
};

View File

@@ -73,7 +73,7 @@ Future<void> setupDependencies() async {
() => UserService(), () => UserService(),
); );
getIt.registerLazySingleton<LicenseService>( getIt.registerLazySingleton<LicenseService>(
() => LicenseService(), () => LicenseService(getIt()),
); );
getIt.registerLazySingleton<WarehouseService>( getIt.registerLazySingleton<WarehouseService>(
() => WarehouseService(), () => WarehouseService(),

View File

@@ -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,
});
}

View File

@@ -1,35 +1,161 @@
class License { class License {
final int? id; final int? id;
final int companyId; final String licenseKey;
final String name; final String? productName;
final int durationMonths; final String? vendor;
final String visitCycle; // 방문주기(월, 격월, 분기 등) 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({ License({
this.id, this.id,
required this.companyId, required this.licenseKey,
required this.name, this.productName,
required this.durationMonths, this.vendor,
required this.visitCycle, 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<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
'id': id, 'id': id,
'companyId': companyId, 'license_key': licenseKey,
'name': name, 'product_name': productName,
'durationMonths': durationMonths, 'vendor': vendor,
'visitCycle': visitCycle, '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<String, dynamic> json) { factory License.fromJson(Map<String, dynamic> json) {
return License( return License(
id: json['id'], id: json['id'] as int?,
companyId: json['companyId'], licenseKey: json['license_key'] as String,
name: json['name'], productName: json['product_name'] as String?,
durationMonths: json['durationMonths'], vendor: json['vendor'] as String?,
visitCycle: json['visitCycle'] 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 => ''; // 기본값
} }

View File

@@ -211,7 +211,7 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
height: 40, height: 40,
decoration: BoxDecoration( decoration: BoxDecoration(
color: ShadcnTheme.card, color: ShadcnTheme.card,
borderRadius: BorderRadius.circular(ShadcnTheme.radius), borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
border: Border.all(color: ShadcnTheme.border), border: Border.all(color: ShadcnTheme.border),
), ),
child: TextField( child: TextField(
@@ -262,7 +262,7 @@ class _CompanyListRedesignState extends State<CompanyListRedesign> {
margin: const EdgeInsets.only(bottom: ShadcnTheme.spacing4), margin: const EdgeInsets.only(bottom: ShadcnTheme.spacing4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.red.shade50, color: Colors.red.shade50,
borderRadius: BorderRadius.circular(ShadcnTheme.radius), borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
border: Border.all(color: Colors.red.shade200), border: Border.all(color: Colors.red.shade200),
), ),
child: Row( child: Row(

View File

@@ -171,7 +171,7 @@ class EquipmentInFormController extends ChangeNotifier {
// API 실패 시 Mock 데이터 사용 // API 실패 시 Mock 데이터 사용
_loadFromMockData(equipmentIn); _loadFromMockData(equipmentIn);
} }
} else { } else if (equipmentIn != null) {
_loadFromMockData(equipmentIn); _loadFromMockData(equipmentIn);
} }
} else { } else {
@@ -311,11 +311,15 @@ class EquipmentInFormController extends ChangeNotifier {
int? warehouseLocationId; int? warehouseLocationId;
if (warehouseLocation != null) { if (warehouseLocation != null) {
// TODO: 창고 위치 ID 가져오기 - 현재는 목 데이터에서 찾기 // TODO: 창고 위치 ID 가져오기 - 현재는 목 데이터에서 찾기
try {
final warehouse = dataService.getAllWarehouseLocations().firstWhere( final warehouse = dataService.getAllWarehouseLocations().firstWhere(
(w) => w.name == warehouseLocation, (w) => w.name == warehouseLocation,
orElse: () => null,
); );
warehouseLocationId = warehouse?.id; warehouseLocationId = warehouse.id;
} catch (e) {
// 창고를 찾을 수 없는 경우
warehouseLocationId = null;
}
} }
await _equipmentService.equipmentIn( await _equipmentService.equipmentIn(

View File

@@ -63,13 +63,12 @@ class EquipmentListController extends ChangeNotifier {
); );
// API 모델을 UnifiedEquipment로 변환 // API 모델을 UnifiedEquipment로 변환
final unifiedEquipments = apiEquipments.map((equipment) { final List<UnifiedEquipment> unifiedEquipments = apiEquipments.map((equipment) {
return UnifiedEquipment( return UnifiedEquipment(
id: equipment.id, id: equipment.id,
equipment: equipment, equipment: equipment,
quantity: equipment.quantity, date: DateTime.now(), // 실제로는 API에서 날짜 정보를 가져와야 함
status: EquipmentStatus.in_, // 기본값, 실제로는 API에서 가져와야 함 status: EquipmentStatus.in_, // 기본값, 실제로는 API에서 가져와야 함
locationTrack: LocationTrack.inStock,
); );
}).toList(); }).toList();

View File

@@ -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<FormState> formKey = GlobalKey<FormState>();
// 선택된 장비 정보
Equipment? selectedEquipment;
int? selectedEquipmentInId;
List<Map<String, dynamic>>? selectedEquipments;
// 출고처 정보
List<String?> selectedCompanies = [null];
List<bool> hasManagersPerCompany = [false];
List<List<String>> filteredManagersPerCompany = [[]];
List<String?> selectedManagersPerCompany = [null];
// 라이선스 정보
String? selectedLicense;
// 출고 타입
String outType = 'O'; // 기본값: 출고
// 드롭다운 데이터
List<CompanyBranchInfo> companies = [];
List<String> licenses = [];
// 날짜
DateTime outDate = DateTime.now();
// 회사 정보 (지점 포함)
List<CompanyBranchInfo> get companiesWithBranches => companies;
// 비고
final TextEditingController remarkController = TextEditingController();
EquipmentOutFormController({
required this.dataService,
this.equipmentOutId,
}) {
isEditMode = equipmentOutId != null;
}
// 이용 가능한 회사 목록 (선택된 회사 제외)
List<List<CompanyBranchInfo>> get availableCompaniesPerDropdown {
return List.generate(selectedCompanies.length, (index) {
final selectedBefore = selectedCompanies.sublist(0, index).whereType<String>().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<bool> 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();
}
}

View File

@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:superport/models/equipment_unified_model.dart'; import 'package:superport/models/equipment_unified_model.dart';
import 'package:superport/models/company_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/models/address_model.dart';
import 'package:superport/screens/common/custom_widgets.dart'; import 'package:superport/screens/common/custom_widgets.dart';
import 'package:superport/screens/common/theme_tailwind.dart'; import 'package:superport/screens/common/theme_tailwind.dart';
@@ -406,25 +407,17 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
} }
} }
controller.saveEquipmentOut( controller.saveEquipmentOut(context).then((success) {
(msg) { if (success) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( const SnackBar(
content: Text(msg), content: Text('출고가 완료되었습니다.'),
duration: const Duration(seconds: 2), duration: Duration(seconds: 2),
), ),
); );
Navigator.pop(context, true); Navigator.pop(context, true);
}, }
(err) { });
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(err),
duration: const Duration(seconds: 2),
),
);
},
);
} }
: null, : null,
style: style:
@@ -510,8 +503,8 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
controller.availableCompaniesPerDropdown[index] controller.availableCompaniesPerDropdown[index]
.map( .map(
(item) => DropdownMenuItem<String>( (item) => DropdownMenuItem<String>(
value: item, value: item.name,
child: _buildCompanyDropdownItem(item, controller), child: _buildCompanyDropdownItem(item.name, controller),
), ),
) )
.toList(), .toList(),
@@ -526,10 +519,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
controller.selectedCompanies[index - 1] != null) controller.selectedCompanies[index - 1] != null)
? (value) { ? (value) {
controller.selectedCompanies[index] = value; controller.selectedCompanies[index] = value;
controller.filterManagersByCompanyAtIndex( controller.filterManagersByCompanyAtIndex(index);
value,
index,
);
controller.updateAvailableCompanies(); controller.updateAvailableCompanies();
} }
: null, : null,

View File

@@ -24,11 +24,35 @@ class LicenseFormController extends ChangeNotifier {
int _durationMonths = 12; // 기본값: 12개월 int _durationMonths = 12; // 기본값: 12개월
String _visitCycle = '미방문'; // 기본값: 미방문 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({ LicenseFormController({
this.useApi = true, this.useApi = false,
this.mockDataService, MockDataService? dataService,
int? licenseId, int? licenseId,
}) { }) : mockDataService = dataService ?? MockDataService() {
if (useApi && GetIt.instance.isRegistered<LicenseService>()) { if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
_licenseService = GetIt.instance<LicenseService>(); _licenseService = GetIt.instance<LicenseService>();
} }
@@ -89,10 +113,11 @@ class LicenseFormController extends ChangeNotifier {
} }
if (_originalLicense != null) { if (_originalLicense != null) {
_name = _originalLicense!.name; _name = _originalLicense!.productName ?? '';
_companyId = _originalLicense!.companyId; _companyId = _originalLicense!.companyId ?? 1;
_durationMonths = _originalLicense!.durationMonths; // durationMonths와 visitCycle은 License 모델에 없으므로 기본값 유지
_visitCycle = _originalLicense!.visitCycle; // _durationMonths = _originalLicense!.durationMonths;
// _visitCycle = _originalLicense!.visitCycle;
} }
} catch (e) { } catch (e) {
_error = e.toString(); _error = e.toString();
@@ -115,10 +140,14 @@ class LicenseFormController extends ChangeNotifier {
try { try {
final license = License( final license = License(
id: _isEditMode ? _licenseId : null, id: _isEditMode ? _licenseId : null,
licenseKey: 'LIC-${DateTime.now().millisecondsSinceEpoch}',
productName: _name,
companyId: _companyId, companyId: _companyId,
name: _name, // durationMonths와 visitCycle은 License 모델에 없음
durationMonths: _durationMonths, // 대신 expiryDate를 설정
visitCycle: _visitCycle, purchaseDate: DateTime.now(),
expiryDate: DateTime.now().add(Duration(days: _durationMonths * 30)),
remark: '방문주기: $_visitCycle',
); );
if (useApi && GetIt.instance.isRegistered<LicenseService>()) { if (useApi && GetIt.instance.isRegistered<LicenseService>()) {

View File

@@ -1,5 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get_it/get_it.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/models/license_model.dart';
import 'package:superport/services/license_service.dart'; import 'package:superport/services/license_service.dart';
import 'package:superport/services/mock_data_service.dart'; import 'package:superport/services/mock_data_service.dart';
@@ -24,8 +26,13 @@ class LicenseListController extends ChangeNotifier {
int? _selectedCompanyId; int? _selectedCompanyId;
bool? _isActive; bool? _isActive;
String? _licenseType; String? _licenseType;
String _sortBy = 'expiry_date';
String _sortOrder = 'asc';
LicenseListController({this.useApi = true, this.mockDataService}) { // 검색 디바운스를 위한 타이머
Timer? _debounceTimer;
LicenseListController({this.useApi = false, this.mockDataService}) {
if (useApi && GetIt.instance.isRegistered<LicenseService>()) { if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
_licenseService = GetIt.instance<LicenseService>(); _licenseService = GetIt.instance<LicenseService>();
} }
@@ -137,11 +144,24 @@ class LicenseListController extends ChangeNotifier {
await loadData(isInitialLoad: false); await loadData(isInitialLoad: false);
} }
// 검색 // 검색 (디바운싱 적용)
void search(String query) { void search(String query) {
_searchQuery = query; _searchQuery = query;
// 기존 타이머 취소
_debounceTimer?.cancel();
// Mock 데이터는 즉시 검색
if (!useApi) {
_applySearchFilter(); _applySearchFilter();
notifyListeners(); notifyListeners();
return;
}
// API 검색은 디바운싱 적용 (300ms)
_debounceTimer = Timer(const Duration(milliseconds: 300), () {
loadData();
});
} }
// 검색 필터 적용 // 검색 필터 적용
@@ -212,10 +232,12 @@ class LicenseListController extends ChangeNotifier {
final allLicenses = mockDataService?.getAllLicenses() ?? []; final allLicenses = mockDataService?.getAllLicenses() ?? [];
return allLicenses.where((license) { return allLicenses.where((license) {
// Mock 데이터는 만료일이 없으므로 임의로 계산 // 실제 License 모델에서 만료일 확인
final expiryDate = now.add(Duration(days: license.durationMonths * 30)); if (license.expiryDate != null) {
final daysUntilExpiry = expiryDate.difference(now).inDays; final daysUntilExpiry = license.expiryDate!.difference(now).inDays;
return daysUntilExpiry > 0 && daysUntilExpiry <= days; return daysUntilExpiry > 0 && daysUntilExpiry <= days;
}
return false;
}).toList(); }).toList();
} }
} catch (e) { } catch (e) {
@@ -224,4 +246,69 @@ class LicenseListController extends ChangeNotifier {
return []; return [];
} }
} }
// 상태별 라이선스 개수 조회
Future<Map<String, int>> getLicenseStatusCounts() async {
if (useApi && GetIt.instance.isRegistered<LicenseService>()) {
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();
}
} }

View File

@@ -41,7 +41,9 @@ class _MaintenanceFormScreenState extends State<MaintenanceFormScreen> {
dataService: MockDataService(), dataService: MockDataService(),
licenseId: widget.maintenanceId, licenseId: widget.maintenanceId,
); );
_controller.isEditMode = widget.maintenanceId != null; if (widget.maintenanceId != null) {
_controller.isEditMode = true;
}
if (_controller.isEditMode) { if (_controller.isEditMode) {
_controller.loadLicense(); _controller.loadLicense();
// TODO: 기존 데이터 로딩 시 _selectedVisitCycle, _selectedInspectionType, _durationMonths 값 세팅 필요 // TODO: 기존 데이터 로딩 시 _selectedVisitCycle, _selectedInspectionType, _durationMonths 값 세팅 필요
@@ -201,10 +203,10 @@ class _MaintenanceFormScreenState extends State<MaintenanceFormScreen> {
_controller.durationMonths = _durationMonths; _controller.durationMonths = _durationMonths;
_controller.visitCycle = _selectedVisitCycle; _controller.visitCycle = _selectedVisitCycle;
// 점검형태 저장 로직 필요 시 추가 // 점검형태 저장 로직 필요 시 추가
setState(() { _controller.saveLicense().then((success) {
_controller.saveLicense(() { if (success) {
Navigator.pop(context, true); Navigator.pop(context, true);
}); }
}); });
} }
}, },

View File

@@ -23,7 +23,7 @@ class _LicenseListRedesignState extends State<LicenseListRedesign> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_controller = LicenseListController(dataService: _dataService); _controller = LicenseListController(mockDataService: _dataService);
_controller.loadData(); _controller.loadData();
} }
@@ -294,7 +294,7 @@ class _LicenseListRedesignState extends State<LicenseListRedesign> {
Expanded( Expanded(
flex: 2, flex: 2,
child: Text( child: Text(
_getCompanyName(license.companyId), _getCompanyName(license.companyId ?? 0),
style: ShadcnTheme.bodySmall, style: ShadcnTheme.bodySmall,
), ),
), ),

View File

@@ -35,6 +35,10 @@ class OverviewController extends ChangeNotifier {
EquipmentStatusDistribution? get equipmentStatus => _equipmentStatus; EquipmentStatusDistribution? get equipmentStatus => _equipmentStatus;
List<ExpiringLicense> get expiringLicenses => _expiringLicenses; List<ExpiringLicense> get expiringLicenses => _expiringLicenses;
// 추가 getter
int get totalCompanies => _overviewStats?.totalCompanies ?? 0;
int get totalUsers => _overviewStats?.totalUsers ?? 0;
bool get isLoading => _isLoadingStats || _isLoadingActivities || bool get isLoading => _isLoadingStats || _isLoadingActivities ||
_isLoadingEquipmentStatus || _isLoadingLicenses; _isLoadingEquipmentStatus || _isLoadingLicenses;
@@ -55,6 +59,11 @@ class OverviewController extends ChangeNotifier {
]); ]);
} }
// 대시보드 데이터 로드 (loadData의 alias)
Future<void> loadDashboardData() async {
await loadData();
}
// 개별 데이터 로드 메서드 // 개별 데이터 로드 메서드
Future<void> _loadOverviewStats() async { Future<void> _loadOverviewStats() async {
_isLoadingStats = true; _isLoadingStats = true;

View File

@@ -1,4 +1,6 @@
import 'package:flutter/material.dart'; 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/theme_shadcn.dart';
import 'package:superport/screens/common/components/shadcn_components.dart'; import 'package:superport/screens/common/components/shadcn_components.dart';
import 'package:superport/screens/overview/controllers/overview_controller.dart'; import 'package:superport/screens/overview/controllers/overview_controller.dart';
@@ -123,13 +125,13 @@ class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
), ),
_buildStatCard( _buildStatCard(
'입고 장비', '입고 장비',
'${_controller.totalEquipmentIn}', '${_controller.overviewStats?.availableEquipment ?? 0}',
Icons.inventory, Icons.inventory,
ShadcnTheme.success, ShadcnTheme.success,
), ),
_buildStatCard( _buildStatCard(
'출고 장비', '출고 장비',
'${_controller.totalEquipmentOut}', '${_controller.overviewStats?.inUseEquipment ?? 0}',
Icons.local_shipping, Icons.local_shipping,
ShadcnTheme.warning, ShadcnTheme.warning,
), ),
@@ -390,7 +392,7 @@ class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
Container( Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: color.withOpacity(0.1), color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
child: Icon(icon, color: color, size: 20), child: Icon(icon, color: color, size: 20),
@@ -398,7 +400,7 @@ class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: ShadcnTheme.success.withOpacity(0.1), color: ShadcnTheme.success.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Row( child: Row(
@@ -474,7 +476,7 @@ class _OverviewScreenRedesignState extends State<OverviewScreenRedesign> {
Container( Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: color.withOpacity(0.1), color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
child: Icon( child: Icon(

View File

@@ -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_item.dart';
import 'package:superport/screens/sidebar/widgets/sidebar_menu_submenu.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/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 { class SidebarMenu extends StatefulWidget {

View File

@@ -4,8 +4,6 @@ import 'package:superport/models/user_model.dart';
import 'package:superport/models/company_model.dart'; import 'package:superport/models/company_model.dart';
import 'package:superport/services/mock_data_service.dart'; import 'package:superport/services/mock_data_service.dart';
import 'package:superport/services/user_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 { class UserListController extends ChangeNotifier {

View File

@@ -1,15 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.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/theme_tailwind.dart';
import 'package:superport/screens/common/custom_widgets.dart';
import 'package:superport/services/mock_data_service.dart'; import 'package:superport/services/mock_data_service.dart';
import 'package:superport/utils/constants.dart'; import 'package:superport/utils/constants.dart';
import 'package:superport/utils/validators.dart'; import 'package:superport/utils/validators.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:superport/screens/user/controllers/user_form_controller.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'; import 'package:superport/screens/common/widgets/company_branch_dropdown.dart';
// 사용자 등록/수정 화면 (UI만 담당, 상태/로직 분리) // 사용자 등록/수정 화면 (UI만 담당, 상태/로직 분리)

View File

@@ -1,6 +1,4 @@
import 'package:flutter/material.dart'; 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/theme_tailwind.dart';
import 'package:superport/screens/common/main_layout.dart'; import 'package:superport/screens/common/main_layout.dart';
import 'package:superport/screens/common/custom_widgets.dart'; import 'package:superport/screens/common/custom_widgets.dart';
@@ -82,6 +80,11 @@ class _UserListScreenState extends State<UserListScreen> {
onPressed: () { onPressed: () {
_controller.deleteUser(id, () { _controller.deleteUser(id, () {
Navigator.pop(context); Navigator.pop(context);
}, (error) {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(error)),
);
}); });
}, },
child: const Text('삭제'), child: const Text('삭제'),

View File

@@ -211,7 +211,7 @@ class _UserListRedesignState extends State<UserListRedesign> {
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'데이터를 불러올 수 없습니다', '데이터를 불러올 수 없습니다',
style: ShadcnTheme.h4, style: ShadcnTheme.headingH4,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
@@ -291,7 +291,7 @@ class _UserListRedesignState extends State<UserListRedesign> {
: null, : null,
); );
}, },
variant: ShadcnButtonVariant.outline, variant: ShadcnButtonVariant.secondary,
icon: const Icon(Icons.filter_list), icon: const Icon(Icons.filter_list),
), ),
const SizedBox(width: ShadcnTheme.spacing2), const SizedBox(width: ShadcnTheme.spacing2),
@@ -302,7 +302,7 @@ class _UserListRedesignState extends State<UserListRedesign> {
? '모든 권한' ? '모든 권한'
: getRoleName(controller.filterRole!), : getRoleName(controller.filterRole!),
onPressed: null, onPressed: null,
variant: ShadcnButtonVariant.outline, variant: ShadcnButtonVariant.secondary,
icon: const Icon(Icons.person), icon: const Icon(Icons.person),
), ),
onSelected: (role) { onSelected: (role) {

View File

@@ -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/address_input.dart';
import 'package:superport/screens/common/widgets/remark_input.dart'; import 'package:superport/screens/common/widgets/remark_input.dart';
import 'package:superport/screens/common/theme_tailwind.dart'; import 'package:superport/screens/common/theme_tailwind.dart';
import 'package:superport/utils/constants.dart';
import 'controllers/warehouse_location_form_controller.dart'; import 'controllers/warehouse_location_form_controller.dart';
/// 입고지 추가/수정 폼 화면 (SRP 적용, 상태/로직 분리) /// 입고지 추가/수정 폼 화면 (SRP 적용, 상태/로직 분리)
@@ -26,7 +25,9 @@ class _WarehouseLocationFormScreenState
super.initState(); super.initState();
// 컨트롤러 생성 및 초기화 // 컨트롤러 생성 및 초기화
_controller = WarehouseLocationFormController(); _controller = WarehouseLocationFormController();
_controller.initialize(widget.id); if (widget.id != null) {
_controller.initialize(widget.id!);
}
} }
@override @override
@@ -107,7 +108,7 @@ class _WarehouseLocationFormScreenState
? null ? null
: () async { : () async {
setState(() {}); // 저장 중 상태 갱신 setState(() {}); // 저장 중 상태 갱신
await _controller.save(context); await _controller.save();
setState(() {}); // 저장 완료 후 상태 갱신 setState(() {}); // 저장 완료 후 상태 갱신
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(

View File

@@ -136,7 +136,7 @@ class _WarehouseLocationListRedesignState
vertical: ShadcnTheme.spacing3, vertical: ShadcnTheme.spacing3,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: ShadcnTheme.muted.withOpacity(0.3), color: ShadcnTheme.muted.withValues(alpha: 0.3),
border: Border( border: Border(
bottom: BorderSide(color: ShadcnTheme.border), bottom: BorderSide(color: ShadcnTheme.border),
), ),

View File

@@ -1,4 +1,3 @@
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:superport/core/errors/exceptions.dart'; import 'package:superport/core/errors/exceptions.dart';
import 'package:superport/core/errors/failures.dart'; import 'package:superport/core/errors/failures.dart';
@@ -32,9 +31,9 @@ class CompanyService {
return response.items.map((dto) => _convertListDtoToCompany(dto)).toList(); return response.items.map((dto) => _convertListDtoToCompany(dto)).toList();
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } 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); final response = await _remoteDataSource.createCompany(request);
return _convertResponseToCompany(response); return _convertResponseToCompany(response);
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } 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); final response = await _remoteDataSource.getCompanyDetail(id);
return _convertResponseToCompany(response); return _convertResponseToCompany(response);
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } 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); return company.copyWith(branches: branches);
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } 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); final response = await _remoteDataSource.updateCompany(id, request);
return _convertResponseToCompany(response); return _convertResponseToCompany(response);
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } 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 { try {
await _remoteDataSource.deleteCompany(id); await _remoteDataSource.deleteCompany(id);
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } 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, 'name': dto.name,
}).toList(); }).toList();
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } 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); final response = await _remoteDataSource.createBranch(companyId, request);
return _convertBranchResponseToBranch(response); return _convertBranchResponseToBranch(response);
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } 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); final response = await _remoteDataSource.getBranchDetail(companyId, branchId);
return _convertBranchResponseToBranch(response); return _convertBranchResponseToBranch(response);
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } 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); final response = await _remoteDataSource.updateBranch(companyId, branchId, request);
return _convertBranchResponseToBranch(response); return _convertBranchResponseToBranch(response);
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } 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 { try {
await _remoteDataSource.deleteBranch(companyId, branchId); await _remoteDataSource.deleteBranch(companyId, branchId);
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } 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); final dtoList = await _remoteDataSource.getCompanyBranches(companyId);
return dtoList.map((dto) => _convertBranchDtoToBranch(dto)).toList(); return dtoList.map((dto) => _convertBranchDtoToBranch(dto)).toList();
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } 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 { try {
return await _remoteDataSource.getCompaniesWithBranches(); return await _remoteDataSource.getCompaniesWithBranches();
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } 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 { try {
return await _remoteDataSource.checkDuplicateCompany(name); return await _remoteDataSource.checkDuplicateCompany(name);
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } 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); final dtoList = await _remoteDataSource.searchCompanies(query);
return dtoList.map((dto) => _convertListDtoToCompany(dto)).toList(); return dtoList.map((dto) => _convertListDtoToCompany(dto)).toList();
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } 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 { try {
await _remoteDataSource.updateCompanyStatus(id, isActive); await _remoteDataSource.updateCompanyStatus(id, isActive);
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } catch (e) {
throw Failure(message: 'Failed to update company status: $e'); throw ServerFailure(message: 'Failed to update company status: $e');
} }
} }

View File

@@ -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_request.dart';
import 'package:superport/data/models/equipment/equipment_response.dart'; import 'package:superport/data/models/equipment/equipment_response.dart';
import 'package:superport/models/equipment_unified_model.dart'; import 'package:superport/models/equipment_unified_model.dart';
import 'package:superport/utils/constants.dart';
class EquipmentService { class EquipmentService {
final EquipmentRemoteDataSource _remoteDataSource = GetIt.instance<EquipmentRemoteDataSource>(); final EquipmentRemoteDataSource _remoteDataSource = GetIt.instance<EquipmentRemoteDataSource>();
@@ -34,9 +33,9 @@ class EquipmentService {
return dtoList.map((dto) => _convertListDtoToEquipment(dto)).toList(); return dtoList.map((dto) => _convertListDtoToEquipment(dto)).toList();
} on ServerException catch (e) { } on ServerException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } 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); final response = await _remoteDataSource.createEquipment(request);
return _convertResponseToEquipment(response); return _convertResponseToEquipment(response);
} on ServerException catch (e) { } on ServerException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } catch (e) {
throw Failure(message: 'Failed to create equipment: $e'); throw ServerFailure(message: 'Failed to create equipment: $e');
} }
} }
@@ -71,12 +70,17 @@ class EquipmentService {
final response = await _remoteDataSource.getEquipmentDetail(id); final response = await _remoteDataSource.getEquipmentDetail(id);
return _convertResponseToEquipment(response); return _convertResponseToEquipment(response);
} on ServerException catch (e) { } on ServerException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } catch (e) {
throw Failure(message: 'Failed to fetch equipment detail: $e'); throw ServerFailure(message: 'Failed to fetch equipment detail: $e');
} }
} }
// 장비 조회 (getEquipmentDetail의 alias)
Future<Equipment> getEquipment(int id) async {
return getEquipmentDetail(id);
}
// 장비 수정 // 장비 수정
Future<Equipment> updateEquipment(int id, Equipment equipment) async { Future<Equipment> updateEquipment(int id, Equipment equipment) async {
try { try {
@@ -96,9 +100,9 @@ class EquipmentService {
final response = await _remoteDataSource.updateEquipment(id, request); final response = await _remoteDataSource.updateEquipment(id, request);
return _convertResponseToEquipment(response); return _convertResponseToEquipment(response);
} on ServerException catch (e) { } on ServerException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } 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 { try {
await _remoteDataSource.deleteEquipment(id); await _remoteDataSource.deleteEquipment(id);
} on ServerException catch (e) { } on ServerException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } 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); final response = await _remoteDataSource.changeEquipmentStatus(id, status, reason);
return _convertResponseToEquipment(response); return _convertResponseToEquipment(response);
} on ServerException catch (e) { } on ServerException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } 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); return await _remoteDataSource.addEquipmentHistory(equipmentId, request);
} on ServerException catch (e) { } on ServerException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } 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 { try {
return await _remoteDataSource.getEquipmentHistory(equipmentId, page: page, perPage: perPage); return await _remoteDataSource.getEquipmentHistory(equipmentId, page: page, perPage: perPage);
} on ServerException catch (e) { } on ServerException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } 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); return await _remoteDataSource.equipmentIn(request);
} on ServerException catch (e) { } on ServerException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } 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); return await _remoteDataSource.equipmentOut(request);
} on ServerException catch (e) { } on ServerException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } catch (e) {
throw Failure(message: 'Failed to process equipment out: $e'); throw ServerFailure(message: 'Failed to process equipment out: $e');
} }
} }

View File

@@ -9,7 +9,9 @@ import 'package:superport/models/license_model.dart';
@lazySingleton @lazySingleton
class LicenseService { class LicenseService {
final LicenseRemoteDataSource _remoteDataSource = GetIt.instance<LicenseRemoteDataSource>(); final LicenseRemoteDataSource _remoteDataSource;
LicenseService(this._remoteDataSource);
// 라이선스 목록 조회 // 라이선스 목록 조회
Future<List<License>> getLicenses({ Future<List<License>> getLicenses({
@@ -32,9 +34,9 @@ class LicenseService {
return response.items.map((dto) => _convertDtoToLicense(dto)).toList(); return response.items.map((dto) => _convertDtoToLicense(dto)).toList();
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } catch (e) {
throw Failure(message: '라이선스 목록을 불러오는 데 실패했습니다: $e'); throw ServerFailure(message: '라이선스 목록을 불러오는 데 실패했습니다: $e');
} }
} }
@@ -44,36 +46,35 @@ class LicenseService {
final dto = await _remoteDataSource.getLicenseById(id); final dto = await _remoteDataSource.getLicenseById(id);
return _convertDtoToLicense(dto); return _convertDtoToLicense(dto);
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } catch (e) {
throw Failure(message: '라이선스 정보를 불러오는 데 실패했습니다: $e'); throw ServerFailure(message: '라이선스 정보를 불러오는 데 실패했습니다: $e');
} }
} }
// 라이선스 생성 // 라이선스 생성
Future<License> createLicense(License license) async { Future<License> createLicense(License license) async {
try { try {
// Flutter 모델의 visitCycle과 durationMonths를 API 필드에 매핑
// visitCycle은 remark에 저장하고, durationMonths는 날짜 계산에 사용
final now = DateTime.now();
final expiryDate = now.add(Duration(days: license.durationMonths * 30));
final request = CreateLicenseRequest( final request = CreateLicenseRequest(
licenseKey: license.name, // name을 licenseKey로 매핑 licenseKey: license.licenseKey,
productName: '유지보수 계약', // 기본값 설정 productName: license.productName,
licenseType: 'maintenance', // 유지보수 타입으로 고정 vendor: license.vendor,
licenseType: license.licenseType,
userCount: license.userCount,
purchaseDate: license.purchaseDate,
expiryDate: license.expiryDate,
purchasePrice: license.purchasePrice,
companyId: license.companyId, companyId: license.companyId,
purchaseDate: now, branchId: license.branchId,
expiryDate: expiryDate, remark: license.remark,
remark: '방문주기: ${license.visitCycle}', // visitCycle을 remark에 저장
); );
final dto = await _remoteDataSource.createLicense(request); final dto = await _remoteDataSource.createLicense(request);
return _convertDtoToLicense(dto); return _convertDtoToLicense(dto);
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } catch (e) {
throw Failure(message: '라이선스 생성에 실패했습니다: $e'); throw ServerFailure(message: '라이선스 생성에 실패했습니다: $e');
} }
} }
@@ -81,30 +82,28 @@ class LicenseService {
Future<License> updateLicense(License license) async { Future<License> updateLicense(License license) async {
try { try {
if (license.id == null) { if (license.id == null) {
throw Failure(message: '라이선스 ID가 없습니다'); throw BusinessFailure(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));
} }
final request = UpdateLicenseRequest( final request = UpdateLicenseRequest(
licenseKey: license.name, licenseKey: license.licenseKey,
expiryDate: newExpiryDate, productName: license.productName,
remark: '방문주기: ${license.visitCycle}', 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); final dto = await _remoteDataSource.updateLicense(license.id!, request);
return _convertDtoToLicense(dto); return _convertDtoToLicense(dto);
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } catch (e) {
throw Failure(message: '라이선스 수정에 실패했습니다: $e'); throw ServerFailure(message: '라이선스 수정에 실패했습니다: $e');
} }
} }
@@ -113,9 +112,9 @@ class LicenseService {
try { try {
await _remoteDataSource.deleteLicense(id); await _remoteDataSource.deleteLicense(id);
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } catch (e) {
throw Failure(message: '라이선스 삭제에 실패했습니다: $e'); throw ServerFailure(message: '라이선스 삭제에 실패했습니다: $e');
} }
} }
@@ -126,9 +125,9 @@ class LicenseService {
final dto = await _remoteDataSource.assignLicense(licenseId, request); final dto = await _remoteDataSource.assignLicense(licenseId, request);
return _convertDtoToLicense(dto); return _convertDtoToLicense(dto);
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } catch (e) {
throw Failure(message: '라이선스 할당에 실패했습니다: $e'); throw ServerFailure(message: '라이선스 할당에 실패했습니다: $e');
} }
} }
@@ -138,9 +137,9 @@ class LicenseService {
final dto = await _remoteDataSource.unassignLicense(licenseId); final dto = await _remoteDataSource.unassignLicense(licenseId);
return _convertDtoToLicense(dto); return _convertDtoToLicense(dto);
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } catch (e) {
throw Failure(message: '라이선스 할당 해제에 실패했습니다: $e'); throw ServerFailure(message: '라이선스 할당 해제에 실패했습니다: $e');
} }
} }
@@ -159,33 +158,34 @@ class LicenseService {
return response.items.map((dto) => _convertExpiringDtoToLicense(dto)).toList(); return response.items.map((dto) => _convertExpiringDtoToLicense(dto)).toList();
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } catch (e) {
throw Failure(message: '만료 예정 라이선스를 불러오는 데 실패했습니다: $e'); throw ServerFailure(message: '만료 예정 라이선스를 불러오는 데 실패했습니다: $e');
} }
} }
// DTO를 Flutter 모델로 변환 // DTO를 Flutter 모델로 변환
License _convertDtoToLicense(LicenseDto dto) { 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( return License(
id: dto.id, id: dto.id,
companyId: dto.companyId ?? 0, licenseKey: dto.licenseKey,
name: dto.licenseKey, productName: dto.productName,
durationMonths: durationMonths, vendor: dto.vendor,
visitCycle: visitCycle, 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) { License _convertExpiringDtoToLicense(ExpiringLicenseDto dto) {
return License( return License(
id: dto.id, id: dto.id,
companyId: 0, // ExpiringLicenseDto에는 companyId가 없으므로 기본값 사용 licenseKey: dto.licenseKey,
name: dto.licenseKey, productName: dto.productName,
durationMonths: 12, // 기본값 vendor: null,
visitCycle: '미방문', // 기본값 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,
); );
} }

View File

@@ -570,10 +570,12 @@ class MockDataService {
for (final inspection in inspectionTypes) { for (final inspection in inspectionTypes) {
addLicense( addLicense(
License( License(
licenseKey: 'LIC-${DateTime.now().millisecondsSinceEpoch}-$visit-$inspection',
productName: '12개월,$visit,$inspection',
companyId: 1, companyId: 1,
name: '12개월,$visit,$inspection', purchaseDate: DateTime.now(),
durationMonths: 12, expiryDate: DateTime.now().add(const Duration(days: 365)),
visitCycle: visit, remark: '방문주기: $visit',
), ),
); );
} }
@@ -672,12 +674,12 @@ class MockDataService {
// 기존 입고 장비를 출고 상태로 변경 // 기존 입고 장비를 출고 상태로 변경
void changeEquipmentStatus(int equipmentInId, EquipmentOut equipmentOut) { void changeEquipmentStatus(int equipmentInId, EquipmentOut equipmentOut) {
print('장비 상태 변경 시작: 입고 ID $equipmentInId'); // 장비 상태 변경 시작: 입고 ID $equipmentInId
// 입고된 장비를 찾습니다 // 입고된 장비를 찾습니다
final index = _equipmentIns.indexWhere((e) => e.id == equipmentInId); final index = _equipmentIns.indexWhere((e) => e.id == equipmentInId);
if (index != -1) { if (index != -1) {
print('장비를 찾음: ${_equipmentIns[index].equipment.name}'); // 장비를 찾음: ${_equipmentIns[index].equipment.name}
// 입고 장비의 상태를 출고(O)로 변경 // 입고 장비의 상태를 출고(O)로 변경
final equipment = _equipmentIns[index].equipment; final equipment = _equipmentIns[index].equipment;
@@ -687,7 +689,7 @@ class MockDataService {
inDate: _equipmentIns[index].inDate, inDate: _equipmentIns[index].inDate,
status: 'O', // 상태를 출고로 변경 status: 'O', // 상태를 출고로 변경
); );
print('입고 장비 상태를 "O"로 변경: ID ${_equipmentIns[index].id}'); // 입고 장비 상태를 "O"로 변경: ID ${_equipmentIns[index].id}
// 출고 정보 저장 // 출고 정보 저장
final newEquipmentOut = EquipmentOut( final newEquipmentOut = EquipmentOut(
@@ -702,11 +704,11 @@ class MockDataService {
returnType: equipmentOut.returnType, returnType: equipmentOut.returnType,
); );
_equipmentOuts.add(newEquipmentOut); _equipmentOuts.add(newEquipmentOut);
print('출고 정보 추가: ID ${newEquipmentOut.id}'); // 출고 정보 추가: ID ${newEquipmentOut.id}
print('장비 상태 변경 완료'); // 장비 상태 변경 완료
} else { } else {
print('오류: ID $equipmentInId인 입고 장비를 찾을 수 없음'); // 오류: ID $equipmentInId인 입고 장비를 찾을 수 없음
} }
} }
@@ -886,6 +888,19 @@ class MockDataService {
} }
} }
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) { void deleteCompany(int id) {
_companies.removeWhere((c) => c.id == id); _companies.removeWhere((c) => c.id == id);
} }
@@ -944,10 +959,19 @@ class MockDataService {
void addLicense(License license) { void addLicense(License license) {
final newLicense = License( final newLicense = License(
id: _licenseIdCounter++, 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, companyId: license.companyId,
name: license.name, branchId: license.branchId,
durationMonths: license.durationMonths, assignedUserId: license.assignedUserId,
visitCycle: license.visitCycle, remark: license.remark,
isActive: license.isActive,
); );
_licenses.add(newLicense); _licenses.add(newLicense);
} }

View File

@@ -1,5 +1,4 @@
import 'package:injectable/injectable.dart'; 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/datasources/remote/user_remote_datasource.dart';
import 'package:superport/data/models/user/user_dto.dart'; import 'package:superport/data/models/user/user_dto.dart';
import 'package:superport/models/user_model.dart'; import 'package:superport/models/user_model.dart';

View File

@@ -26,9 +26,9 @@ class WarehouseService {
return response.items.map((dto) => _convertDtoToWarehouseLocation(dto)).toList(); return response.items.map((dto) => _convertDtoToWarehouseLocation(dto)).toList();
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } catch (e) {
throw Failure(message: '창고 위치 목록을 불러오는 데 실패했습니다: $e'); throw ServerFailure(message: '창고 위치 목록을 불러오는 데 실패했습니다: $e');
} }
} }
@@ -38,9 +38,9 @@ class WarehouseService {
final dto = await _remoteDataSource.getWarehouseLocationById(id); final dto = await _remoteDataSource.getWarehouseLocationById(id);
return _convertDtoToWarehouseLocation(dto); return _convertDtoToWarehouseLocation(dto);
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } catch (e) {
throw Failure(message: '창고 위치 정보를 불러오는 데 실패했습니다: $e'); throw ServerFailure(message: '창고 위치 정보를 불러오는 데 실패했습니다: $e');
} }
} }
@@ -58,9 +58,9 @@ class WarehouseService {
final dto = await _remoteDataSource.createWarehouseLocation(request); final dto = await _remoteDataSource.createWarehouseLocation(request);
return _convertDtoToWarehouseLocation(dto); return _convertDtoToWarehouseLocation(dto);
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } catch (e) {
throw Failure(message: '창고 위치 생성에 실패했습니다: $e'); throw ServerFailure(message: '창고 위치 생성에 실패했습니다: $e');
} }
} }
@@ -77,9 +77,9 @@ class WarehouseService {
final dto = await _remoteDataSource.updateWarehouseLocation(location.id, request); final dto = await _remoteDataSource.updateWarehouseLocation(location.id, request);
return _convertDtoToWarehouseLocation(dto); return _convertDtoToWarehouseLocation(dto);
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } catch (e) {
throw Failure(message: '창고 위치 수정에 실패했습니다: $e'); throw ServerFailure(message: '창고 위치 수정에 실패했습니다: $e');
} }
} }
@@ -88,9 +88,9 @@ class WarehouseService {
try { try {
await _remoteDataSource.deleteWarehouseLocation(id); await _remoteDataSource.deleteWarehouseLocation(id);
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } catch (e) {
throw Failure(message: '창고 위치 삭제에 실패했습니다: $e'); throw ServerFailure(message: '창고 위치 삭제에 실패했습니다: $e');
} }
} }
@@ -118,9 +118,9 @@ class WarehouseService {
'storedAt': dto.storedAt, 'storedAt': dto.storedAt,
}).toList(); }).toList();
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } catch (e) {
throw Failure(message: '창고 장비 목록을 불러오는 데 실패했습니다: $e'); throw ServerFailure(message: '창고 장비 목록을 불러오는 데 실패했습니다: $e');
} }
} }
@@ -129,9 +129,9 @@ class WarehouseService {
try { try {
return await _remoteDataSource.getWarehouseCapacity(id); return await _remoteDataSource.getWarehouseCapacity(id);
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } catch (e) {
throw Failure(message: '창고 용량 정보를 불러오는 데 실패했습니다: $e'); throw ServerFailure(message: '창고 용량 정보를 불러오는 데 실패했습니다: $e');
} }
} }
@@ -141,9 +141,9 @@ class WarehouseService {
final dtos = await _remoteDataSource.getInUseWarehouseLocations(); final dtos = await _remoteDataSource.getInUseWarehouseLocations();
return dtos.map((dto) => _convertDtoToWarehouseLocation(dto)).toList(); return dtos.map((dto) => _convertDtoToWarehouseLocation(dto)).toList();
} on ApiException catch (e) { } on ApiException catch (e) {
throw Failure(message: e.message); throw ServerFailure(message: e.message);
} catch (e) { } catch (e) {
throw Failure(message: '사용 중인 창고 위치를 불러오는 데 실패했습니다: $e'); throw ServerFailure(message: '사용 중인 창고 위치를 불러오는 데 실패했습니다: $e');
} }
} }