From c573096d84a1cf9f47bdea71049fd78cae32d40f Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Thu, 24 Jul 2025 15:14:53 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20API=20=EC=9D=B8=EC=A6=9D=20=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=85=9C=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=ED=99=94=EB=A9=B4=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AuthService, AuthRemoteDataSource 구현 - JWT 토큰 관리 (SecureStorage 사용) - 로그인 화면 API 연동 및 에러 처리 - freezed 패키지로 Auth 관련 DTO 모델 생성 - 의존성 주입 설정 업데이트 --- doc/API_Integration_Plan.md | 69 +++-- .../remote/auth_remote_datasource.dart | 104 +++++++ lib/data/models/auth/auth_user.dart | 18 ++ lib/data/models/auth/auth_user.freezed.dart | 256 ++++++++++++++++ lib/data/models/auth/auth_user.g.dart | 25 ++ lib/data/models/auth/login_request.dart | 15 + .../models/auth/login_request.freezed.dart | 183 ++++++++++++ lib/data/models/auth/login_request.g.dart | 19 ++ lib/data/models/auth/login_response.dart | 19 ++ .../models/auth/login_response.freezed.dart | 280 ++++++++++++++++++ lib/data/models/auth/login_response.g.dart | 25 ++ lib/data/models/auth/logout_request.dart | 14 + .../models/auth/logout_request.freezed.dart | 171 +++++++++++ lib/data/models/auth/logout_request.g.dart | 17 ++ .../models/auth/refresh_token_request.dart | 14 + .../auth/refresh_token_request.freezed.dart | 172 +++++++++++ .../models/auth/refresh_token_request.g.dart | 19 ++ lib/data/models/auth/token_response.dart | 17 ++ .../models/auth/token_response.freezed.dart | 246 +++++++++++++++ lib/data/models/auth/token_response.g.dart | 23 ++ lib/di/injection_container.dart | 11 +- .../login/controllers/login_controller.dart | 80 ++++- .../login/widgets/login_view_redesign.dart | 86 +++--- lib/services/auth_service.dart | 221 ++++++++++++++ pubspec.lock | 16 + pubspec.yaml | 2 + 26 files changed, 2063 insertions(+), 59 deletions(-) create mode 100644 lib/data/datasources/remote/auth_remote_datasource.dart create mode 100644 lib/data/models/auth/auth_user.dart create mode 100644 lib/data/models/auth/auth_user.freezed.dart create mode 100644 lib/data/models/auth/auth_user.g.dart create mode 100644 lib/data/models/auth/login_request.dart create mode 100644 lib/data/models/auth/login_request.freezed.dart create mode 100644 lib/data/models/auth/login_request.g.dart create mode 100644 lib/data/models/auth/login_response.dart create mode 100644 lib/data/models/auth/login_response.freezed.dart create mode 100644 lib/data/models/auth/login_response.g.dart create mode 100644 lib/data/models/auth/logout_request.dart create mode 100644 lib/data/models/auth/logout_request.freezed.dart create mode 100644 lib/data/models/auth/logout_request.g.dart create mode 100644 lib/data/models/auth/refresh_token_request.dart create mode 100644 lib/data/models/auth/refresh_token_request.freezed.dart create mode 100644 lib/data/models/auth/refresh_token_request.g.dart create mode 100644 lib/data/models/auth/token_response.dart create mode 100644 lib/data/models/auth/token_response.freezed.dart create mode 100644 lib/data/models/auth/token_response.g.dart create mode 100644 lib/services/auth_service.dart diff --git a/doc/API_Integration_Plan.md b/doc/API_Integration_Plan.md index 46e3394..aed75ff 100644 --- a/doc/API_Integration_Plan.md +++ b/doc/API_Integration_Plan.md @@ -175,22 +175,22 @@ class EquipmentController extends ChangeNotifier { - POST /api/v1/auth/refresh **작업 Task**: -- [ ] AuthService 클래스 생성 -- [ ] JWT 토큰 저장/관리 로직 구현 +- [x] AuthService 클래스 생성 +- [x] JWT 토큰 저장/관리 로직 구현 - [x] SecureStorage 설정 - - [ ] Access Token 저장 - - [ ] Refresh Token 저장 -- [ ] 로그인 폼 검증 추가 - - [ ] 이메일 형식 검증 - - [ ] 비밀번호 최소 길이 검증 -- [ ] 로그인 실패 에러 처리 - - [ ] 401: 잘못된 인증 정보 + - [x] Access Token 저장 + - [x] Refresh Token 저장 +- [x] 로그인 폼 검증 추가 + - [x] 이메일 형식 검증 + - [x] 비밀번호 최소 길이 검증 +- [x] 로그인 실패 에러 처리 + - [x] 401: 잘못된 인증 정보 - [ ] 429: 너무 많은 시도 - - [ ] 500: 서버 오류 + - [x] 500: 서버 오류 - [ ] 자동 로그인 구현 - [ ] 토큰 유효성 검사 - [ ] 토큰 자동 갱신 -- [ ] 로그아웃 기능 구현 +- [x] 로그아웃 기능 구현 ### 4.2 대시보드 @@ -692,13 +692,13 @@ class ErrorHandler { **1주차: 네트워크 레이어** - [x] Dio 설정 및 인터셉터 구현 - [x] API 클라이언트 기본 구조 -- [ ] 에러 처리 프레임워크 +- [x] 에러 처리 프레임워크 - [x] 환경 설정 관리 -**2주차: 인증 시스템** -- [ ] AuthService 구현 -- [ ] 토큰 관리 로직 -- [ ] 로그인/로그아웃 화면 연동 +**2주차: 인증 시스템** *(2025-07-24 진행)* +- [x] AuthService 구현 +- [x] 토큰 관리 로직 +- [x] 로그인/로그아웃 화면 연동 - [ ] 자동 토큰 갱신 **3주차: 기본 데이터 레이어** @@ -890,4 +890,39 @@ class ErrorHandler { --- -_마지막 업데이트: 2025-07-24_ (네트워크 레이어 및 기본 인프라 구현 완료) \ No newline at end of file +## 🔄 구현 진행 상황 (2025-07-24) + +### 완료된 작업 +1. **Auth 관련 DTO 모델 생성** + - LoginRequest, LoginResponse, TokenResponse, RefreshTokenRequest + - AuthUser, LogoutRequest + - Freezed 패키지 적용 및 코드 생성 완료 + +2. **AuthRemoteDataSource 구현** + - login, logout, refreshToken 메서드 구현 + - 에러 처리 및 응답 변환 로직 완료 + +3. **AuthService 구현** + - 토큰 저장/관리 (SecureStorage 사용) + - 로그인 상태 관리 및 스트림 + - 자동 토큰 갱신 준비 + +4. **로그인 화면 API 연동** + - LoginController 수정 (API 호출 로직 추가) + - 이메일 형식 검증 및 에러 메시지 표시 + - 로딩 상태 관리 + +5. **의존성 주입 설정** + - AuthRemoteDataSource, AuthService DI 등록 + - GetIt을 통한 의존성 관리 + +### 다음 작업 +1. API 서버 실행 및 연동 테스트 +2. 자동 로그인 구현 +3. AuthInterceptor 개선 (AuthService 사용) +4. 로그아웃 기능 UI 추가 +5. 대시보드 및 기타 화면 API 연동 + +--- + +_마지막 업데이트: 2025-07-24_ (인증 시스템 구현 완료) \ No newline at end of file diff --git a/lib/data/datasources/remote/auth_remote_datasource.dart b/lib/data/datasources/remote/auth_remote_datasource.dart new file mode 100644 index 0000000..79192e1 --- /dev/null +++ b/lib/data/datasources/remote/auth_remote_datasource.dart @@ -0,0 +1,104 @@ +import 'package:dartz/dartz.dart'; +import 'package:injectable/injectable.dart'; +import 'package:superport/core/errors/exceptions.dart'; +import 'package:superport/core/errors/failures.dart'; +import 'package:superport/data/datasources/remote/api_client.dart'; +import 'package:superport/data/models/auth/login_request.dart'; +import 'package:superport/data/models/auth/login_response.dart'; +import 'package:superport/data/models/auth/logout_request.dart'; +import 'package:superport/data/models/auth/refresh_token_request.dart'; +import 'package:superport/data/models/auth/token_response.dart'; + +abstract class AuthRemoteDataSource { + Future> login(LoginRequest request); + Future> logout(LogoutRequest request); + Future> refreshToken(RefreshTokenRequest request); +} + +@LazySingleton(as: AuthRemoteDataSource) +class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { + final ApiClient _apiClient; + + AuthRemoteDataSourceImpl(this._apiClient); + + @override + Future> login(LoginRequest request) async { + try { + final response = await _apiClient.post( + '/auth/login', + data: request.toJson(), + ); + + if (response.success && response.data != null) { + final loginResponse = LoginResponse.fromJson(response.data); + return Right(loginResponse); + } else { + return Left(ServerFailure( + message: response.error?.message ?? '로그인 실패', + )); + } + } catch (e) { + if (e is ApiException) { + if (e.statusCode == 401) { + return Left(AuthenticationFailure( + message: '이메일 또는 비밀번호가 올바르지 않습니다.', + )); + } + return Left(ServerFailure(message: e.message)); + } + return Left(ServerFailure(message: '로그인 중 오류가 발생했습니다.')); + } + } + + @override + Future> logout(LogoutRequest request) async { + try { + final response = await _apiClient.post( + '/auth/logout', + data: request.toJson(), + ); + + if (response.success) { + return const Right(null); + } else { + return Left(ServerFailure( + message: response.error?.message ?? '로그아웃 실패', + )); + } + } catch (e) { + if (e is ApiException) { + return Left(ServerFailure(message: e.message)); + } + return Left(ServerFailure(message: '로그아웃 중 오류가 발생했습니다.')); + } + } + + @override + Future> refreshToken(RefreshTokenRequest request) async { + try { + final response = await _apiClient.post( + '/auth/refresh', + data: request.toJson(), + ); + + if (response.success && response.data != null) { + final tokenResponse = TokenResponse.fromJson(response.data); + return Right(tokenResponse); + } else { + return Left(ServerFailure( + message: response.error?.message ?? '토큰 갱신 실패', + )); + } + } catch (e) { + if (e is ApiException) { + if (e.statusCode == 401) { + return Left(AuthenticationFailure( + message: '인증이 만료되었습니다. 다시 로그인해주세요.', + )); + } + return Left(ServerFailure(message: e.message)); + } + return Left(ServerFailure(message: '토큰 갱신 중 오류가 발생했습니다.')); + } + } +} \ No newline at end of file diff --git a/lib/data/models/auth/auth_user.dart b/lib/data/models/auth/auth_user.dart new file mode 100644 index 0000000..a596698 --- /dev/null +++ b/lib/data/models/auth/auth_user.dart @@ -0,0 +1,18 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'auth_user.freezed.dart'; +part 'auth_user.g.dart'; + +@freezed +class AuthUser with _$AuthUser { + const factory AuthUser({ + required int id, + required String email, + @JsonKey(name: 'first_name') required String firstName, + @JsonKey(name: 'last_name') required String lastName, + required String role, + }) = _AuthUser; + + factory AuthUser.fromJson(Map json) => + _$AuthUserFromJson(json); +} \ No newline at end of file diff --git a/lib/data/models/auth/auth_user.freezed.dart b/lib/data/models/auth/auth_user.freezed.dart new file mode 100644 index 0000000..edf17bd --- /dev/null +++ b/lib/data/models/auth/auth_user.freezed.dart @@ -0,0 +1,256 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'auth_user.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +AuthUser _$AuthUserFromJson(Map json) { + return _AuthUser.fromJson(json); +} + +/// @nodoc +mixin _$AuthUser { + int get id => throw _privateConstructorUsedError; + String get email => throw _privateConstructorUsedError; + @JsonKey(name: 'first_name') + String get firstName => throw _privateConstructorUsedError; + @JsonKey(name: 'last_name') + String get lastName => throw _privateConstructorUsedError; + String get role => throw _privateConstructorUsedError; + + /// Serializes this AuthUser to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AuthUser + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AuthUserCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AuthUserCopyWith<$Res> { + factory $AuthUserCopyWith(AuthUser value, $Res Function(AuthUser) then) = + _$AuthUserCopyWithImpl<$Res, AuthUser>; + @useResult + $Res call( + {int id, + String email, + @JsonKey(name: 'first_name') String firstName, + @JsonKey(name: 'last_name') String lastName, + String role}); +} + +/// @nodoc +class _$AuthUserCopyWithImpl<$Res, $Val extends AuthUser> + implements $AuthUserCopyWith<$Res> { + _$AuthUserCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AuthUser + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? email = null, + Object? firstName = null, + Object? lastName = null, + Object? role = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + firstName: null == firstName + ? _value.firstName + : firstName // ignore: cast_nullable_to_non_nullable + as String, + lastName: null == lastName + ? _value.lastName + : lastName // ignore: cast_nullable_to_non_nullable + as String, + role: null == role + ? _value.role + : role // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$AuthUserImplCopyWith<$Res> + implements $AuthUserCopyWith<$Res> { + factory _$$AuthUserImplCopyWith( + _$AuthUserImpl value, $Res Function(_$AuthUserImpl) then) = + __$$AuthUserImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {int id, + String email, + @JsonKey(name: 'first_name') String firstName, + @JsonKey(name: 'last_name') String lastName, + String role}); +} + +/// @nodoc +class __$$AuthUserImplCopyWithImpl<$Res> + extends _$AuthUserCopyWithImpl<$Res, _$AuthUserImpl> + implements _$$AuthUserImplCopyWith<$Res> { + __$$AuthUserImplCopyWithImpl( + _$AuthUserImpl _value, $Res Function(_$AuthUserImpl) _then) + : super(_value, _then); + + /// Create a copy of AuthUser + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? email = null, + Object? firstName = null, + Object? lastName = null, + Object? role = null, + }) { + return _then(_$AuthUserImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + firstName: null == firstName + ? _value.firstName + : firstName // ignore: cast_nullable_to_non_nullable + as String, + lastName: null == lastName + ? _value.lastName + : lastName // ignore: cast_nullable_to_non_nullable + as String, + role: null == role + ? _value.role + : role // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$AuthUserImpl implements _AuthUser { + const _$AuthUserImpl( + {required this.id, + required this.email, + @JsonKey(name: 'first_name') required this.firstName, + @JsonKey(name: 'last_name') required this.lastName, + required this.role}); + + factory _$AuthUserImpl.fromJson(Map json) => + _$$AuthUserImplFromJson(json); + + @override + final int id; + @override + final String email; + @override + @JsonKey(name: 'first_name') + final String firstName; + @override + @JsonKey(name: 'last_name') + final String lastName; + @override + final String role; + + @override + String toString() { + return 'AuthUser(id: $id, email: $email, firstName: $firstName, lastName: $lastName, role: $role)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AuthUserImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.email, email) || other.email == email) && + (identical(other.firstName, firstName) || + other.firstName == firstName) && + (identical(other.lastName, lastName) || + other.lastName == lastName) && + (identical(other.role, role) || other.role == role)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => + Object.hash(runtimeType, id, email, firstName, lastName, role); + + /// Create a copy of AuthUser + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AuthUserImplCopyWith<_$AuthUserImpl> get copyWith => + __$$AuthUserImplCopyWithImpl<_$AuthUserImpl>(this, _$identity); + + @override + Map toJson() { + return _$$AuthUserImplToJson( + this, + ); + } +} + +abstract class _AuthUser implements AuthUser { + const factory _AuthUser( + {required final int id, + required final String email, + @JsonKey(name: 'first_name') required final String firstName, + @JsonKey(name: 'last_name') required final String lastName, + required final String role}) = _$AuthUserImpl; + + factory _AuthUser.fromJson(Map json) = + _$AuthUserImpl.fromJson; + + @override + int get id; + @override + String get email; + @override + @JsonKey(name: 'first_name') + String get firstName; + @override + @JsonKey(name: 'last_name') + String get lastName; + @override + String get role; + + /// Create a copy of AuthUser + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AuthUserImplCopyWith<_$AuthUserImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/data/models/auth/auth_user.g.dart b/lib/data/models/auth/auth_user.g.dart new file mode 100644 index 0000000..c770ffa --- /dev/null +++ b/lib/data/models/auth/auth_user.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'auth_user.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$AuthUserImpl _$$AuthUserImplFromJson(Map json) => + _$AuthUserImpl( + id: (json['id'] as num).toInt(), + email: json['email'] as String, + firstName: json['first_name'] as String, + lastName: json['last_name'] as String, + role: json['role'] as String, + ); + +Map _$$AuthUserImplToJson(_$AuthUserImpl instance) => + { + 'id': instance.id, + 'email': instance.email, + 'first_name': instance.firstName, + 'last_name': instance.lastName, + 'role': instance.role, + }; diff --git a/lib/data/models/auth/login_request.dart b/lib/data/models/auth/login_request.dart new file mode 100644 index 0000000..ba6d9fd --- /dev/null +++ b/lib/data/models/auth/login_request.dart @@ -0,0 +1,15 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'login_request.freezed.dart'; +part 'login_request.g.dart'; + +@freezed +class LoginRequest with _$LoginRequest { + const factory LoginRequest({ + required String email, + required String password, + }) = _LoginRequest; + + factory LoginRequest.fromJson(Map json) => + _$LoginRequestFromJson(json); +} \ No newline at end of file diff --git a/lib/data/models/auth/login_request.freezed.dart b/lib/data/models/auth/login_request.freezed.dart new file mode 100644 index 0000000..010e5dc --- /dev/null +++ b/lib/data/models/auth/login_request.freezed.dart @@ -0,0 +1,183 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'login_request.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +LoginRequest _$LoginRequestFromJson(Map json) { + return _LoginRequest.fromJson(json); +} + +/// @nodoc +mixin _$LoginRequest { + String get email => throw _privateConstructorUsedError; + String get password => throw _privateConstructorUsedError; + + /// Serializes this LoginRequest to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of LoginRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $LoginRequestCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LoginRequestCopyWith<$Res> { + factory $LoginRequestCopyWith( + LoginRequest value, $Res Function(LoginRequest) then) = + _$LoginRequestCopyWithImpl<$Res, LoginRequest>; + @useResult + $Res call({String email, String password}); +} + +/// @nodoc +class _$LoginRequestCopyWithImpl<$Res, $Val extends LoginRequest> + implements $LoginRequestCopyWith<$Res> { + _$LoginRequestCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of LoginRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? email = null, + Object? password = null, + }) { + return _then(_value.copyWith( + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$LoginRequestImplCopyWith<$Res> + implements $LoginRequestCopyWith<$Res> { + factory _$$LoginRequestImplCopyWith( + _$LoginRequestImpl value, $Res Function(_$LoginRequestImpl) then) = + __$$LoginRequestImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String email, String password}); +} + +/// @nodoc +class __$$LoginRequestImplCopyWithImpl<$Res> + extends _$LoginRequestCopyWithImpl<$Res, _$LoginRequestImpl> + implements _$$LoginRequestImplCopyWith<$Res> { + __$$LoginRequestImplCopyWithImpl( + _$LoginRequestImpl _value, $Res Function(_$LoginRequestImpl) _then) + : super(_value, _then); + + /// Create a copy of LoginRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? email = null, + Object? password = null, + }) { + return _then(_$LoginRequestImpl( + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$LoginRequestImpl implements _LoginRequest { + const _$LoginRequestImpl({required this.email, required this.password}); + + factory _$LoginRequestImpl.fromJson(Map json) => + _$$LoginRequestImplFromJson(json); + + @override + final String email; + @override + final String password; + + @override + String toString() { + return 'LoginRequest(email: $email, password: $password)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LoginRequestImpl && + (identical(other.email, email) || other.email == email) && + (identical(other.password, password) || + other.password == password)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, email, password); + + /// Create a copy of LoginRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$LoginRequestImplCopyWith<_$LoginRequestImpl> get copyWith => + __$$LoginRequestImplCopyWithImpl<_$LoginRequestImpl>(this, _$identity); + + @override + Map toJson() { + return _$$LoginRequestImplToJson( + this, + ); + } +} + +abstract class _LoginRequest implements LoginRequest { + const factory _LoginRequest( + {required final String email, + required final String password}) = _$LoginRequestImpl; + + factory _LoginRequest.fromJson(Map json) = + _$LoginRequestImpl.fromJson; + + @override + String get email; + @override + String get password; + + /// Create a copy of LoginRequest + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$LoginRequestImplCopyWith<_$LoginRequestImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/data/models/auth/login_request.g.dart b/lib/data/models/auth/login_request.g.dart new file mode 100644 index 0000000..0a11bd9 --- /dev/null +++ b/lib/data/models/auth/login_request.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'login_request.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$LoginRequestImpl _$$LoginRequestImplFromJson(Map json) => + _$LoginRequestImpl( + email: json['email'] as String, + password: json['password'] as String, + ); + +Map _$$LoginRequestImplToJson(_$LoginRequestImpl instance) => + { + 'email': instance.email, + 'password': instance.password, + }; diff --git a/lib/data/models/auth/login_response.dart b/lib/data/models/auth/login_response.dart new file mode 100644 index 0000000..1dce765 --- /dev/null +++ b/lib/data/models/auth/login_response.dart @@ -0,0 +1,19 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'auth_user.dart'; + +part 'login_response.freezed.dart'; +part 'login_response.g.dart'; + +@freezed +class LoginResponse with _$LoginResponse { + const factory LoginResponse({ + @JsonKey(name: 'access_token') required String accessToken, + @JsonKey(name: 'refresh_token') required String refreshToken, + @JsonKey(name: 'token_type') required String tokenType, + @JsonKey(name: 'expires_in') required int expiresIn, + required AuthUser user, + }) = _LoginResponse; + + factory LoginResponse.fromJson(Map json) => + _$LoginResponseFromJson(json); +} \ No newline at end of file diff --git a/lib/data/models/auth/login_response.freezed.dart b/lib/data/models/auth/login_response.freezed.dart new file mode 100644 index 0000000..403bdc5 --- /dev/null +++ b/lib/data/models/auth/login_response.freezed.dart @@ -0,0 +1,280 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'login_response.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +LoginResponse _$LoginResponseFromJson(Map json) { + return _LoginResponse.fromJson(json); +} + +/// @nodoc +mixin _$LoginResponse { + @JsonKey(name: 'access_token') + String get accessToken => throw _privateConstructorUsedError; + @JsonKey(name: 'refresh_token') + String get refreshToken => throw _privateConstructorUsedError; + @JsonKey(name: 'token_type') + String get tokenType => throw _privateConstructorUsedError; + @JsonKey(name: 'expires_in') + int get expiresIn => throw _privateConstructorUsedError; + AuthUser get user => throw _privateConstructorUsedError; + + /// Serializes this LoginResponse to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of LoginResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $LoginResponseCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LoginResponseCopyWith<$Res> { + factory $LoginResponseCopyWith( + LoginResponse value, $Res Function(LoginResponse) then) = + _$LoginResponseCopyWithImpl<$Res, LoginResponse>; + @useResult + $Res call( + {@JsonKey(name: 'access_token') String accessToken, + @JsonKey(name: 'refresh_token') String refreshToken, + @JsonKey(name: 'token_type') String tokenType, + @JsonKey(name: 'expires_in') int expiresIn, + AuthUser user}); + + $AuthUserCopyWith<$Res> get user; +} + +/// @nodoc +class _$LoginResponseCopyWithImpl<$Res, $Val extends LoginResponse> + implements $LoginResponseCopyWith<$Res> { + _$LoginResponseCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of LoginResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? accessToken = null, + Object? refreshToken = null, + Object? tokenType = null, + Object? expiresIn = null, + Object? user = null, + }) { + return _then(_value.copyWith( + accessToken: null == accessToken + ? _value.accessToken + : accessToken // ignore: cast_nullable_to_non_nullable + as String, + refreshToken: null == refreshToken + ? _value.refreshToken + : refreshToken // ignore: cast_nullable_to_non_nullable + as String, + tokenType: null == tokenType + ? _value.tokenType + : tokenType // ignore: cast_nullable_to_non_nullable + as String, + expiresIn: null == expiresIn + ? _value.expiresIn + : expiresIn // ignore: cast_nullable_to_non_nullable + as int, + user: null == user + ? _value.user + : user // ignore: cast_nullable_to_non_nullable + as AuthUser, + ) as $Val); + } + + /// Create a copy of LoginResponse + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AuthUserCopyWith<$Res> get user { + return $AuthUserCopyWith<$Res>(_value.user, (value) { + return _then(_value.copyWith(user: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$LoginResponseImplCopyWith<$Res> + implements $LoginResponseCopyWith<$Res> { + factory _$$LoginResponseImplCopyWith( + _$LoginResponseImpl value, $Res Function(_$LoginResponseImpl) then) = + __$$LoginResponseImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {@JsonKey(name: 'access_token') String accessToken, + @JsonKey(name: 'refresh_token') String refreshToken, + @JsonKey(name: 'token_type') String tokenType, + @JsonKey(name: 'expires_in') int expiresIn, + AuthUser user}); + + @override + $AuthUserCopyWith<$Res> get user; +} + +/// @nodoc +class __$$LoginResponseImplCopyWithImpl<$Res> + extends _$LoginResponseCopyWithImpl<$Res, _$LoginResponseImpl> + implements _$$LoginResponseImplCopyWith<$Res> { + __$$LoginResponseImplCopyWithImpl( + _$LoginResponseImpl _value, $Res Function(_$LoginResponseImpl) _then) + : super(_value, _then); + + /// Create a copy of LoginResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? accessToken = null, + Object? refreshToken = null, + Object? tokenType = null, + Object? expiresIn = null, + Object? user = null, + }) { + return _then(_$LoginResponseImpl( + accessToken: null == accessToken + ? _value.accessToken + : accessToken // ignore: cast_nullable_to_non_nullable + as String, + refreshToken: null == refreshToken + ? _value.refreshToken + : refreshToken // ignore: cast_nullable_to_non_nullable + as String, + tokenType: null == tokenType + ? _value.tokenType + : tokenType // ignore: cast_nullable_to_non_nullable + as String, + expiresIn: null == expiresIn + ? _value.expiresIn + : expiresIn // ignore: cast_nullable_to_non_nullable + as int, + user: null == user + ? _value.user + : user // ignore: cast_nullable_to_non_nullable + as AuthUser, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$LoginResponseImpl implements _LoginResponse { + const _$LoginResponseImpl( + {@JsonKey(name: 'access_token') required this.accessToken, + @JsonKey(name: 'refresh_token') required this.refreshToken, + @JsonKey(name: 'token_type') required this.tokenType, + @JsonKey(name: 'expires_in') required this.expiresIn, + required this.user}); + + factory _$LoginResponseImpl.fromJson(Map json) => + _$$LoginResponseImplFromJson(json); + + @override + @JsonKey(name: 'access_token') + final String accessToken; + @override + @JsonKey(name: 'refresh_token') + final String refreshToken; + @override + @JsonKey(name: 'token_type') + final String tokenType; + @override + @JsonKey(name: 'expires_in') + final int expiresIn; + @override + final AuthUser user; + + @override + String toString() { + return 'LoginResponse(accessToken: $accessToken, refreshToken: $refreshToken, tokenType: $tokenType, expiresIn: $expiresIn, user: $user)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LoginResponseImpl && + (identical(other.accessToken, accessToken) || + other.accessToken == accessToken) && + (identical(other.refreshToken, refreshToken) || + other.refreshToken == refreshToken) && + (identical(other.tokenType, tokenType) || + other.tokenType == tokenType) && + (identical(other.expiresIn, expiresIn) || + other.expiresIn == expiresIn) && + (identical(other.user, user) || other.user == user)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, accessToken, refreshToken, tokenType, expiresIn, user); + + /// Create a copy of LoginResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$LoginResponseImplCopyWith<_$LoginResponseImpl> get copyWith => + __$$LoginResponseImplCopyWithImpl<_$LoginResponseImpl>(this, _$identity); + + @override + Map toJson() { + return _$$LoginResponseImplToJson( + this, + ); + } +} + +abstract class _LoginResponse implements LoginResponse { + const factory _LoginResponse( + {@JsonKey(name: 'access_token') required final String accessToken, + @JsonKey(name: 'refresh_token') required final String refreshToken, + @JsonKey(name: 'token_type') required final String tokenType, + @JsonKey(name: 'expires_in') required final int expiresIn, + required final AuthUser user}) = _$LoginResponseImpl; + + factory _LoginResponse.fromJson(Map json) = + _$LoginResponseImpl.fromJson; + + @override + @JsonKey(name: 'access_token') + String get accessToken; + @override + @JsonKey(name: 'refresh_token') + String get refreshToken; + @override + @JsonKey(name: 'token_type') + String get tokenType; + @override + @JsonKey(name: 'expires_in') + int get expiresIn; + @override + AuthUser get user; + + /// Create a copy of LoginResponse + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$LoginResponseImplCopyWith<_$LoginResponseImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/data/models/auth/login_response.g.dart b/lib/data/models/auth/login_response.g.dart new file mode 100644 index 0000000..4197561 --- /dev/null +++ b/lib/data/models/auth/login_response.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'login_response.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$LoginResponseImpl _$$LoginResponseImplFromJson(Map json) => + _$LoginResponseImpl( + accessToken: json['access_token'] as String, + refreshToken: json['refresh_token'] as String, + tokenType: json['token_type'] as String, + expiresIn: (json['expires_in'] as num).toInt(), + user: AuthUser.fromJson(json['user'] as Map), + ); + +Map _$$LoginResponseImplToJson(_$LoginResponseImpl instance) => + { + 'access_token': instance.accessToken, + 'refresh_token': instance.refreshToken, + 'token_type': instance.tokenType, + 'expires_in': instance.expiresIn, + 'user': instance.user, + }; diff --git a/lib/data/models/auth/logout_request.dart b/lib/data/models/auth/logout_request.dart new file mode 100644 index 0000000..63179c4 --- /dev/null +++ b/lib/data/models/auth/logout_request.dart @@ -0,0 +1,14 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'logout_request.freezed.dart'; +part 'logout_request.g.dart'; + +@freezed +class LogoutRequest with _$LogoutRequest { + const factory LogoutRequest({ + @JsonKey(name: 'refresh_token') required String refreshToken, + }) = _LogoutRequest; + + factory LogoutRequest.fromJson(Map json) => + _$LogoutRequestFromJson(json); +} \ No newline at end of file diff --git a/lib/data/models/auth/logout_request.freezed.dart b/lib/data/models/auth/logout_request.freezed.dart new file mode 100644 index 0000000..345ca86 --- /dev/null +++ b/lib/data/models/auth/logout_request.freezed.dart @@ -0,0 +1,171 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'logout_request.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +LogoutRequest _$LogoutRequestFromJson(Map json) { + return _LogoutRequest.fromJson(json); +} + +/// @nodoc +mixin _$LogoutRequest { + @JsonKey(name: 'refresh_token') + String get refreshToken => throw _privateConstructorUsedError; + + /// Serializes this LogoutRequest to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of LogoutRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $LogoutRequestCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LogoutRequestCopyWith<$Res> { + factory $LogoutRequestCopyWith( + LogoutRequest value, $Res Function(LogoutRequest) then) = + _$LogoutRequestCopyWithImpl<$Res, LogoutRequest>; + @useResult + $Res call({@JsonKey(name: 'refresh_token') String refreshToken}); +} + +/// @nodoc +class _$LogoutRequestCopyWithImpl<$Res, $Val extends LogoutRequest> + implements $LogoutRequestCopyWith<$Res> { + _$LogoutRequestCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of LogoutRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? refreshToken = null, + }) { + return _then(_value.copyWith( + refreshToken: null == refreshToken + ? _value.refreshToken + : refreshToken // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$LogoutRequestImplCopyWith<$Res> + implements $LogoutRequestCopyWith<$Res> { + factory _$$LogoutRequestImplCopyWith( + _$LogoutRequestImpl value, $Res Function(_$LogoutRequestImpl) then) = + __$$LogoutRequestImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({@JsonKey(name: 'refresh_token') String refreshToken}); +} + +/// @nodoc +class __$$LogoutRequestImplCopyWithImpl<$Res> + extends _$LogoutRequestCopyWithImpl<$Res, _$LogoutRequestImpl> + implements _$$LogoutRequestImplCopyWith<$Res> { + __$$LogoutRequestImplCopyWithImpl( + _$LogoutRequestImpl _value, $Res Function(_$LogoutRequestImpl) _then) + : super(_value, _then); + + /// Create a copy of LogoutRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? refreshToken = null, + }) { + return _then(_$LogoutRequestImpl( + refreshToken: null == refreshToken + ? _value.refreshToken + : refreshToken // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$LogoutRequestImpl implements _LogoutRequest { + const _$LogoutRequestImpl( + {@JsonKey(name: 'refresh_token') required this.refreshToken}); + + factory _$LogoutRequestImpl.fromJson(Map json) => + _$$LogoutRequestImplFromJson(json); + + @override + @JsonKey(name: 'refresh_token') + final String refreshToken; + + @override + String toString() { + return 'LogoutRequest(refreshToken: $refreshToken)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LogoutRequestImpl && + (identical(other.refreshToken, refreshToken) || + other.refreshToken == refreshToken)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, refreshToken); + + /// Create a copy of LogoutRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$LogoutRequestImplCopyWith<_$LogoutRequestImpl> get copyWith => + __$$LogoutRequestImplCopyWithImpl<_$LogoutRequestImpl>(this, _$identity); + + @override + Map toJson() { + return _$$LogoutRequestImplToJson( + this, + ); + } +} + +abstract class _LogoutRequest implements LogoutRequest { + const factory _LogoutRequest( + {@JsonKey(name: 'refresh_token') + required final String refreshToken}) = _$LogoutRequestImpl; + + factory _LogoutRequest.fromJson(Map json) = + _$LogoutRequestImpl.fromJson; + + @override + @JsonKey(name: 'refresh_token') + String get refreshToken; + + /// Create a copy of LogoutRequest + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$LogoutRequestImplCopyWith<_$LogoutRequestImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/data/models/auth/logout_request.g.dart b/lib/data/models/auth/logout_request.g.dart new file mode 100644 index 0000000..620221b --- /dev/null +++ b/lib/data/models/auth/logout_request.g.dart @@ -0,0 +1,17 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'logout_request.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$LogoutRequestImpl _$$LogoutRequestImplFromJson(Map json) => + _$LogoutRequestImpl( + refreshToken: json['refresh_token'] as String, + ); + +Map _$$LogoutRequestImplToJson(_$LogoutRequestImpl instance) => + { + 'refresh_token': instance.refreshToken, + }; diff --git a/lib/data/models/auth/refresh_token_request.dart b/lib/data/models/auth/refresh_token_request.dart new file mode 100644 index 0000000..f103195 --- /dev/null +++ b/lib/data/models/auth/refresh_token_request.dart @@ -0,0 +1,14 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'refresh_token_request.freezed.dart'; +part 'refresh_token_request.g.dart'; + +@freezed +class RefreshTokenRequest with _$RefreshTokenRequest { + const factory RefreshTokenRequest({ + @JsonKey(name: 'refresh_token') required String refreshToken, + }) = _RefreshTokenRequest; + + factory RefreshTokenRequest.fromJson(Map json) => + _$RefreshTokenRequestFromJson(json); +} \ No newline at end of file diff --git a/lib/data/models/auth/refresh_token_request.freezed.dart b/lib/data/models/auth/refresh_token_request.freezed.dart new file mode 100644 index 0000000..6685b7d --- /dev/null +++ b/lib/data/models/auth/refresh_token_request.freezed.dart @@ -0,0 +1,172 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'refresh_token_request.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +RefreshTokenRequest _$RefreshTokenRequestFromJson(Map json) { + return _RefreshTokenRequest.fromJson(json); +} + +/// @nodoc +mixin _$RefreshTokenRequest { + @JsonKey(name: 'refresh_token') + String get refreshToken => throw _privateConstructorUsedError; + + /// Serializes this RefreshTokenRequest to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of RefreshTokenRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $RefreshTokenRequestCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $RefreshTokenRequestCopyWith<$Res> { + factory $RefreshTokenRequestCopyWith( + RefreshTokenRequest value, $Res Function(RefreshTokenRequest) then) = + _$RefreshTokenRequestCopyWithImpl<$Res, RefreshTokenRequest>; + @useResult + $Res call({@JsonKey(name: 'refresh_token') String refreshToken}); +} + +/// @nodoc +class _$RefreshTokenRequestCopyWithImpl<$Res, $Val extends RefreshTokenRequest> + implements $RefreshTokenRequestCopyWith<$Res> { + _$RefreshTokenRequestCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of RefreshTokenRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? refreshToken = null, + }) { + return _then(_value.copyWith( + refreshToken: null == refreshToken + ? _value.refreshToken + : refreshToken // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$RefreshTokenRequestImplCopyWith<$Res> + implements $RefreshTokenRequestCopyWith<$Res> { + factory _$$RefreshTokenRequestImplCopyWith(_$RefreshTokenRequestImpl value, + $Res Function(_$RefreshTokenRequestImpl) then) = + __$$RefreshTokenRequestImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({@JsonKey(name: 'refresh_token') String refreshToken}); +} + +/// @nodoc +class __$$RefreshTokenRequestImplCopyWithImpl<$Res> + extends _$RefreshTokenRequestCopyWithImpl<$Res, _$RefreshTokenRequestImpl> + implements _$$RefreshTokenRequestImplCopyWith<$Res> { + __$$RefreshTokenRequestImplCopyWithImpl(_$RefreshTokenRequestImpl _value, + $Res Function(_$RefreshTokenRequestImpl) _then) + : super(_value, _then); + + /// Create a copy of RefreshTokenRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? refreshToken = null, + }) { + return _then(_$RefreshTokenRequestImpl( + refreshToken: null == refreshToken + ? _value.refreshToken + : refreshToken // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$RefreshTokenRequestImpl implements _RefreshTokenRequest { + const _$RefreshTokenRequestImpl( + {@JsonKey(name: 'refresh_token') required this.refreshToken}); + + factory _$RefreshTokenRequestImpl.fromJson(Map json) => + _$$RefreshTokenRequestImplFromJson(json); + + @override + @JsonKey(name: 'refresh_token') + final String refreshToken; + + @override + String toString() { + return 'RefreshTokenRequest(refreshToken: $refreshToken)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$RefreshTokenRequestImpl && + (identical(other.refreshToken, refreshToken) || + other.refreshToken == refreshToken)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, refreshToken); + + /// Create a copy of RefreshTokenRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$RefreshTokenRequestImplCopyWith<_$RefreshTokenRequestImpl> get copyWith => + __$$RefreshTokenRequestImplCopyWithImpl<_$RefreshTokenRequestImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$RefreshTokenRequestImplToJson( + this, + ); + } +} + +abstract class _RefreshTokenRequest implements RefreshTokenRequest { + const factory _RefreshTokenRequest( + {@JsonKey(name: 'refresh_token') + required final String refreshToken}) = _$RefreshTokenRequestImpl; + + factory _RefreshTokenRequest.fromJson(Map json) = + _$RefreshTokenRequestImpl.fromJson; + + @override + @JsonKey(name: 'refresh_token') + String get refreshToken; + + /// Create a copy of RefreshTokenRequest + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$RefreshTokenRequestImplCopyWith<_$RefreshTokenRequestImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/data/models/auth/refresh_token_request.g.dart b/lib/data/models/auth/refresh_token_request.g.dart new file mode 100644 index 0000000..2569fe0 --- /dev/null +++ b/lib/data/models/auth/refresh_token_request.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'refresh_token_request.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$RefreshTokenRequestImpl _$$RefreshTokenRequestImplFromJson( + Map json) => + _$RefreshTokenRequestImpl( + refreshToken: json['refresh_token'] as String, + ); + +Map _$$RefreshTokenRequestImplToJson( + _$RefreshTokenRequestImpl instance) => + { + 'refresh_token': instance.refreshToken, + }; diff --git a/lib/data/models/auth/token_response.dart b/lib/data/models/auth/token_response.dart new file mode 100644 index 0000000..88bbb97 --- /dev/null +++ b/lib/data/models/auth/token_response.dart @@ -0,0 +1,17 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'token_response.freezed.dart'; +part 'token_response.g.dart'; + +@freezed +class TokenResponse with _$TokenResponse { + const factory TokenResponse({ + @JsonKey(name: 'access_token') required String accessToken, + @JsonKey(name: 'refresh_token') required String refreshToken, + @JsonKey(name: 'token_type') required String tokenType, + @JsonKey(name: 'expires_in') required int expiresIn, + }) = _TokenResponse; + + factory TokenResponse.fromJson(Map json) => + _$TokenResponseFromJson(json); +} \ No newline at end of file diff --git a/lib/data/models/auth/token_response.freezed.dart b/lib/data/models/auth/token_response.freezed.dart new file mode 100644 index 0000000..0a1073b --- /dev/null +++ b/lib/data/models/auth/token_response.freezed.dart @@ -0,0 +1,246 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'token_response.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +TokenResponse _$TokenResponseFromJson(Map json) { + return _TokenResponse.fromJson(json); +} + +/// @nodoc +mixin _$TokenResponse { + @JsonKey(name: 'access_token') + String get accessToken => throw _privateConstructorUsedError; + @JsonKey(name: 'refresh_token') + String get refreshToken => throw _privateConstructorUsedError; + @JsonKey(name: 'token_type') + String get tokenType => throw _privateConstructorUsedError; + @JsonKey(name: 'expires_in') + int get expiresIn => throw _privateConstructorUsedError; + + /// Serializes this TokenResponse to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of TokenResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $TokenResponseCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $TokenResponseCopyWith<$Res> { + factory $TokenResponseCopyWith( + TokenResponse value, $Res Function(TokenResponse) then) = + _$TokenResponseCopyWithImpl<$Res, TokenResponse>; + @useResult + $Res call( + {@JsonKey(name: 'access_token') String accessToken, + @JsonKey(name: 'refresh_token') String refreshToken, + @JsonKey(name: 'token_type') String tokenType, + @JsonKey(name: 'expires_in') int expiresIn}); +} + +/// @nodoc +class _$TokenResponseCopyWithImpl<$Res, $Val extends TokenResponse> + implements $TokenResponseCopyWith<$Res> { + _$TokenResponseCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of TokenResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? accessToken = null, + Object? refreshToken = null, + Object? tokenType = null, + Object? expiresIn = null, + }) { + return _then(_value.copyWith( + accessToken: null == accessToken + ? _value.accessToken + : accessToken // ignore: cast_nullable_to_non_nullable + as String, + refreshToken: null == refreshToken + ? _value.refreshToken + : refreshToken // ignore: cast_nullable_to_non_nullable + as String, + tokenType: null == tokenType + ? _value.tokenType + : tokenType // ignore: cast_nullable_to_non_nullable + as String, + expiresIn: null == expiresIn + ? _value.expiresIn + : expiresIn // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$TokenResponseImplCopyWith<$Res> + implements $TokenResponseCopyWith<$Res> { + factory _$$TokenResponseImplCopyWith( + _$TokenResponseImpl value, $Res Function(_$TokenResponseImpl) then) = + __$$TokenResponseImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {@JsonKey(name: 'access_token') String accessToken, + @JsonKey(name: 'refresh_token') String refreshToken, + @JsonKey(name: 'token_type') String tokenType, + @JsonKey(name: 'expires_in') int expiresIn}); +} + +/// @nodoc +class __$$TokenResponseImplCopyWithImpl<$Res> + extends _$TokenResponseCopyWithImpl<$Res, _$TokenResponseImpl> + implements _$$TokenResponseImplCopyWith<$Res> { + __$$TokenResponseImplCopyWithImpl( + _$TokenResponseImpl _value, $Res Function(_$TokenResponseImpl) _then) + : super(_value, _then); + + /// Create a copy of TokenResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? accessToken = null, + Object? refreshToken = null, + Object? tokenType = null, + Object? expiresIn = null, + }) { + return _then(_$TokenResponseImpl( + accessToken: null == accessToken + ? _value.accessToken + : accessToken // ignore: cast_nullable_to_non_nullable + as String, + refreshToken: null == refreshToken + ? _value.refreshToken + : refreshToken // ignore: cast_nullable_to_non_nullable + as String, + tokenType: null == tokenType + ? _value.tokenType + : tokenType // ignore: cast_nullable_to_non_nullable + as String, + expiresIn: null == expiresIn + ? _value.expiresIn + : expiresIn // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$TokenResponseImpl implements _TokenResponse { + const _$TokenResponseImpl( + {@JsonKey(name: 'access_token') required this.accessToken, + @JsonKey(name: 'refresh_token') required this.refreshToken, + @JsonKey(name: 'token_type') required this.tokenType, + @JsonKey(name: 'expires_in') required this.expiresIn}); + + factory _$TokenResponseImpl.fromJson(Map json) => + _$$TokenResponseImplFromJson(json); + + @override + @JsonKey(name: 'access_token') + final String accessToken; + @override + @JsonKey(name: 'refresh_token') + final String refreshToken; + @override + @JsonKey(name: 'token_type') + final String tokenType; + @override + @JsonKey(name: 'expires_in') + final int expiresIn; + + @override + String toString() { + return 'TokenResponse(accessToken: $accessToken, refreshToken: $refreshToken, tokenType: $tokenType, expiresIn: $expiresIn)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$TokenResponseImpl && + (identical(other.accessToken, accessToken) || + other.accessToken == accessToken) && + (identical(other.refreshToken, refreshToken) || + other.refreshToken == refreshToken) && + (identical(other.tokenType, tokenType) || + other.tokenType == tokenType) && + (identical(other.expiresIn, expiresIn) || + other.expiresIn == expiresIn)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => + Object.hash(runtimeType, accessToken, refreshToken, tokenType, expiresIn); + + /// Create a copy of TokenResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$TokenResponseImplCopyWith<_$TokenResponseImpl> get copyWith => + __$$TokenResponseImplCopyWithImpl<_$TokenResponseImpl>(this, _$identity); + + @override + Map toJson() { + return _$$TokenResponseImplToJson( + this, + ); + } +} + +abstract class _TokenResponse implements TokenResponse { + const factory _TokenResponse( + {@JsonKey(name: 'access_token') required final String accessToken, + @JsonKey(name: 'refresh_token') required final String refreshToken, + @JsonKey(name: 'token_type') required final String tokenType, + @JsonKey(name: 'expires_in') required final int expiresIn}) = + _$TokenResponseImpl; + + factory _TokenResponse.fromJson(Map json) = + _$TokenResponseImpl.fromJson; + + @override + @JsonKey(name: 'access_token') + String get accessToken; + @override + @JsonKey(name: 'refresh_token') + String get refreshToken; + @override + @JsonKey(name: 'token_type') + String get tokenType; + @override + @JsonKey(name: 'expires_in') + int get expiresIn; + + /// Create a copy of TokenResponse + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$TokenResponseImplCopyWith<_$TokenResponseImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/data/models/auth/token_response.g.dart b/lib/data/models/auth/token_response.g.dart new file mode 100644 index 0000000..2e042d3 --- /dev/null +++ b/lib/data/models/auth/token_response.g.dart @@ -0,0 +1,23 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'token_response.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$TokenResponseImpl _$$TokenResponseImplFromJson(Map json) => + _$TokenResponseImpl( + accessToken: json['access_token'] as String, + refreshToken: json['refresh_token'] as String, + tokenType: json['token_type'] as String, + expiresIn: (json['expires_in'] as num).toInt(), + ); + +Map _$$TokenResponseImplToJson(_$TokenResponseImpl instance) => + { + 'access_token': instance.accessToken, + 'refresh_token': instance.refreshToken, + 'token_type': instance.tokenType, + 'expires_in': instance.expiresIn, + }; diff --git a/lib/di/injection_container.dart b/lib/di/injection_container.dart index c571082..274dfe1 100644 --- a/lib/di/injection_container.dart +++ b/lib/di/injection_container.dart @@ -3,6 +3,8 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:get_it/get_it.dart'; import '../core/config/environment.dart'; import '../data/datasources/remote/api_client.dart'; +import '../data/datasources/remote/auth_remote_datasource.dart'; +import '../services/auth_service.dart'; /// GetIt 인스턴스 final getIt = GetIt.instance; @@ -20,7 +22,14 @@ Future setupDependencies() async { getIt.registerLazySingleton(() => ApiClient()); // 데이터소스 - // TODO: Remote datasources will be registered here + getIt.registerLazySingleton( + () => AuthRemoteDataSourceImpl(getIt()), + ); + + // 서비스 + getIt.registerLazySingleton( + () => AuthServiceImpl(getIt(), getIt()), + ); // 리포지토리 // TODO: Repositories will be registered here diff --git a/lib/screens/login/controllers/login_controller.dart b/lib/screens/login/controllers/login_controller.dart index 68ef78f..9376ab2 100644 --- a/lib/screens/login/controllers/login_controller.dart +++ b/lib/screens/login/controllers/login_controller.dart @@ -1,7 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:superport/core/errors/failures.dart'; +import 'package:superport/data/models/auth/login_request.dart'; +import 'package:superport/di/injection_container.dart'; +import 'package:superport/services/auth_service.dart'; /// 로그인 화면의 상태 및 비즈니스 로직을 담당하는 ChangeNotifier 기반 컨트롤러 class LoginController extends ChangeNotifier { + final AuthService _authService = inject(); /// 아이디 입력 컨트롤러 final TextEditingController idController = TextEditingController(); @@ -16,6 +21,14 @@ class LoginController extends ChangeNotifier { /// 아이디 저장 여부 bool saveId = false; + + /// 로딩 상태 + bool _isLoading = false; + bool get isLoading => _isLoading; + + /// 에러 메시지 + String? _errorMessage; + String? get errorMessage => _errorMessage; /// 아이디 저장 체크박스 상태 변경 void setSaveId(bool value) { @@ -23,11 +36,68 @@ class LoginController extends ChangeNotifier { notifyListeners(); } - /// 로그인 처리 (샘플) - bool login() { - // 실제 인증 로직은 구현하지 않음 - // 항상 true 반환 (샘플) - return true; + /// 로그인 처리 + Future login() async { + // 입력값 검증 + if (idController.text.trim().isEmpty) { + _errorMessage = '이메일을 입력해주세요.'; + notifyListeners(); + return false; + } + + if (pwController.text.isEmpty) { + _errorMessage = '비밀번호를 입력해주세요.'; + notifyListeners(); + return false; + } + + // 이메일 형식 검증 + final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); + if (!emailRegex.hasMatch(idController.text.trim())) { + _errorMessage = '올바른 이메일 형식이 아닙니다.'; + notifyListeners(); + return false; + } + + // 로딩 시작 + _isLoading = true; + _errorMessage = null; + notifyListeners(); + + try { + // 로그인 요청 + final request = LoginRequest( + email: idController.text.trim(), + password: pwController.text, + ); + + final result = await _authService.login(request); + + return result.fold( + (failure) { + _errorMessage = failure.message; + _isLoading = false; + notifyListeners(); + return false; + }, + (loginResponse) { + _isLoading = false; + notifyListeners(); + return true; + }, + ); + } catch (e) { + _errorMessage = '로그인 중 오류가 발생했습니다.'; + _isLoading = false; + notifyListeners(); + return false; + } + } + + /// 에러 메시지 초기화 + void clearError() { + _errorMessage = null; + notifyListeners(); } @override diff --git a/lib/screens/login/widgets/login_view_redesign.dart b/lib/screens/login/widgets/login_view_redesign.dart index 9d1e68f..9c7af0e 100644 --- a/lib/screens/login/widgets/login_view_redesign.dart +++ b/lib/screens/login/widgets/login_view_redesign.dart @@ -27,10 +27,7 @@ class _LoginViewRedesignState extends State late AnimationController _slideController; late Animation _slideAnimation; - final _usernameController = TextEditingController(); - final _passwordController = TextEditingController(); bool _rememberMe = false; - bool _isLoading = false; @override void initState() { @@ -66,39 +63,23 @@ class _LoginViewRedesignState extends State void dispose() { _fadeController.dispose(); _slideController.dispose(); - _usernameController.dispose(); - _passwordController.dispose(); super.dispose(); } Future _handleLogin() async { - if (_usernameController.text.isEmpty || _passwordController.text.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('사용자명과 비밀번호를 입력해주세요.'), - backgroundColor: ShadcnTheme.destructive, - ), - ); - return; + final success = await widget.controller.login(); + if (success) { + widget.onLoginSuccess(); } - - setState(() { - _isLoading = true; - }); - - // 실제 로그인 로직 (임시로 2초 대기) - await Future.delayed(const Duration(seconds: 2)); - - setState(() { - _isLoading = false; - }); - - widget.onLoginSuccess(); } @override Widget build(BuildContext context) { - return Scaffold( + return ChangeNotifierProvider.value( + value: widget.controller, + child: Consumer( + builder: (context, controller, _) { + return Scaffold( backgroundColor: ShadcnTheme.background, body: SafeArea( child: Center( @@ -128,6 +109,8 @@ class _LoginViewRedesignState extends State ), ), ), + ); + }, ), ); } @@ -190,6 +173,7 @@ class _LoginViewRedesignState extends State } Widget _buildLoginCard() { + final controller = context.watch(); return ShadcnCard( padding: const EdgeInsets.all(ShadcnTheme.spacing8), child: Column( @@ -210,7 +194,7 @@ class _LoginViewRedesignState extends State ShadcnInput( label: '사용자명', placeholder: '사용자명을 입력하세요', - controller: _usernameController, + controller: controller.idController, prefixIcon: const Icon(Icons.person_outline), keyboardType: TextInputType.text, ), @@ -220,7 +204,7 @@ class _LoginViewRedesignState extends State ShadcnInput( label: '비밀번호', placeholder: '비밀번호를 입력하세요', - controller: _passwordController, + controller: controller.pwController, prefixIcon: const Icon(Icons.lock_outline), obscureText: true, keyboardType: TextInputType.visiblePassword, @@ -231,11 +215,9 @@ class _LoginViewRedesignState extends State Row( children: [ Checkbox( - value: _rememberMe, + value: controller.saveId, onChanged: (value) { - setState(() { - _rememberMe = value ?? false; - }); + controller.setSaveId(value ?? false); }, activeColor: ShadcnTheme.primary, ), @@ -244,6 +226,38 @@ class _LoginViewRedesignState extends State ], ), const SizedBox(height: ShadcnTheme.spacing8), + + // 에러 메시지 표시 + if (controller.errorMessage != null) + Container( + padding: const EdgeInsets.all(ShadcnTheme.spacing3), + margin: const EdgeInsets.only(bottom: ShadcnTheme.spacing4), + decoration: BoxDecoration( + color: ShadcnTheme.destructive.withOpacity(0.1), + borderRadius: BorderRadius.circular(ShadcnTheme.borderRadius), + border: Border.all( + color: ShadcnTheme.destructive.withOpacity(0.3), + ), + ), + child: Row( + children: [ + Icon( + Icons.error_outline, + size: 20, + color: ShadcnTheme.destructive, + ), + const SizedBox(width: ShadcnTheme.spacing2), + Expanded( + child: Text( + controller.errorMessage!, + style: ShadcnTheme.bodyMedium.copyWith( + color: ShadcnTheme.destructive, + ), + ), + ), + ], + ), + ), // 로그인 버튼 ShadcnButton( @@ -253,7 +267,7 @@ class _LoginViewRedesignState extends State textColor: Colors.white, size: ShadcnButtonSize.large, fullWidth: true, - loading: _isLoading, + loading: controller.isLoading, ), const SizedBox(height: ShadcnTheme.spacing4), @@ -261,8 +275,8 @@ class _LoginViewRedesignState extends State ShadcnButton( text: '테스트 로그인', onPressed: () { - _usernameController.text = 'admin'; - _passwordController.text = 'password'; + controller.idController.text = 'admin@example.com'; + controller.pwController.text = 'admin123'; _handleLogin(); }, variant: ShadcnButtonVariant.secondary, diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart new file mode 100644 index 0000000..c7cd858 --- /dev/null +++ b/lib/services/auth_service.dart @@ -0,0 +1,221 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:dartz/dartz.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:injectable/injectable.dart'; +import 'package:superport/core/errors/failures.dart'; +import 'package:superport/data/datasources/remote/auth_remote_datasource.dart'; +import 'package:superport/data/models/auth/auth_user.dart'; +import 'package:superport/data/models/auth/login_request.dart'; +import 'package:superport/data/models/auth/login_response.dart'; +import 'package:superport/data/models/auth/logout_request.dart'; +import 'package:superport/data/models/auth/refresh_token_request.dart'; +import 'package:superport/data/models/auth/token_response.dart'; + +abstract class AuthService { + Future> login(LoginRequest request); + Future> logout(); + Future> refreshToken(); + Future isLoggedIn(); + Future getCurrentUser(); + Future getAccessToken(); + Future getRefreshToken(); + Future clearSession(); + Stream get authStateChanges; +} + +@LazySingleton(as: AuthService) +class AuthServiceImpl implements AuthService { + final AuthRemoteDataSource _authRemoteDataSource; + final FlutterSecureStorage _secureStorage; + + static const String _accessTokenKey = 'access_token'; + static const String _refreshTokenKey = 'refresh_token'; + static const String _userKey = 'user'; + static const String _tokenExpiryKey = 'token_expiry'; + + final _authStateController = StreamController.broadcast(); + + AuthServiceImpl( + this._authRemoteDataSource, + this._secureStorage, + ); + + @override + Future> login(LoginRequest request) async { + try { + final result = await _authRemoteDataSource.login(request); + + return await result.fold( + (failure) async => Left(failure), + (loginResponse) async { + // 토큰 및 사용자 정보 저장 + await _saveTokens( + loginResponse.accessToken, + loginResponse.refreshToken, + loginResponse.expiresIn, + ); + await _saveUser(loginResponse.user); + + // 인증 상태 변경 알림 + _authStateController.add(true); + + return Right(loginResponse); + }, + ); + } catch (e) { + return Left(ServerFailure(message: '로그인 처리 중 오류가 발생했습니다.')); + } + } + + @override + Future> logout() async { + try { + final refreshToken = await getRefreshToken(); + if (refreshToken != null) { + final request = LogoutRequest(refreshToken: refreshToken); + await _authRemoteDataSource.logout(request); + } + + await clearSession(); + _authStateController.add(false); + + return const Right(null); + } catch (e) { + // 로그아웃 API 실패 시에도 로컬 세션은 정리 + await clearSession(); + _authStateController.add(false); + return const Right(null); + } + } + + @override + Future> refreshToken() async { + try { + final refreshToken = await getRefreshToken(); + if (refreshToken == null) { + return Left(AuthenticationFailure( + message: '리프레시 토큰이 없습니다. 다시 로그인해주세요.', + )); + } + + final request = RefreshTokenRequest(refreshToken: refreshToken); + final result = await _authRemoteDataSource.refreshToken(request); + + return await result.fold( + (failure) async { + // 토큰 갱신 실패 시 세션 정리 + await clearSession(); + _authStateController.add(false); + return Left(failure); + }, + (tokenResponse) async { + // 새 토큰 저장 + await _saveTokens( + tokenResponse.accessToken, + tokenResponse.refreshToken, + tokenResponse.expiresIn, + ); + return Right(tokenResponse); + }, + ); + } catch (e) { + return Left(ServerFailure(message: '토큰 갱신 중 오류가 발생했습니다.')); + } + } + + @override + Future isLoggedIn() async { + try { + final accessToken = await getAccessToken(); + if (accessToken == null) return false; + + // 토큰 만료 확인 + final expiryStr = await _secureStorage.read(key: _tokenExpiryKey); + if (expiryStr != null) { + final expiry = DateTime.parse(expiryStr); + if (DateTime.now().isAfter(expiry)) { + // 토큰이 만료되었으면 갱신 시도 + final refreshResult = await refreshToken(); + return refreshResult.isRight(); + } + } + + return true; + } catch (e) { + return false; + } + } + + @override + Future getCurrentUser() async { + try { + final userStr = await _secureStorage.read(key: _userKey); + if (userStr == null) return null; + + final userJson = jsonDecode(userStr); + return AuthUser.fromJson(userJson); + } catch (e) { + return null; + } + } + + @override + Future getAccessToken() async { + try { + return await _secureStorage.read(key: _accessTokenKey); + } catch (e) { + return null; + } + } + + @override + Future getRefreshToken() async { + try { + return await _secureStorage.read(key: _refreshTokenKey); + } catch (e) { + return null; + } + } + + @override + Future clearSession() async { + try { + await _secureStorage.delete(key: _accessTokenKey); + await _secureStorage.delete(key: _refreshTokenKey); + await _secureStorage.delete(key: _userKey); + await _secureStorage.delete(key: _tokenExpiryKey); + } catch (e) { + // 에러가 발생해도 계속 진행 + } + } + + @override + Stream get authStateChanges => _authStateController.stream; + + Future _saveTokens( + String accessToken, + String refreshToken, + int expiresIn, + ) async { + await _secureStorage.write(key: _accessTokenKey, value: accessToken); + await _secureStorage.write(key: _refreshTokenKey, value: refreshToken); + + // 토큰 만료 시간 저장 (현재 시간 + expiresIn 초) + final expiry = DateTime.now().add(Duration(seconds: expiresIn)); + await _secureStorage.write( + key: _tokenExpiryKey, + value: expiry.toIso8601String(), + ); + } + + Future _saveUser(AuthUser user) async { + final userJson = jsonEncode(user.toJson()); + await _secureStorage.write(key: _userKey, value: userJson); + } + + void dispose() { + _authStateController.close(); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 20caff3..21c3780 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -346,6 +346,22 @@ packages: description: flutter source: sdk version: "0.0.0" + freezed: + dependency: "direct dev" + description: + name: freezed + sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" + url: "https://pub.dev" + source: hosted + version: "2.5.7" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + url: "https://pub.dev" + source: hosted + version: "2.4.4" frontend_server_client: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9080a73..7db8e02 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,6 +31,7 @@ dependencies: # JSON 처리 json_annotation: ^4.8.1 + freezed_annotation: ^2.4.1 # 환경 설정 flutter_dotenv: ^5.1.0 @@ -51,6 +52,7 @@ dev_dependencies: build_runner: ^2.4.8 json_serializable: ^6.7.1 injectable_generator: ^2.4.1 + freezed: ^2.4.6 flutter: uses-material-design: true