diff --git a/front/lib/app/config/routes.dart b/front/lib/app/config/routes.dart index b616b38..7d69c41 100644 --- a/front/lib/app/config/routes.dart +++ b/front/lib/app/config/routes.dart @@ -55,14 +55,16 @@ final router = GoRouter(initialLocation: '/', routes: [ path: 'teamDetail/:teamId', name: 'teamDetail/:teamId', builder: (context, state) { - return TeamDetailView(state.pathParameters['teamId']!); + return TeamDetailView(int.parse(state.pathParameters['teamId']!)); }, ), GoRoute( - path: 'projectDetail/:projectId', - name: 'projectDetail/:projectId', + path: 'projectDetail/:teamId/:projectId', + name: 'projectDetail/:teamId/:projectId', builder: (context, state) => ProjectDetailScreen( - projectId: int.parse(state.pathParameters['projectId']!)), + teamId: int.parse(state.pathParameters['teamId']!), + projectId: int.parse(state.pathParameters['projectId']!), + ), ), GoRoute( path: 'projectCreate/:teamId', @@ -71,10 +73,10 @@ final router = GoRouter(initialLocation: '/', routes: [ teamId: int.parse(state.pathParameters['teamId']!)), ), GoRoute( - path: 'projectUpdate', - name: 'projectUpdate', + path: 'projectUpdate/:teamId', + name: 'projectUpdate/:teamId', builder: (context, state) => - ProjectUpdateScreen(project: state.extra! as Project), + ProjectUpdateScreen(project: state.extra! as Project, teamId: int.parse(state.pathParameters['teamId']!)), ), GoRoute( path: 'main', diff --git a/front/lib/core/config/custom_interceptor.dart b/front/lib/core/config/custom_interceptor.dart index 593efee..1cd798f 100644 --- a/front/lib/core/config/custom_interceptor.dart +++ b/front/lib/core/config/custom_interceptor.dart @@ -1,7 +1,13 @@ +import 'dart:io'; + +import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart' hide Options; import 'package:front/core/const/const.dart'; +import 'package:front/core/utils/exception.dart'; +import 'package:front/core/utils/failure.dart'; ///토큰을 헤더에 추가, 에러를 다루기 위한 인터셉터 class CustomInterceptor extends Interceptor { @@ -101,3 +107,43 @@ class CustomInterceptor extends Interceptor { // return handler.reject(err); // } } + +mixin ErrorHandler { + Future> catchError(Future Function() f) async { + try { + return Future.value(Right(await f())); + } on DioException catch (err) { + debugPrint(err.response?.data.toString()); + switch (err.type) { + case DioExceptionType.badCertificate: + return Future.value(Left(ResponseFailure.unknown.getFailure())); + case DioExceptionType.badResponse: + return Future.value(Left(ResponseFailure.values + .firstWhere((value) => value.code == err.response?.statusCode, + orElse: () => ResponseFailure.unknown) + .failure + .copyWith(message: err.response?.data['message']))); + case DioExceptionType.cancel: + return Future.value(Left(ResponseFailure.cancel.getFailure())); + case DioExceptionType.connectionError: + return Future.value( + Left(ResponseFailure.connectionError.getFailure())); + case DioExceptionType.connectionTimeout: + return Future.value( + Left(ResponseFailure.connectionTimeout.getFailure())); + case DioExceptionType.receiveTimeout: + return Future.value( + Left(ResponseFailure.receiveTimeout.getFailure())); + case DioExceptionType.sendTimeout: + return Future.value(Left(ResponseFailure.sendTimeout.getFailure())); + case DioExceptionType.unknown: + return Future.value(Left(ResponseFailure.unknown.getFailure())); + } + } on ServerException { + return Future.value(const Left(ServerFailure('An error has occurred'))); + } on SocketException { + return Future.value( + const Left(ServerFailure('Failed to connect to the network'))); + } + } +} diff --git a/front/lib/core/config/providers/dio.dart b/front/lib/core/config/providers/dio.dart index fbcf920..b38f43e 100644 --- a/front/lib/core/config/providers/dio.dart +++ b/front/lib/core/config/providers/dio.dart @@ -1,26 +1,14 @@ import 'package:dio/dio.dart'; import 'package:front/core/config/custom_interceptor.dart'; import 'package:front/core/config/providers/secure_storage.dart'; +import 'package:front/core/network_handling/app_dio.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'dio.g.dart'; @riverpod Dio dio(DioRef ref) { - var dio = Dio( - BaseOptions( - baseUrl: - 'http://ec2-3-34-95-39.ap-northeast-2.compute.amazonaws.com:8083', - connectTimeout: const Duration(milliseconds: 5000), - receiveTimeout: const Duration(milliseconds: 3000), - ), - ); - - var storage = ref.watch(secureStorageProvider); - - dio.interceptors.add( - CustomInterceptor(storage: storage), - ); + var dio = AppDio.instance; return dio; } diff --git a/front/lib/core/network_handling/app_dio.dart b/front/lib/core/network_handling/app_dio.dart index c034f58..3c8cbc4 100644 --- a/front/lib/core/network_handling/app_dio.dart +++ b/front/lib/core/network_handling/app_dio.dart @@ -24,6 +24,7 @@ class _AppDio with DioMixin implements Dio { connectTimeout: Duration(milliseconds: 30000), receiveTimeout: Duration(milliseconds: 30000), sendTimeout: Duration(milliseconds: 30000), + baseUrl: 'http://ec2-3-34-95-39.ap-northeast-2.compute.amazonaws.com:8083', receiveDataWhenStatusError: true, ); diff --git a/front/lib/core/network_handling/interceptors/token_interceptor.dart b/front/lib/core/network_handling/interceptors/token_interceptor.dart index 6c61f45..451067b 100644 --- a/front/lib/core/network_handling/interceptors/token_interceptor.dart +++ b/front/lib/core/network_handling/interceptors/token_interceptor.dart @@ -1,4 +1,5 @@ import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:front/core/const/const.dart'; @@ -9,14 +10,16 @@ class TokenInterceptor implements InterceptorsWrapper { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) async { - final accessToken = options.headers.remove('accessToken') == 'true'; + if(options.headers['accessToken'] == 'true') { + debugPrint(options.headers.toString()); - if (!accessToken) return handler.next(options); + options.headers.remove('accessToken'); - final token = await storage.read(key: accessTokenKey); + final token = await storage.read(key: accessTokenKey); - if (token != null) { - options.headers['Authorization'] = 'Bearer $token'; + if (token != null) { + options.headers['Authorization'] = 'Bearer $token'; + } } return handler.next(options); } diff --git a/front/lib/core/utils/failure.dart b/front/lib/core/utils/failure.dart index 6dc2dd2..8ba0c43 100644 --- a/front/lib/core/utils/failure.dart +++ b/front/lib/core/utils/failure.dart @@ -1,9 +1,13 @@ import 'package:equatable/equatable.dart'; -abstract class Failure extends Equatable { +class Failure extends Equatable { const Failure(this.message); final String message; + Failure copyWith({String? message}) { + return Failure(message ?? this.message); + } + @override List get props => [message]; } @@ -18,4 +22,56 @@ class ConnectionFailure extends Failure { class DatabaseFailure extends Failure { const DatabaseFailure(String message) : super(message); -} \ No newline at end of file +} + +enum ResponseFailure { + badRequest( + code: 400, message: 'badRequest', failure: ServerFailure('badRequest')), + forbidden( + code: 403, message: 'forbidden', failure: ServerFailure('forbidden')), + unauthorized( + code: 401, + message: 'unauthorized', + failure: ServerFailure('unauthorized')), + notFound(code: 404, message: 'notFound', failure: ServerFailure('notFound')), + + cancel(code: 701, message: 'cancel', failure: ServerFailure('cancel')), + connectionError( + code: 702, + message: 'connectionError', + failure: ServerFailure('connectionError')), + connectionTimeout( + code: 703, + message: 'connectionTimeout', + failure: ServerFailure('connectionTimeout')), + receiveTimeout( + code: 704, + message: 'receiveTimeout', + failure: ServerFailure('receiveTimeout')), + sendTimeout( + code: 705, message: 'sendTimeout', failure: ServerFailure('sendTimeout')), + unknown(code: 706, message: 'unknown', failure: ServerFailure('unknown')); + + factory ResponseFailure.getByCode(int code) { + return ResponseFailure.values.firstWhere((value) => value.code == code, + orElse: () => ResponseFailure.unknown); + } + + const ResponseFailure( + {required this.code, required this.message, required this.failure}); + + final int code; + final String message; + final Failure failure; + Failure getFailure() { + return ServerFailure(message); + } +} + +enum ResponseSuccess { + success(200), + created(201); + + const ResponseSuccess(this.code); + final int code; +} diff --git a/front/lib/features/project/data/data_sources/remote_data_source.dart b/front/lib/features/project/data/data_sources/remote_data_source.dart index 29359b3..e01003e 100644 --- a/front/lib/features/project/data/data_sources/remote_data_source.dart +++ b/front/lib/features/project/data/data_sources/remote_data_source.dart @@ -1,5 +1,6 @@ import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; +import 'package:front/core/network_handling/app_dio.dart'; import 'package:front/core/utils/exception.dart'; import 'package:front/features/project/data/models/project.dart'; @@ -15,16 +16,20 @@ abstract class ProjectRemoteDataSource { } class ProjectRemoteDataSourceImpl implements ProjectRemoteDataSource { - ProjectRemoteDataSourceImpl({required this.dio}); - final Dio dio; + ProjectRemoteDataSourceImpl(); + final Dio dio = AppDio.instance; @override Future> getProjectsByTeamId(int teamId) async { try { - dio.options.headers = {'accessToken': 'true'}; debugPrint('teamId: $teamId'); var response = await dio.get( '/v1/team/$teamId/projects', + options: Options( + headers: { + 'accessToken': 'true', + }, + ), ); if (response.statusCode == 200 && response.data?['resultCode'] == 200) { @@ -35,17 +40,19 @@ class ProjectRemoteDataSourceImpl implements ProjectRemoteDataSource { } else { throw ServerException(); } - } on DioException catch (e) { - debugPrint(e.response?.data.toString()); - throw ServerException(); + } on DioException { + rethrow; } } @override Future getProjectById(int projectId) async { try { - dio.options.headers = {'accessToken': 'true'}; - var response = await dio.get('/v1/project/$projectId'); + var response = await dio.get('/v1/project/$projectId',options: Options( + headers: { + 'accessToken': 'true', + }, + ),); if (response.statusCode == 200 && response.data?['resultCode'] == 200) { return ProjectModel.fromJson(response.data['result']); @@ -53,7 +60,7 @@ class ProjectRemoteDataSourceImpl implements ProjectRemoteDataSource { throw ServerException(); } } on DioException { - throw ServerException(); + rethrow; } } @@ -61,7 +68,6 @@ class ProjectRemoteDataSourceImpl implements ProjectRemoteDataSource { Future createProject( ProjectRequestModel project, int teamId) async { try { - dio.options.headers = {'accessToken': 'true'}; var response = await dio.post( '/v1/team/$teamId/project', data: project.toJson(), @@ -76,26 +82,27 @@ class ProjectRemoteDataSourceImpl implements ProjectRemoteDataSource { } else { throw ServerException(); } - } on DioException catch (e) { - debugPrint(e.response?.data.toString()); - throw ServerException(); + } on DioException { + rethrow; } } @override Future deleteProjectById(int projectId) async { try { - dio.options.headers = {'accessToken': 'true'}; - var response = await dio.delete('/v1/project/$projectId'); + var response = await dio.delete('/v1/project/$projectId',options: Options( + headers: { + 'accessToken': 'true', + }, + ),); if (response.statusCode == 200 && response.data?['resultCode'] == 200) { return; } else { throw ServerException(); } - } on DioException catch (e){ - debugPrint(e.response?.toString()); - throw ServerException(); + } on DioException { + rethrow; } } @@ -103,18 +110,23 @@ class ProjectRemoteDataSourceImpl implements ProjectRemoteDataSource { Future updateProjectById( ProjectRequestModel project, int projectId) async { try { - dio.options.headers = {'accessToken': 'true'}; - var response = - await dio.patch('/v1/project/$projectId', data: project.toJson(),); + var response = await dio.patch( + '/v1/project/$projectId', + data: project.toJson(), + options: Options( + headers: { + 'accessToken': 'true', + }, + ), + ); if (response.statusCode == 204 && response.data?['resultCode'] == 204) { return project; } else { throw ServerException(); } - } on DioException catch (e) { - debugPrint(e.response?.toString()); - throw ServerException(); + } on DioException { + rethrow; } } } diff --git a/front/lib/features/project/data/data_sources/riverpod.dart b/front/lib/features/project/data/data_sources/riverpod.dart index 0887a2e..73f8de8 100644 --- a/front/lib/features/project/data/data_sources/riverpod.dart +++ b/front/lib/features/project/data/data_sources/riverpod.dart @@ -12,7 +12,7 @@ ProjectRemoteDataSource projectRemoteDataSource( ProjectRemoteDataSourceRef ref, ) { var dio = ref.read(dioProvider); - return ProjectRemoteDataSourceImpl(dio: dio); + return ProjectRemoteDataSourceImpl(); } @riverpod diff --git a/front/lib/features/project/data/repositories/project_repository_impl.dart b/front/lib/features/project/data/repositories/project_repository_impl.dart index 1661456..e37c92c 100644 --- a/front/lib/features/project/data/repositories/project_repository_impl.dart +++ b/front/lib/features/project/data/repositories/project_repository_impl.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:dartz/dartz.dart'; +import 'package:front/core/config/custom_interceptor.dart'; import 'package:front/core/utils/exception.dart'; import 'package:front/core/utils/failure.dart'; import 'package:front/features/project/data/data_sources/remote_data_source.dart'; @@ -9,7 +10,7 @@ import 'package:front/features/project/data/models/project_request.dart'; import 'package:front/features/project/entities/project.dart'; import 'package:front/features/project/repositories/project_repository.dart'; -class ProjectRepositoryImpl implements ProjectRepository { +class ProjectRepositoryImpl with ErrorHandler implements ProjectRepository { ProjectRepositoryImpl( {required this.projectRemoteDataSource, required this.projectTempDataSource}); @@ -20,77 +21,50 @@ class ProjectRepositoryImpl implements ProjectRepository { @override Future>> getProjectsByTeamId(int teamId) async { - try { + return catchError(() async { var result = await projectRemoteDataSource.getProjectsByTeamId(teamId); for (var element in result) { projects.addAll({element.projectId: element.toEntity()}); } - return Right(result.map((e) => e.toEntity()).toList()); - } on ServerException { - return const Left(ServerFailure('An error has occurred')); - } on SocketException { - return const Left(ServerFailure('Failed to connect to the network')); - } on UnimplementedError { - var result = await projectTempDataSource.getProjectsByTeamId(teamId); - return Right(result.map((e) => e.toEntity()).toList()); - } + return result.map((e) => e.toEntity()).toList(); + }); } @override Future> getProjectById(int projectId) async { - try { + return catchError(() async { var result = await projectRemoteDataSource.getProjectById(projectId); projects.addAll({projectId: result.toEntity()}); - return Right(projects[projectId]!); - } on ServerException { - return const Left(ServerFailure('An error has occurred')); - } on SocketException { - return const Left(ServerFailure('Failed to connect to the network')); - } + return projects[projectId]!; + }); } @override Future> deleteProjectById(int projectId) async { - try { + return catchError(() async { await projectRemoteDataSource.deleteProjectById(projectId); projects.remove(projectId); - return const Right(null); - } on ServerException { - return const Left(ServerFailure('An error has occurred')); - } on SocketException { - return const Left(ServerFailure('Failed to connect to the network')); - } + }); } @override Future> updateProjectById( Project project, int projectId) async { - try { - var result = - await projectRemoteDataSource.updateProjectById(ProjectRequestModel.fromEntity(project), projectId); + return catchError(() async { + await projectRemoteDataSource.updateProjectById( + ProjectRequestModel.fromEntity(project), projectId); projects.addAll({projectId: project}); - return Right(project); - } on ServerException { - return const Left(ServerFailure('An error has occurred')); - } on SocketException { - return const Left(ServerFailure('Failed to connect to the network')); - } + return project; + }); } @override Future> createProject( ProjectRequestModel project, int teamId) async { - try { + return catchError(() async { var result = await projectRemoteDataSource.createProject(project, teamId); projects.addAll({result.projectId: result.toEntity()}); - return Right(result.toEntity()); - } on ServerException { - return const Left(ServerFailure('An error has occurred')); - } on SocketException { - return const Left(ServerFailure('Failed to connect to the network')); - } on UnimplementedError { - var result = await projectTempDataSource.createProject(project); - return Right(result.toEntity()); - } + return result.toEntity(); + }); } } diff --git a/front/lib/features/project/presentaion/screen/project_creation.dart b/front/lib/features/project/presentaion/screen/project_creation.dart index e7670f1..a5b3e35 100644 --- a/front/lib/features/project/presentaion/screen/project_creation.dart +++ b/front/lib/features/project/presentaion/screen/project_creation.dart @@ -5,9 +5,10 @@ import 'package:front/features/user/models/user.dart'; import 'package:front/features/project/data/models/project_request.dart'; import 'package:front/features/project/presentaion/component/project_from.dart'; import 'package:front/features/project/presentaion/component/project_manager_selector.dart'; -import 'package:front/features/project/presentaion/viewmodel/project.dart'; +import 'package:front/features/project/presentaion/viewmodel/project_viewmodel.dart'; import 'package:front/shared/atom/bottom_navigation_bar.dart'; import 'package:front/shared/helper/FormHelper/form.dart'; +import 'package:go_router/go_router.dart'; import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; class ProjectCreateScreen extends ConsumerStatefulWidget { @@ -36,9 +37,9 @@ class _ProjectCreateScreenState extends ConsumerState { final _formKey = GlobalKey(); late CustomFormState? currentState; var manager = - UserModel(id: 1, nickname: '코난', email: 'test@gmail.com', type: 'LEADER'); + UserModel(id: 4, nickname: '코난', email: 'test@gmail.com', type: 'LEADER'); final List teamMembers = [ - UserModel(id: 1, email: '코난', nickname: 'test@gmail.com', type: 'LEADER'), + UserModel(id: 4, email: '코난', nickname: 'test@gmail.com', type: 'LEADER'), ]; @override @@ -79,7 +80,7 @@ class _ProjectCreateScreenState extends ConsumerState { void initState() { super.initState(); currentState = _formKey.currentState; - viewmodel = ref.read(projectViewmodelProvider.notifier); + viewmodel = ref.read(projectViewmodelProvider(widget.teamId).notifier); } void onFormChanged() { @@ -118,15 +119,18 @@ class _ProjectCreateScreenState extends ConsumerState { TextButton( onPressed: () async { if (_formKey.currentState?.validate(null) ?? false) { - var result = await viewmodel.createProject( - ProjectRequestModel( - managerId: manager.id, - name: currentState!.fields['name']!.value, - description: currentState!.fields['description']!.value, - ), - teamId); - result.fold((l) => debugPrint(l.toString()), - (r) => debugPrint(r.toString())); + try { + await viewmodel.createProject( + ProjectRequestModel( + managerId: manager.id, + name: currentState!.fields['name']!.value, + description: currentState!.fields['description']!.value, + ), + teamId); + context.pop(); + } catch (e) { + debugPrint(e.toString()); + } } }, child: Text( diff --git a/front/lib/features/project/presentaion/screen/project_detail.dart b/front/lib/features/project/presentaion/screen/project_detail.dart index bbf0418..bbe8a9e 100644 --- a/front/lib/features/project/presentaion/screen/project_detail.dart +++ b/front/lib/features/project/presentaion/screen/project_detail.dart @@ -3,16 +3,18 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:front/core/const/enum.dart'; import 'package:front/features/project/entities/project.dart'; import 'package:front/features/project/presentaion/component/task_component.dart'; -import 'package:front/features/project/presentaion/viewmodel/project.dart'; +import 'package:front/features/project/presentaion/viewmodel/project_viewmodel.dart'; import 'package:front/shared/atom/bottom_navigation_bar.dart'; import 'package:front/shared/utils/intl_format_date.dart'; import 'package:go_router/go_router.dart'; import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; class ProjectDetailScreen extends ConsumerStatefulWidget { - const ProjectDetailScreen({super.key, required this.projectId}); + const ProjectDetailScreen( + {super.key, required this.projectId, required this.teamId}); final int projectId; + final int teamId; @override ConsumerState createState() => @@ -25,13 +27,29 @@ class _ProjectDetailScreenState extends ConsumerState { @override void initState() { super.initState(); - viewmodel = ref.read(projectViewmodelProvider.notifier); + viewmodel = ref.read(projectViewmodelProvider(widget.teamId).notifier); viewmodel.getProjectById(widget.projectId); } + Widget buildDeleteButton() { + return TextButton( + onPressed: () async { + try { + await viewmodel.deleteProject(widget.projectId); + context.pop(); + } catch (e) { + debugPrint(e.toString()); + } + }, + child: Text( + '삭제', + ), + ); + } + @override Widget build(BuildContext context) { - var projectState = ref.watch(projectViewmodelProvider); + var projectState = viewmodel.getProjectById(widget.projectId); var projectStartTime = DateTime.now(); var projectTask = []; var h1Textstyle = @@ -42,28 +60,37 @@ class _ProjectDetailScreenState extends ConsumerState { child: Padding( padding: const EdgeInsets.all(20.0), child: SingleChildScrollView( - child: projectState.when( - (project) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${project.name}의 TimeLine', - style: h1Textstyle, - ), - Text('${intlFormatDate(projectStartTime)}~'), - Text(project.description), - for (int i = 0; i < projectTask.length; i++) - TaskComponent(task: projectTask[i]), - TextButton( - child: Text('수정d'), - onPressed: () => - context.push('/projectUpdate', extra: project), - ) - ], - ), - loading: () => const CircularProgressIndicator(), - error: (message) => Text(message.toString()), - + child: FutureBuilder( + future: projectState, + builder: (context, snapshot) { + if (snapshot.hasData) { + var project = snapshot.data!; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${project.name}의 TimeLine', + style: h1Textstyle, + ), + Text('${intlFormatDate(projectStartTime)}~'), + Text(project.description), + for (int i = 0; i < projectTask.length; i++) + TaskComponent(task: projectTask[i]), + TextButton( + child: Text('수정'), + onPressed: () => context.push( + '/projectUpdate/${widget.teamId}', + extra: project), + ), + buildDeleteButton(), + ], + ); + } else if (snapshot.hasError) { + return Text(snapshot.error.toString()); + } else { + return const CircularProgressIndicator(); + } + }, ), ), ), @@ -97,5 +124,7 @@ class _AddTaskButton extends StatelessWidget { ) Widget ProjectDetailScreenUseCase(BuildContext context) { return ProjectDetailScreen( - projectId: 1); + projectId: 1, + teamId: 1, + ); } diff --git a/front/lib/features/project/presentaion/screen/project_update.dart b/front/lib/features/project/presentaion/screen/project_update.dart index c75e8ab..9fbe289 100644 --- a/front/lib/features/project/presentaion/screen/project_update.dart +++ b/front/lib/features/project/presentaion/screen/project_update.dart @@ -8,22 +8,21 @@ import 'package:front/features/project/data/models/project_request.dart'; import 'package:front/features/project/entities/project.dart'; import 'package:front/features/project/presentaion/component/project_from.dart'; import 'package:front/features/project/presentaion/component/project_manager_selector.dart'; -import 'package:front/features/project/presentaion/viewmodel/project.dart'; +import 'package:front/features/project/presentaion/viewmodel/project_viewmodel.dart'; import 'package:front/shared/atom/bottom_navigation_bar.dart'; import 'package:front/shared/helper/FormHelper/form.dart'; -import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; +import 'package:go_router/go_router.dart'; class ProjectUpdateScreen extends ConsumerStatefulWidget { - ProjectUpdateScreen({ - Key? key, - required this.project, - }) : super(key: key); + ProjectUpdateScreen({Key? key, required this.project, required this.teamId}) + : super(key: key); final Project project; + final int teamId; final team = { - 'id': '1', + 'id': 1, 'name': '팀이름', }; @@ -80,7 +79,6 @@ class _ProjectUpdateScreenState extends ConsumerState { Row( children: [ buildUpdateButton(), - buildDeleteButton(), ], ) ], @@ -96,7 +94,7 @@ class _ProjectUpdateScreenState extends ConsumerState { void initState() { super.initState(); currentState = _formKey.currentState; - viewmodel = ref.read(projectViewmodelProvider.notifier); + viewmodel = ref.read(projectViewmodelProvider(widget.teamId).notifier); } void onFormChanged() { @@ -132,17 +130,20 @@ class _ProjectUpdateScreenState extends ConsumerState { return TextButton( onPressed: () async { if (_formKey.currentState?.validate(null) ?? false) { - var result = await viewmodel.updateProject( - Project( - managerId: manager.id, - name: _formKey.currentState!.fields['name']!.value, - description: - _formKey.currentState!.fields['description']!.value, - projectId: widget.project.projectId, - projectState: widget.project.projectState), - widget.project.projectId); - result.fold( - (l) => debugPrint(l.toString()), (r) => debugPrint(r.toString())); + try { + await viewmodel.updateProject( + Project( + managerId: manager.id, + name: _formKey.currentState!.fields['name']!.value, + description: + _formKey.currentState!.fields['description']!.value, + projectId: widget.project.projectId, + projectState: widget.project.projectState), + widget.project.projectId); + context.pop(); + } catch (e) { + debugPrint(e.toString()); + } } }, child: Text( @@ -151,19 +152,6 @@ class _ProjectUpdateScreenState extends ConsumerState { ), ); } - - Widget buildDeleteButton() { - return TextButton( - onPressed: () async { - var result = await viewmodel.deleteProject(widget.project.projectId); - result.fold((l) => debugPrint(l.toString()), (r) => null); - }, - child: Text( - '삭제', - style: widget.h1TextStyle.copyWith(fontSize: 15), - ), - ); - } } // //component diff --git a/front/lib/features/project/presentaion/viewmodel/project.dart b/front/lib/features/project/presentaion/viewmodel/project.dart deleted file mode 100644 index cbcd641..0000000 --- a/front/lib/features/project/presentaion/viewmodel/project.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:dartz/dartz.dart'; -import 'package:front/core/utils/failure.dart'; -import 'package:front/features/project/data/models/project_request.dart'; -import 'package:front/features/project/entities/project.dart'; -import 'package:front/features/project/presentaion/state/project_state.dart'; -import 'package:front/features/project/usecases/riverpod.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'project.g.dart'; - -@riverpod -class ProjectViewmodel extends _$ProjectViewmodel { - - @override - ProjectState build() => const ProjectState.loading(); - - Future getProjectById(int projectId) async { - var result = await ref.read(getProjectByIdUseCaseProvider)(projectId); - result.fold( - (l) => state = ProjectState.error(message: l.message), - (r) => state = ProjectState(r), - ); - } - - Future> createProject( - ProjectRequestModel project, int teamId) async { - return await ref.read(createProjectUseCaseProvider)(project, teamId); - } - - Future> updateProject( - Project project, int projectId) async { - return await ref.read(updateProjectByIdUseCaseProvider)(project, projectId); - } - - Future> deleteProject(int projectId) async { - var result = await ref.read(deleteProjectByIdUseCaseProvider)(projectId); - return result; - } -} diff --git a/front/lib/features/project/presentaion/viewmodel/project_viewmodel.dart b/front/lib/features/project/presentaion/viewmodel/project_viewmodel.dart new file mode 100644 index 0000000..bbdd636 --- /dev/null +++ b/front/lib/features/project/presentaion/viewmodel/project_viewmodel.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:front/features/project/data/models/project_request.dart'; +import 'package:front/features/project/entities/project.dart'; +import 'package:front/features/project/usecases/riverpod.dart'; +import 'package:front/features/team/presentation/providers/projects_state.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'project_viewmodel.g.dart'; + +@riverpod +class ProjectViewmodel extends _$ProjectViewmodel { + @override + ProjectsState build(int teamId) { + return const ProjectsState.loading(); + } + + Future getProjectsByTeamId(int teamId) async { + var result = await ref.read(getProjectsByTeamIdUseCaseProvider)(teamId); + var projectState = result.fold( + (l) => ProjectsState.error(message: l.message), + (r) => ProjectsState(r), + ); + state = projectState; + return projectState; + } + + Future getProjectById(int projectId) async { + var project = state.whenOrNull((projects) { + return projects.firstWhere((element) => element.projectId == projectId); + }); + if (project != null) { + return project; + } else { + var result = await ref.read(getProjectByIdUseCaseProvider)(projectId); + + return result.fold( + (l) => Future.error(l.message), + (r) { + return r; + }, + ); + } + } + + Future createProject(ProjectRequestModel project, int teamId) async { + var result = await ref.read(createProjectUseCaseProvider)(project, teamId); + return result.fold( + (l) => throw l, + (r) { + if (state.mapOrNull((value) => true) != null) { + state = + ProjectsState([...state.whenOrNull((projects) => projects)!, r]); + } + }, + ); + } + + Future updateProject(Project project, int projectId) async { + var result = + await ref.read(updateProjectByIdUseCaseProvider)(project, projectId); + return result.fold( + (l) => throw l, + (r) { + if (state.mapOrNull((value) => true) != null) { + state = ProjectsState([ + ...state.whenOrNull((projects) { + projects.removeWhere((element) => element.projectId == projectId); + return projects; + })!, + r + ]); + } + }, + ); + } + + Future deleteProject(int projectId) async { + var result = await ref.read(deleteProjectByIdUseCaseProvider)(projectId); + result.fold( + (l) => throw l, + (r) { + if (state.mapOrNull((value) => true) != null) { + state = ProjectsState([ + ...state.whenOrNull((projects) { + return projects.where((element) => element.projectId != projectId).toList(); + })! + ]); + } + }, + ); + } +} diff --git a/front/lib/features/team/data/data_source/team_remote_data_source_impl.dart b/front/lib/features/team/data/data_source/team_remote_data_source_impl.dart index 18d8dc5..e338f99 100644 --- a/front/lib/features/team/data/data_source/team_remote_data_source_impl.dart +++ b/front/lib/features/team/data/data_source/team_remote_data_source_impl.dart @@ -13,7 +13,7 @@ final class TeamRemoteDataSourceImpl implements TeamRemoteDataSource { final TeamAPI _teamAPI = TeamAPI( AppDio.instance, baseUrl: - 'http://ec2-3-34-95-39.ap-northeast-2.compute.amazonaws.com:8083/v1', + 'v1/', ); @override diff --git a/front/lib/features/team/presentation/pages/team/team_datail_view.dart b/front/lib/features/team/presentation/pages/team/team_datail_view.dart index 88a2a51..cf547e6 100644 --- a/front/lib/features/team/presentation/pages/team/team_datail_view.dart +++ b/front/lib/features/team/presentation/pages/team/team_datail_view.dart @@ -1,17 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:front/features/project/presentaion/viewmodel/project_viewmodel.dart'; import 'package:front/app/locator.dart'; import 'package:front/features/team/data/models/team_detail.dart'; import 'package:front/features/team/presentation/pages/team/widgets/dialog.dart'; import 'package:front/features/team/presentation/pages/team/widgets/selecting_sharing_method_dialog.dart'; import 'package:front/features/team/presentation/providers/projects_state.dart'; import 'package:front/features/team/presentation/providers/team_controller.dart'; -import 'package:front/features/team/presentation/providers/team_detail.dart'; import 'package:front/features/team/presentation/providers/team_detail_controller.dart'; +import 'package:go_router/go_router.dart'; import 'package:front/features/user/presentation/viewmodel/main_screen_viewmodel.dart'; class TeamDetailView extends ConsumerStatefulWidget { - final String teamId; + final int teamId; const TeamDetailView(this.teamId, {super.key}); @@ -20,26 +21,22 @@ class TeamDetailView extends ConsumerStatefulWidget { } class _TeamDetailScreenState extends ConsumerState { - late TeamDetailViewmodel viewmodel; + late ProjectViewmodel viewmodel; late TeamController teamController; final memberIdController = TextEditingController(); @override void initState() { super.initState(); - viewmodel = ref.read(teamDetailViewmodelProvider.notifier); + viewmodel = ref.read(projectViewmodelProvider(widget.teamId).notifier); viewmodel.getProjectsByTeamId(1); - ref - .read(teamDetailControllerProvider.notifier) - .getTeamById(int.parse(widget.teamId)); + ref.read(teamDetailControllerProvider.notifier).getTeamById(widget.teamId); } @override Widget build(BuildContext context) { - var projectsState = ref.watch(teamDetailViewmodelProvider); - final teamState = ref.watch(teamDetailControllerProvider); - int? memberId; - + var projectsState = ref.watch(projectViewmodelProvider(widget.teamId)); + // final mainScreenViewModelState = ref.watch(mainScreenViewModelProvider); var textStyle = const TextStyle( color: Colors.black, fontFamily: 'Inter', @@ -48,183 +45,55 @@ class _TeamDetailScreenState extends ConsumerState { ); return Scaffold( - appBar: AppBar( - automaticallyImplyLeading: false, - scrolledUnderElevation: 0.0, - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'TeamW2aver', - style: TextStyle( - color: Colors.black, - fontSize: 12, - fontFamily: 'Inter', - fontWeight: FontWeight.w400, - height: 0, - ), - ), - Column( - children: [ - Container( - width: 30, - height: 30, - decoration: const ShapeDecoration( - color: Color(0xFFD9D9D9), - shape: OvalBorder(), - ), + appBar: AppBar( + automaticallyImplyLeading: false, + scrolledUnderElevation: 0.0, + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'TeamW2aver', + style: TextStyle( + color: Colors.black, + fontSize: 12, + fontFamily: 'Inter', + fontWeight: FontWeight.w400, + height: 0, ), - Text( - '알림', - textAlign: TextAlign.center, - style: textStyle.copyWith(fontSize: 15), - ), - ], - ), - ], - ), - ), - body: teamState.when( - (teamDetailModel) { - teamDetailModel as TeamDetailModel; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - //TODO: extract these widgets and move to component - Text( - '${teamDetailModel.name} 의 Board', - style: textStyle.copyWith(fontSize: 20), - ), - const SizedBox( - height: 22, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Team Member (${teamDetailModel.memberCount})', - style: textStyle.copyWith(fontSize: 15), - ), - TextButton( - onPressed: () => - context.dialog( - child: SelectingSharingMethodDialog( - teamId: int.parse(widget.teamId), - ), - ), - child: const Text('share'), + ), + Column( + children: [ + Container( + width: 30, + height: 30, + decoration: const ShapeDecoration( + color: Color(0xFFD9D9D9), + shape: OvalBorder(), ), - ], - ), - const SizedBox( - height: 8, - ), - SizedBox( - height: 40, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: teamDetailModel.memberCount, - itemExtent: 48, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Container( - width: 40, - height: 40, - decoration: const ShapeDecoration( - color: Color(0xFFD9D9D9), - shape: OvalBorder(), - ), - ), - ); - }), - ), - Padding( - padding: const EdgeInsets.only(bottom: 16.0, top: 16.0), - child: Text( - 'Team Project', + ), + Text( + '알림', + textAlign: TextAlign.center, style: textStyle.copyWith(fontSize: 15), ), - ), - ElevatedButton( - onPressed: () { - context.dialog( - child: SizedBox( - child: Column( - mainAxisSize: MainAxisSize.min, - - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - SizedBox( - width: 150, - child: TextField( - controller: memberIdController, - onChanged: (value) { - memberId = int.parse(value); - }, - ), - ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - onPressed: () async { - if (memberId != null) { - final result = await deleteMemberUseCase - .call( - teamId: int.parse(widget.teamId), - memberId: [memberId!], - ); - - result.fold( - onSuccess: (value) { - ref.read(teamDetailControllerProvider.notifier).getTeamById(int.parse(widget.teamId)); - Navigator.pop(context); - }, - onFailure: (e) { - Text(e.toString()); - Navigator.pop(context); - }, - ); - } else { - } - }, - child: const Text('삭제하기'), - ), - const SizedBox(width: 10), - ElevatedButton( - onPressed: () => Navigator.pop(context), - child: const Text('닫기'), - ), - ], - ), - ], - ), - ), - ); - }, - child: const Text('팀원 삭제'), - ), - - Expanded( - child: ProjectList( - textStyle: textStyle, - projectsState: projectsState), - ), - ], - ), - ); - }, - loading: () => - const Center( - child: CircularProgressIndicator(), + ], + ), + Expanded( + child: ProjectList( + widget: widget, + textStyle: textStyle, + projectsState: projectsState), + ), + ], + ), ), - error: (message) => Text(message ?? ''), - ), - ); + floatingActionButton: FloatingActionButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(100), + ), + onPressed: () => context.push('/projectCreate/${widget.teamId}'), + child: const Icon(Icons.add), + )); } } @@ -233,15 +102,17 @@ class ProjectList extends StatelessWidget { super.key, required this.textStyle, required this.projectsState, + required this.widget, }); final TextStyle textStyle; final ProjectsState projectsState; + final TeamDetailView widget; @override Widget build(BuildContext context) { return projectsState.when( - (projects) { + (projects) { return CustomScrollView( slivers: [ SliverList.builder( @@ -250,7 +121,10 @@ class ProjectList extends StatelessWidget { return Padding( padding: const EdgeInsets.only(bottom: 16), child: ElevatedButton( - onPressed: () {}, + onPressed: () { + context.push( + '/projectDetail/${widget.teamId}/${projects[index].projectId}'); + }, child: Text( projects[index].name, style: textStyle.copyWith(fontSize: 15), @@ -262,8 +136,7 @@ class ProjectList extends StatelessWidget { ], ); }, - loading: () => - const Center( + loading: () => const Center( child: CircularProgressIndicator(), ), error: (message) { diff --git a/front/lib/features/team/presentation/providers/team_detail.dart b/front/lib/features/team/presentation/providers/team_detail.dart deleted file mode 100644 index 6814f6e..0000000 --- a/front/lib/features/team/presentation/providers/team_detail.dart +++ /dev/null @@ -1,20 +0,0 @@ - -import 'package:front/features/project/usecases/riverpod.dart'; -import 'package:front/features/team/presentation/providers/projects_state.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'team_detail.g.dart'; - -@riverpod -class TeamDetailViewmodel extends _$TeamDetailViewmodel { - @override - ProjectsState build() => const ProjectsState.loading(); - - Future getProjectsByTeamId(int teamId) async { - var result = await ref.read(getProjectsByTeamIdUseCaseProvider)(teamId); - result.fold( - (l) => state = ProjectsState.error(message: l.message), - (r) => state = ProjectsState(r), - ); - } -} diff --git a/front/lib/features/user/data_sources/remote_data_source.dart b/front/lib/features/user/data_sources/remote_data_source.dart index 0389587..8f7f75b 100644 --- a/front/lib/features/user/data_sources/remote_data_source.dart +++ b/front/lib/features/user/data_sources/remote_data_source.dart @@ -74,8 +74,14 @@ class UserRemoteDataSourceImpl extends UserRemoteDataSource { // } @override Future getMe() async { - dio.options.headers = {'accessToken': 'true'}; - var response = await dio.get('/v1/user'); + var response = await dio.get( + '/v1/user', + options: Options( + headers: { + 'accessToken': 'true', + }, + ), + ); if (response.statusCode == 200) { return UserModel.fromJson(response.data['result']); } else {