refactor: Repository 패턴 적용 및 Clean Architecture 완성
## 주요 변경사항 ### 🏗️ 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:
214
lib/data/repositories/auth_repository_impl.dart
Normal file
214
lib/data/repositories/auth_repository_impl.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user