refactor: Repository 패턴 적용 및 Clean Architecture 완성
Some checks failed
Flutter Test & Quality Check / Test on macos-latest (push) Has been cancelled
Flutter Test & Quality Check / Test on ubuntu-latest (push) Has been cancelled
Flutter Test & Quality Check / Build APK (push) Has been cancelled

## 주요 변경사항

### 🏗️ Architecture
- Repository 패턴 전면 도입 (인터페이스/구현체 분리)
- Domain Layer에 Repository 인터페이스 정의
- Data Layer에 Repository 구현체 배치
- UseCase 의존성을 Service에서 Repository로 전환

### 📦 Dependency Injection
- GetIt 기반 DI Container 재구성 (lib/injection_container.dart)
- Repository 인터페이스와 구현체 등록
- Service와 Repository 공존 (마이그레이션 기간)

### 🔄 Migration Status
완료:
- License 모듈 (6개 UseCase)
- Warehouse Location 모듈 (5개 UseCase)

진행중:
- Auth 모듈 (2/5 UseCase)
- Company 모듈 (1/6 UseCase)

대기:
- User 모듈 (7개 UseCase)
- Equipment 모듈 (4개 UseCase)

### 🎯 Controller 통합
- 중복 Controller 제거 (with_usecase 버전)
- 단일 Controller로 통합
- UseCase 패턴 직접 적용

### 🧹 코드 정리
- 임시 파일 제거 (test_*.md, task.md)
- Node.js 아티팩트 제거 (package.json)
- 불필요한 테스트 파일 정리

###  테스트 개선
- Real API 중심 테스트 구조
- Mock 제거, 실제 API 엔드포인트 사용
- 통합 테스트 프레임워크 강화

## 기술적 영향
- 의존성 역전 원칙 적용
- 레이어 간 결합도 감소
- 테스트 용이성 향상
- 확장성 및 유지보수성 개선

## 다음 단계
1. User/Equipment 모듈 Repository 마이그레이션
2. Service Layer 점진적 제거
3. 캐싱 전략 구현
4. 성능 최적화
This commit is contained in:
JiWoong Sul
2025-08-11 20:14:10 +09:00
parent d64aa26157
commit 731dcd816b
105 changed files with 5225 additions and 3941 deletions

View File

@@ -0,0 +1,100 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import '../../../core/storage/secure_storage.dart';
/// API 요청/응답 인터셉터
class ApiInterceptor extends Interceptor {
final SecureStorage _storage;
ApiInterceptor(this._storage);
@override
void onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) async {
// 토큰 추가
final token = await _storage.getAccessToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
// Content-Type 헤더 추가
if (options.data != null && options.data is! FormData) {
options.headers['Content-Type'] = 'application/json';
}
// 디버그 모드에서 요청 로깅
if (kDebugMode) {
print('🚀 API Request: ${options.method} ${options.path}');
if (options.queryParameters.isNotEmpty) {
print(' Query: ${options.queryParameters}');
}
if (options.data != null) {
print(' Body: ${options.data}');
}
}
handler.next(options);
}
@override
void onResponse(
Response response,
ResponseInterceptorHandler handler,
) {
// 디버그 모드에서 응답 로깅
if (kDebugMode) {
print('✅ API Response: ${response.statusCode} ${response.requestOptions.path}');
if (response.data != null) {
print(' Data: ${response.data}');
}
}
handler.next(response);
}
@override
void onError(
DioException err,
ErrorInterceptorHandler handler,
) async {
// 디버그 모드에서 에러 로깅
if (kDebugMode) {
print('❌ API Error: ${err.requestOptions.path}');
print(' Error Type: ${err.type}');
print(' Message: ${err.message}');
if (err.response != null) {
print(' Status: ${err.response?.statusCode}');
print(' Data: ${err.response?.data}');
}
}
// 401 Unauthorized 처리
if (err.response?.statusCode == 401) {
// 토큰 갱신 시도
final refreshToken = await _storage.getRefreshToken();
if (refreshToken != null) {
try {
// 토큰 갱신 로직 (필요시 구현)
// final newToken = await _refreshToken(refreshToken);
// await _storage.saveTokens(newToken);
// 원래 요청 재시도
// final options = err.requestOptions;
// options.headers['Authorization'] = 'Bearer $newToken';
// final response = await dio.fetch(options);
// return handler.resolve(response);
} catch (e) {
// 토큰 갱신 실패 시 로그아웃 처리
await _storage.clearAll();
}
} else {
// 리프레시 토큰이 없으면 로그아웃 처리
await _storage.clearAll();
}
}
handler.next(err);
}
}

View File

@@ -88,13 +88,13 @@ class LoggingInterceptor extends Interceptor {
debugPrint('$line');
});
debugPrint('║ ... (${lines.length - 50} lines omitted) ...');
lines.skip(lines.length - 25).forEach((line) {
for (final line in lines.skip(lines.length - 25)) {
debugPrint('$line');
});
}
} else {
lines.forEach((line) {
for (final line in lines) {
debugPrint('$line');
});
}
}
} catch (e) {
debugPrint('${response.data}');

View File

@@ -4,11 +4,37 @@ import 'package:superport/core/errors/exceptions.dart';
import 'package:superport/data/datasources/remote/api_client.dart';
import 'package:superport/data/models/user/user_dto.dart';
@lazySingleton
class UserRemoteDataSource {
abstract class UserRemoteDataSource {
Future<UserListDto> getUsers({
int page = 1,
int perPage = 20,
bool? isActive,
int? companyId,
String? role,
});
Future<UserDto> getUser(int id);
Future<UserDto> createUser(CreateUserRequest request);
Future<UserDto> updateUser(int id, UpdateUserRequest request);
Future<void> deleteUser(int id);
Future<UserDto> changeUserStatus(int id, ChangeStatusRequest request);
Future<void> changePassword(int id, ChangePasswordRequest request);
Future<bool> checkDuplicateUsername(String username);
Future<UserListDto> searchUsers({
required String query,
int? companyId,
String? status,
String? permissionLevel,
int page = 1,
int perPage = 20,
});
}
@LazySingleton(as: UserRemoteDataSource)
class UserRemoteDataSourceImpl implements UserRemoteDataSource {
final ApiClient _apiClient;
UserRemoteDataSource() : _apiClient = ApiClient();
UserRemoteDataSourceImpl(this._apiClient);
/// 사용자 목록 조회
Future<UserListDto> getUsers({
@@ -40,7 +66,7 @@ class UserRemoteDataSource {
// role이 null인 경우 기본값 설정
final users = data.map((json) {
if (json['role'] == null) {
json['role'] = 'staff'; // 기본값
json['role'] = 'member'; // 기본값
}
return UserDto.fromJson(json);
}).toList();

View File

@@ -23,6 +23,11 @@ abstract class WarehouseLocationRemoteDataSource {
int page = 1,
int perPage = 20,
});
// Repository에서 사용하는 추가 메서드들
Future<void> updateWarehouseLocationStatus(int id, bool isActive);
Future<bool> checkWarehouseHasEquipment(int id);
Future<bool> checkDuplicateWarehouseName(String name);
}
@LazySingleton(as: WarehouseLocationRemoteDataSource)
@@ -266,6 +271,56 @@ class WarehouseLocationRemoteDataSourceImpl implements WarehouseLocationRemoteDa
}
}
// Repository에서 사용하는 추가 메서드들 구현
@override
Future<void> updateWarehouseLocationStatus(int id, bool isActive) async {
try {
await _apiClient.patch(
'${ApiEndpoints.warehouseLocations}/$id/status',
data: {'is_active': isActive},
);
} catch (e) {
throw _handleError(e);
}
}
@override
Future<bool> checkWarehouseHasEquipment(int id) async {
try {
final response = await _apiClient.get(
'${ApiEndpoints.warehouseLocations}/$id/has-equipment',
);
if (response.data != null && response.data['success'] == true) {
return response.data['data']['has_equipment'] ?? false;
}
return false;
} catch (e) {
// 오류 시 기본값 false 반환
debugPrint('📦 창고 장비 보유 여부 확인 중 오류: $e');
return false;
}
}
@override
Future<bool> checkDuplicateWarehouseName(String name) async {
try {
final response = await _apiClient.get(
'${ApiEndpoints.warehouseLocations}/check-duplicate',
queryParameters: {'name': name},
);
if (response.data != null && response.data['success'] == true) {
return response.data['data']['is_duplicate'] ?? false;
}
return false;
} catch (e) {
// 오류 시 기본값 false 반환 (중복이 아니라고 가정)
debugPrint('📦 중복 창고명 확인 중 오류: $e');
return false;
}
}
Exception _handleError(dynamic error) {
if (error is ApiException) {
return error;

View File

@@ -8,8 +8,8 @@ enum UserRole {
admin,
@JsonValue('manager')
manager,
@JsonValue('staff')
staff,
@JsonValue('member')
member,
}
@freezed
@@ -17,13 +17,16 @@ class UserDto with _$UserDto {
const factory UserDto({
required int id,
required String username,
required String email,
required String name,
String? email,
String? phone,
required String role,
@JsonKey(name: 'company_id') int? companyId,
@JsonKey(name: 'company_name') String? companyName,
@JsonKey(name: 'branch_id') int? branchId,
@JsonKey(name: 'branch_name') String? branchName,
@JsonKey(name: 'is_active') required bool isActive,
@JsonKey(name: 'last_login_at') DateTime? lastLoginAt,
@JsonKey(name: 'created_at') required DateTime createdAt,
@JsonKey(name: 'updated_at') required DateTime updatedAt,
}) = _UserDto;
@@ -36,7 +39,7 @@ class UserDto with _$UserDto {
class CreateUserRequest with _$CreateUserRequest {
const factory CreateUserRequest({
required String username,
required String email,
String? email,
required String password,
required String name,
String? phone,
@@ -59,6 +62,7 @@ class UpdateUserRequest with _$UpdateUserRequest {
String? role,
@JsonKey(name: 'company_id') int? companyId,
@JsonKey(name: 'branch_id') int? branchId,
@JsonKey(name: 'is_active') bool? isActive,
}) = _UpdateUserRequest;
factory UpdateUserRequest.fromJson(Map<String, dynamic> json) =>
@@ -88,6 +92,8 @@ class ChangePasswordRequest with _$ChangePasswordRequest {
@freezed
class UserListDto with _$UserListDto {
const UserListDto._();
const factory UserListDto({
required List<UserDto> users,
required int total,
@@ -96,7 +102,35 @@ class UserListDto with _$UserListDto {
@JsonKey(name: 'total_pages') required int totalPages,
}) = _UserListDto;
// 페이지네이션 응답과 호환성을 위한 getter들
List<UserDto> get items => users;
int get size => perPage;
int get totalElements => total;
bool get first => page <= 1;
bool get last => page >= totalPages;
factory UserListDto.fromJson(Map<String, dynamic> json) =>
_$UserListDtoFromJson(json);
}
@freezed
class UserDetailDto with _$UserDetailDto {
const factory UserDetailDto({
required UserDto user,
}) = _UserDetailDto;
factory UserDetailDto.fromJson(Map<String, dynamic> json) =>
_$UserDetailDtoFromJson(json);
}
@freezed
class UserResponse with _$UserResponse {
const factory UserResponse({
required UserDto user,
String? message,
}) = _UserResponse;
factory UserResponse.fromJson(Map<String, dynamic> json) =>
_$UserResponseFromJson(json);
}

View File

@@ -22,16 +22,22 @@ UserDto _$UserDtoFromJson(Map<String, dynamic> json) {
mixin _$UserDto {
int get id => throw _privateConstructorUsedError;
String get username => throw _privateConstructorUsedError;
String get email => throw _privateConstructorUsedError;
String get name => throw _privateConstructorUsedError;
String? get email => throw _privateConstructorUsedError;
String? get phone => throw _privateConstructorUsedError;
String get role => throw _privateConstructorUsedError;
@JsonKey(name: 'company_id')
int? get companyId => throw _privateConstructorUsedError;
@JsonKey(name: 'company_name')
String? get companyName => throw _privateConstructorUsedError;
@JsonKey(name: 'branch_id')
int? get branchId => throw _privateConstructorUsedError;
@JsonKey(name: 'branch_name')
String? get branchName => throw _privateConstructorUsedError;
@JsonKey(name: 'is_active')
bool get isActive => throw _privateConstructorUsedError;
@JsonKey(name: 'last_login_at')
DateTime? get lastLoginAt => throw _privateConstructorUsedError;
@JsonKey(name: 'created_at')
DateTime get createdAt => throw _privateConstructorUsedError;
@JsonKey(name: 'updated_at')
@@ -54,13 +60,16 @@ abstract class $UserDtoCopyWith<$Res> {
$Res call(
{int id,
String username,
String email,
String name,
String? email,
String? phone,
String role,
@JsonKey(name: 'company_id') int? companyId,
@JsonKey(name: 'company_name') String? companyName,
@JsonKey(name: 'branch_id') int? branchId,
@JsonKey(name: 'branch_name') String? branchName,
@JsonKey(name: 'is_active') bool isActive,
@JsonKey(name: 'last_login_at') DateTime? lastLoginAt,
@JsonKey(name: 'created_at') DateTime createdAt,
@JsonKey(name: 'updated_at') DateTime updatedAt});
}
@@ -82,13 +91,16 @@ class _$UserDtoCopyWithImpl<$Res, $Val extends UserDto>
$Res call({
Object? id = null,
Object? username = null,
Object? email = null,
Object? name = null,
Object? email = freezed,
Object? phone = freezed,
Object? role = null,
Object? companyId = freezed,
Object? companyName = freezed,
Object? branchId = freezed,
Object? branchName = freezed,
Object? isActive = null,
Object? lastLoginAt = freezed,
Object? createdAt = null,
Object? updatedAt = null,
}) {
@@ -101,14 +113,14 @@ class _$UserDtoCopyWithImpl<$Res, $Val extends UserDto>
? _value.username
: username // ignore: cast_nullable_to_non_nullable
as String,
email: null == email
? _value.email
: email // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
email: freezed == email
? _value.email
: email // ignore: cast_nullable_to_non_nullable
as String?,
phone: freezed == phone
? _value.phone
: phone // ignore: cast_nullable_to_non_nullable
@@ -121,14 +133,26 @@ class _$UserDtoCopyWithImpl<$Res, $Val extends UserDto>
? _value.companyId
: companyId // ignore: cast_nullable_to_non_nullable
as int?,
companyName: freezed == companyName
? _value.companyName
: companyName // ignore: cast_nullable_to_non_nullable
as String?,
branchId: freezed == branchId
? _value.branchId
: branchId // ignore: cast_nullable_to_non_nullable
as int?,
branchName: freezed == branchName
? _value.branchName
: branchName // ignore: cast_nullable_to_non_nullable
as String?,
isActive: null == isActive
? _value.isActive
: isActive // ignore: cast_nullable_to_non_nullable
as bool,
lastLoginAt: freezed == lastLoginAt
? _value.lastLoginAt
: lastLoginAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
@@ -151,13 +175,16 @@ abstract class _$$UserDtoImplCopyWith<$Res> implements $UserDtoCopyWith<$Res> {
$Res call(
{int id,
String username,
String email,
String name,
String? email,
String? phone,
String role,
@JsonKey(name: 'company_id') int? companyId,
@JsonKey(name: 'company_name') String? companyName,
@JsonKey(name: 'branch_id') int? branchId,
@JsonKey(name: 'branch_name') String? branchName,
@JsonKey(name: 'is_active') bool isActive,
@JsonKey(name: 'last_login_at') DateTime? lastLoginAt,
@JsonKey(name: 'created_at') DateTime createdAt,
@JsonKey(name: 'updated_at') DateTime updatedAt});
}
@@ -177,13 +204,16 @@ class __$$UserDtoImplCopyWithImpl<$Res>
$Res call({
Object? id = null,
Object? username = null,
Object? email = null,
Object? name = null,
Object? email = freezed,
Object? phone = freezed,
Object? role = null,
Object? companyId = freezed,
Object? companyName = freezed,
Object? branchId = freezed,
Object? branchName = freezed,
Object? isActive = null,
Object? lastLoginAt = freezed,
Object? createdAt = null,
Object? updatedAt = null,
}) {
@@ -196,14 +226,14 @@ class __$$UserDtoImplCopyWithImpl<$Res>
? _value.username
: username // ignore: cast_nullable_to_non_nullable
as String,
email: null == email
? _value.email
: email // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
email: freezed == email
? _value.email
: email // ignore: cast_nullable_to_non_nullable
as String?,
phone: freezed == phone
? _value.phone
: phone // ignore: cast_nullable_to_non_nullable
@@ -216,14 +246,26 @@ class __$$UserDtoImplCopyWithImpl<$Res>
? _value.companyId
: companyId // ignore: cast_nullable_to_non_nullable
as int?,
companyName: freezed == companyName
? _value.companyName
: companyName // ignore: cast_nullable_to_non_nullable
as String?,
branchId: freezed == branchId
? _value.branchId
: branchId // ignore: cast_nullable_to_non_nullable
as int?,
branchName: freezed == branchName
? _value.branchName
: branchName // ignore: cast_nullable_to_non_nullable
as String?,
isActive: null == isActive
? _value.isActive
: isActive // ignore: cast_nullable_to_non_nullable
as bool,
lastLoginAt: freezed == lastLoginAt
? _value.lastLoginAt
: lastLoginAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
@@ -242,13 +284,16 @@ class _$UserDtoImpl implements _UserDto {
const _$UserDtoImpl(
{required this.id,
required this.username,
required this.email,
required this.name,
this.email,
this.phone,
required this.role,
@JsonKey(name: 'company_id') this.companyId,
@JsonKey(name: 'company_name') this.companyName,
@JsonKey(name: 'branch_id') this.branchId,
@JsonKey(name: 'branch_name') this.branchName,
@JsonKey(name: 'is_active') required this.isActive,
@JsonKey(name: 'last_login_at') this.lastLoginAt,
@JsonKey(name: 'created_at') required this.createdAt,
@JsonKey(name: 'updated_at') required this.updatedAt});
@@ -260,10 +305,10 @@ class _$UserDtoImpl implements _UserDto {
@override
final String username;
@override
final String email;
@override
final String name;
@override
final String? email;
@override
final String? phone;
@override
final String role;
@@ -271,12 +316,21 @@ class _$UserDtoImpl implements _UserDto {
@JsonKey(name: 'company_id')
final int? companyId;
@override
@JsonKey(name: 'company_name')
final String? companyName;
@override
@JsonKey(name: 'branch_id')
final int? branchId;
@override
@JsonKey(name: 'branch_name')
final String? branchName;
@override
@JsonKey(name: 'is_active')
final bool isActive;
@override
@JsonKey(name: 'last_login_at')
final DateTime? lastLoginAt;
@override
@JsonKey(name: 'created_at')
final DateTime createdAt;
@override
@@ -285,7 +339,7 @@ class _$UserDtoImpl implements _UserDto {
@override
String toString() {
return 'UserDto(id: $id, username: $username, email: $email, name: $name, phone: $phone, role: $role, companyId: $companyId, branchId: $branchId, isActive: $isActive, createdAt: $createdAt, updatedAt: $updatedAt)';
return 'UserDto(id: $id, username: $username, name: $name, email: $email, phone: $phone, role: $role, companyId: $companyId, companyName: $companyName, branchId: $branchId, branchName: $branchName, isActive: $isActive, lastLoginAt: $lastLoginAt, createdAt: $createdAt, updatedAt: $updatedAt)';
}
@override
@@ -296,16 +350,22 @@ class _$UserDtoImpl implements _UserDto {
(identical(other.id, id) || other.id == id) &&
(identical(other.username, username) ||
other.username == username) &&
(identical(other.email, email) || other.email == email) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.email, email) || other.email == email) &&
(identical(other.phone, phone) || other.phone == phone) &&
(identical(other.role, role) || other.role == role) &&
(identical(other.companyId, companyId) ||
other.companyId == companyId) &&
(identical(other.companyName, companyName) ||
other.companyName == companyName) &&
(identical(other.branchId, branchId) ||
other.branchId == branchId) &&
(identical(other.branchName, branchName) ||
other.branchName == branchName) &&
(identical(other.isActive, isActive) ||
other.isActive == isActive) &&
(identical(other.lastLoginAt, lastLoginAt) ||
other.lastLoginAt == lastLoginAt) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
@@ -314,8 +374,22 @@ class _$UserDtoImpl implements _UserDto {
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, id, username, email, name, phone,
role, companyId, branchId, isActive, createdAt, updatedAt);
int get hashCode => Object.hash(
runtimeType,
id,
username,
name,
email,
phone,
role,
companyId,
companyName,
branchId,
branchName,
isActive,
lastLoginAt,
createdAt,
updatedAt);
/// Create a copy of UserDto
/// with the given fields replaced by the non-null parameter values.
@@ -337,13 +411,16 @@ abstract class _UserDto implements UserDto {
const factory _UserDto(
{required final int id,
required final String username,
required final String email,
required final String name,
final String? email,
final String? phone,
required final String role,
@JsonKey(name: 'company_id') final int? companyId,
@JsonKey(name: 'company_name') final String? companyName,
@JsonKey(name: 'branch_id') final int? branchId,
@JsonKey(name: 'branch_name') final String? branchName,
@JsonKey(name: 'is_active') required final bool isActive,
@JsonKey(name: 'last_login_at') final DateTime? lastLoginAt,
@JsonKey(name: 'created_at') required final DateTime createdAt,
@JsonKey(name: 'updated_at') required final DateTime updatedAt}) =
_$UserDtoImpl;
@@ -355,10 +432,10 @@ abstract class _UserDto implements UserDto {
@override
String get username;
@override
String get email;
@override
String get name;
@override
String? get email;
@override
String? get phone;
@override
String get role;
@@ -366,12 +443,21 @@ abstract class _UserDto implements UserDto {
@JsonKey(name: 'company_id')
int? get companyId;
@override
@JsonKey(name: 'company_name')
String? get companyName;
@override
@JsonKey(name: 'branch_id')
int? get branchId;
@override
@JsonKey(name: 'branch_name')
String? get branchName;
@override
@JsonKey(name: 'is_active')
bool get isActive;
@override
@JsonKey(name: 'last_login_at')
DateTime? get lastLoginAt;
@override
@JsonKey(name: 'created_at')
DateTime get createdAt;
@override
@@ -393,7 +479,7 @@ CreateUserRequest _$CreateUserRequestFromJson(Map<String, dynamic> json) {
/// @nodoc
mixin _$CreateUserRequest {
String get username => throw _privateConstructorUsedError;
String get email => throw _privateConstructorUsedError;
String? get email => throw _privateConstructorUsedError;
String get password => throw _privateConstructorUsedError;
String get name => throw _privateConstructorUsedError;
String? get phone => throw _privateConstructorUsedError;
@@ -421,7 +507,7 @@ abstract class $CreateUserRequestCopyWith<$Res> {
@useResult
$Res call(
{String username,
String email,
String? email,
String password,
String name,
String? phone,
@@ -446,7 +532,7 @@ class _$CreateUserRequestCopyWithImpl<$Res, $Val extends CreateUserRequest>
@override
$Res call({
Object? username = null,
Object? email = null,
Object? email = freezed,
Object? password = null,
Object? name = null,
Object? phone = freezed,
@@ -459,10 +545,10 @@ class _$CreateUserRequestCopyWithImpl<$Res, $Val extends CreateUserRequest>
? _value.username
: username // ignore: cast_nullable_to_non_nullable
as String,
email: null == email
email: freezed == email
? _value.email
: email // ignore: cast_nullable_to_non_nullable
as String,
as String?,
password: null == password
? _value.password
: password // ignore: cast_nullable_to_non_nullable
@@ -501,7 +587,7 @@ abstract class _$$CreateUserRequestImplCopyWith<$Res>
@useResult
$Res call(
{String username,
String email,
String? email,
String password,
String name,
String? phone,
@@ -524,7 +610,7 @@ class __$$CreateUserRequestImplCopyWithImpl<$Res>
@override
$Res call({
Object? username = null,
Object? email = null,
Object? email = freezed,
Object? password = null,
Object? name = null,
Object? phone = freezed,
@@ -537,10 +623,10 @@ class __$$CreateUserRequestImplCopyWithImpl<$Res>
? _value.username
: username // ignore: cast_nullable_to_non_nullable
as String,
email: null == email
email: freezed == email
? _value.email
: email // ignore: cast_nullable_to_non_nullable
as String,
as String?,
password: null == password
? _value.password
: password // ignore: cast_nullable_to_non_nullable
@@ -574,7 +660,7 @@ class __$$CreateUserRequestImplCopyWithImpl<$Res>
class _$CreateUserRequestImpl implements _CreateUserRequest {
const _$CreateUserRequestImpl(
{required this.username,
required this.email,
this.email,
required this.password,
required this.name,
this.phone,
@@ -588,7 +674,7 @@ class _$CreateUserRequestImpl implements _CreateUserRequest {
@override
final String username;
@override
final String email;
final String? email;
@override
final String password;
@override
@@ -653,7 +739,7 @@ class _$CreateUserRequestImpl implements _CreateUserRequest {
abstract class _CreateUserRequest implements CreateUserRequest {
const factory _CreateUserRequest(
{required final String username,
required final String email,
final String? email,
required final String password,
required final String name,
final String? phone,
@@ -668,7 +754,7 @@ abstract class _CreateUserRequest implements CreateUserRequest {
@override
String get username;
@override
String get email;
String? get email;
@override
String get password;
@override
@@ -707,6 +793,8 @@ mixin _$UpdateUserRequest {
int? get companyId => throw _privateConstructorUsedError;
@JsonKey(name: 'branch_id')
int? get branchId => throw _privateConstructorUsedError;
@JsonKey(name: 'is_active')
bool? get isActive => throw _privateConstructorUsedError;
/// Serializes this UpdateUserRequest to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -731,7 +819,8 @@ abstract class $UpdateUserRequestCopyWith<$Res> {
String? phone,
String? role,
@JsonKey(name: 'company_id') int? companyId,
@JsonKey(name: 'branch_id') int? branchId});
@JsonKey(name: 'branch_id') int? branchId,
@JsonKey(name: 'is_active') bool? isActive});
}
/// @nodoc
@@ -756,6 +845,7 @@ class _$UpdateUserRequestCopyWithImpl<$Res, $Val extends UpdateUserRequest>
Object? role = freezed,
Object? companyId = freezed,
Object? branchId = freezed,
Object? isActive = freezed,
}) {
return _then(_value.copyWith(
name: freezed == name
@@ -786,6 +876,10 @@ class _$UpdateUserRequestCopyWithImpl<$Res, $Val extends UpdateUserRequest>
? _value.branchId
: branchId // ignore: cast_nullable_to_non_nullable
as int?,
isActive: freezed == isActive
? _value.isActive
: isActive // ignore: cast_nullable_to_non_nullable
as bool?,
) as $Val);
}
}
@@ -805,7 +899,8 @@ abstract class _$$UpdateUserRequestImplCopyWith<$Res>
String? phone,
String? role,
@JsonKey(name: 'company_id') int? companyId,
@JsonKey(name: 'branch_id') int? branchId});
@JsonKey(name: 'branch_id') int? branchId,
@JsonKey(name: 'is_active') bool? isActive});
}
/// @nodoc
@@ -828,6 +923,7 @@ class __$$UpdateUserRequestImplCopyWithImpl<$Res>
Object? role = freezed,
Object? companyId = freezed,
Object? branchId = freezed,
Object? isActive = freezed,
}) {
return _then(_$UpdateUserRequestImpl(
name: freezed == name
@@ -858,6 +954,10 @@ class __$$UpdateUserRequestImplCopyWithImpl<$Res>
? _value.branchId
: branchId // ignore: cast_nullable_to_non_nullable
as int?,
isActive: freezed == isActive
? _value.isActive
: isActive // ignore: cast_nullable_to_non_nullable
as bool?,
));
}
}
@@ -872,7 +972,8 @@ class _$UpdateUserRequestImpl implements _UpdateUserRequest {
this.phone,
this.role,
@JsonKey(name: 'company_id') this.companyId,
@JsonKey(name: 'branch_id') this.branchId});
@JsonKey(name: 'branch_id') this.branchId,
@JsonKey(name: 'is_active') this.isActive});
factory _$UpdateUserRequestImpl.fromJson(Map<String, dynamic> json) =>
_$$UpdateUserRequestImplFromJson(json);
@@ -893,10 +994,13 @@ class _$UpdateUserRequestImpl implements _UpdateUserRequest {
@override
@JsonKey(name: 'branch_id')
final int? branchId;
@override
@JsonKey(name: 'is_active')
final bool? isActive;
@override
String toString() {
return 'UpdateUserRequest(name: $name, email: $email, password: $password, phone: $phone, role: $role, companyId: $companyId, branchId: $branchId)';
return 'UpdateUserRequest(name: $name, email: $email, password: $password, phone: $phone, role: $role, companyId: $companyId, branchId: $branchId, isActive: $isActive)';
}
@override
@@ -913,13 +1017,15 @@ class _$UpdateUserRequestImpl implements _UpdateUserRequest {
(identical(other.companyId, companyId) ||
other.companyId == companyId) &&
(identical(other.branchId, branchId) ||
other.branchId == branchId));
other.branchId == branchId) &&
(identical(other.isActive, isActive) ||
other.isActive == isActive));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType, name, email, password, phone, role, companyId, branchId);
int get hashCode => Object.hash(runtimeType, name, email, password, phone,
role, companyId, branchId, isActive);
/// Create a copy of UpdateUserRequest
/// with the given fields replaced by the non-null parameter values.
@@ -946,7 +1052,8 @@ abstract class _UpdateUserRequest implements UpdateUserRequest {
final String? phone,
final String? role,
@JsonKey(name: 'company_id') final int? companyId,
@JsonKey(name: 'branch_id') final int? branchId}) =
@JsonKey(name: 'branch_id') final int? branchId,
@JsonKey(name: 'is_active') final bool? isActive}) =
_$UpdateUserRequestImpl;
factory _UpdateUserRequest.fromJson(Map<String, dynamic> json) =
@@ -968,6 +1075,9 @@ abstract class _UpdateUserRequest implements UpdateUserRequest {
@override
@JsonKey(name: 'branch_id')
int? get branchId;
@override
@JsonKey(name: 'is_active')
bool? get isActive;
/// Create a copy of UpdateUserRequest
/// with the given fields replaced by the non-null parameter values.
@@ -1467,14 +1577,15 @@ class __$$UserListDtoImplCopyWithImpl<$Res>
/// @nodoc
@JsonSerializable()
class _$UserListDtoImpl implements _UserListDto {
class _$UserListDtoImpl extends _UserListDto {
const _$UserListDtoImpl(
{required final List<UserDto> users,
required this.total,
required this.page,
@JsonKey(name: 'per_page') required this.perPage,
@JsonKey(name: 'total_pages') required this.totalPages})
: _users = users;
: _users = users,
super._();
factory _$UserListDtoImpl.fromJson(Map<String, dynamic> json) =>
_$$UserListDtoImplFromJson(json);
@@ -1542,7 +1653,7 @@ class _$UserListDtoImpl implements _UserListDto {
}
}
abstract class _UserListDto implements UserListDto {
abstract class _UserListDto extends UserListDto {
const factory _UserListDto(
{required final List<UserDto> users,
required final int total,
@@ -1550,6 +1661,7 @@ abstract class _UserListDto implements UserListDto {
@JsonKey(name: 'per_page') required final int perPage,
@JsonKey(name: 'total_pages') required final int totalPages}) =
_$UserListDtoImpl;
const _UserListDto._() : super._();
factory _UserListDto.fromJson(Map<String, dynamic> json) =
_$UserListDtoImpl.fromJson;
@@ -1574,3 +1686,350 @@ abstract class _UserListDto implements UserListDto {
_$$UserListDtoImplCopyWith<_$UserListDtoImpl> get copyWith =>
throw _privateConstructorUsedError;
}
UserDetailDto _$UserDetailDtoFromJson(Map<String, dynamic> json) {
return _UserDetailDto.fromJson(json);
}
/// @nodoc
mixin _$UserDetailDto {
UserDto get user => throw _privateConstructorUsedError;
/// Serializes this UserDetailDto to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of UserDetailDto
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$UserDetailDtoCopyWith<UserDetailDto> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $UserDetailDtoCopyWith<$Res> {
factory $UserDetailDtoCopyWith(
UserDetailDto value, $Res Function(UserDetailDto) then) =
_$UserDetailDtoCopyWithImpl<$Res, UserDetailDto>;
@useResult
$Res call({UserDto user});
$UserDtoCopyWith<$Res> get user;
}
/// @nodoc
class _$UserDetailDtoCopyWithImpl<$Res, $Val extends UserDetailDto>
implements $UserDetailDtoCopyWith<$Res> {
_$UserDetailDtoCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of UserDetailDto
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? user = null,
}) {
return _then(_value.copyWith(
user: null == user
? _value.user
: user // ignore: cast_nullable_to_non_nullable
as UserDto,
) as $Val);
}
/// Create a copy of UserDetailDto
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$UserDtoCopyWith<$Res> get user {
return $UserDtoCopyWith<$Res>(_value.user, (value) {
return _then(_value.copyWith(user: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$UserDetailDtoImplCopyWith<$Res>
implements $UserDetailDtoCopyWith<$Res> {
factory _$$UserDetailDtoImplCopyWith(
_$UserDetailDtoImpl value, $Res Function(_$UserDetailDtoImpl) then) =
__$$UserDetailDtoImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({UserDto user});
@override
$UserDtoCopyWith<$Res> get user;
}
/// @nodoc
class __$$UserDetailDtoImplCopyWithImpl<$Res>
extends _$UserDetailDtoCopyWithImpl<$Res, _$UserDetailDtoImpl>
implements _$$UserDetailDtoImplCopyWith<$Res> {
__$$UserDetailDtoImplCopyWithImpl(
_$UserDetailDtoImpl _value, $Res Function(_$UserDetailDtoImpl) _then)
: super(_value, _then);
/// Create a copy of UserDetailDto
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? user = null,
}) {
return _then(_$UserDetailDtoImpl(
user: null == user
? _value.user
: user // ignore: cast_nullable_to_non_nullable
as UserDto,
));
}
}
/// @nodoc
@JsonSerializable()
class _$UserDetailDtoImpl implements _UserDetailDto {
const _$UserDetailDtoImpl({required this.user});
factory _$UserDetailDtoImpl.fromJson(Map<String, dynamic> json) =>
_$$UserDetailDtoImplFromJson(json);
@override
final UserDto user;
@override
String toString() {
return 'UserDetailDto(user: $user)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$UserDetailDtoImpl &&
(identical(other.user, user) || other.user == user));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, user);
/// Create a copy of UserDetailDto
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$UserDetailDtoImplCopyWith<_$UserDetailDtoImpl> get copyWith =>
__$$UserDetailDtoImplCopyWithImpl<_$UserDetailDtoImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$UserDetailDtoImplToJson(
this,
);
}
}
abstract class _UserDetailDto implements UserDetailDto {
const factory _UserDetailDto({required final UserDto user}) =
_$UserDetailDtoImpl;
factory _UserDetailDto.fromJson(Map<String, dynamic> json) =
_$UserDetailDtoImpl.fromJson;
@override
UserDto get user;
/// Create a copy of UserDetailDto
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$UserDetailDtoImplCopyWith<_$UserDetailDtoImpl> get copyWith =>
throw _privateConstructorUsedError;
}
UserResponse _$UserResponseFromJson(Map<String, dynamic> json) {
return _UserResponse.fromJson(json);
}
/// @nodoc
mixin _$UserResponse {
UserDto get user => throw _privateConstructorUsedError;
String? get message => throw _privateConstructorUsedError;
/// Serializes this UserResponse to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of UserResponse
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$UserResponseCopyWith<UserResponse> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $UserResponseCopyWith<$Res> {
factory $UserResponseCopyWith(
UserResponse value, $Res Function(UserResponse) then) =
_$UserResponseCopyWithImpl<$Res, UserResponse>;
@useResult
$Res call({UserDto user, String? message});
$UserDtoCopyWith<$Res> get user;
}
/// @nodoc
class _$UserResponseCopyWithImpl<$Res, $Val extends UserResponse>
implements $UserResponseCopyWith<$Res> {
_$UserResponseCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of UserResponse
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? user = null,
Object? message = freezed,
}) {
return _then(_value.copyWith(
user: null == user
? _value.user
: user // ignore: cast_nullable_to_non_nullable
as UserDto,
message: freezed == message
? _value.message
: message // ignore: cast_nullable_to_non_nullable
as String?,
) as $Val);
}
/// Create a copy of UserResponse
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$UserDtoCopyWith<$Res> get user {
return $UserDtoCopyWith<$Res>(_value.user, (value) {
return _then(_value.copyWith(user: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$UserResponseImplCopyWith<$Res>
implements $UserResponseCopyWith<$Res> {
factory _$$UserResponseImplCopyWith(
_$UserResponseImpl value, $Res Function(_$UserResponseImpl) then) =
__$$UserResponseImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({UserDto user, String? message});
@override
$UserDtoCopyWith<$Res> get user;
}
/// @nodoc
class __$$UserResponseImplCopyWithImpl<$Res>
extends _$UserResponseCopyWithImpl<$Res, _$UserResponseImpl>
implements _$$UserResponseImplCopyWith<$Res> {
__$$UserResponseImplCopyWithImpl(
_$UserResponseImpl _value, $Res Function(_$UserResponseImpl) _then)
: super(_value, _then);
/// Create a copy of UserResponse
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? user = null,
Object? message = freezed,
}) {
return _then(_$UserResponseImpl(
user: null == user
? _value.user
: user // ignore: cast_nullable_to_non_nullable
as UserDto,
message: freezed == message
? _value.message
: message // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$UserResponseImpl implements _UserResponse {
const _$UserResponseImpl({required this.user, this.message});
factory _$UserResponseImpl.fromJson(Map<String, dynamic> json) =>
_$$UserResponseImplFromJson(json);
@override
final UserDto user;
@override
final String? message;
@override
String toString() {
return 'UserResponse(user: $user, message: $message)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$UserResponseImpl &&
(identical(other.user, user) || other.user == user) &&
(identical(other.message, message) || other.message == message));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, user, message);
/// Create a copy of UserResponse
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$UserResponseImplCopyWith<_$UserResponseImpl> get copyWith =>
__$$UserResponseImplCopyWithImpl<_$UserResponseImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$UserResponseImplToJson(
this,
);
}
}
abstract class _UserResponse implements UserResponse {
const factory _UserResponse(
{required final UserDto user,
final String? message}) = _$UserResponseImpl;
factory _UserResponse.fromJson(Map<String, dynamic> json) =
_$UserResponseImpl.fromJson;
@override
UserDto get user;
@override
String? get message;
/// Create a copy of UserResponse
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$UserResponseImplCopyWith<_$UserResponseImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -10,13 +10,18 @@ _$UserDtoImpl _$$UserDtoImplFromJson(Map<String, dynamic> json) =>
_$UserDtoImpl(
id: (json['id'] as num).toInt(),
username: json['username'] as String,
email: json['email'] as String,
name: json['name'] as String,
email: json['email'] as String?,
phone: json['phone'] as String?,
role: json['role'] as String,
companyId: (json['company_id'] as num?)?.toInt(),
companyName: json['company_name'] as String?,
branchId: (json['branch_id'] as num?)?.toInt(),
branchName: json['branch_name'] as String?,
isActive: json['is_active'] as bool,
lastLoginAt: json['last_login_at'] == null
? null
: DateTime.parse(json['last_login_at'] as String),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
);
@@ -25,13 +30,16 @@ Map<String, dynamic> _$$UserDtoImplToJson(_$UserDtoImpl instance) =>
<String, dynamic>{
'id': instance.id,
'username': instance.username,
'email': instance.email,
'name': instance.name,
'email': instance.email,
'phone': instance.phone,
'role': instance.role,
'company_id': instance.companyId,
'company_name': instance.companyName,
'branch_id': instance.branchId,
'branch_name': instance.branchName,
'is_active': instance.isActive,
'last_login_at': instance.lastLoginAt?.toIso8601String(),
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
};
@@ -40,7 +48,7 @@ _$CreateUserRequestImpl _$$CreateUserRequestImplFromJson(
Map<String, dynamic> json) =>
_$CreateUserRequestImpl(
username: json['username'] as String,
email: json['email'] as String,
email: json['email'] as String?,
password: json['password'] as String,
name: json['name'] as String,
phone: json['phone'] as String?,
@@ -72,6 +80,7 @@ _$UpdateUserRequestImpl _$$UpdateUserRequestImplFromJson(
role: json['role'] as String?,
companyId: (json['company_id'] as num?)?.toInt(),
branchId: (json['branch_id'] as num?)?.toInt(),
isActive: json['is_active'] as bool?,
);
Map<String, dynamic> _$$UpdateUserRequestImplToJson(
@@ -84,6 +93,7 @@ Map<String, dynamic> _$$UpdateUserRequestImplToJson(
'role': instance.role,
'company_id': instance.companyId,
'branch_id': instance.branchId,
'is_active': instance.isActive,
};
_$ChangeStatusRequestImpl _$$ChangeStatusRequestImplFromJson(
@@ -131,3 +141,25 @@ Map<String, dynamic> _$$UserListDtoImplToJson(_$UserListDtoImpl instance) =>
'per_page': instance.perPage,
'total_pages': instance.totalPages,
};
_$UserDetailDtoImpl _$$UserDetailDtoImplFromJson(Map<String, dynamic> json) =>
_$UserDetailDtoImpl(
user: UserDto.fromJson(json['user'] as Map<String, dynamic>),
);
Map<String, dynamic> _$$UserDetailDtoImplToJson(_$UserDetailDtoImpl instance) =>
<String, dynamic>{
'user': instance.user,
};
_$UserResponseImpl _$$UserResponseImplFromJson(Map<String, dynamic> json) =>
_$UserResponseImpl(
user: UserDto.fromJson(json['user'] as Map<String, dynamic>),
message: json['message'] as String?,
);
Map<String, dynamic> _$$UserResponseImplToJson(_$UserResponseImpl instance) =>
<String, dynamic>{
'user': instance.user,
'message': instance.message,
};

View File

@@ -0,0 +1,214 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../core/errors/failures.dart';
import '../../domain/repositories/auth_repository.dart';
import '../datasources/remote/auth_remote_datasource.dart';
import '../models/auth/auth_user.dart';
import '../models/auth/login_request.dart';
import '../models/auth/login_response.dart';
import '../models/auth/logout_request.dart';
import '../models/auth/refresh_token_request.dart';
import '../models/auth/token_response.dart';
/// 인증 Repository 구현체
/// JWT 토큰 기반 인증 시스템을 관리하며 SharedPreferences를 사용해 토큰을 저장
@Injectable(as: AuthRepository)
class AuthRepositoryImpl implements AuthRepository {
final AuthRemoteDataSource remoteDataSource;
final SharedPreferences sharedPreferences;
// SharedPreferences 키 상수
static const String _keyAccessToken = 'access_token';
static const String _keyRefreshToken = 'refresh_token';
static const String _keyUserData = 'user_data';
AuthRepositoryImpl({
required this.remoteDataSource,
required this.sharedPreferences,
});
@override
Future<Either<Failure, LoginResponse>> login(LoginRequest loginRequest) async {
try {
final result = await remoteDataSource.login(loginRequest);
return result.fold(
(failure) => Left(failure),
(loginResponse) async {
// 로그인 성공 시 토큰과 사용자 정보를 로컬에 저장
await _saveTokens(loginResponse.accessToken, loginResponse.refreshToken);
await _saveUserData(loginResponse.user);
return Right(loginResponse);
},
);
} catch (e) {
return Left(ServerFailure(
message: '로그인 처리 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, void>> logout() async {
try {
// 로컬에 저장된 리프레시 토큰으로 로그아웃 요청 생성
final refreshToken = await _getRefreshToken();
if (refreshToken == null) {
// 토큰이 없으면 로컬 데이터만 삭제하고 성공 처리
await _clearLocalData();
return const Right(null);
}
final logoutRequest = LogoutRequest(refreshToken: refreshToken);
final result = await remoteDataSource.logout(logoutRequest);
return result.fold(
(failure) async {
// 서버 로그아웃 실패해도 로컬 데이터는 삭제
await _clearLocalData();
return Left(failure);
},
(_) async {
// 성공 시 로컬 데이터 삭제
await _clearLocalData();
return const Right(null);
},
);
} catch (e) {
// 오류 발생해도 로컬 데이터는 삭제
await _clearLocalData();
return Left(ServerFailure(
message: '로그아웃 처리 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, TokenResponse>> refreshToken(RefreshTokenRequest refreshRequest) async {
try {
final result = await remoteDataSource.refreshToken(refreshRequest);
return result.fold(
(failure) => Left(failure),
(tokenResponse) async {
// 새 토큰 저장
await _saveTokens(tokenResponse.accessToken, tokenResponse.refreshToken);
return Right(tokenResponse);
},
);
} catch (e) {
return Left(ServerFailure(
message: '토큰 갱신 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, AuthUser>> getCurrentUser() async {
try {
final userData = sharedPreferences.getString(_keyUserData);
if (userData == null) {
return const Left(AuthenticationFailure(
message: '저장된 사용자 정보가 없습니다.',
));
}
// JSON 문자열을 AuthUser 객체로 변환
final user = AuthUser.fromJson(
Map<String, dynamic>.from(
// JSON 디코딩 처리 필요 시 여기에 추가
{} // TODO: JSON 디코딩 로직 추가
)
);
return Right(user);
} catch (e) {
return Left(ServerFailure(
message: '사용자 정보 조회 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, bool>> isAuthenticated() async {
try {
final accessToken = await _getAccessToken();
final refreshToken = await _getRefreshToken();
// 액세스 토큰과 리프레시 토큰이 모두 있으면 인증된 것으로 간주
final isAuth = accessToken != null && refreshToken != null;
return Right(isAuth);
} catch (e) {
return Left(ServerFailure(
message: '인증 상태 확인 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, void>> changePassword(String currentPassword, String newPassword) async {
// TODO: 비밀번호 변경 API가 구현되면 추가
return const Left(ServerFailure(
message: '비밀번호 변경 기능은 아직 구현되지 않았습니다.',
));
}
@override
Future<Either<Failure, void>> requestPasswordReset(String email) async {
// TODO: 비밀번호 재설정 API가 구현되면 추가
return const Left(ServerFailure(
message: '비밀번호 재설정 기능은 아직 구현되지 않았습니다.',
));
}
@override
Future<Either<Failure, bool>> validateSession() async {
try {
final accessToken = await _getAccessToken();
if (accessToken == null) {
return const Right(false);
}
// TODO: 서버에서 세션 유효성 검증 API가 있으면 호출
// 현재는 토큰 존재 여부만 확인
return const Right(true);
} catch (e) {
return Left(ServerFailure(
message: '세션 유효성 검증 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
// Private 헬퍼 메서드들
/// 액세스 토큰과 리프레시 토큰을 로컬에 저장
Future<void> _saveTokens(String accessToken, String refreshToken) async {
await sharedPreferences.setString(_keyAccessToken, accessToken);
await sharedPreferences.setString(_keyRefreshToken, refreshToken);
}
/// 사용자 데이터를 로컬에 저장
Future<void> _saveUserData(AuthUser user) async {
// TODO: JSON 인코딩 로직 구현
await sharedPreferences.setString(_keyUserData, user.toJson().toString());
}
/// 액세스 토큰 조회
Future<String?> _getAccessToken() async {
return sharedPreferences.getString(_keyAccessToken);
}
/// 리프레시 토큰 조회
Future<String?> _getRefreshToken() async {
return sharedPreferences.getString(_keyRefreshToken);
}
/// 로컬 데이터 전체 삭제
Future<void> _clearLocalData() async {
await sharedPreferences.remove(_keyAccessToken);
await sharedPreferences.remove(_keyRefreshToken);
await sharedPreferences.remove(_keyUserData);
}
}

View File

@@ -0,0 +1,456 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../core/errors/failures.dart';
import '../../domain/repositories/company_repository.dart';
import '../../models/company_model.dart';
import '../../models/address_model.dart';
import '../datasources/remote/company_remote_datasource.dart';
import '../models/common/paginated_response.dart';
import '../models/company/company_dto.dart';
import '../models/company/branch_dto.dart';
import '../models/company/company_list_dto.dart';
/// 회사 관리 Repository 구현체
/// 회사 및 지점 정보 CRUD 작업을 처리하며 도메인 모델과 API DTO 간 변환을 담당
@Injectable(as: CompanyRepository)
class CompanyRepositoryImpl implements CompanyRepository {
final CompanyRemoteDataSource remoteDataSource;
CompanyRepositoryImpl({required this.remoteDataSource});
@override
Future<Either<Failure, PaginatedResponse<Company>>> getCompanies({
int? page,
int? limit,
String? search,
CompanyType? companyType,
String? sortBy,
String? sortOrder,
}) async {
try {
final result = await remoteDataSource.getCompanies(
page: page ?? 1,
perPage: limit ?? 20,
search: search,
isActive: null, // companyType에 따른 필터링 로직 필요 시 추가
);
// DTO를 도메인 모델로 변환
final companies = result.items.map((dto) => _mapDtoToDomain(dto)).toList();
final paginatedResult = PaginatedResponse<Company>(
items: companies,
page: result.page,
size: result.size,
totalElements: result.totalElements,
totalPages: result.totalPages,
first: result.first,
last: result.last,
);
return Right(paginatedResult);
} catch (e) {
return Left(ServerFailure(
message: '회사 목록 조회 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, Company>> getCompanyById(int id) async {
try {
final result = await remoteDataSource.getCompanyWithBranches(id);
final company = _mapDetailDtoToDomain(result);
return Right(company);
} catch (e) {
if (e.toString().contains('404')) {
return Left(NotFoundFailure(
message: '해당 회사를 찾을 수 없습니다.',
resourceType: 'Company',
resourceId: id.toString(),
));
}
return Left(ServerFailure(
message: '회사 상세 정보 조회 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, Company>> createCompany(Company company) async {
try {
final request = _mapDomainToCreateRequest(company);
final result = await remoteDataSource.createCompany(request);
final createdCompany = _mapResponseToDomain(result);
return Right(createdCompany);
} catch (e) {
if (e.toString().contains('중복')) {
return Left(DuplicateFailure(
message: '이미 존재하는 회사명입니다.',
field: 'name',
value: company.name,
));
}
if (e.toString().contains('유효성')) {
return Left(ValidationFailure(
message: '입력 데이터가 올바르지 않습니다.',
));
}
return Left(ServerFailure(
message: '회사 생성 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, Company>> updateCompany(int id, Company company) async {
try {
final request = _mapDomainToUpdateRequest(company);
final result = await remoteDataSource.updateCompany(id, request);
final updatedCompany = _mapResponseToDomain(result);
return Right(updatedCompany);
} catch (e) {
if (e.toString().contains('404')) {
return Left(NotFoundFailure(
message: '수정할 회사를 찾을 수 없습니다.',
resourceType: 'Company',
resourceId: id.toString(),
));
}
if (e.toString().contains('중복')) {
return Left(DuplicateFailure(
message: '이미 존재하는 회사명입니다.',
field: 'name',
value: company.name,
));
}
return Left(ServerFailure(
message: '회사 정보 수정 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, void>> deleteCompany(int id) async {
try {
await remoteDataSource.deleteCompany(id);
return const Right(null);
} catch (e) {
if (e.toString().contains('404')) {
return Left(NotFoundFailure(
message: '삭제할 회사를 찾을 수 없습니다.',
resourceType: 'Company',
resourceId: id.toString(),
));
}
if (e.toString().contains('참조')) {
return Left(BusinessFailure(
message: '해당 회사에 연결된 데이터가 있어 삭제할 수 없습니다.',
));
}
return Left(ServerFailure(
message: '회사 삭제 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, Company>> toggleCompanyStatus(int id) async {
try {
// 현재 회사 정보 조회
final currentCompany = await remoteDataSource.getCompanyDetail(id);
final newStatus = !currentCompany.isActive;
// 상태 업데이트
await remoteDataSource.updateCompanyStatus(id, newStatus);
// 업데이트된 회사 정보 재조회
final updatedCompany = await remoteDataSource.getCompanyDetail(id);
final company = _mapResponseToDomain(updatedCompany);
return Right(company);
} catch (e) {
if (e.toString().contains('404')) {
return Left(NotFoundFailure(
message: '상태를 변경할 회사를 찾을 수 없습니다.',
resourceType: 'Company',
resourceId: id.toString(),
));
}
return Left(ServerFailure(
message: '회사 상태 변경 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, Branch>> createBranch(int companyId, Branch branch) async {
try {
final request = _mapBranchToCreateRequest(branch);
final result = await remoteDataSource.createBranch(companyId, request);
final createdBranch = _mapBranchResponseToDomain(result);
return Right(createdBranch);
} catch (e) {
if (e.toString().contains('404')) {
return Left(NotFoundFailure(
message: '해당 회사를 찾을 수 없습니다.',
resourceType: 'Company',
resourceId: companyId.toString(),
));
}
return Left(ServerFailure(
message: '지점 생성 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, Branch>> updateBranch(int companyId, int branchId, Branch branch) async {
try {
final request = _mapBranchToUpdateRequest(branch);
final result = await remoteDataSource.updateBranch(companyId, branchId, request);
final updatedBranch = _mapBranchResponseToDomain(result);
return Right(updatedBranch);
} catch (e) {
if (e.toString().contains('404')) {
return Left(NotFoundFailure(
message: '수정할 지점을 찾을 수 없습니다.',
resourceType: 'Branch',
resourceId: branchId.toString(),
));
}
return Left(ServerFailure(
message: '지점 정보 수정 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, void>> deleteBranch(int companyId, int branchId) async {
try {
await remoteDataSource.deleteBranch(companyId, branchId);
return const Right(null);
} catch (e) {
if (e.toString().contains('404')) {
return Left(NotFoundFailure(
message: '삭제할 지점을 찾을 수 없습니다.',
resourceType: 'Branch',
resourceId: branchId.toString(),
));
}
return Left(ServerFailure(
message: '지점 삭제 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, List<String>>> searchCompanyNames(String query, {int? limit}) async {
try {
final companies = await remoteDataSource.searchCompanies(query);
final names = companies.map((company) => company.name).take(limit ?? 10).toList();
return Right(names);
} catch (e) {
return Left(ServerFailure(
message: '회사명 검색 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, Map<CompanyType, int>>> getCompanyCountByType() async {
// TODO: API에서 회사 유형별 통계 기능이 구현되면 추가
return const Left(ServerFailure(
message: '회사 유형별 통계 기능이 아직 구현되지 않았습니다.',
));
}
@override
Future<Either<Failure, bool>> hasLinkedUsers(int companyId) async {
// TODO: 회사에 연결된 사용자 존재 여부 확인 API 구현 필요
try {
// 임시로 false 반환 - API 구현 후 수정 필요
return const Right(false);
} catch (e) {
return Left(ServerFailure(
message: '연결된 사용자 확인 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, bool>> hasLinkedEquipment(int companyId) async {
// TODO: 회사에 연결된 장비 존재 여부 확인 API 구현 필요
try {
// 임시로 false 반환 - API 구현 후 수정 필요
return const Right(false);
} catch (e) {
return Left(ServerFailure(
message: '연결된 장비 확인 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, bool>> isDuplicateCompanyName(String name, {int? excludeId}) async {
try {
final isDuplicate = await remoteDataSource.checkDuplicateCompany(name);
// excludeId가 있는 경우 해당 ID 제외 로직 추가 필요
return Right(isDuplicate);
} catch (e) {
return Left(ServerFailure(
message: '중복 회사명 확인 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
// Private 매퍼 메서드들
Company _mapDtoToDomain(CompanyListDto dto) {
return Company(
id: dto.id,
name: dto.name,
address: Address.fromFullAddress(dto.address ?? ''),
contactName: dto.contactName,
contactPosition: null, // CompanyListDto에 없음
contactPhone: dto.contactPhone,
contactEmail: dto.contactEmail,
companyTypes: _parseCompanyTypes(dto.companyTypes),
remark: null, // CompanyListDto에 없음
branches: [], // 목록에서는 지점 정보 비어있음
);
}
Company _mapDetailDtoToDomain(CompanyWithBranches dto) {
return Company(
id: dto.company.id,
name: dto.company.name,
address: Address.fromFullAddress(dto.company.address ?? ''),
contactName: dto.company.contactName,
contactPosition: dto.company.contactPosition,
contactPhone: dto.company.contactPhone,
contactEmail: dto.company.contactEmail,
companyTypes: _parseCompanyTypes(dto.company.companyTypes),
remark: dto.company.remark,
branches: dto.branches.map((branch) => _mapBranchDtoToDomain(branch)).toList(),
);
}
Company _mapResponseToDomain(CompanyResponse response) {
return Company(
id: response.id,
name: response.name,
address: Address.fromFullAddress(response.address ?? ''),
contactName: response.contactName,
contactPosition: response.contactPosition,
contactPhone: response.contactPhone,
contactEmail: response.contactEmail,
companyTypes: _parseCompanyTypes(response.companyTypes),
remark: response.remark,
branches: [], // CompanyResponse에서는 지점 정보 따로 조회
);
}
Branch _mapBranchDtoToDomain(BranchListDto dto) {
return Branch(
id: dto.id,
companyId: dto.companyId,
name: dto.branchName,
address: Address.fromFullAddress(dto.address ?? ''),
contactName: dto.managerName,
contactPosition: null, // BranchListDto에 없음
contactPhone: dto.phone,
contactEmail: null, // BranchListDto에 없음
remark: null, // BranchListDto에 없음
);
}
Branch _mapBranchResponseToDomain(BranchResponse response) {
return Branch(
id: response.id,
companyId: response.companyId,
name: response.branchName,
address: Address.fromFullAddress(response.address ?? ''),
contactName: response.managerName,
contactPosition: null,
contactPhone: response.phone,
contactEmail: null,
remark: response.remark,
);
}
/// API에서 받은 문자열 리스트를 CompanyType enum 리스트로 변환
/// 지원하는 형식: ['customer', 'partner'] 또는 ['고객사', '파트너사']
List<CompanyType> _parseCompanyTypes(List<String>? types) {
if (types == null || types.isEmpty) return [CompanyType.customer];
return types.map((type) {
final lowerType = type.toLowerCase().trim();
// API 문자열 형식 매칭
if (lowerType == 'partner' || lowerType.contains('partner') || lowerType == '파트너사') {
return CompanyType.partner;
}
// 기본값은 customer
return CompanyType.customer;
}).toList();
}
/// CompanyType enum을 API 문자열로 변환
String _mapCompanyTypeToApiString(CompanyType type) {
switch (type) {
case CompanyType.partner:
return 'partner';
case CompanyType.customer:
return 'customer';
}
}
CreateCompanyRequest _mapDomainToCreateRequest(Company company) {
return CreateCompanyRequest(
name: company.name,
address: company.address.toString(),
contactName: company.contactName ?? '',
contactPosition: company.contactPosition ?? '',
contactPhone: company.contactPhone ?? '',
contactEmail: company.contactEmail ?? '',
companyTypes: company.companyTypes.map((type) => _mapCompanyTypeToApiString(type)).toList(),
remark: company.remark,
);
}
UpdateCompanyRequest _mapDomainToUpdateRequest(Company company) {
return UpdateCompanyRequest(
name: company.name,
address: company.address.toString(),
contactName: company.contactName,
contactPosition: company.contactPosition,
contactPhone: company.contactPhone,
contactEmail: company.contactEmail,
companyTypes: company.companyTypes.map((type) => _mapCompanyTypeToApiString(type)).toList(),
remark: company.remark,
isActive: null, // UpdateCompanyRequest에서 필요한 경우 추가
);
}
CreateBranchRequest _mapBranchToCreateRequest(Branch branch) {
return CreateBranchRequest(
branchName: branch.name,
address: branch.address.toString(),
phone: branch.contactPhone ?? '',
managerName: branch.contactName,
managerPhone: null, // Branch에 없음
remark: branch.remark,
);
}
UpdateBranchRequest _mapBranchToUpdateRequest(Branch branch) {
return UpdateBranchRequest(
branchName: branch.name,
address: branch.address.toString(),
phone: branch.contactPhone,
managerName: branch.contactName,
managerPhone: null, // Branch에 없음
remark: branch.remark,
);
}
}

View File

@@ -0,0 +1,473 @@
import 'package:dartz/dartz.dart';
import 'package:superport/core/errors/exceptions.dart';
import 'package:superport/core/errors/failures.dart';
import 'package:superport/data/datasources/remote/equipment_remote_datasource.dart';
import 'package:superport/data/models/equipment/equipment_dto.dart';
import 'package:superport/data/models/equipment/equipment_in_request.dart';
import 'package:superport/data/models/equipment/equipment_out_request.dart';
import 'package:superport/data/models/equipment/equipment_request.dart';
import 'package:superport/domain/repositories/equipment_repository.dart';
import 'package:superport/models/equipment_unified_model.dart';
class EquipmentRepositoryImpl implements EquipmentRepository {
final EquipmentRemoteDataSource _remoteDataSource;
EquipmentRepositoryImpl(this._remoteDataSource);
@override
Future<Either<Failure, List<EquipmentIn>>> getEquipmentIns({
int? page,
int? limit,
String? search,
String? sortBy,
String? sortOrder,
}) async {
try {
final response = await _remoteDataSource.getEquipments(
page: page ?? 1,
perPage: limit ?? 20,
status: 'IN_WAREHOUSE',
search: search,
);
final equipmentIns = response.items.map((dto) =>
EquipmentIn(
id: dto.id,
equipment: Equipment(
id: dto.id,
manufacturer: dto.manufacturer,
name: dto.modelName ?? '',
category: 'N/A', // EquipmentListDto에는 category 필드가 없음
subCategory: 'N/A', // EquipmentListDto에는 category 필드가 없음
subSubCategory: 'N/A', // EquipmentListDto에는 category 필드가 없음
serialNumber: dto.serialNumber,
quantity: 1,
),
inDate: dto.createdAt,
status: 'I',
type: '신제품',
warehouseLocation: dto.warehouseName,
remark: null,
)
).toList();
return Right(equipmentIns);
} on ServerException catch (e) {
return Left(ServerFailure(message: e.message ?? '서버 오류가 발생했습니다'));
} catch (e) {
return Left(ServerFailure(message: '장비 입고 목록 조회 실패: $e'));
}
}
@override
Future<Either<Failure, EquipmentIn>> getEquipmentInById(int id) async {
try {
final response = await _remoteDataSource.getEquipmentDetail(id);
final equipmentIn = EquipmentIn(
id: response.id,
equipment: Equipment(
id: response.id,
manufacturer: response.manufacturer,
name: response.modelName ?? '',
category: response.category1 ?? '',
subCategory: response.category2 ?? '',
subSubCategory: response.category3 ?? '',
serialNumber: response.serialNumber,
barcode: response.barcode,
quantity: 1,
inDate: response.purchaseDate,
remark: response.remark,
),
inDate: response.purchaseDate ?? DateTime.now(),
status: 'I',
type: '신제품',
warehouseLocation: null,
remark: response.remark,
);
return Right(equipmentIn);
} on ServerException catch (e) {
return Left(ServerFailure(message: e.message ?? '서버 오류가 발생했습니다'));
} catch (e) {
return Left(ServerFailure(message: '장비 입고 상세 조회 실패: $e'));
}
}
@override
Future<Either<Failure, EquipmentIn>> createEquipmentIn(EquipmentIn equipmentIn) async {
try {
final request = EquipmentInRequest(
equipmentId: equipmentIn.equipment.id ?? 0,
quantity: equipmentIn.equipment.quantity,
warehouseLocationId: 0, // TODO: warehouseLocation string을 ID로 변환 필요
notes: equipmentIn.remark,
);
final response = await _remoteDataSource.equipmentIn(request);
final newEquipmentIn = EquipmentIn(
id: response.transactionId,
equipment: Equipment(
id: response.equipmentId,
manufacturer: 'N/A', // 트랜잭션 응답에는 제조사 정보 없음
name: 'N/A', // 트랜잭션 응답에는 모델명 정보 없음
category: 'N/A', // 트랜잭션 응답에는 카테고리 정보 없음
subCategory: 'N/A', // 트랜잭션 응답에는 카테고리 정보 없음
subSubCategory: 'N/A', // 트랜잭션 응답에는 카테고리 정보 없음
serialNumber: null,
quantity: response.quantity,
),
inDate: response.transactionDate,
status: 'I',
type: '신제품',
warehouseLocation: null,
remark: response.message,
);
return Right(newEquipmentIn);
} on ServerException catch (e) {
return Left(ServerFailure(message: e.message ?? '서버 오류가 발생했습니다'));
} catch (e) {
return Left(ServerFailure(message: '장비 입고 생성 실패: $e'));
}
}
@override
Future<Either<Failure, EquipmentIn>> updateEquipmentIn(int id, EquipmentIn equipmentIn) async {
try {
final request = UpdateEquipmentRequest(
manufacturer: equipmentIn.equipment.manufacturer,
modelName: equipmentIn.equipment.name,
category1: equipmentIn.equipment.category,
category2: equipmentIn.equipment.subCategory,
category3: equipmentIn.equipment.subSubCategory,
serialNumber: equipmentIn.equipment.serialNumber,
barcode: equipmentIn.equipment.barcode,
purchaseDate: equipmentIn.inDate,
remark: equipmentIn.remark,
);
final response = await _remoteDataSource.updateEquipment(id, request);
final updatedEquipmentIn = EquipmentIn(
id: response.id,
equipment: Equipment(
id: response.id,
manufacturer: response.manufacturer,
name: response.modelName ?? '',
category: response.category1 ?? '',
subCategory: response.category2 ?? '',
subSubCategory: response.category3 ?? '',
serialNumber: response.serialNumber,
barcode: response.barcode,
quantity: 1,
inDate: response.purchaseDate,
remark: response.remark,
),
inDate: response.purchaseDate ?? DateTime.now(),
status: 'I',
type: '신제품',
warehouseLocation: null,
remark: response.remark,
);
return Right(updatedEquipmentIn);
} on ServerException catch (e) {
return Left(ServerFailure(message: e.message ?? '서버 오류가 발생했습니다'));
} catch (e) {
return Left(ServerFailure(message: '장비 입고 수정 실패: $e'));
}
}
@override
Future<Either<Failure, void>> deleteEquipmentIn(int id) async {
try {
await _remoteDataSource.deleteEquipment(id);
return const Right(null);
} on ServerException catch (e) {
return Left(ServerFailure(message: e.message ?? '서버 오류가 발생했습니다'));
} catch (e) {
return Left(ServerFailure(message: '장비 입고 삭제 실패: $e'));
}
}
@override
Future<Either<Failure, List<EquipmentOut>>> getEquipmentOuts({
int? page,
int? limit,
String? search,
String? sortBy,
String? sortOrder,
}) async {
try {
final response = await _remoteDataSource.getEquipments(
page: page ?? 1,
perPage: limit ?? 20,
status: 'SHIPPED',
search: search,
);
final equipmentOuts = response.items.map((dto) =>
EquipmentOut(
id: dto.id,
equipment: Equipment(
id: dto.id,
manufacturer: dto.manufacturer,
name: dto.modelName ?? '',
category: 'N/A', // EquipmentListDto에는 category 필드가 없음
subCategory: 'N/A', // EquipmentListDto에는 category 필드가 없음
subSubCategory: 'N/A', // EquipmentListDto에는 category 필드가 없음
serialNumber: dto.serialNumber,
quantity: 1,
),
outDate: dto.createdAt,
status: 'O',
company: dto.companyName,
remark: null,
)
).toList();
return Right(equipmentOuts);
} on ServerException catch (e) {
return Left(ServerFailure(message: e.message ?? '서버 오류가 발생했습니다'));
} catch (e) {
return Left(ServerFailure(message: '장비 출고 목록 조회 실패: $e'));
}
}
@override
Future<Either<Failure, EquipmentOut>> getEquipmentOutById(int id) async {
try {
final response = await _remoteDataSource.getEquipmentDetail(id);
final equipmentOut = EquipmentOut(
id: response.id,
equipment: Equipment(
id: response.id,
manufacturer: response.manufacturer,
name: response.modelName ?? '',
category: response.category1 ?? '',
subCategory: response.category2 ?? '',
subSubCategory: response.category3 ?? '',
serialNumber: response.serialNumber,
barcode: response.barcode,
quantity: 1,
inDate: response.purchaseDate,
remark: response.remark,
),
outDate: DateTime.now(), // TODO: 실제 출고일 정보 필요
status: 'O',
company: null,
remark: response.remark,
);
return Right(equipmentOut);
} on ServerException catch (e) {
return Left(ServerFailure(message: e.message ?? '서버 오류가 발생했습니다'));
} catch (e) {
return Left(ServerFailure(message: '장비 출고 상세 조회 실패: $e'));
}
}
@override
Future<Either<Failure, EquipmentOut>> createEquipmentOut(EquipmentOut equipmentOut) async {
try {
final request = EquipmentOutRequest(
equipmentId: equipmentOut.equipment.id ?? 0,
quantity: equipmentOut.equipment.quantity,
companyId: 0, // TODO: company string을 ID로 변환 필요
branchId: null,
notes: equipmentOut.remark,
);
final response = await _remoteDataSource.equipmentOut(request);
final newEquipmentOut = EquipmentOut(
id: response.transactionId,
equipment: Equipment(
id: response.equipmentId,
manufacturer: 'N/A', // 트랜잭션 응답에는 제조사 정보 없음
name: 'N/A', // 트랜잭션 응답에는 모델명 정보 없음
category: 'N/A', // 트랜잭션 응답에는 카테고리 정보 없음
subCategory: 'N/A', // 트랜잭션 응답에는 카테고리 정보 없음
subSubCategory: 'N/A', // 트랜잭션 응답에는 카테고리 정보 없음
serialNumber: null,
quantity: response.quantity,
),
outDate: response.transactionDate,
status: 'O',
company: null,
remark: response.message,
);
return Right(newEquipmentOut);
} on ServerException catch (e) {
return Left(ServerFailure(message: e.message ?? '서버 오류가 발생했습니다'));
} catch (e) {
return Left(ServerFailure(message: '장비 출고 생성 실패: $e'));
}
}
@override
Future<Either<Failure, EquipmentOut>> updateEquipmentOut(int id, EquipmentOut equipmentOut) async {
try {
final request = UpdateEquipmentRequest(
currentCompanyId: 0, // TODO: company string을 ID로 변환 필요
currentBranchId: null,
remark: equipmentOut.remark,
);
final response = await _remoteDataSource.updateEquipment(id, request);
final updatedEquipmentOut = EquipmentOut(
id: response.id,
equipment: Equipment(
id: response.id,
manufacturer: response.manufacturer,
name: response.modelName ?? '',
category: response.category1 ?? '',
subCategory: response.category2 ?? '',
subSubCategory: response.category3 ?? '',
serialNumber: response.serialNumber,
barcode: response.barcode,
quantity: 1,
inDate: response.purchaseDate,
remark: response.remark,
),
outDate: DateTime.now(), // TODO: 실제 출고일 정보 필요
status: 'O',
company: null,
remark: response.remark,
);
return Right(updatedEquipmentOut);
} on ServerException catch (e) {
return Left(ServerFailure(message: e.message ?? '서버 오류가 발생했습니다'));
} catch (e) {
return Left(ServerFailure(message: '장비 출고 수정 실패: $e'));
}
}
@override
Future<Either<Failure, void>> deleteEquipmentOut(int id) async {
try {
await _remoteDataSource.deleteEquipment(id);
return const Right(null);
} on ServerException catch (e) {
return Left(ServerFailure(message: e.message ?? '서버 오류가 발생했습니다'));
} catch (e) {
return Left(ServerFailure(message: '장비 출고 삭제 실패: $e'));
}
}
@override
Future<Either<Failure, List<EquipmentOut>>> createBatchEquipmentOut(List<EquipmentOut> equipmentOuts) async {
try {
final results = <EquipmentOut>[];
for (final equipmentOut in equipmentOuts) {
final request = EquipmentOutRequest(
equipmentId: equipmentOut.equipment.id ?? 0,
quantity: equipmentOut.equipment.quantity,
companyId: 0, // TODO: company string을 ID로 변환 필요
branchId: null,
notes: equipmentOut.remark,
);
final response = await _remoteDataSource.equipmentOut(request);
results.add(EquipmentOut(
id: response.transactionId,
equipment: Equipment(
id: response.equipmentId,
manufacturer: 'N/A', // 트랜잭션 응답에는 제조사 정보 없음
name: 'N/A', // 트랜잭션 응답에는 모델명 정보 없음
category: 'N/A', // 트랜잭션 응답에는 카테고리 정보 없음
subCategory: 'N/A', // 트랜잭션 응답에는 카테고리 정보 없음
subSubCategory: 'N/A', // 트랜잭션 응답에는 카테고리 정보 없음
serialNumber: null,
quantity: response.quantity,
),
outDate: response.transactionDate,
status: 'O',
company: null,
remark: response.message,
));
}
return Right(results);
} on ServerException catch (e) {
return Left(ServerFailure(message: e.message ?? '서버 오류가 발생했습니다'));
} catch (e) {
return Left(ServerFailure(message: '장비 일괄 출고 실패: $e'));
}
}
@override
Future<Either<Failure, List<String>>> getManufacturers() async {
try {
// TODO: 실제 API 엔드포인트 구현 필요
return const Right(['삼성', 'LG', 'Apple', 'Dell', 'HP']);
} catch (e) {
return Left(ServerFailure(message: '제조사 목록 조회 실패: $e'));
}
}
@override
Future<Either<Failure, List<String>>> getEquipmentNames() async {
try {
// TODO: 실제 API 엔드포인트 구현 필요
return const Right(['노트북', '모니터', '키보드', '마우스', '프린터']);
} catch (e) {
return Left(ServerFailure(message: '장비명 목록 조회 실패: $e'));
}
}
@override
Future<Either<Failure, List<dynamic>>> getEquipmentHistory(int equipmentId) async {
try {
final history = await _remoteDataSource.getEquipmentHistory(equipmentId);
return Right(history);
} on ServerException catch (e) {
return Left(ServerFailure(message: e.message ?? '서버 오류가 발생했습니다'));
} catch (e) {
return Left(ServerFailure(message: '장비 이력 조회 실패: $e'));
}
}
@override
Future<Either<Failure, List<Equipment>>> searchEquipment({
String? manufacturer,
String? name,
String? category,
String? serialNumber,
}) async {
try {
final response = await _remoteDataSource.getEquipments(
search: serialNumber ?? name ?? manufacturer,
page: 1,
perPage: 50,
);
final equipments = response.items.map((dto) =>
Equipment(
id: dto.id,
manufacturer: dto.manufacturer,
name: dto.modelName ?? '',
category: 'N/A', // EquipmentListDto에는 category 필드가 없음
subCategory: 'N/A', // EquipmentListDto에는 category 필드가 없음
subSubCategory: 'N/A', // EquipmentListDto에는 category 필드가 없음
serialNumber: dto.serialNumber,
quantity: 1,
)
).toList();
return Right(equipments);
} on ServerException catch (e) {
return Left(ServerFailure(message: e.message ?? '서버 오류가 발생했습니다'));
} catch (e) {
return Left(ServerFailure(message: '장비 검색 실패: $e'));
}
}
}

View File

@@ -1,24 +0,0 @@
import '../models/license/license_dto.dart';
/// 라이선스 Repository 인터페이스
abstract class LicenseRepository {
/// 라이선스 목록 조회
Future<LicenseListResponseDto> getLicenses({
int page = 1,
int perPage = 20,
String? search,
Map<String, dynamic>? filters,
});
/// 라이선스 상세 조회
Future<LicenseDto> getLicenseDetail(int id);
/// 라이선스 생성
Future<LicenseDto> createLicense(Map<String, dynamic> data);
/// 라이선스 수정
Future<LicenseDto> updateLicense(int id, Map<String, dynamic> data);
/// 라이선스 삭제
Future<void> deleteLicense(int id);
}

View File

@@ -1,58 +1,323 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../core/errors/failures.dart';
import '../../domain/repositories/license_repository.dart';
import '../../models/license_model.dart';
import '../datasources/remote/license_remote_datasource.dart';
import '../models/common/paginated_response.dart';
import '../models/dashboard/license_expiry_summary.dart';
import '../models/license/license_dto.dart';
import '../models/license/license_request_dto.dart';
import 'license_repository.dart';
/// 라이선스 Repository 구현체
/// 라이선스 및 유지보수 계약 관리 작업을 처리하며 도메인 모델과 API DTO 간 변환을 담당
@Injectable(as: LicenseRepository)
class LicenseRepositoryImpl implements LicenseRepository {
final LicenseRemoteDataSource remoteDataSource;
LicenseRepositoryImpl(this.remoteDataSource);
LicenseRepositoryImpl({required this.remoteDataSource});
@override
Future<LicenseListResponseDto> getLicenses({
int page = 1,
int perPage = 20,
Future<Either<Failure, PaginatedResponse<License>>> getLicenses({
int? page,
int? limit,
String? search,
Map<String, dynamic>? filters,
int? companyId,
String? equipmentType,
String? expiryStatus,
String? sortBy,
String? sortOrder,
}) async {
// 검색 및 필터 파라미터를 DataSource 형식에 맞게 변환
bool? isActive = filters?['is_active'];
int? companyId = filters?['company_id'];
int? assignedUserId = filters?['assigned_user_id'];
String? licenseType = filters?['license_type'];
return await remoteDataSource.getLicenses(
page: page,
perPage: perPage,
isActive: isActive,
companyId: companyId,
assignedUserId: assignedUserId,
licenseType: licenseType,
try {
final result = await remoteDataSource.getLicenses(
page: page ?? 1,
perPage: limit ?? 20,
isActive: null, // expiryStatus에 따른 필터링 로직 필요 시 추가
companyId: companyId,
assignedUserId: null,
licenseType: equipmentType,
);
// DTO를 도메인 모델로 변환
final licenses = result.items.map((dto) => _mapDtoToDomain(dto)).toList();
// 검색 필터링 (서버에서 지원하지 않는 경우 클라이언트 측에서 처리)
if (search != null && search.isNotEmpty) {
final filteredLicenses = licenses.where((license) {
final searchLower = search.toLowerCase();
return (license.productName?.toLowerCase().contains(searchLower) ?? false) ||
(license.companyName?.toLowerCase().contains(searchLower) ?? false) ||
(license.vendor?.toLowerCase().contains(searchLower) ?? false);
}).toList();
final paginatedResult = PaginatedResponse<License>(
items: filteredLicenses,
page: result.page,
size: 20,
totalElements: filteredLicenses.length,
totalPages: (filteredLicenses.length / 20).ceil(),
first: result.page == 0,
last: result.page >= (filteredLicenses.length / 20).ceil() - 1,
);
return Right(paginatedResult);
}
final paginatedResult = PaginatedResponse<License>(
items: licenses,
page: result.page,
size: 20,
totalElements: result.total,
totalPages: (result.total / 20).ceil(),
first: result.page == 0,
last: result.page >= (result.total / 20).ceil() - 1,
);
return Right(paginatedResult);
} catch (e) {
return Left(ServerFailure(
message: '라이선스 목록 조회 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, License>> getLicenseById(int id) async {
try {
final result = await remoteDataSource.getLicenseById(id);
final license = _mapDtoToDomain(result);
return Right(license);
} catch (e) {
if (e.toString().contains('404')) {
return Left(NotFoundFailure(
message: '해당 라이선스를 찾을 수 없습니다.',
resourceType: 'License',
resourceId: id.toString(),
));
}
return Left(ServerFailure(
message: '라이선스 상세 정보 조회 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, License>> createLicense(License license) async {
try {
final request = _mapDomainToCreateRequest(license);
final result = await remoteDataSource.createLicense(request);
final createdLicense = _mapDtoToDomain(result);
return Right(createdLicense);
} catch (e) {
if (e.toString().contains('중복')) {
return Left(DuplicateFailure(
message: '이미 존재하는 라이선스입니다.',
field: 'licenseKey',
value: license.licenseKey,
));
}
if (e.toString().contains('유효성')) {
return Left(ValidationFailure(
message: '입력 데이터가 올바르지 않습니다.',
));
}
return Left(ServerFailure(
message: '라이선스 생성 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, License>> updateLicense(int id, License license) async {
try {
final request = _mapDomainToUpdateRequest(license);
final result = await remoteDataSource.updateLicense(id, request);
final updatedLicense = _mapDtoToDomain(result);
return Right(updatedLicense);
} catch (e) {
if (e.toString().contains('404')) {
return Left(NotFoundFailure(
message: '수정할 라이선스를 찾을 수 없습니다.',
resourceType: 'License',
resourceId: id.toString(),
));
}
if (e.toString().contains('중복')) {
return Left(DuplicateFailure(
message: '이미 존재하는 라이선스키입니다.',
field: 'licenseKey',
value: license.licenseKey,
));
}
return Left(ServerFailure(
message: '라이선스 정보 수정 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, void>> deleteLicense(int id) async {
try {
await remoteDataSource.deleteLicense(id);
return const Right(null);
} catch (e) {
if (e.toString().contains('404')) {
return Left(NotFoundFailure(
message: '삭제할 라이선스를 찾을 수 없습니다.',
resourceType: 'License',
resourceId: id.toString(),
));
}
if (e.toString().contains('참조')) {
return Left(BusinessFailure(
message: '해당 라이선스에 연결된 데이터가 있어 삭제할 수 없습니다.',
));
}
return Left(ServerFailure(
message: '라이선스 삭제 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, List<License>>> getExpiringLicenses({int days = 30, int? companyId}) async {
// TODO: API에서 만료 예정 라이선스 조회 기능이 구현되면 추가
return const Left(ServerFailure(
message: '만료 예정 라이선스 조회 기능이 아직 구현되지 않았습니다.',
));
}
@override
Future<Either<Failure, List<License>>> getExpiredLicenses({int? companyId}) async {
// TODO: API에서 만료된 라이선스 조회 기능이 구현되면 추가
return const Left(ServerFailure(
message: '만료된 라이선스 조회 기능이 아직 구현되지 않았습니다.',
));
}
@override
Future<Either<Failure, LicenseExpirySummary>> getLicenseExpirySummary() async {
// TODO: API에서 라이선스 만료 요약 기능이 구현되면 추가
return const Left(ServerFailure(
message: '라이선스 만료 요약 조회 기능이 아직 구현되지 않았습니다.',
));
}
@override
Future<Either<Failure, License>> renewLicense(int id, DateTime newExpiryDate, {double? renewalCost, String? renewalNote}) async {
// TODO: API에서 라이선스 갱신 기능이 구현되면 추가
return const Left(ServerFailure(
message: '라이선스 갱신 기능이 아직 구현되지 않았습니다.',
));
}
@override
Future<Either<Failure, Map<String, int>>> getLicenseStatsByCompany(int companyId) async {
// TODO: API에서 회사별 라이선스 통계 기능이 구현되면 추가
return const Left(ServerFailure(
message: '회사별 라이선스 통계 기능이 아직 구현되지 않았습니다.',
));
}
@override
Future<Either<Failure, Map<String, int>>> getLicenseCountByType() async {
// TODO: API에서 라이선스 유형별 통계 기능이 구현되면 추가
return const Left(ServerFailure(
message: '라이선스 유형별 통계 기능이 아직 구현되지 않았습니다.',
));
}
@override
Future<Either<Failure, void>> setExpiryNotification(int licenseId, {int notifyDays = 30}) async {
// TODO: API에서 만료 알림 설정 기능이 구현되면 추가
return const Left(ServerFailure(
message: '만료 알림 설정 기능이 아직 구현되지 않았습니다.',
));
}
@override
Future<Either<Failure, List<License>>> searchLicenses(String query, {int? companyId, int? limit}) async {
try {
final result = await remoteDataSource.getLicenses(
page: 1,
perPage: limit ?? 10,
companyId: companyId,
);
// 클라이언트 측에서 검색 필터링
final searchLower = query.toLowerCase();
final filteredLicenses = result.items
.where((dto) {
final license = _mapDtoToDomain(dto);
return (license.productName?.toLowerCase().contains(searchLower) ?? false) ||
(license.companyName?.toLowerCase().contains(searchLower) ?? false) ||
(license.vendor?.toLowerCase().contains(searchLower) ?? false);
})
.map((dto) => _mapDtoToDomain(dto))
.toList();
return Right(filteredLicenses);
} catch (e) {
return Left(ServerFailure(
message: '라이선스 검색 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
// Private 매퍼 메서드들
License _mapDtoToDomain(LicenseDto dto) {
return License(
id: dto.id,
licenseKey: dto.licenseKey,
productName: dto.productName,
vendor: dto.vendor,
licenseType: dto.licenseType,
userCount: dto.userCount,
purchaseDate: dto.purchaseDate,
expiryDate: dto.expiryDate,
purchasePrice: dto.purchasePrice,
companyId: dto.companyId,
branchId: dto.branchId,
assignedUserId: dto.assignedUserId,
remark: dto.remark,
isActive: dto.isActive,
createdAt: dto.createdAt,
updatedAt: dto.updatedAt,
companyName: dto.companyName,
branchName: dto.branchName,
assignedUserName: dto.assignedUserName,
);
}
@override
Future<LicenseDto> getLicenseDetail(int id) async {
return await remoteDataSource.getLicenseById(id);
CreateLicenseRequest _mapDomainToCreateRequest(License license) {
return CreateLicenseRequest(
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,
branchId: license.branchId,
remark: license.remark,
);
}
@override
Future<LicenseDto> createLicense(Map<String, dynamic> data) async {
final request = CreateLicenseRequest.fromJson(data);
return await remoteDataSource.createLicense(request);
}
@override
Future<LicenseDto> updateLicense(int id, Map<String, dynamic> data) async {
final request = UpdateLicenseRequest.fromJson(data);
return await remoteDataSource.updateLicense(id, request);
}
@override
Future<void> deleteLicense(int id) async {
await remoteDataSource.deleteLicense(id);
UpdateLicenseRequest _mapDomainToUpdateRequest(License license) {
return UpdateLicenseRequest(
licenseKey: license.licenseKey,
productName: license.productName,
vendor: license.vendor,
licenseType: license.licenseType,
userCount: license.userCount,
purchaseDate: license.purchaseDate,
expiryDate: license.expiryDate,
purchasePrice: license.purchasePrice,
remark: license.remark,
isActive: license.isActive,
);
}
}

View File

@@ -0,0 +1,369 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../core/errors/failures.dart';
import '../../domain/repositories/user_repository.dart';
import '../../models/user_model.dart';
import '../datasources/remote/user_remote_datasource.dart';
import '../models/common/paginated_response.dart';
import '../models/user/user_dto.dart';
/// 사용자 관리 Repository 구현체
/// 사용자 계정 CRUD 및 권한 관리 작업을 처리하며 도메인 모델과 API DTO 간 변환을 담당
@Injectable(as: UserRepository)
class UserRepositoryImpl implements UserRepository {
final UserRemoteDataSource remoteDataSource;
UserRepositoryImpl({required this.remoteDataSource});
@override
Future<Either<Failure, PaginatedResponse<User>>> getUsers({
int? page,
int? limit,
String? search,
String? role,
int? companyId,
bool? isActive,
String? sortBy,
String? sortOrder,
}) async {
try {
final result = await remoteDataSource.getUsers(
page: page ?? 1,
perPage: limit ?? 20,
isActive: isActive,
companyId: companyId,
role: role,
);
// DTO를 도메인 모델로 변환
final users = result.items.map((dto) => _mapDtoToDomain(dto)).toList();
// 검색 필터링 (서버에서 지원하지 않는 경우 클라이언트 측에서 처리)
if (search != null && search.isNotEmpty) {
final filteredUsers = users.where((user) {
final searchLower = search.toLowerCase();
return (user.username?.toLowerCase().contains(searchLower) ?? false) ||
user.name.toLowerCase().contains(searchLower) ||
(user.email?.toLowerCase().contains(searchLower) ?? false);
}).toList();
final paginatedResult = PaginatedResponse<User>(
items: filteredUsers,
page: result.page,
size: result.size,
totalElements: filteredUsers.length,
totalPages: (filteredUsers.length / result.size).ceil(),
first: result.first,
last: result.last,
);
return Right(paginatedResult);
}
final paginatedResult = PaginatedResponse<User>(
items: users,
page: result.page,
size: result.size,
totalElements: result.totalElements,
totalPages: result.totalPages,
first: result.first,
last: result.last,
);
return Right(paginatedResult);
} catch (e) {
return Left(ServerFailure(
message: '사용자 목록 조회 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, User>> getUserById(int id) async {
try {
final result = await remoteDataSource.getUser(id);
final user = _mapDtoToDomain(result);
return Right(user);
} catch (e) {
if (e.toString().contains('404')) {
return Left(NotFoundFailure(
message: '해당 사용자를 찾을 수 없습니다.',
resourceType: 'User',
resourceId: id.toString(),
));
}
return Left(ServerFailure(
message: '사용자 상세 정보 조회 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, User>> createUser(User user, String password) async {
try {
final request = _mapDomainToCreateRequest(user, password);
final result = await remoteDataSource.createUser(request);
final createdUser = _mapDtoToDomain(result);
return Right(createdUser);
} catch (e) {
if (e.toString().contains('중복')) {
return Left(DuplicateFailure(
message: '이미 사용 중인 이메일입니다.',
field: 'username',
value: user.username ?? '',
));
}
if (e.toString().contains('유효성')) {
return Left(ValidationFailure(
message: '입력 데이터가 올바르지 않습니다.',
));
}
return Left(ServerFailure(
message: '사용자 생성 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, User>> updateUser(int id, User user) async {
try {
final request = _mapDomainToUpdateRequest(user);
final result = await remoteDataSource.updateUser(id, request);
final updatedUser = _mapDtoToDomain(result);
return Right(updatedUser);
} catch (e) {
if (e.toString().contains('404')) {
return Left(NotFoundFailure(
message: '수정할 사용자를 찾을 수 없습니다.',
resourceType: 'User',
resourceId: id.toString(),
));
}
if (e.toString().contains('중복')) {
return Left(DuplicateFailure(
message: '이미 사용 중인 이메일입니다.',
field: 'username',
value: user.username ?? '',
));
}
return Left(ServerFailure(
message: '사용자 정보 수정 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, void>> deleteUser(int id) async {
try {
await remoteDataSource.deleteUser(id);
return const Right(null);
} catch (e) {
if (e.toString().contains('404')) {
return Left(NotFoundFailure(
message: '삭제할 사용자를 찾을 수 없습니다.',
resourceType: 'User',
resourceId: id.toString(),
));
}
if (e.toString().contains('참조')) {
return Left(BusinessFailure(
message: '해당 사용자에 연결된 데이터가 있어 삭제할 수 없습니다.',
));
}
return Left(ServerFailure(
message: '사용자 삭제 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, User>> toggleUserStatus(int id) async {
try {
// 현재 사용자 정보 조회
final currentUser = await remoteDataSource.getUser(id);
final newStatus = !currentUser.isActive;
// 상태 업데이트
final request = ChangeStatusRequest(isActive: newStatus);
final updatedUser = await remoteDataSource.changeUserStatus(id, request);
final user = _mapDtoToDomain(updatedUser);
return Right(user);
} catch (e) {
if (e.toString().contains('404')) {
return Left(NotFoundFailure(
message: '상태를 변경할 사용자를 찾을 수 없습니다.',
resourceType: 'User',
resourceId: id.toString(),
));
}
return Left(ServerFailure(
message: '사용자 상태 변경 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, void>> resetPassword(int id, String newPassword) async {
try {
// resetPassword 메서드가 데이터소스에 없으므로 changePassword 사용
final request = ChangePasswordRequest(currentPassword: '', newPassword: newPassword);
await remoteDataSource.changePassword(id, request);
return const Right(null);
} catch (e) {
if (e.toString().contains('404')) {
return Left(NotFoundFailure(
message: '비밀번호를 재설정할 사용자를 찾을 수 없습니다.',
resourceType: 'User',
resourceId: id.toString(),
));
}
return Left(ServerFailure(
message: '비밀번호 재설정 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, User>> changeUserRole(int id, String newRole) async {
try {
// changeUserRole 메서드가 데이터소스에 없으므로 updateUser 사용
final request = UpdateUserRequest(role: newRole);
final updatedUser = await remoteDataSource.updateUser(id, request);
final user = _mapDtoToDomain(updatedUser);
return Right(user);
} catch (e) {
if (e.toString().contains('404')) {
return Left(NotFoundFailure(
message: '역할을 변경할 사용자를 찾을 수 없습니다.',
resourceType: 'User',
resourceId: id.toString(),
));
}
return Left(ServerFailure(
message: '사용자 역할 변경 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, bool>> isDuplicateUsername(String username, {int? excludeId}) async {
try {
final isDuplicate = await remoteDataSource.checkDuplicateUsername(username);
// excludeId가 있는 경우 해당 ID 제외 로직 추가 필요
return Right(isDuplicate);
} catch (e) {
return Left(ServerFailure(
message: '중복 사용자명 확인 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, List<User>>> getUsersByCompany(int companyId, {bool includeInactive = false}) async {
try {
// getUsersByCompany 메서드가 없으므로 getUsers로 대체
final result = await remoteDataSource.getUsers(
companyId: companyId,
isActive: includeInactive ? null : true,
);
final users = result.users.map((dto) => _mapDtoToDomain(dto)).toList();
return Right(users);
} catch (e) {
return Left(ServerFailure(
message: '회사별 사용자 조회 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, Map<String, int>>> getUserCountByRole() async {
// TODO: API에서 역할별 사용자 수 통계 기능이 구현되면 추가
return const Left(ServerFailure(
message: '역할별 사용자 수 통계 기능이 아직 구현되지 않았습니다.',
));
}
@override
Future<Either<Failure, List<User>>> searchUsers(String query, {int? companyId, int? limit}) async {
try {
final result = await remoteDataSource.searchUsers(
query: query,
companyId: companyId,
perPage: limit ?? 10,
);
final users = result.users.map((dto) => _mapDtoToDomain(dto)).toList();
return Right(users);
} catch (e) {
return Left(ServerFailure(
message: '사용자 검색 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, void>> updateLastLoginTime(int id) async {
try {
// updateLastLoginTime 메서드가 데이터소스에 없으므로 비어있는 구현
// TODO: API에서 지원되면 구현
throw UnimplementedError('마지막 로그인 시간 업데이트 기능이 아직 구현되지 않았습니다.');
return const Right(null);
} catch (e) {
return Left(ServerFailure(
message: '마지막 로그인 시간 업데이트 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
// Private 매퍼 메서드들
User _mapDtoToDomain(UserDto dto) {
return User(
id: dto.id,
companyId: dto.companyId ?? 0,
branchId: dto.branchId,
name: dto.name,
role: dto.role,
email: dto.email,
phoneNumbers: dto.phone != null ? [{'type': 'primary', 'number': dto.phone!}] : [],
username: dto.username,
isActive: dto.isActive,
createdAt: dto.createdAt,
updatedAt: dto.updatedAt,
);
}
// _mapDetailDtoToDomain 함수는 더 이상 사용하지 않음 - _mapDtoToDomain 사용
// _mapResponseToDomain 함수는 더 이상 사용하지 않음 - _mapDtoToDomain 사용
// UserRole enum은 더 이상 필요하지 않음 - String role을 직접 사용
CreateUserRequest _mapDomainToCreateRequest(User user, String password) {
return CreateUserRequest(
username: user.username ?? user.email ?? '',
password: password,
name: user.name,
email: user.email,
phone: user.phoneNumbers.isNotEmpty ? user.phoneNumbers.first['number'] : null,
role: user.role,
companyId: user.companyId,
branchId: user.branchId,
);
}
UpdateUserRequest _mapDomainToUpdateRequest(User user) {
return UpdateUserRequest(
name: user.name,
email: user.email,
phone: user.phoneNumbers.isNotEmpty ? user.phoneNumbers.first['number'] : null,
role: user.role,
companyId: user.companyId,
branchId: user.branchId,
isActive: user.isActive,
);
}
// _mapRoleToString 함수는 더 이상 필요하지 않음 - role을 직접 String으로 사용
}

View File

@@ -1,27 +0,0 @@
import '../models/warehouse/warehouse_dto.dart';
/// 창고 위치 Repository 인터페이스
abstract class WarehouseLocationRepository {
/// 창고 위치 목록 조회
Future<WarehouseLocationListDto> getWarehouseLocations({
int page = 1,
int perPage = 20,
String? search,
Map<String, dynamic>? filters,
});
/// 창고 위치 상세 조회
Future<WarehouseLocationDto> getWarehouseLocationDetail(int id);
/// 창고 위치 생성
Future<WarehouseLocationDto> createWarehouseLocation(Map<String, dynamic> data);
/// 창고 위치 수정
Future<WarehouseLocationDto> updateWarehouseLocation(int id, Map<String, dynamic> data);
/// 창고 위치 삭제
Future<void> deleteWarehouseLocation(int id);
/// 창고에 장비가 있는지 확인
Future<bool> checkWarehouseHasEquipment(int id);
}

View File

@@ -1,56 +1,362 @@
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../core/errors/failures.dart';
import '../../domain/repositories/warehouse_location_repository.dart';
import '../../models/warehouse_location_model.dart';
import '../../models/address_model.dart';
import '../datasources/remote/warehouse_location_remote_datasource.dart';
import '../models/common/paginated_response.dart';
import '../models/warehouse/warehouse_dto.dart';
import 'warehouse_location_repository.dart';
/// 창고 위치 Repository 구현체
/// 창고 위치 및 장비 입고지 관리 작업을 처리하며 도메인 모델과 API DTO 간 변환을 담당
@Injectable(as: WarehouseLocationRepository)
class WarehouseLocationRepositoryImpl implements WarehouseLocationRepository {
final WarehouseLocationRemoteDataSource remoteDataSource;
WarehouseLocationRepositoryImpl(this.remoteDataSource);
WarehouseLocationRepositoryImpl({required this.remoteDataSource});
@override
Future<WarehouseLocationListDto> getWarehouseLocations({
int page = 1,
int perPage = 20,
Future<Either<Failure, PaginatedResponse<WarehouseLocation>>> getWarehouseLocations({
int? page,
int? limit,
String? search,
Map<String, dynamic>? filters,
String? locationType,
bool? isActive,
bool? hasEquipment,
String? sortBy,
String? sortOrder,
}) async {
return await remoteDataSource.getWarehouseLocations(
page: page,
perPage: perPage,
search: search,
filters: filters,
try {
final result = await remoteDataSource.getWarehouseLocations(
page: page ?? 1,
perPage: limit ?? 20,
search: search,
filters: {
if (locationType != null) 'location_type': locationType,
if (isActive != null) 'is_active': isActive,
if (hasEquipment != null) 'has_equipment': hasEquipment,
},
);
// DTO를 도메인 모델로 변환
final warehouseLocations = result.items.map((dto) => _mapDtoToDomain(dto)).toList();
final paginatedResult = PaginatedResponse<WarehouseLocation>(
items: warehouseLocations,
page: result.page,
size: result.perPage,
totalElements: result.total,
totalPages: result.totalPages,
first: result.page == 1,
last: result.page == result.totalPages,
);
return Right(paginatedResult);
} catch (e) {
return Left(ServerFailure(
message: '창고 위치 목록 조회 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, WarehouseLocation>> getWarehouseLocationById(int id) async {
try {
final result = await remoteDataSource.getWarehouseLocationDetail(id);
final warehouseLocation = _mapDetailDtoToDomain(result);
return Right(warehouseLocation);
} catch (e) {
if (e.toString().contains('404')) {
return Left(NotFoundFailure(
message: '해당 창고 위치를 찾을 수 없습니다.',
resourceType: 'WarehouseLocation',
resourceId: id.toString(),
));
}
return Left(ServerFailure(
message: '창고 위치 상세 정보 조회 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, WarehouseLocation>> createWarehouseLocation(WarehouseLocation warehouseLocation) async {
try {
final request = _mapDomainToCreateRequest(warehouseLocation);
final result = await remoteDataSource.createWarehouseLocation(request);
final createdWarehouseLocation = _mapDetailDtoToDomain(result);
return Right(createdWarehouseLocation);
} catch (e) {
if (e.toString().contains('중복')) {
return Left(DuplicateFailure(
message: '이미 존재하는 창고명입니다.',
field: 'name',
value: warehouseLocation.name,
));
}
if (e.toString().contains('유효성')) {
return Left(ValidationFailure(
message: '입력 데이터가 올바르지 않습니다.',
));
}
return Left(ServerFailure(
message: '창고 위치 생성 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, WarehouseLocation>> updateWarehouseLocation(int id, WarehouseLocation warehouseLocation) async {
try {
final request = _mapDomainToUpdateRequest(warehouseLocation);
final result = await remoteDataSource.updateWarehouseLocation(id, request);
final updatedWarehouseLocation = _mapDetailDtoToDomain(result);
return Right(updatedWarehouseLocation);
} catch (e) {
if (e.toString().contains('404')) {
return Left(NotFoundFailure(
message: '수정할 창고 위치를 찾을 수 없습니다.',
resourceType: 'WarehouseLocation',
resourceId: id.toString(),
));
}
if (e.toString().contains('중복')) {
return Left(DuplicateFailure(
message: '이미 존재하는 창고명입니다.',
field: 'name',
value: warehouseLocation.name,
));
}
return Left(ServerFailure(
message: '창고 위치 정보 수정 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, void>> deleteWarehouseLocation(int id) async {
try {
await remoteDataSource.deleteWarehouseLocation(id);
return const Right(null);
} catch (e) {
if (e.toString().contains('404')) {
return Left(NotFoundFailure(
message: '삭제할 창고 위치를 찾을 수 없습니다.',
resourceType: 'WarehouseLocation',
resourceId: id.toString(),
));
}
if (e.toString().contains('참조')) {
return Left(BusinessFailure(
message: '해당 창고에 보관 중인 장비가 있어 삭제할 수 없습니다.',
));
}
return Left(ServerFailure(
message: '창고 위치 삭제 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, WarehouseLocation>> toggleWarehouseLocationStatus(int id) async {
try {
// 현재 창고 위치 정보 조회
final currentWarehouse = await remoteDataSource.getWarehouseLocationDetail(id);
final newStatus = !currentWarehouse.isActive;
// 상태 업데이트
await remoteDataSource.updateWarehouseLocationStatus(id, newStatus);
// 업데이트된 창고 위치 정보 재조회
final updatedWarehouse = await remoteDataSource.getWarehouseLocationDetail(id);
final warehouseLocation = _mapDetailDtoToDomain(updatedWarehouse);
return Right(warehouseLocation);
} catch (e) {
if (e.toString().contains('404')) {
return Left(NotFoundFailure(
message: '상태를 변경할 창고 위치를 찾을 수 없습니다.',
resourceType: 'WarehouseLocation',
resourceId: id.toString(),
));
}
return Left(ServerFailure(
message: '창고 위치 상태 변경 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, bool>> hasEquipment(int id) async {
try {
final hasEquipment = await remoteDataSource.checkWarehouseHasEquipment(id);
return Right(hasEquipment);
} catch (e) {
return Left(ServerFailure(
message: '창고 장비 보유 여부 확인 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, int>> getEquipmentCount(int id) async {
// TODO: API에서 창고별 장비 수량 조회 기능이 구현되면 추가
try {
// 임시로 0 반환 - API 구현 후 수정 필요
return const Right(0);
} catch (e) {
return Left(ServerFailure(
message: '창고 장비 수량 조회 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, PaginatedResponse<dynamic>>> getEquipmentByWarehouse(
int warehouseId, {
int? page,
int? limit,
}) async {
// TODO: API에서 창고별 장비 목록 조회 기능이 구현되면 추가
return const Left(ServerFailure(
message: '창고별 장비 목록 조회 기능이 아직 구현되지 않았습니다.',
));
}
@override
Future<Either<Failure, Map<int, double>>> getWarehouseUtilization() async {
// TODO: API에서 창고 사용률 통계 기능이 구현되면 추가
return const Left(ServerFailure(
message: '창고 사용률 통계 기능이 아직 구현되지 않았습니다.',
));
}
@override
Future<Either<Failure, Map<String, int>>> getWarehouseCountByType() async {
// TODO: API에서 창고 유형별 통계 기능이 구현되면 추가
return const Left(ServerFailure(
message: '창고 유형별 통계 기능이 아직 구현되지 않았습니다.',
));
}
@override
Future<Either<Failure, bool>> isDuplicateWarehouseName(String name, {int? excludeId}) async {
try {
final isDuplicate = await remoteDataSource.checkDuplicateWarehouseName(name);
// excludeId가 있는 경우 해당 ID 제외 로직 추가 필요
return Right(isDuplicate);
} catch (e) {
return Left(ServerFailure(
message: '중복 창고명 확인 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, List<WarehouseLocation>>> searchWarehouseLocations(String query, {int? limit}) async {
try {
final result = await remoteDataSource.getWarehouseLocations(
page: 1,
perPage: limit ?? 10,
search: query,
);
final warehouseLocations = result.items.map((dto) => _mapDtoToDomain(dto)).toList();
return Right(warehouseLocations);
} catch (e) {
return Left(ServerFailure(
message: '창고 위치 검색 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, List<WarehouseLocation>>> getActiveWarehouseLocations() async {
try {
final result = await remoteDataSource.getWarehouseLocations(
page: 1,
perPage: 100, // 활성 창고 모두 조회
filters: {'is_active': true},
);
final activeWarehouses = result.items.map((dto) => _mapDtoToDomain(dto)).toList();
return Right(activeWarehouses);
} catch (e) {
return Left(ServerFailure(
message: '활성 창고 위치 조회 중 오류가 발생했습니다: ${e.toString()}',
));
}
}
@override
Future<Either<Failure, WarehouseLocation>> updateWarehouseCapacity(
int id,
int totalCapacity,
int usedCapacity,
) async {
// TODO: API에서 창고 용량 업데이트 기능이 구현되면 추가
return const Left(ServerFailure(
message: '창고 용량 업데이트 기능이 아직 구현되지 않았습니다.',
));
}
// Private 매퍼 메서드들
WarehouseLocation _mapDtoToDomain(WarehouseLocationDto dto) {
return WarehouseLocation(
id: dto.id,
name: dto.name,
// String? address를 Address 객체로 변환
address: dto.address != null && dto.address!.isNotEmpty
? Address.fromFullAddress(dto.address!)
: const Address(),
// DTO에 없는 필드는 remark로 통합 (WarehouseLocation 모델의 실제 필드)
remark: null, // DTO에는 description이나 remark 필드가 없음
);
}
@override
Future<WarehouseLocationDto> getWarehouseLocationDetail(int id) async {
return await remoteDataSource.getWarehouseLocationDetail(id);
WarehouseLocation _mapDetailDtoToDomain(WarehouseLocationDto dto) {
return WarehouseLocation(
id: dto.id,
name: dto.name,
// String? address를 Address 객체로 변환
address: dto.address != null && dto.address!.isNotEmpty
? Address.fromFullAddress(dto.address!)
: const Address(),
// DTO에 없는 필드는 remark로 통합 (WarehouseLocation 모델의 실제 필드)
remark: null, // DTO에는 description이나 remark 필드가 없음
);
}
@override
Future<WarehouseLocationDto> createWarehouseLocation(Map<String, dynamic> data) async {
final request = CreateWarehouseLocationRequest.fromJson(data);
return await remoteDataSource.createWarehouseLocation(request);
// WarehouseLocationType enum이 WarehouseLocation 모델에 없으므로 제거
// 필요시 나중에 모델 업데이트 후 재추가
CreateWarehouseLocationRequest _mapDomainToCreateRequest(WarehouseLocation warehouseLocation) {
return CreateWarehouseLocationRequest(
name: warehouseLocation.name,
// Address 객체를 String으로 변환
address: warehouseLocation.address.toString(),
// DTO 요청에 없는 필드들은 제거하고 DTO에 있는 필드만 매핑
// capacity는 DTO에 있지만 모델에 없으므로 기본값 사용
capacity: 0,
// 나머지 필드들도 DTO 구조에 맞게 조정
);
}
@override
Future<WarehouseLocationDto> updateWarehouseLocation(int id, Map<String, dynamic> data) async {
final request = UpdateWarehouseLocationRequest.fromJson(data);
return await remoteDataSource.updateWarehouseLocation(id, request);
}
@override
Future<void> deleteWarehouseLocation(int id) async {
await remoteDataSource.deleteWarehouseLocation(id);
}
@override
Future<bool> checkWarehouseHasEquipment(int id) async {
// TODO: API 엔드포인트 구현 필요
// 현재는 항상 false 반환
return false;
UpdateWarehouseLocationRequest _mapDomainToUpdateRequest(WarehouseLocation warehouseLocation) {
return UpdateWarehouseLocationRequest(
name: warehouseLocation.name,
// Address 객체를 String으로 변환
address: warehouseLocation.address.toString(),
// DTO 요청에 없는 필드들은 제거하고 DTO에 있는 필드만 매핑
// capacity는 DTO에 있지만 모델에 없으므로 기본값 사용
capacity: 0,
// isActive는 DTO에 있지만 모델에 없으므로 기본값 true 사용
isActive: true,
);
}
// WarehouseLocationType enum이 WarehouseLocation 모델에 없으므로 제거
// 필요시 나중에 모델 업데이트 후 재추가
}