From dd747004fbc6ae5abdc395ad0aed269f162312c1 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Mon, 30 Jun 2025 20:00:53 +0900 Subject: [PATCH 01/54] =?UTF-8?q?=F0=9F=99=88=20::=20Adds=20generated=20Da?= =?UTF-8?q?rt=20files=20to=20.gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ensures that generated Dart files are excluded from version control, preventing unnecessary tracking of build artifacts. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 8fd601c..488203a 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ pubspec.lock # If you don't generate documentation locally you can remove this line. doc/api/ +# .g.dart files are generated by the Dart build system. +*.g.dart +*.freezed.dart # dotenv environment variables file .env* From 7c28b9f4290908c6eee86f995b0cf9191ed828a0 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Mon, 30 Jun 2025 20:01:16 +0900 Subject: [PATCH 02/54] =?UTF-8?q?=E2=9E=95=20::=20Sets=20up=20dependency?= =?UTF-8?q?=20injection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Configures dependency injection using GetIt for Dio, CookieJar, APIs, DataSources, and Repositories. This centralizes dependency management and improves testability. --- lib/core/config/di/dependencies.dart | 62 +++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/lib/core/config/di/dependencies.dart b/lib/core/config/di/dependencies.dart index 5c80a11..af6096a 100644 --- a/lib/core/config/di/dependencies.dart +++ b/lib/core/config/di/dependencies.dart @@ -3,11 +3,21 @@ import 'package:cookie_jar/cookie_jar.dart'; import 'package:dio/dio.dart'; import 'package:get_it/get_it.dart'; import 'package:jusicool_ios/core/network/api/api_client.dart'; +import 'package:jusicool_ios/data/account/api/account_api.dart'; +import 'package:jusicool_ios/data/account/data_sources/account_data_source.dart'; +import 'package:jusicool_ios/data/user/api/user_api.dart'; +import 'package:jusicool_ios/data/user/data_sources/user_data_source.dart'; +import '../../../data/account/data_sources/account_data_source_impl.dart'; +import '../../../data/account/repositories/account_repository.dart'; +import '../../../data/account/repositories/account_repository_impl.dart'; +import '../../../data/user/data_sources/user_data_source_impl.dart'; +import '../../../data/user/repositories/user_repository.dart'; +import '../../../data/user/repositories/user_repository_impl.dart'; import '../../network/interceptor/cookie_interceptor.dart'; final di = GetIt.instance; -void setDio() { +void _setDio() { try { di.registerLazySingleton(() => dio()); di.registerSingletonAsync(() async => await cookieJar()); @@ -16,3 +26,53 @@ void setDio() { log("⛔ :: DIO DI 실패 \n$e"); } } + +void _setApi() { + try { + di.registerLazySingleton(() => UserApi(di.get())); + di.registerLazySingleton(() => AccountApi(di.get())); + log('✅ :: API DI 성공'); + } catch (e) { + log("⛔ :: API DI 실패 \n$e"); + } +} + +void _setDataSources() { + try { + di.registerLazySingleton( + () => UserDataSourceImpl(di.get()), + ); + di.registerLazySingleton( + () => AccountDataSourceImpl(di.get()), + ); + log('✅ :: DataSources DI 성공'); + } catch (e) { + log("⛔ :: DataSources DI 실패 \n$e"); + } +} + +void _setRepository() { + try { + di.registerLazySingleton( + () => UserRepositoryImpl(di.get()), + ); + di.registerLazySingleton( + () => AccountRepositoryImpl(di.get()), + ); + log('✅ :: Repository DI 성공'); + } catch (e) { + log("⛔ :: Repository DI 실패 \n$e"); + } +} + +void setDependencies() { + try { + _setDio(); + _setApi(); + _setDataSources(); + _setRepository(); + log('✅ :: DI 설정 완료'); + } catch (e) { + log("⛔ :: DI 실패 \n$e"); + } +} From 39d1ad35ac7f069c9770e8f31707847dea460395 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Mon, 30 Jun 2025 20:01:37 +0900 Subject: [PATCH 03/54] :recycle: :: Handles missing base URL in .env Prevents the app from crashing when the base URL is not defined in the .env file. Logs an error message instead of throwing an exception. Uses an empty string as the base URL if it is null. --- lib/core/network/api/api_client.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/core/network/api/api_client.dart b/lib/core/network/api/api_client.dart index 4256899..d0b3257 100644 --- a/lib/core/network/api/api_client.dart +++ b/lib/core/network/api/api_client.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:cookie_jar/cookie_jar.dart'; import 'package:dio/dio.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart'; @@ -13,15 +15,15 @@ Dio dio() { bool _homeDebugMode = dotenv.env['HOME_DEBUG_MODE'] == 'true'; if (_baseUrlDev == null || _baseUrlProd == null) { - throw Exception('BASE_URL_DEV or BASE_URL_PROD is not set in .env file'); + log('⛔ :: BASE_URL_DEV or BASE_URL_PROD is not set in .env file'); } - String _baseUrl = + String? _baseUrl = (kReleaseMode || _homeDebugMode) ? _baseUrlProd : _baseUrlDev; Dio dio = Dio( BaseOptions( - baseUrl: _baseUrl, + baseUrl: _baseUrl ?? "", connectTimeout: Duration(seconds: 30), receiveTimeout: Duration(seconds: 30), ), From 43175393f57fed3283912574328ceb000b5e55c2 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Mon, 30 Jun 2025 20:02:07 +0900 Subject: [PATCH 04/54] =?UTF-8?q?=F0=9F=9A=9A=20::=20Moves=20router=20and?= =?UTF-8?q?=20menu=20bottom?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the router and menu bottom files to the core directory to better organize the project structure and improve maintainability. --- lib/{ => core/router}/router.dart | 2 +- lib/{ => core/widget}/menu_bottom.dart | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename lib/{ => core/router}/router.dart (99%) rename lib/{ => core/widget}/menu_bottom.dart (100%) diff --git a/lib/router.dart b/lib/core/router/router.dart similarity index 99% rename from lib/router.dart rename to lib/core/router/router.dart index 73b7094..82049bc 100644 --- a/lib/router.dart +++ b/lib/core/router/router.dart @@ -9,7 +9,7 @@ import 'package:jusicool_ios/presentation/sign_up/screens/name_input_screen.dart import 'package:jusicool_ios/presentation/sign_up/screens/password_create_screen.dart'; import 'package:jusicool_ios/presentation/splash/screens/splash_screen.dart'; -import 'main.dart'; +import '../../main.dart'; class RoutePaths { static const String splash = '/splash'; diff --git a/lib/menu_bottom.dart b/lib/core/widget/menu_bottom.dart similarity index 100% rename from lib/menu_bottom.dart rename to lib/core/widget/menu_bottom.dart From 44db5c1d9f6bc34555a8b9b0a93181a290c14fa4 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Mon, 30 Jun 2025 20:02:33 +0900 Subject: [PATCH 05/54] =?UTF-8?q?=F0=9F=9A=9A=20::=20Updates=20data=20mode?= =?UTF-8?q?l=20import=20paths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactors import paths for data models to align with the project's domain-driven structure. This change improves code organization and maintainability by ensuring components import data models from the correct domain layer. --- lib/presentation/my_capital/screens/my_assets_screen.dart | 2 +- lib/presentation/sign_in/screens/login_screen.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/presentation/my_capital/screens/my_assets_screen.dart b/lib/presentation/my_capital/screens/my_assets_screen.dart index 659f1c3..c86a32b 100644 --- a/lib/presentation/my_capital/screens/my_assets_screen.dart +++ b/lib/presentation/my_capital/screens/my_assets_screen.dart @@ -6,7 +6,7 @@ import 'package:intl/intl.dart'; import 'package:jusicool_design_system/src/core/theme/texts/typography.dart'; import 'package:jusicool_design_system/src/core/theme/colors/color_palette.dart'; import 'package:jusicool_ios/presentation/my_capital/widgets/my_asset_tile.dart'; -import '../../../data/models/my_assets.dart'; +import '../../../domain/my_capital/entities/my_assets.dart'; class MyAssetsScreen extends StatefulWidget { const MyAssetsScreen({super.key}); diff --git a/lib/presentation/sign_in/screens/login_screen.dart b/lib/presentation/sign_in/screens/login_screen.dart index 31ce09f..c5cab62 100644 --- a/lib/presentation/sign_in/screens/login_screen.dart +++ b/lib/presentation/sign_in/screens/login_screen.dart @@ -3,7 +3,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:go_router/go_router.dart'; import 'package:jusicool_design_system/jusicool_design_system.dart'; import 'package:email_validator/email_validator.dart'; -import 'package:jusicool_ios/router.dart'; +import 'package:jusicool_ios/core/router/router.dart'; import '../../sign_up/screens/name_input_screen.dart'; class LoginScreen extends StatefulWidget { From 9e42a839056323e32cb3afa6d6c193181154120d Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Mon, 30 Jun 2025 20:02:48 +0900 Subject: [PATCH 06/54] :recycle: :: Refactors project structure and dependencies Updates imports to reflect changes in folder structure. Refactors dependency injection initialization. --- lib/main.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index f03c36f..125bbda 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,8 +4,8 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:jusicool_design_system/src/core/theme/colors/color_palette.dart'; import 'package:jusicool_ios/core/config/di/dependencies.dart'; -import 'package:jusicool_ios/menu_bottom.dart'; -import 'package:jusicool_ios/router.dart'; +import 'package:jusicool_ios/core/widget/menu_bottom.dart'; +import 'package:jusicool_ios/core/router/router.dart'; import 'core/config/theme/app_theme.dart'; @@ -14,7 +14,7 @@ void main() async { await dotenv.load(fileName: '.env'); - setDio(); + setDependencies(); await di.allReady(); SystemChrome.setSystemUIOverlayStyle( From c2bc6aeea0e77a9f4c0b4e848cda18d3f52fcc18 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Mon, 30 Jun 2025 20:03:21 +0900 Subject: [PATCH 07/54] =?UTF-8?q?=E2=9E=95=20::=20Updates=20dependencies?= =?UTF-8?q?=20and=20adds=20code=20generation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates dependencies to their latest versions, including dio, retrofit and go_router. Adds retrofit_generator, json_serializable, build_runner, and freezed dependencies for code generation. Introduces freezed_annotation dependency and adjusts retrofit version for compatibility. --- pubspec.yaml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 86ec9f7..34d124d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,8 +33,6 @@ dependencies: dio: ^5.8.0+1 - retrofit: ^4.4.2 - get_it: ^8.0.3 json_annotation: ^4.9.0 @@ -49,6 +47,17 @@ dependencies: go_router: ^15.2.0 + retrofit: ^4.5.0 + + freezed_annotation: ^3.0.0 + +dev_dependencies: + retrofit_generator: + json_serializable: ^6.9.5 + build_runner: ^2.5.4 + freezed: ^3.0.6 + + flutter_native_splash: color: "#FFFFFF" image: assets/images/JUSICOOL.png From e99b572c55eabff96eae059e37f1b40fb53eed82 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Mon, 30 Jun 2025 20:03:58 +0900 Subject: [PATCH 08/54] :sparkles: :: Adds user API for authentication Defines the user API using Retrofit for handling sign-in, sign-up, email sending, and email verification functionalities. This API will serve as the interface for interacting with the user authentication backend. --- lib/data/user/api/user_api.dart | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 lib/data/user/api/user_api.dart diff --git a/lib/data/user/api/user_api.dart b/lib/data/user/api/user_api.dart new file mode 100644 index 0000000..0472d87 --- /dev/null +++ b/lib/data/user/api/user_api.dart @@ -0,0 +1,24 @@ +import 'package:dio/dio.dart'; +import 'package:retrofit/error_logger.dart'; +import 'package:retrofit/http.dart'; +import '../dto/remote/request/sign_in_request_dto.dart'; +import '../dto/remote/request/sign_up_request_dto.dart'; + +part 'user_api.g.dart'; + +@RestApi() +abstract class UserApi { + factory UserApi(Dio dio, {String baseUrl}) = _UserApi; + + @POST('/user/signin') + Future signIn(@Body() SignInRequestDto body); + + @POST('/user/signup') + Future signUp(@Body() SignUpRequestDto body); + + @POST('/user/email/send') + Future sendEmail(); + + @POST('/user/email/verify') + Future verifyEmail(); +} From 5845add4084658736560eceb88b0eb3ff8eea697 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Mon, 30 Jun 2025 20:04:56 +0900 Subject: [PATCH 09/54] :sparkles: :: Adds user authentication DTOs Defines data transfer objects (DTOs) for user sign-in and sign-up requests. These DTOs facilitate communication between the client and server for user authentication purposes. The DTOs are generated using Freezed for immutability and JSON serialization. --- lib/data/user/dto/local/request/.gitkeep | 0 lib/data/user/dto/local/response/.gitkeep | 0 .../dto/remote/request/sign_in_request_dto.dart | 14 ++++++++++++++ .../dto/remote/request/sign_up_request_dto.dart | 17 +++++++++++++++++ lib/data/user/dto/remote/response/.gitkeep | 0 5 files changed, 31 insertions(+) create mode 100644 lib/data/user/dto/local/request/.gitkeep create mode 100644 lib/data/user/dto/local/response/.gitkeep create mode 100644 lib/data/user/dto/remote/request/sign_in_request_dto.dart create mode 100644 lib/data/user/dto/remote/request/sign_up_request_dto.dart create mode 100644 lib/data/user/dto/remote/response/.gitkeep diff --git a/lib/data/user/dto/local/request/.gitkeep b/lib/data/user/dto/local/request/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/data/user/dto/local/response/.gitkeep b/lib/data/user/dto/local/response/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/data/user/dto/remote/request/sign_in_request_dto.dart b/lib/data/user/dto/remote/request/sign_in_request_dto.dart new file mode 100644 index 0000000..048bf30 --- /dev/null +++ b/lib/data/user/dto/remote/request/sign_in_request_dto.dart @@ -0,0 +1,14 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'sign_in_request_dto.g.dart'; + +part 'sign_in_request_dto.freezed.dart'; + +@freezed +abstract class SignInRequestDto with _$SignInRequestDto { + factory SignInRequestDto({required String email, required String password}) = + _SignInRequestDto; + + factory SignInRequestDto.fromJson(Map json) => + _$SignInRequestDtoFromJson(json); +} diff --git a/lib/data/user/dto/remote/request/sign_up_request_dto.dart b/lib/data/user/dto/remote/request/sign_up_request_dto.dart new file mode 100644 index 0000000..83001ef --- /dev/null +++ b/lib/data/user/dto/remote/request/sign_up_request_dto.dart @@ -0,0 +1,17 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'sign_up_request_dto.g.dart'; + +part 'sign_up_request_dto.freezed.dart'; + +@freezed +abstract class SignUpRequestDto with _$SignUpRequestDto { + factory SignUpRequestDto({ + required String email, + required String password, + required String name, + }) = _SignUpRequestDto; + + factory SignUpRequestDto.fromJson(Map json) => + _$SignUpRequestDtoFromJson(json); +} diff --git a/lib/data/user/dto/remote/response/.gitkeep b/lib/data/user/dto/remote/response/.gitkeep new file mode 100644 index 0000000..e69de29 From 2cd0cd41a2f540a1746f58d987bdf0a9f0d8f7c7 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Mon, 30 Jun 2025 20:05:19 +0900 Subject: [PATCH 10/54] :sparkles: :: Adds user data source abstraction Defines an abstract class and its implementation for handling user authentication and email verification operations. This introduces a layer of abstraction for accessing user-related data. --- .../user/data_sources/user_data_source.dart | 15 ++++++++++ .../data_sources/user_data_source_impl.dart | 30 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 lib/data/user/data_sources/user_data_source.dart create mode 100644 lib/data/user/data_sources/user_data_source_impl.dart diff --git a/lib/data/user/data_sources/user_data_source.dart b/lib/data/user/data_sources/user_data_source.dart new file mode 100644 index 0000000..c302ecb --- /dev/null +++ b/lib/data/user/data_sources/user_data_source.dart @@ -0,0 +1,15 @@ +import 'package:dio/dio.dart'; + +import '../dto/remote/request/sign_in_request_dto.dart'; +import '../dto/remote/request/sign_up_request_dto.dart'; + +abstract class UserDataSource { + + Future signIn(SignInRequestDto body); + + Future signUp(SignUpRequestDto body); + + Future sendEmail(); + + Future verifyEmail(); +} diff --git a/lib/data/user/data_sources/user_data_source_impl.dart b/lib/data/user/data_sources/user_data_source_impl.dart new file mode 100644 index 0000000..8dd82d7 --- /dev/null +++ b/lib/data/user/data_sources/user_data_source_impl.dart @@ -0,0 +1,30 @@ +import 'package:jusicool_ios/data/user/api/user_api.dart'; +import 'package:jusicool_ios/data/user/data_sources/user_data_source.dart'; +import 'package:jusicool_ios/data/user/dto/remote/request/sign_in_request_dto.dart'; +import 'package:jusicool_ios/data/user/dto/remote/request/sign_up_request_dto.dart'; + +class UserDataSourceImpl extends UserDataSource { + final UserApi _userApi; + + UserDataSourceImpl(this._userApi); + + @override + Future signIn(SignInRequestDto body) async { + return await _userApi.signIn(body); + } + + @override + Future signUp(SignUpRequestDto body) async { + return await _userApi.signUp(body); + } + + @override + Future sendEmail() async { + return await _userApi.sendEmail(); + } + + @override + Future verifyEmail() async { + return await _userApi.verifyEmail(); + } +} From 73a70b214c166416a439369e5618d427ca199abc Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Mon, 30 Jun 2025 20:05:37 +0900 Subject: [PATCH 11/54] :sparkles: :: Adds user repository interface and implementation Defines the UserRepository interface with methods for sign-in, sign-up, email verification, and sending email. Implements the UserRepositoryImpl class, which provides concrete implementations of the methods defined in the UserRepository interface, utilizing a UserDataSource for data access. --- .../user/repositories/user_repository.dart | 12 ++++++++ .../repositories/user_repository_impl.dart | 30 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 lib/data/user/repositories/user_repository.dart create mode 100644 lib/data/user/repositories/user_repository_impl.dart diff --git a/lib/data/user/repositories/user_repository.dart b/lib/data/user/repositories/user_repository.dart new file mode 100644 index 0000000..619b809 --- /dev/null +++ b/lib/data/user/repositories/user_repository.dart @@ -0,0 +1,12 @@ +import 'package:jusicool_ios/data/user/dto/remote/request/sign_in_request_dto.dart'; +import 'package:jusicool_ios/data/user/dto/remote/request/sign_up_request_dto.dart'; + +abstract class UserRepository { + Future signIn(SignInRequestDto body); + + Future signUp(SignUpRequestDto body); + + Future sendEmail(); + + Future verifyEmail(); +} diff --git a/lib/data/user/repositories/user_repository_impl.dart b/lib/data/user/repositories/user_repository_impl.dart new file mode 100644 index 0000000..7efe50b --- /dev/null +++ b/lib/data/user/repositories/user_repository_impl.dart @@ -0,0 +1,30 @@ +import 'package:jusicool_ios/data/user/data_sources/user_data_source.dart'; +import 'package:jusicool_ios/data/user/dto/remote/request/sign_in_request_dto.dart'; +import 'package:jusicool_ios/data/user/dto/remote/request/sign_up_request_dto.dart'; +import 'package:jusicool_ios/data/user/repositories/user_repository.dart'; + +class UserRepositoryImpl extends UserRepository { + final UserDataSource _userDataSource; + + UserRepositoryImpl(this._userDataSource); + + @override + Future signIn(SignInRequestDto body) async { + return await _userDataSource.signIn(body); + } + + @override + Future signUp(SignUpRequestDto body) async { + return await _userDataSource.signUp(body); + } + + @override + Future sendEmail() async { + return await _userDataSource.sendEmail(); + } + + @override + Future verifyEmail() async { + return await _userDataSource.verifyEmail(); + } +} From 6109ddeb1f158b79dd685cc56f173cd7e7972a17 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Mon, 30 Jun 2025 20:07:51 +0900 Subject: [PATCH 12/54] :truck: :: Moves asset entity to domain layer Moves the asset entity to the domain layer for better separation of concerns. Adds .gitkeep files to the my_capital repositories and use_cases directories to ensure they are included in the repository. --- lib/{data/models => domain/my_capital/entities}/my_assets.dart | 0 lib/domain/my_capital/repositories/.gitkeep | 0 lib/domain/my_capital/use_cases/.gitkeep | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename lib/{data/models => domain/my_capital/entities}/my_assets.dart (100%) create mode 100644 lib/domain/my_capital/repositories/.gitkeep create mode 100644 lib/domain/my_capital/use_cases/.gitkeep diff --git a/lib/data/models/my_assets.dart b/lib/domain/my_capital/entities/my_assets.dart similarity index 100% rename from lib/data/models/my_assets.dart rename to lib/domain/my_capital/entities/my_assets.dart diff --git a/lib/domain/my_capital/repositories/.gitkeep b/lib/domain/my_capital/repositories/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/domain/my_capital/use_cases/.gitkeep b/lib/domain/my_capital/use_cases/.gitkeep new file mode 100644 index 0000000..e69de29 From af7aadd0d27fa26920cf90e320345af50165d51c Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 15:57:29 +0900 Subject: [PATCH 13/54] =?UTF-8?q?=E2=9E=95=20::=20Updates=20dependencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates various dependencies to their latest versions, including fl_chart, go_router, retrofit_generator, riverpod, flutter_riverpod and rxdart. --- pubspec.yaml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 34d124d..6fe4a71 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,7 +23,7 @@ dependencies: url_launcher: ^6.2.1 - fl_chart: ^0.64.0 + fl_chart: ^1.0.0 intl: ^0.20.2 @@ -45,14 +45,21 @@ dependencies: pretty_dio_logger: ^1.4.0 - go_router: ^15.2.0 + go_router: ^16.0.0 retrofit: ^4.5.0 freezed_annotation: ^3.0.0 + riverpod: ^2.6.1 + + flutter_riverpod: ^2.6.1 + + rxdart: ^0.28.0 + + dev_dependencies: - retrofit_generator: + retrofit_generator: ^9.2.0 json_serializable: ^6.9.5 build_runner: ^2.5.4 freezed: ^3.0.6 From e68b6a0e6c90a87559e12c46e2da0f56af0add0a Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 15:58:11 +0900 Subject: [PATCH 14/54] =?UTF-8?q?=E2=9E=95=20::=20Configures=20analyzer=20?= =?UTF-8?q?options?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Configures the analyzer to exclude generated files and ignore specific annotation errors, improving code analysis efficiency and reducing noise. --- analysis_options.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 analysis_options.yml diff --git a/analysis_options.yml b/analysis_options.yml new file mode 100644 index 0000000..1374fca --- /dev/null +++ b/analysis_options.yml @@ -0,0 +1,6 @@ +analyzer: + exclude: + - "**/*.g.dart" + - "**/*.freezed.dart" + errors: + invalid_annotation_target: ignore \ No newline at end of file From 767b53e684d0c3e7b4d94ade838ca4f62d8b8c83 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 15:58:29 +0900 Subject: [PATCH 15/54] :truck: :: Moves router and widget to config This commit moves the router and menu bottom widget to the config directory. This change aims to improve the project's organization and maintainability by centralizing configuration-related files. --- lib/core/{ => config}/router/router.dart | 27 ++++--------------- lib/core/{ => config}/widget/menu_bottom.dart | 0 2 files changed, 5 insertions(+), 22 deletions(-) rename lib/core/{ => config}/router/router.dart (80%) rename lib/core/{ => config}/widget/menu_bottom.dart (100%) diff --git a/lib/core/router/router.dart b/lib/core/config/router/router.dart similarity index 80% rename from lib/core/router/router.dart rename to lib/core/config/router/router.dart index 74ea921..f0c4ab8 100644 --- a/lib/core/router/router.dart +++ b/lib/core/config/router/router.dart @@ -10,8 +10,7 @@ import 'package:jusicool_ios/presentation/sign_up/screens/name_input_screen.dart import 'package:jusicool_ios/presentation/sign_up/screens/password_create_screen.dart'; import 'package:jusicool_ios/presentation/splash/screens/splash_screen.dart'; import 'package:jusicool_ios/presentation/community/screens/community_post_list_screen.dart'; - -import '../../main.dart'; +import '../../../main.dart'; class RoutePaths { static const String splash = '/splash'; @@ -45,7 +44,7 @@ class AppRouter { ), GoRoute( path: RoutePaths.login, - builder: (context, state) => const LoginScreen(), + builder: (context, state) => LoginScreen(), ), GoRoute( path: RoutePaths.main, @@ -57,31 +56,15 @@ class AppRouter { ), GoRoute( path: RoutePaths.emailAuth, - builder: (context, state) { - final username = state.extra as String?; - return EmailAuthScreen(username: username ?? ''); - }, + builder: (context, state) => EmailAuthScreen(), ), GoRoute( path: RoutePaths.passwordCreate, - builder: (context, state) { - final extra = state.extra as Map?; - return PasswordCreateScreen( - username: extra?['username'] ?? '', - email: extra?['email'] ?? '', - ); - }, + builder: (context, state) => PasswordCreateScreen(), ), GoRoute( path: RoutePaths.findSchool, - builder: (context, state) { - final extra = state.extra as Map?; - return FindSchoolScreen( - username: extra?['username'] ?? '', - email: extra?['email'] ?? '', - password: extra?['password'] ?? '', - ); - }, + builder: (context, state) => FindSchoolScreen(), ), GoRoute( path: RoutePaths.mainCapital, diff --git a/lib/core/widget/menu_bottom.dart b/lib/core/config/widget/menu_bottom.dart similarity index 100% rename from lib/core/widget/menu_bottom.dart rename to lib/core/config/widget/menu_bottom.dart From 4863d4a69499edf7c737c9649df5d64debc50f28 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 15:58:48 +0900 Subject: [PATCH 16/54] :recycle: :: Replaces log with print for Dio error output Changes the logging mechanism from 'log' to 'print' for Dio error messages. This simplifies debugging and ensures error messages are consistently displayed in the console. --- .../interceptor/dio_error_interceptor.dart | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/core/network/interceptor/dio_error_interceptor.dart b/lib/core/network/interceptor/dio_error_interceptor.dart index 6dee083..aa20744 100644 --- a/lib/core/network/interceptor/dio_error_interceptor.dart +++ b/lib/core/network/interceptor/dio_error_interceptor.dart @@ -1,51 +1,49 @@ -import 'dart:developer'; - import 'package:dio/dio.dart'; class DioErrorInterceptor extends InterceptorsWrapper { @override void onError(DioException err, ErrorInterceptorHandler handler) { - log('⛔ DIO 에러 :: ${err.type}'); + print('⛔ DIO 에러 :: ${err.type}'); switch (err.type) { case DioExceptionType.connectionTimeout: - log('⏳ 연결 시간 초과'); + print('⏳ 연결 시간 초과'); break; case DioExceptionType.sendTimeout: - log('⏳ 전송 시간 초과'); + print('⏳ 전송 시간 초과'); break; case DioExceptionType.receiveTimeout: - log('⏳ 수신 시간 초과'); + print('⏳ 수신 시간 초과'); break; case DioExceptionType.badResponse: switch (err.response?.statusCode) { case 400: - log('🚫 잘못된 요청: ${err.response?.data}'); + print('🚫 잘못된 요청: ${err.response?.data}'); break; case 401: - log('🚫 인증 실패: ${err.response?.data}'); + print('🚫 인증 실패: ${err.response?.data}'); break; case 403: - log('🚫 권한 없음: ${err.response?.data}'); + print('🚫 권한 없음: ${err.response?.data}'); break; case 404: - log('🚫 리소스 없음: ${err.response?.data}'); + print('🚫 리소스 없음: ${err.response?.data}'); break; case 500: - log('🚫 서버 오류: ${err.response?.data}'); + print('🚫 서버 오류: ${err.response?.data}'); break; default: - log('🚫 기타 오류: ${err.response?.data}'); + print('🚫 기타 오류: ${err.response?.data}'); break; } case DioExceptionType.cancel: - log('❌ 요청 취소됨'); + print('❌ 요청 취소됨'); break; case DioExceptionType.connectionError: - log('🚫 인터넷 연결 오류: ${err.message}'); + print('🚫 인터넷 연결 오류: ${err.message}'); break; case DioExceptionType.unknown: default: - log('❓ 알 수 없는 에러: ${err.message}'); + print('❓ 알 수 없는 에러: ${err.message}'); } handler.next(err); } From 66ad32f91a5b9ca0264a180ea5e4e261c7e02532 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 15:59:05 +0900 Subject: [PATCH 17/54] :sparkles: :: Adds request body logging interceptor Adds a Dio interceptor that logs the request body to the console. This aids in debugging network requests by providing visibility into the data being sent to the server. --- .../interceptor/dio_request_interceptor.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 lib/core/network/interceptor/dio_request_interceptor.dart diff --git a/lib/core/network/interceptor/dio_request_interceptor.dart b/lib/core/network/interceptor/dio_request_interceptor.dart new file mode 100644 index 0000000..6683c5b --- /dev/null +++ b/lib/core/network/interceptor/dio_request_interceptor.dart @@ -0,0 +1,13 @@ +import 'dart:convert'; + +import 'package:dio/dio.dart'; + +class DioRequestInterceptor extends InterceptorsWrapper { + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + print("╔ Body"); + print("║ ${jsonEncode(options.data)}"); + print("╚${'═' * 90}╝"); + super.onRequest(options, handler); + } +} From dd2892bb35971bb438c38d10c59564e8a99639fb Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 15:59:21 +0900 Subject: [PATCH 18/54] :sparkles: :: Configures DI for user authentication Sets up dependency injection for sign-in and sign-up features. Registers Dio instances, User and Neis APIs, data sources, repositories, and use cases related to user authentication. --- lib/core/config/di/dependencies.dart | 46 +++++++++++++++++++--------- lib/core/network/api/api_client.dart | 28 +++++++++++++++-- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/lib/core/config/di/dependencies.dart b/lib/core/config/di/dependencies.dart index af6096a..fbc7ff4 100644 --- a/lib/core/config/di/dependencies.dart +++ b/lib/core/config/di/dependencies.dart @@ -3,16 +3,17 @@ import 'package:cookie_jar/cookie_jar.dart'; import 'package:dio/dio.dart'; import 'package:get_it/get_it.dart'; import 'package:jusicool_ios/core/network/api/api_client.dart'; -import 'package:jusicool_ios/data/account/api/account_api.dart'; -import 'package:jusicool_ios/data/account/data_sources/account_data_source.dart'; -import 'package:jusicool_ios/data/user/api/user_api.dart'; +import 'package:jusicool_ios/data/user/service/user_api.dart'; import 'package:jusicool_ios/data/user/data_sources/user_data_source.dart'; -import '../../../data/account/data_sources/account_data_source_impl.dart'; -import '../../../data/account/repositories/account_repository.dart'; -import '../../../data/account/repositories/account_repository_impl.dart'; +import 'package:jusicool_ios/domain/sign_in/repositories/sign_in_repository.dart'; +import 'package:jusicool_ios/domain/sign_in/usecase/sign_in_usecase.dart'; +import 'package:jusicool_ios/domain/sign_in/usecase/sign_in_usecase_impl.dart'; +import 'package:jusicool_ios/domain/sign_up/repositories/sign_up_repository.dart'; +import 'package:jusicool_ios/domain/sign_up/usecase/sign_up_usecase.dart'; +import 'package:jusicool_ios/domain/sign_up/usecase/sign_up_usecase_impl.dart'; import '../../../data/user/data_sources/user_data_source_impl.dart'; -import '../../../data/user/repositories/user_repository.dart'; import '../../../data/user/repositories/user_repository_impl.dart'; +import '../../../data/user/service/neis_api.dart'; import '../../network/interceptor/cookie_interceptor.dart'; final di = GetIt.instance; @@ -21,6 +22,7 @@ void _setDio() { try { di.registerLazySingleton(() => dio()); di.registerSingletonAsync(() async => await cookieJar()); + di.registerLazySingleton(() => neis(), instanceName: 'neis'); log('✅ :: DIO DI 성공'); } catch (e) { log("⛔ :: DIO DI 실패 \n$e"); @@ -30,7 +32,9 @@ void _setDio() { void _setApi() { try { di.registerLazySingleton(() => UserApi(di.get())); - di.registerLazySingleton(() => AccountApi(di.get())); + di.registerLazySingleton( + () => NeisApi(di.get(instanceName: 'neis')), + ); log('✅ :: API DI 성공'); } catch (e) { log("⛔ :: API DI 실패 \n$e"); @@ -40,10 +44,7 @@ void _setApi() { void _setDataSources() { try { di.registerLazySingleton( - () => UserDataSourceImpl(di.get()), - ); - di.registerLazySingleton( - () => AccountDataSourceImpl(di.get()), + () => UserDataSourceImpl(di.get(), di.get()), ); log('✅ :: DataSources DI 성공'); } catch (e) { @@ -53,11 +54,11 @@ void _setDataSources() { void _setRepository() { try { - di.registerLazySingleton( + di.registerLazySingleton( () => UserRepositoryImpl(di.get()), ); - di.registerLazySingleton( - () => AccountRepositoryImpl(di.get()), + di.registerLazySingleton( + () => UserRepositoryImpl(di.get()), ); log('✅ :: Repository DI 성공'); } catch (e) { @@ -65,12 +66,27 @@ void _setRepository() { } } +void _setUseCase() { + try { + di.registerLazySingleton( + () => SignInUseCaseImpl(di.get()), + ); + di.registerLazySingleton( + () => SignUpUseCaseImpl(di.get()), + ); + log('✅ :: UseCase DI 성공'); + } catch (e) { + log("⛔ :: UseCase DI 실패 \n$e"); + } +} + void setDependencies() { try { _setDio(); _setApi(); _setDataSources(); _setRepository(); + _setUseCase(); log('✅ :: DI 설정 완료'); } catch (e) { log("⛔ :: DI 실패 \n$e"); diff --git a/lib/core/network/api/api_client.dart b/lib/core/network/api/api_client.dart index d0b3257..170a641 100644 --- a/lib/core/network/api/api_client.dart +++ b/lib/core/network/api/api_client.dart @@ -1,11 +1,11 @@ import 'dart:developer'; - import 'package:cookie_jar/cookie_jar.dart'; import 'package:dio/dio.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:get_it/get_it.dart'; +import 'package:jusicool_ios/core/network/interceptor/dio_request_interceptor.dart'; import 'package:pretty_dio_logger/pretty_dio_logger.dart'; import '../interceptor/dio_error_interceptor.dart'; @@ -24,6 +24,10 @@ Dio dio() { Dio dio = Dio( BaseOptions( baseUrl: _baseUrl ?? "", + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, connectTimeout: Duration(seconds: 30), receiveTimeout: Duration(seconds: 30), ), @@ -33,15 +37,33 @@ Dio dio() { dio.interceptors.add( PrettyDioLogger( requestHeader: true, - requestBody: true, responseHeader: true, responseBody: true, error: false, compact: true, - maxWidth: 90, enabled: kDebugMode, ), ); dio.interceptors.add(DioErrorInterceptor()); + dio.interceptors.add(DioRequestInterceptor()); + return dio; +} + +Dio neis() { + String? _neisApiKey = dotenv.env['NEIS_API_KEY']; + if (_neisApiKey == null) { + log('⛔ :: NEIS_API_KEY is not set in .env file'); + } + + Dio dio = Dio( + BaseOptions( + baseUrl: 'https://open.neis.go.kr/hub/schoolInfo', + connectTimeout: Duration(seconds: 30), + receiveTimeout: Duration(seconds: 30), + queryParameters: {'KEY': _neisApiKey ?? '', 'Type': 'json'}, + ), + ); + dio.interceptors.add(DioErrorInterceptor()); + dio.interceptors.add(PrettyDioLogger(responseBody: true)); return dio; } From 7dd094d7889571266fdc44aa64aa3226ebb2176e Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 15:59:41 +0900 Subject: [PATCH 19/54] :sparkles: :: Adds user data source implementation Implements the user data source and its implementation. It introduces methods for user authentication, email verification, and searching schools. It also creates a remote data source to handle the requests and responses from the APIs. --- .../user/data_sources/user_data_source.dart | 15 +++++++---- .../data_sources/user_data_source_impl.dart | 25 ++++++++++++++----- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/lib/data/user/data_sources/user_data_source.dart b/lib/data/user/data_sources/user_data_source.dart index c302ecb..c90cd7c 100644 --- a/lib/data/user/data_sources/user_data_source.dart +++ b/lib/data/user/data_sources/user_data_source.dart @@ -1,15 +1,20 @@ -import 'package:dio/dio.dart'; - +import 'package:jusicool_ios/data/user/dto/remote/request/sign_up_search_school_request_dto.dart'; +import 'package:jusicool_ios/data/user/dto/remote/request/sign_up_send_email_request_dto.dart'; +import 'package:jusicool_ios/data/user/dto/remote/request/sign_up_verify_email_request_dto.dart'; +import 'package:jusicool_ios/data/user/dto/remote/response/sign_up_search_school_response_dto.dart'; import '../dto/remote/request/sign_in_request_dto.dart'; import '../dto/remote/request/sign_up_request_dto.dart'; abstract class UserDataSource { - Future signIn(SignInRequestDto body); Future signUp(SignUpRequestDto body); - Future sendEmail(); + Future sendEmail(SignUpSendEmailRequestDto body); + + Future verifyEmail(SignUpVerifyEmailRequestDto body); - Future verifyEmail(); + Future searchSchools( + SignUpSearchSchoolRequestDto schoolName, + ); } diff --git a/lib/data/user/data_sources/user_data_source_impl.dart b/lib/data/user/data_sources/user_data_source_impl.dart index 8dd82d7..0113c13 100644 --- a/lib/data/user/data_sources/user_data_source_impl.dart +++ b/lib/data/user/data_sources/user_data_source_impl.dart @@ -1,12 +1,18 @@ -import 'package:jusicool_ios/data/user/api/user_api.dart'; +import 'package:jusicool_ios/data/user/dto/remote/request/sign_up_search_school_request_dto.dart'; +import 'package:jusicool_ios/data/user/dto/remote/request/sign_up_send_email_request_dto.dart'; +import 'package:jusicool_ios/data/user/dto/remote/request/sign_up_verify_email_request_dto.dart'; +import 'package:jusicool_ios/data/user/service/neis_api.dart'; +import 'package:jusicool_ios/data/user/service/user_api.dart'; import 'package:jusicool_ios/data/user/data_sources/user_data_source.dart'; import 'package:jusicool_ios/data/user/dto/remote/request/sign_in_request_dto.dart'; import 'package:jusicool_ios/data/user/dto/remote/request/sign_up_request_dto.dart'; +import '../dto/remote/response/sign_up_search_school_response_dto.dart'; class UserDataSourceImpl extends UserDataSource { final UserApi _userApi; + final NeisApi _neisClient; - UserDataSourceImpl(this._userApi); + UserDataSourceImpl(this._userApi, this._neisClient); @override Future signIn(SignInRequestDto body) async { @@ -19,12 +25,19 @@ class UserDataSourceImpl extends UserDataSource { } @override - Future sendEmail() async { - return await _userApi.sendEmail(); + Future sendEmail(SignUpSendEmailRequestDto body) async { + return await _userApi.sendEmail(body); } @override - Future verifyEmail() async { - return await _userApi.verifyEmail(); + Future verifyEmail(SignUpVerifyEmailRequestDto body) async { + return await _userApi.verifyEmail(body); + } + + @override + Future searchSchools( + SignUpSearchSchoolRequestDto school, + ) async { + return await _neisClient.fetchSchools(school.schoolName); } } From 3d548ea599293a694cdec66b64e66f0b41006077 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 15:59:57 +0900 Subject: [PATCH 20/54] :recycle: :: Updates user sign-up and sign-in DTOs Updates the sign-up DTO to include username and school information. Makes the SignInRequestDto constructor a const factory. --- lib/data/user/dto/remote/request/sign_in_request_dto.dart | 6 ++++-- lib/data/user/dto/remote/request/sign_up_request_dto.dart | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/data/user/dto/remote/request/sign_in_request_dto.dart b/lib/data/user/dto/remote/request/sign_in_request_dto.dart index 048bf30..64c2a05 100644 --- a/lib/data/user/dto/remote/request/sign_in_request_dto.dart +++ b/lib/data/user/dto/remote/request/sign_in_request_dto.dart @@ -6,8 +6,10 @@ part 'sign_in_request_dto.freezed.dart'; @freezed abstract class SignInRequestDto with _$SignInRequestDto { - factory SignInRequestDto({required String email, required String password}) = - _SignInRequestDto; + const factory SignInRequestDto({ + required String email, + required String password, + }) = _SignInRequestDto; factory SignInRequestDto.fromJson(Map json) => _$SignInRequestDtoFromJson(json); diff --git a/lib/data/user/dto/remote/request/sign_up_request_dto.dart b/lib/data/user/dto/remote/request/sign_up_request_dto.dart index 83001ef..523e840 100644 --- a/lib/data/user/dto/remote/request/sign_up_request_dto.dart +++ b/lib/data/user/dto/remote/request/sign_up_request_dto.dart @@ -9,7 +9,8 @@ abstract class SignUpRequestDto with _$SignUpRequestDto { factory SignUpRequestDto({ required String email, required String password, - required String name, + required String username, + required String school, }) = _SignUpRequestDto; factory SignUpRequestDto.fromJson(Map json) => From 2f485e8b13dbffec5d4084214ec0e08853a54b9a Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:00:10 +0900 Subject: [PATCH 21/54] :sparkles: :: Adds sign up request/response DTOs Adds data transfer objects for sign up related API requests and responses. These DTOs are used for school search, email verification, and sending email functionalities. They define the data structure for communication between the client and server during the sign-up process. --- .../sign_up_search_school_request_dto.dart | 15 ++++++++ .../sign_up_send_email_request_dto.dart | 14 +++++++ .../sign_up_verify_email_request_dto.dart | 16 ++++++++ .../sign_up_search_school_response_dto.dart | 37 +++++++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 lib/data/user/dto/remote/request/sign_up_search_school_request_dto.dart create mode 100644 lib/data/user/dto/remote/request/sign_up_send_email_request_dto.dart create mode 100644 lib/data/user/dto/remote/request/sign_up_verify_email_request_dto.dart create mode 100644 lib/data/user/dto/remote/response/sign_up_search_school_response_dto.dart diff --git a/lib/data/user/dto/remote/request/sign_up_search_school_request_dto.dart b/lib/data/user/dto/remote/request/sign_up_search_school_request_dto.dart new file mode 100644 index 0000000..d4bc32e --- /dev/null +++ b/lib/data/user/dto/remote/request/sign_up_search_school_request_dto.dart @@ -0,0 +1,15 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'sign_up_search_school_request_dto.freezed.dart'; + +part 'sign_up_search_school_request_dto.g.dart'; + +@freezed +abstract class SignUpSearchSchoolRequestDto + with _$SignUpSearchSchoolRequestDto { + factory SignUpSearchSchoolRequestDto({@Default("") String schoolName}) = + _SignUpSearchSchoolRequestDto; + + factory SignUpSearchSchoolRequestDto.fromJson(Map json) => + _$SignUpSearchSchoolRequestDtoFromJson(json); +} diff --git a/lib/data/user/dto/remote/request/sign_up_send_email_request_dto.dart b/lib/data/user/dto/remote/request/sign_up_send_email_request_dto.dart new file mode 100644 index 0000000..47d5811 --- /dev/null +++ b/lib/data/user/dto/remote/request/sign_up_send_email_request_dto.dart @@ -0,0 +1,14 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'sign_up_send_email_request_dto.freezed.dart'; + +part 'sign_up_send_email_request_dto.g.dart'; + +@freezed +abstract class SignUpSendEmailRequestDto with _$SignUpSendEmailRequestDto { + factory SignUpSendEmailRequestDto({required String email}) = + _SignUpSendEmailRequestDto; + + factory SignUpSendEmailRequestDto.fromJson(Map json) => + _$SignUpSendEmailRequestDtoFromJson(json); +} diff --git a/lib/data/user/dto/remote/request/sign_up_verify_email_request_dto.dart b/lib/data/user/dto/remote/request/sign_up_verify_email_request_dto.dart new file mode 100644 index 0000000..5352509 --- /dev/null +++ b/lib/data/user/dto/remote/request/sign_up_verify_email_request_dto.dart @@ -0,0 +1,16 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'sign_up_verify_email_request_dto.freezed.dart'; + +part 'sign_up_verify_email_request_dto.g.dart'; + +@freezed +abstract class SignUpVerifyEmailRequestDto with _$SignUpVerifyEmailRequestDto { + factory SignUpVerifyEmailRequestDto({ + required String email, + required String code, + }) = _SignUpVerifyEmailRequestDto; + + factory SignUpVerifyEmailRequestDto.fromJson(Map json) => + _$SignUpVerifyEmailRequestDtoFromJson(json); +} diff --git a/lib/data/user/dto/remote/response/sign_up_search_school_response_dto.dart b/lib/data/user/dto/remote/response/sign_up_search_school_response_dto.dart new file mode 100644 index 0000000..bc4bf9b --- /dev/null +++ b/lib/data/user/dto/remote/response/sign_up_search_school_response_dto.dart @@ -0,0 +1,37 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'sign_up_search_school_response_dto.freezed.dart'; + +part 'sign_up_search_school_response_dto.g.dart'; + +@freezed +abstract class SignUpSearchSchoolResponseDto + with _$SignUpSearchSchoolResponseDto { + const factory SignUpSearchSchoolResponseDto({ + @JsonKey(name: 'schoolInfo') required List schoolInfo, + }) = _SignUpSearchSchoolResponseDto; + + factory SignUpSearchSchoolResponseDto.fromJson( + Map json, + ) => _$SignUpSearchSchoolResponseDtoFromJson(json); +} + +@freezed +abstract class SchoolInfoDto with _$SchoolInfoDto { + const factory SchoolInfoDto({List? head, List? row}) = + _SchoolInfoDto; + + factory SchoolInfoDto.fromJson(Map json) => + _$SchoolInfoDtoFromJson(json); +} + +@freezed +abstract class SchoolRowDto with _$SchoolRowDto { + const factory SchoolRowDto({ + @JsonKey(name: 'SCHUL_NM') required String schoolName, + @JsonKey(name: 'ORG_RDNMA') required String schoolAddress, + }) = _SchoolRowDto; + + factory SchoolRowDto.fromJson(Map json) => + _$SchoolRowDtoFromJson(json); +} From 48a8a322c2ee762230dcfb9b22e2902622ffeb86 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:00:25 +0900 Subject: [PATCH 22/54] :sparkles: :: Adds remote request mappers for user sign up Implements mappers for remote requests related to user sign-up, including sending email, verifying email, and searching for schools. Also includes sign-in request mapper. --- .../local/request}/.gitkeep | 0 .../user/mappers/local/response}/.gitkeep | 0 .../request/sign_in_request_mapper.dart | 10 ++++++++ .../request/sign_up_request_mapper.dart | 18 ++++++++++++++ .../sign_up_search_school_request_mapper.dart | 7 ++++++ .../sign_up_send_email_request_mapper.dart | 8 +++++++ .../sign_up_verify_email_request_mapper.dart | 8 +++++++ ...sign_up_search_school_response_mapper.dart | 24 +++++++++++++++++++ 8 files changed, 75 insertions(+) rename lib/data/user/{dto/remote/response => mappers/local/request}/.gitkeep (100%) rename lib/{domain/my_capital/use_cases => data/user/mappers/local/response}/.gitkeep (100%) create mode 100644 lib/data/user/mappers/remote/request/sign_in_request_mapper.dart create mode 100644 lib/data/user/mappers/remote/request/sign_up_request_mapper.dart create mode 100644 lib/data/user/mappers/remote/request/sign_up_search_school_request_mapper.dart create mode 100644 lib/data/user/mappers/remote/request/sign_up_send_email_request_mapper.dart create mode 100644 lib/data/user/mappers/remote/request/sign_up_verify_email_request_mapper.dart create mode 100644 lib/data/user/mappers/remote/response/sign_up_search_school_response_mapper.dart diff --git a/lib/data/user/dto/remote/response/.gitkeep b/lib/data/user/mappers/local/request/.gitkeep similarity index 100% rename from lib/data/user/dto/remote/response/.gitkeep rename to lib/data/user/mappers/local/request/.gitkeep diff --git a/lib/domain/my_capital/use_cases/.gitkeep b/lib/data/user/mappers/local/response/.gitkeep similarity index 100% rename from lib/domain/my_capital/use_cases/.gitkeep rename to lib/data/user/mappers/local/response/.gitkeep diff --git a/lib/data/user/mappers/remote/request/sign_in_request_mapper.dart b/lib/data/user/mappers/remote/request/sign_in_request_mapper.dart new file mode 100644 index 0000000..485e922 --- /dev/null +++ b/lib/data/user/mappers/remote/request/sign_in_request_mapper.dart @@ -0,0 +1,10 @@ +import 'package:jusicool_ios/data/user/dto/remote/request/sign_in_request_dto.dart'; +import 'package:jusicool_ios/domain/sign_in/entity/sign_in_entity.dart'; + +class SignInRequestMapper { + static SignInRequestDto toDto(SignInEntity entity) => + SignInRequestDto(email: entity.email, password: entity.password); + + static SignInEntity toEntity(SignInRequestDto dto) => + SignInEntity(email: dto.email, password: dto.password); +} diff --git a/lib/data/user/mappers/remote/request/sign_up_request_mapper.dart b/lib/data/user/mappers/remote/request/sign_up_request_mapper.dart new file mode 100644 index 0000000..1dec46a --- /dev/null +++ b/lib/data/user/mappers/remote/request/sign_up_request_mapper.dart @@ -0,0 +1,18 @@ +import 'package:jusicool_ios/data/user/dto/remote/request/sign_up_request_dto.dart'; +import 'package:jusicool_ios/domain/sign_up/entity/sign_up_entity.dart'; + +class SignUpRequestMapper { + static SignUpRequestDto toDto(SignUpEntity entity) => SignUpRequestDto( + email: entity.email, + password: entity.password, + username: entity.name, + school: entity.school, + ); + + static SignUpEntity toEntity(SignUpRequestDto dto) => SignUpEntity( + email: dto.email, + password: dto.password, + name: dto.username, + school: dto.school, + ); +} diff --git a/lib/data/user/mappers/remote/request/sign_up_search_school_request_mapper.dart b/lib/data/user/mappers/remote/request/sign_up_search_school_request_mapper.dart new file mode 100644 index 0000000..e165842 --- /dev/null +++ b/lib/data/user/mappers/remote/request/sign_up_search_school_request_mapper.dart @@ -0,0 +1,7 @@ +import 'package:jusicool_ios/data/user/dto/remote/request/sign_up_search_school_request_dto.dart'; + +class SignUpSearchSchoolRequestMapper { + static SignUpSearchSchoolRequestDto toDto(String entity) { + return SignUpSearchSchoolRequestDto(schoolName: entity); + } +} diff --git a/lib/data/user/mappers/remote/request/sign_up_send_email_request_mapper.dart b/lib/data/user/mappers/remote/request/sign_up_send_email_request_mapper.dart new file mode 100644 index 0000000..8cd512e --- /dev/null +++ b/lib/data/user/mappers/remote/request/sign_up_send_email_request_mapper.dart @@ -0,0 +1,8 @@ +import 'package:jusicool_ios/domain/sign_up/entity/sign_up_email_entity.dart'; + +import '../../../dto/remote/request/sign_up_send_email_request_dto.dart'; + +class SignUpSendEmailRequestMapper { + static SignUpSendEmailRequestDto toDto(SignUpEmailEntity entity) => + SignUpSendEmailRequestDto(email: entity.email); +} diff --git a/lib/data/user/mappers/remote/request/sign_up_verify_email_request_mapper.dart b/lib/data/user/mappers/remote/request/sign_up_verify_email_request_mapper.dart new file mode 100644 index 0000000..8231896 --- /dev/null +++ b/lib/data/user/mappers/remote/request/sign_up_verify_email_request_mapper.dart @@ -0,0 +1,8 @@ +import 'package:jusicool_ios/data/user/dto/remote/request/sign_up_verify_email_request_dto.dart'; + +import '../../../../../domain/sign_up/entity/sign_up_email_entity.dart'; + +class SignUpVerifyEmailRequestMapper { + static SignUpVerifyEmailRequestDto toDto(SignUpEmailEntity entity) => + SignUpVerifyEmailRequestDto(email: entity.email, code: entity.verifyCode); +} diff --git a/lib/data/user/mappers/remote/response/sign_up_search_school_response_mapper.dart b/lib/data/user/mappers/remote/response/sign_up_search_school_response_mapper.dart new file mode 100644 index 0000000..ec6456c --- /dev/null +++ b/lib/data/user/mappers/remote/response/sign_up_search_school_response_mapper.dart @@ -0,0 +1,24 @@ +import 'package:jusicool_ios/data/user/dto/remote/response/sign_up_search_school_response_dto.dart'; +import 'package:jusicool_ios/domain/sign_up/entity/sign_up_search_school_entity.dart'; + +class SignUpSearchSchoolResponseMapper { + static SignUpSearchSchoolResponseEntity toEntity( + SignUpSearchSchoolResponseDto dto, + ) { + return SignUpSearchSchoolResponseEntity( + schoolInfo: + dto.schoolInfo.map((infoDto) { + return SchoolInfoEntity( + head: infoDto.head, + row: + infoDto.row?.map((rowDto) { + return SchoolRowEntity( + schoolName: rowDto.schoolName, + schoolAddress: rowDto.schoolAddress, + ); + }).toList(), + ); + }).toList(), + ); + } +} From 0915de4c99ee554e12c4814b68f44ec7488c5ab0 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:00:53 +0900 Subject: [PATCH 23/54] :fire: :: Removes UserRepository abstraction Removes the old UserRepository interface. This change is part of a refactoring effort to decouple the repository interface from its implementation, enhancing separation of concerns. --- lib/data/user/repositories/user_repository.dart | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 lib/data/user/repositories/user_repository.dart diff --git a/lib/data/user/repositories/user_repository.dart b/lib/data/user/repositories/user_repository.dart deleted file mode 100644 index 619b809..0000000 --- a/lib/data/user/repositories/user_repository.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:jusicool_ios/data/user/dto/remote/request/sign_in_request_dto.dart'; -import 'package:jusicool_ios/data/user/dto/remote/request/sign_up_request_dto.dart'; - -abstract class UserRepository { - Future signIn(SignInRequestDto body); - - Future signUp(SignUpRequestDto body); - - Future sendEmail(); - - Future verifyEmail(); -} From 74be8301f6dd6aa7f2ce60945009bd63191116d9 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:01:06 +0900 Subject: [PATCH 24/54] :sparkles: :: Adds user authentication services Introduces the NeisApi for school search during signup. Renames UserApi file and updates the UserApi to include methods for sending and verifying email during user registration, enhancing user authentication and validation processes. --- lib/data/user/service/neis_api.dart | 16 ++++++++++++++++ lib/data/user/{api => service}/user_api.dart | 8 +++++--- 2 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 lib/data/user/service/neis_api.dart rename lib/data/user/{api => service}/user_api.dart (65%) diff --git a/lib/data/user/service/neis_api.dart b/lib/data/user/service/neis_api.dart new file mode 100644 index 0000000..2ab3a8b --- /dev/null +++ b/lib/data/user/service/neis_api.dart @@ -0,0 +1,16 @@ +import 'package:dio/dio.dart'; +import 'package:retrofit/error_logger.dart'; +import 'package:retrofit/http.dart'; +import '../dto/remote/response/sign_up_search_school_response_dto.dart'; + +part 'neis_api.g.dart'; + +@RestApi() +abstract class NeisApi { + factory NeisApi(Dio dio, {String baseUrl}) = _NeisApi; + + @GET('/') + Future fetchSchools( + @Query('SCHUL_NM') String schoolName, + ); +} diff --git a/lib/data/user/api/user_api.dart b/lib/data/user/service/user_api.dart similarity index 65% rename from lib/data/user/api/user_api.dart rename to lib/data/user/service/user_api.dart index 0472d87..6c0944b 100644 --- a/lib/data/user/api/user_api.dart +++ b/lib/data/user/service/user_api.dart @@ -1,8 +1,10 @@ import 'package:dio/dio.dart'; +import 'package:jusicool_ios/data/user/dto/remote/request/sign_up_verify_email_request_dto.dart'; import 'package:retrofit/error_logger.dart'; import 'package:retrofit/http.dart'; import '../dto/remote/request/sign_in_request_dto.dart'; import '../dto/remote/request/sign_up_request_dto.dart'; +import '../dto/remote/request/sign_up_send_email_request_dto.dart'; part 'user_api.g.dart'; @@ -17,8 +19,8 @@ abstract class UserApi { Future signUp(@Body() SignUpRequestDto body); @POST('/user/email/send') - Future sendEmail(); + Future sendEmail(@Body() SignUpSendEmailRequestDto body); @POST('/user/email/verify') - Future verifyEmail(); -} + Future verifyEmail(@Body() SignUpVerifyEmailRequestDto body); +} \ No newline at end of file From 19df2aa347aa2cae260546b70105ad9659d3aff1 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:01:22 +0900 Subject: [PATCH 25/54] :recycle: :: Implements user repository Implements the user repository to handle sign-in, sign-up, email verification, and school search functionalities. Uses data sources and mappers for data transformation between the data and domain layers. --- .../repositories/user_repository_impl.dart | 51 +++++++++++++++---- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/lib/data/user/repositories/user_repository_impl.dart b/lib/data/user/repositories/user_repository_impl.dart index 7efe50b..e8128c6 100644 --- a/lib/data/user/repositories/user_repository_impl.dart +++ b/lib/data/user/repositories/user_repository_impl.dart @@ -1,30 +1,61 @@ import 'package:jusicool_ios/data/user/data_sources/user_data_source.dart'; import 'package:jusicool_ios/data/user/dto/remote/request/sign_in_request_dto.dart'; import 'package:jusicool_ios/data/user/dto/remote/request/sign_up_request_dto.dart'; -import 'package:jusicool_ios/data/user/repositories/user_repository.dart'; +import 'package:jusicool_ios/data/user/dto/remote/request/sign_up_search_school_request_dto.dart'; +import 'package:jusicool_ios/data/user/dto/remote/request/sign_up_verify_email_request_dto.dart'; +import 'package:jusicool_ios/data/user/dto/remote/response/sign_up_search_school_response_dto.dart'; +import 'package:jusicool_ios/data/user/mappers/remote/request/sign_in_request_mapper.dart'; +import 'package:jusicool_ios/data/user/mappers/remote/request/sign_up_request_mapper.dart'; +import 'package:jusicool_ios/data/user/mappers/remote/request/sign_up_send_email_request_mapper.dart'; +import 'package:jusicool_ios/data/user/mappers/remote/request/sign_up_verify_email_request_mapper.dart'; +import 'package:jusicool_ios/data/user/mappers/remote/response/sign_up_search_school_response_mapper.dart'; +import 'package:jusicool_ios/domain/sign_in/entity/sign_in_entity.dart'; +import 'package:jusicool_ios/domain/sign_in/repositories/sign_in_repository.dart'; +import 'package:jusicool_ios/domain/sign_up/entity/sign_up_email_entity.dart'; +import 'package:jusicool_ios/domain/sign_up/entity/sign_up_entity.dart'; +import 'package:jusicool_ios/domain/sign_up/entity/sign_up_search_school_entity.dart'; +import 'package:jusicool_ios/domain/sign_up/repositories/sign_up_repository.dart'; +import '../dto/remote/request/sign_up_send_email_request_dto.dart'; +import '../mappers/remote/request/sign_up_search_school_request_mapper.dart'; -class UserRepositoryImpl extends UserRepository { +class UserRepositoryImpl implements SignInRepository, SignUpRepository { final UserDataSource _userDataSource; UserRepositoryImpl(this._userDataSource); @override - Future signIn(SignInRequestDto body) async { - return await _userDataSource.signIn(body); + Future signIn(SignInEntity body) async { + final SignInRequestDto request = SignInRequestMapper.toDto(body); + return await _userDataSource.signIn(request); } @override - Future signUp(SignUpRequestDto body) async { - return await _userDataSource.signUp(body); + Future signUp(SignUpEntity body) async { + final SignUpRequestDto request = SignUpRequestMapper.toDto(body); + return await _userDataSource.signUp(request); } @override - Future sendEmail() async { - return await _userDataSource.sendEmail(); + Future sendEmail(SignUpEmailEntity body) async { + final SignUpSendEmailRequestDto request = + SignUpSendEmailRequestMapper.toDto(body); + return await _userDataSource.sendEmail(request); } @override - Future verifyEmail() async { - return await _userDataSource.verifyEmail(); + Future verifyEmail(SignUpEmailEntity entity) async { + final SignUpVerifyEmailRequestDto request = + SignUpVerifyEmailRequestMapper.toDto(entity); + return await _userDataSource.verifyEmail(request); + } + + @override + Future searchSchool( + String schoolName, + ) async { + final SignUpSearchSchoolRequestDto request = + SignUpSearchSchoolRequestMapper.toDto(schoolName); + final response = await _userDataSource.searchSchools(request); + return SignUpSearchSchoolResponseMapper.toEntity(response); } } From 33a608ef688441527099675d8efb9fd5e3b3d417 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:01:42 +0900 Subject: [PATCH 26/54] :sparkles: :: Adds sign-in entity for data handling. Creates the `SignInEntity` using Freezed to handle sign-in data. This entity includes email and password fields, along with JSON serialization/deserialization support. --- lib/domain/my_capital/usecase/.gitkeep | 0 lib/domain/sign_in/entity/sign_in_entity.dart | 14 ++++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 lib/domain/my_capital/usecase/.gitkeep create mode 100644 lib/domain/sign_in/entity/sign_in_entity.dart diff --git a/lib/domain/my_capital/usecase/.gitkeep b/lib/domain/my_capital/usecase/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/domain/sign_in/entity/sign_in_entity.dart b/lib/domain/sign_in/entity/sign_in_entity.dart new file mode 100644 index 0000000..e544e9f --- /dev/null +++ b/lib/domain/sign_in/entity/sign_in_entity.dart @@ -0,0 +1,14 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'sign_in_entity.freezed.dart'; + +part 'sign_in_entity.g.dart'; + +@freezed +abstract class SignInEntity with _$SignInEntity { + factory SignInEntity({required String email, required String password}) = + _SignInEntity; + + factory SignInEntity.fromJson(Map json) => + _$SignInEntityFromJson(json); +} From f9c32666fc4fd612dbdb1f81a8ece3a2117cdb54 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:01:47 +0900 Subject: [PATCH 27/54] :sparkles: :: Creates sign-in repository abstraction Defines the abstract `SignInRepository` class. This interface outlines the contract for sign-in operations, allowing for different implementations (e.g., local storage, remote API) while maintaining a consistent interface for the rest of the application. --- lib/domain/sign_in/repositories/sign_in_repository.dart | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 lib/domain/sign_in/repositories/sign_in_repository.dart diff --git a/lib/domain/sign_in/repositories/sign_in_repository.dart b/lib/domain/sign_in/repositories/sign_in_repository.dart new file mode 100644 index 0000000..0c64680 --- /dev/null +++ b/lib/domain/sign_in/repositories/sign_in_repository.dart @@ -0,0 +1,5 @@ +import 'package:jusicool_ios/domain/sign_in/entity/sign_in_entity.dart'; + +abstract class SignInRepository { + Future signIn(SignInEntity entity); +} From 1a6c93b0b64b69b13cb4ea552be98035efbf390e Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:01:52 +0900 Subject: [PATCH 28/54] :sparkles: :: Defines the sign-in use case abstraction. Introduces an abstract class for the sign-in use case. This defines the contract for any class that implements sign-in functionality, promoting loose coupling and testability. --- lib/domain/sign_in/usecase/sign_in_usecase.dart | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 lib/domain/sign_in/usecase/sign_in_usecase.dart diff --git a/lib/domain/sign_in/usecase/sign_in_usecase.dart b/lib/domain/sign_in/usecase/sign_in_usecase.dart new file mode 100644 index 0000000..46fbbcd --- /dev/null +++ b/lib/domain/sign_in/usecase/sign_in_usecase.dart @@ -0,0 +1,5 @@ +import 'package:jusicool_ios/domain/sign_in/entity/sign_in_entity.dart'; + +abstract class SignInUseCase { + Future signIn(SignInEntity entity); +} From 9485a977fb994724912152af813a3170a6d9c381 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:01:57 +0900 Subject: [PATCH 29/54] :sparkles: :: Implements sign-in use case. Adds the implementation for the sign-in use case, which orchestrates the sign-in process by utilizing the sign-in repository. --- .../sign_in/usecase/sign_in_usecase_impl.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 lib/domain/sign_in/usecase/sign_in_usecase_impl.dart diff --git a/lib/domain/sign_in/usecase/sign_in_usecase_impl.dart b/lib/domain/sign_in/usecase/sign_in_usecase_impl.dart new file mode 100644 index 0000000..7efb2aa --- /dev/null +++ b/lib/domain/sign_in/usecase/sign_in_usecase_impl.dart @@ -0,0 +1,14 @@ +import 'package:jusicool_ios/domain/sign_in/entity/sign_in_entity.dart'; +import 'package:jusicool_ios/domain/sign_in/usecase/sign_in_usecase.dart'; +import '../repositories/sign_in_repository.dart'; + +class SignInUseCaseImpl extends SignInUseCase { + final SignInRepository _signInRepository; + + SignInUseCaseImpl(this._signInRepository); + + @override + Future signIn(SignInEntity entity) async { + return await _signInRepository.signIn(entity); + } +} From 9b6e430ca086f7cfcf8a261c32da1e348ce1fbb4 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:02:02 +0900 Subject: [PATCH 30/54] :sparkles: :: Creates sign-up email entity Introduces an entity to represent the email and verification code used during the sign-up process. This entity facilitates data transfer and serialization, supporting both standard object creation and JSON conversion via Freezed and JsonSerializable. --- .../sign_up/entity/sign_up_email_entity.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 lib/domain/sign_up/entity/sign_up_email_entity.dart diff --git a/lib/domain/sign_up/entity/sign_up_email_entity.dart b/lib/domain/sign_up/entity/sign_up_email_entity.dart new file mode 100644 index 0000000..38ebbd9 --- /dev/null +++ b/lib/domain/sign_up/entity/sign_up_email_entity.dart @@ -0,0 +1,16 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'sign_up_email_entity.freezed.dart'; + +part 'sign_up_email_entity.g.dart'; + +@freezed +abstract class SignUpEmailEntity with _$SignUpEmailEntity { + const factory SignUpEmailEntity({ + required String email, + required String verifyCode, + }) = _SignUpEmailEntity; + + factory SignUpEmailEntity.fromJson(Map json) => + _$SignUpEmailEntityFromJson(json); +} From 1f77aec4d57d624a76e8461b5a4d23a8c5309515 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:02:08 +0900 Subject: [PATCH 31/54] :sparkles: :: Creates sign up entity Adds the `SignUpEntity` using Freezed to handle immutable data and JSON serialization. This entity will be used to represent the data required for user sign-up. --- lib/domain/sign_up/entity/sign_up_entity.dart | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 lib/domain/sign_up/entity/sign_up_entity.dart diff --git a/lib/domain/sign_up/entity/sign_up_entity.dart b/lib/domain/sign_up/entity/sign_up_entity.dart new file mode 100644 index 0000000..b65a9aa --- /dev/null +++ b/lib/domain/sign_up/entity/sign_up_entity.dart @@ -0,0 +1,18 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'sign_up_entity.freezed.dart'; + +part 'sign_up_entity.g.dart'; + +@freezed +abstract class SignUpEntity with _$SignUpEntity { + const factory SignUpEntity({ + required String email, + required String password, + required String name, + required String school, + }) = _SignUpEntity; + + factory SignUpEntity.fromJson(Map json) => + _$SignUpEntityFromJson(json); +} From 363e8c3ee57c49967e88720c071cf07d57f66e90 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:02:12 +0900 Subject: [PATCH 32/54] :sparkles: :: Adds school search entity for sign-up Implements the data structures required to handle the school search functionality during user sign-up. This introduces entities to represent the school information received from the API, including nested structures for school details and address. --- .../entity/sign_up_search_school_entity.dart | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 lib/domain/sign_up/entity/sign_up_search_school_entity.dart diff --git a/lib/domain/sign_up/entity/sign_up_search_school_entity.dart b/lib/domain/sign_up/entity/sign_up_search_school_entity.dart new file mode 100644 index 0000000..45ed788 --- /dev/null +++ b/lib/domain/sign_up/entity/sign_up_search_school_entity.dart @@ -0,0 +1,39 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'sign_up_search_school_entity.freezed.dart'; + +part 'sign_up_search_school_entity.g.dart'; + +@freezed +abstract class SignUpSearchSchoolResponseEntity + with _$SignUpSearchSchoolResponseEntity { + const factory SignUpSearchSchoolResponseEntity({ + @JsonKey(name: 'schoolInfo') required List schoolInfo, + }) = _SignUpSearchSchoolResponseEntity; + + factory SignUpSearchSchoolResponseEntity.fromJson( + Map json, + ) => _$SignUpSearchSchoolResponseEntityFromJson(json); +} + +@freezed +abstract class SchoolInfoEntity with _$SchoolInfoEntity { + const factory SchoolInfoEntity({ + List? head, + List? row, + }) = _SchoolInfoEntity; + + factory SchoolInfoEntity.fromJson(Map json) => + _$SchoolInfoEntityFromJson(json); +} + +@freezed +abstract class SchoolRowEntity with _$SchoolRowEntity { + const factory SchoolRowEntity({ + @JsonKey(name: 'SCHUL_NM') required String schoolName, + @JsonKey(name: 'ORG_RDNMA') required String schoolAddress, + }) = _SchoolRowEntity; + + factory SchoolRowEntity.fromJson(Map json) => + _$SchoolRowEntityFromJson(json); +} From 149b58ef5b9d462d7993b84d3011a6e11409eb61 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:02:17 +0900 Subject: [PATCH 33/54] :sparkles: :: Creates sign up repository Defines an abstract repository for sign up functionality. This repository outlines methods for user registration, email verification, and school searching, providing a contract for data access implementations. --- .../sign_up/repositories/sign_up_repository.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 lib/domain/sign_up/repositories/sign_up_repository.dart diff --git a/lib/domain/sign_up/repositories/sign_up_repository.dart b/lib/domain/sign_up/repositories/sign_up_repository.dart new file mode 100644 index 0000000..a60c975 --- /dev/null +++ b/lib/domain/sign_up/repositories/sign_up_repository.dart @@ -0,0 +1,14 @@ +import 'package:jusicool_ios/domain/sign_up/entity/sign_up_email_entity.dart'; +import 'package:jusicool_ios/domain/sign_up/entity/sign_up_entity.dart'; + +import '../entity/sign_up_search_school_entity.dart'; + +abstract class SignUpRepository { + Future signUp(SignUpEntity entity); + + Future sendEmail(SignUpEmailEntity entity); + + Future verifyEmail(SignUpEmailEntity entity); + + Future searchSchool(String schoolName); +} From aa31c8c9d4d3051aa6d5f0bde677ae93c1ea3b55 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:02:21 +0900 Subject: [PATCH 34/54] :sparkles: :: Defines sign up use case Defines the abstract `SignUpUseCase` class, which outlines the required methods for handling user sign-up, email verification, email sending, and school search functionalities. --- lib/domain/sign_up/usecase/sign_up_usecase.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 lib/domain/sign_up/usecase/sign_up_usecase.dart diff --git a/lib/domain/sign_up/usecase/sign_up_usecase.dart b/lib/domain/sign_up/usecase/sign_up_usecase.dart new file mode 100644 index 0000000..475f9e8 --- /dev/null +++ b/lib/domain/sign_up/usecase/sign_up_usecase.dart @@ -0,0 +1,13 @@ +import 'package:jusicool_ios/domain/sign_up/entity/sign_up_email_entity.dart'; +import 'package:jusicool_ios/domain/sign_up/entity/sign_up_entity.dart'; +import 'package:jusicool_ios/domain/sign_up/entity/sign_up_search_school_entity.dart'; + +abstract class SignUpUseCase { + Future signUp(SignUpEntity entity); + + Future verifyEmail(SignUpEmailEntity entity); + + Future sendEmail(SignUpEmailEntity entity); + + Future searchSchool(String schoolName); +} From 23940614a5d85a0f58f64b2b2aa17e68da7376e8 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:02:25 +0900 Subject: [PATCH 35/54] I:sparkles: :: mplements sign up use case Creates an implementation of the sign-up use case, delegating calls to the repository layer. This includes methods for user registration, email verification, and school searching, providing a concrete implementation for the abstract use case. --- .../sign_up/usecase/sign_up_usecase_impl.dart | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 lib/domain/sign_up/usecase/sign_up_usecase_impl.dart diff --git a/lib/domain/sign_up/usecase/sign_up_usecase_impl.dart b/lib/domain/sign_up/usecase/sign_up_usecase_impl.dart new file mode 100644 index 0000000..0eaa2aa --- /dev/null +++ b/lib/domain/sign_up/usecase/sign_up_usecase_impl.dart @@ -0,0 +1,32 @@ +import 'package:jusicool_ios/domain/sign_up/entity/sign_up_email_entity.dart'; +import 'package:jusicool_ios/domain/sign_up/entity/sign_up_entity.dart'; +import 'package:jusicool_ios/domain/sign_up/entity/sign_up_search_school_entity.dart'; +import 'package:jusicool_ios/domain/sign_up/usecase/sign_up_usecase.dart'; + +import '../repositories/sign_up_repository.dart'; + +class SignUpUseCaseImpl extends SignUpUseCase { + final SignUpRepository _signUpRepository; + + SignUpUseCaseImpl(this._signUpRepository); + + @override + Future signUp(SignUpEntity entity) { + return _signUpRepository.signUp(entity); + } + + @override + Future sendEmail(SignUpEmailEntity entity) { + return _signUpRepository.sendEmail(entity); + } + + @override + Future verifyEmail(SignUpEmailEntity entity) { + return _signUpRepository.verifyEmail(entity); + } + + @override + Future searchSchool(String schoolName) { + return _signUpRepository.searchSchool(schoolName); + } +} From 75666a371ec04dcd7445dbcc43d22417a08774fa Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:02:34 +0900 Subject: [PATCH 36/54] :sparkles: :: Adds sign-in controller logic Implements sign-in controller with email/password validation using a use case for authentication. Includes logic for handling success and error states, and navigation to the main screen upon successful login. Adds email and password validation with specific format requirements. --- .../controller/sign_in_controller.dart | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 lib/presentation/sign_in/controller/sign_in_controller.dart diff --git a/lib/presentation/sign_in/controller/sign_in_controller.dart b/lib/presentation/sign_in/controller/sign_in_controller.dart new file mode 100644 index 0000000..d9223f1 --- /dev/null +++ b/lib/presentation/sign_in/controller/sign_in_controller.dart @@ -0,0 +1,112 @@ +import 'package:email_validator/email_validator.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:jusicool_ios/core/config/router/router.dart'; +import 'package:jusicool_ios/domain/sign_in/usecase/sign_in_usecase.dart'; +import '../../../core/config/di/dependencies.dart'; +import '../mapper/sign_in_mapper.dart'; +import '../state/sign_in_state.dart'; + +final signInControllerProvider = + StateNotifierProvider( + (ref) => UserSignInController(di.get()), + ); + +class UserSignInController extends StateNotifier { + final SignInUseCase _signInUseCase; + + UserSignInController(this._signInUseCase) : super(SignInState()) { + _emailController.addListener(() { + _setEmail(_emailController.text); + }); + _passwordController.addListener(() { + _setPassword(_passwordController.text); + }); + } + + bool _isValidPassword(String password) { + if (password.length < 8 || password.length > 13) return false; + + final hasLetter = RegExp(r'[A-Za-z]').hasMatch(password); + final hasNumber = RegExp(r'\d').hasMatch(password); + final hasSpecial = RegExp(r'[@$!%*?&]').hasMatch(password); + + int satisfiedConditions = + [hasLetter, hasNumber, hasSpecial].where((e) => e).length; + + return satisfiedConditions >= 2; + } + + final TextEditingController _emailController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + + TextEditingController get emailController => _emailController; + + TextEditingController get passwordController => _passwordController; + + void _setEmail(String email) { + state = state.copyWith(email: email); + if (email.isNotEmpty && EmailValidator.validate(email)) { + _clearError(); + } else { + _setEnableButton(false); + } + } + + void _setPassword(String password) { + state = state.copyWith(password: password); + if (password.isNotEmpty && _isValidPassword(password)) { + _clearError(); + } else { + _setEnableButton(false); + } + } + + void _setError(String message) { + state = state.copyWith(hasError: true, errorMessage: message); + } + + void _clearError() { + state = state.copyWith(hasError: false, errorMessage: ""); + _setEnableButton(true); + } + + void _setEnableButton(bool enable) { + state = state.copyWith(enableButton: enable); + } + + void signIn(BuildContext context) { + if (state.email.isEmpty || state.password.isEmpty) { + _setError("이메일과 비밀번호를 입력해주세요."); + return; + } + if (!EmailValidator.validate(state.email)) { + _setError("유효한 이메일을 입력해주세요."); + return; + } + if (!_isValidPassword(state.password)) { + _setError("비밀번호는 8~13자이며, 문자, 숫자, 특수문자 중 2가지 이상 포함해야 합니다."); + return; + } + + _clearError(); + + final request = SignInMapper.toEntity(state); + _signInUseCase + .signIn(request) + .then((_) { + context.pushReplacement(RoutePaths.main); + }) + .catchError((error) { + _setError("아이디와 비밀번호를 다시 한 번 확인해주세요"); + }); + } + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + super.dispose(); + } +} From 0f68e52d2589e73992acb992d75ec1a2e2deddbf Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:02:41 +0900 Subject: [PATCH 37/54] :sparkles: :: Adds Riverpod for state management Integrates Riverpod for managing application state. This prepares the application for more complex state management needs, enhancing scalability and maintainability. Also, moves router and bottom menu to the correct directory. --- lib/main.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 125bbda..dbe5db4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:jusicool_design_system/src/core/theme/colors/color_palette.dart'; import 'package:jusicool_ios/core/config/di/dependencies.dart'; -import 'package:jusicool_ios/core/widget/menu_bottom.dart'; -import 'package:jusicool_ios/core/router/router.dart'; - +import 'core/config/router/router.dart'; import 'core/config/theme/app_theme.dart'; +import 'core/config/widget/menu_bottom.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -26,7 +26,7 @@ void main() async { ), ); - runApp(const MyApp()); + runApp(ProviderScope(child: const MyApp())); } class MyApp extends StatelessWidget { From ed32f9e0c647fb477327a5898a044ffd06e0b430 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:02:46 +0900 Subject: [PATCH 38/54] :sparkles: :: Creates sign-in mapper Adds a mapper to convert the sign-in state to a sign-in entity, encapsulating the data transfer logic between the presentation and domain layers. --- lib/presentation/sign_in/mapper/sign_in_mapper.dart | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 lib/presentation/sign_in/mapper/sign_in_mapper.dart diff --git a/lib/presentation/sign_in/mapper/sign_in_mapper.dart b/lib/presentation/sign_in/mapper/sign_in_mapper.dart new file mode 100644 index 0000000..69f868c --- /dev/null +++ b/lib/presentation/sign_in/mapper/sign_in_mapper.dart @@ -0,0 +1,7 @@ +import 'package:jusicool_ios/domain/sign_in/entity/sign_in_entity.dart'; +import 'package:jusicool_ios/presentation/sign_in/state/sign_in_state.dart'; + +class SignInMapper { + static SignInEntity toEntity(SignInState state) => + SignInEntity(email: state.email, password: state.password); +} From 7bd3240f1d8380ee97df42cf87a47902652a6ab2 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:02:52 +0900 Subject: [PATCH 39/54] :sparkles: :: Implements the login screen UI Adds the login screen with email and password input fields. Includes UI elements for navigation to the registration screen. Implements basic input validation and button state management. --- .../sign_in/screens/login_screen.dart | 241 ++---------------- 1 file changed, 22 insertions(+), 219 deletions(-) diff --git a/lib/presentation/sign_in/screens/login_screen.dart b/lib/presentation/sign_in/screens/login_screen.dart index b10dc56..b114c1d 100644 --- a/lib/presentation/sign_in/screens/login_screen.dart +++ b/lib/presentation/sign_in/screens/login_screen.dart @@ -1,195 +1,20 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:go_router/go_router.dart'; import 'package:jusicool_design_system/jusicool_design_system.dart'; -import 'package:email_validator/email_validator.dart'; -import 'package:jusicool_ios/presentation/sign_in/widgets/input_field.dart'; -import 'package:jusicool_ios/router.dart'; +import 'package:jusicool_ios/presentation/sign_in/screens/widgets/input_field.dart'; import '../../sign_up/screens/name_input_screen.dart'; +import '../controller/sign_in_controller.dart'; -class LoginScreen extends StatefulWidget { - const LoginScreen({super.key}); +class LoginScreen extends ConsumerWidget { + LoginScreen({super.key}); @override - State createState() => _LoginScreenState(); -} - -class _LoginScreenState extends State { - final TextEditingController _emailController = TextEditingController(); - final TextEditingController _passwordController = TextEditingController(); - - bool showEmailError = false; - String emailErrorMessage = ''; - bool showPasswordError = false; - String passwordErrorMessage = ''; - bool showLoginError = false; - String loginErrorMessage = ''; - - static const double FIELD_HEIGHT = 56.0; - static const double FORM_WIDTH = 312.0; - - /// ==================================== - final List> database = [ - {'email': 'admin@admin.com', 'password': '12341234!'}, - {'email': 's24001@gsm.hs.kr', 'password': '12345678!'}, - ]; - - /// ==================================== - - // 공통 에러 처리 함수 - void setError({ - required bool emailError, - required String emailMsg, - required bool passwordError, - required String passwordMsg, - required bool loginError, - required String loginMsg, - }) { - setState(() { - showEmailError = emailError; - emailErrorMessage = emailMsg; - showPasswordError = passwordError; - passwordErrorMessage = passwordMsg; - showLoginError = loginError; - loginErrorMessage = loginMsg; - }); - } - - // 텍스트 필드 스타일을 관리하는 공통 함수 - InputDecoration getInputDecoration(String hint, bool hasError) { - return InputDecoration( - hintText: hint, - hintStyle: JusicoolTypography.bodySmall.copyWith( - color: hasError ? JusicoolColor.error : JusicoolColor.gray500, - ), - contentPadding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 18.h), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.r), - borderSide: BorderSide( - color: hasError ? JusicoolColor.error : JusicoolColor.gray300, - width: 1.w, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.r), - borderSide: BorderSide( - color: hasError ? JusicoolColor.error : JusicoolColor.main, - width: 2.w, - ), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.r), - borderSide: BorderSide(color: JusicoolColor.error, width: 1.w), - ), - focusedErrorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.r), - borderSide: BorderSide(color: JusicoolColor.error, width: 2.w), - ), - ); - } - - bool isValidPassword(String password) { - if (password.length < 8 || password.length > 13) return false; - - final hasLetter = RegExp(r'[A-Za-z]').hasMatch(password); - final hasNumber = RegExp(r'\d').hasMatch(password); - final hasSpecial = RegExp(r'[@$!%*?&]').hasMatch(password); - - int satisfiedConditions = - [hasLetter, hasNumber, hasSpecial].where((e) => e).length; - - return satisfiedConditions >= 2; - } - - void validateEmail(String email) { - if (email.isEmpty || EmailValidator.validate(email)) { - setError( - emailError: false, - emailMsg: '', - passwordError: showPasswordError, - passwordMsg: passwordErrorMessage, - loginError: showLoginError, - loginMsg: loginErrorMessage, - ); - } else { - setError( - emailError: true, - emailMsg: '유효한 이메일 주소를 입력해주세요.', - passwordError: showPasswordError, - passwordMsg: passwordErrorMessage, - loginError: showLoginError, - loginMsg: loginErrorMessage, - ); - } - } - - void validatePassword(String password) { - if (password.isEmpty || isValidPassword(password)) { - setError( - emailError: showEmailError, - emailMsg: emailErrorMessage, - passwordError: false, - passwordMsg: '', - loginError: showLoginError, - loginMsg: loginErrorMessage, - ); - } else { - setError( - emailError: showEmailError, - emailMsg: emailErrorMessage, - passwordError: true, - passwordMsg: '영문, 숫자, 특수문자 중 2개 이상 조합으로 8글자 이상.', - loginError: showLoginError, - loginMsg: loginErrorMessage, - ); - } - } - - void handleLogin() { - final email = _emailController.text; - final password = _passwordController.text; - - final isEmailValid = EmailValidator.validate(email); - final isPasswordValid = isValidPassword(password); - - if (!isEmailValid || !isPasswordValid) { - setError( - emailError: !isEmailValid, - emailMsg: !isEmailValid ? '유효한 이메일 주소를 입력해주세요.' : '', - passwordError: !isPasswordValid, - passwordMsg: - !isPasswordValid ? '영문, 숫자, 특수문자 중 2개 이상 조합으로 8글자 이상.' : '', - loginError: false, - loginMsg: '', - ); - return; - } - - final user = database.firstWhere( - (user) => user['email'] == email && user['password'] == password, - orElse: () => {}, - ); - - if (user.isNotEmpty) { - context.pushReplacement(RoutePaths.main); - } else { - setError( - emailError: false, - emailMsg: '', - passwordError: false, - passwordMsg: '', - loginError: true, - loginMsg: '아이디와 비밀번호를 다시 한 번 확인해주세요', - ); - } - } - - @override - Widget build(BuildContext context) { - final isFormFilled = - _emailController.text.isNotEmpty && _passwordController.text.isNotEmpty; - + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(signInControllerProvider); + final provider = ref.watch(signInControllerProvider.notifier); return Scaffold( + resizeToAvoidBottomInset: false, backgroundColor: JusicoolColor.white, body: Padding( padding: EdgeInsets.fromLTRB(24.w, 112.h, 24.w, 84.h), @@ -215,44 +40,18 @@ class _LoginScreenState extends State { children: [ InputField( label: '이메일', - controller: _emailController, + controller: provider.emailController, hint: '이메일을 입력해주세요', - hasError: showEmailError || showLoginError, - errorMessage: emailErrorMessage, - onChanged: validateEmail, + hasError: state.hasError, obscureText: false, - width: FORM_WIDTH.w, - height: FIELD_HEIGHT.h, - getInputDecoration: getInputDecoration, - showLoginError: showLoginError, - clearLoginError: () { - setState(() { - showLoginError = false; - loginErrorMessage = ''; - }); - }, ), InputField( label: '비밀번호', - controller: _passwordController, + controller: provider.passwordController, hint: '비밀번호를 입력해주세요', - hasError: showPasswordError || showLoginError, - errorMessage: - showPasswordError - ? passwordErrorMessage - : (showLoginError ? loginErrorMessage : ''), + errorMessage: state.errorMessage, + hasError: state.hasError, obscureText: true, - onChanged: validatePassword, - width: FORM_WIDTH.w, - height: FIELD_HEIGHT.h, - getInputDecoration: getInputDecoration, - showLoginError: showLoginError, - clearLoginError: () { - setState(() { - showLoginError = false; - loginErrorMessage = ''; - }); - }, ), ], ), @@ -262,15 +61,19 @@ class _LoginScreenState extends State { children: [ AppButtonMedium( text: '로그인', - onPressed: handleLogin, + onPressed: () => provider.signIn(context), backgroundColor: - isFormFilled ? JusicoolColor.main : JusicoolColor.gray300, + state.enableButton + ? JusicoolColor.main + : JusicoolColor.gray300, textColor: - isFormFilled + state.enableButton ? JusicoolColor.white : JusicoolColor.gray600, borderColor: - isFormFilled ? JusicoolColor.main : JusicoolColor.gray300, + state.enableButton + ? JusicoolColor.main + : JusicoolColor.gray300, ), Text( '아직 계정이 없으신가요?', From 6e46941e12c15925185913429d31e970699b8e32 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:03:01 +0900 Subject: [PATCH 40/54] :fire: :: Removes dedicated input field widget Removes the dedicated `InputField` widget. This likely indicates a refactoring where the input field functionality is either moved to a more general component or directly integrated into the relevant screens. The rationale could be to simplify the component structure, reduce code duplication, or facilitate more flexible customization of input fields within specific contexts. --- .../sign_in/widgets/input_field.dart | 81 ------------------- 1 file changed, 81 deletions(-) delete mode 100644 lib/presentation/sign_in/widgets/input_field.dart diff --git a/lib/presentation/sign_in/widgets/input_field.dart b/lib/presentation/sign_in/widgets/input_field.dart deleted file mode 100644 index 681d99c..0000000 --- a/lib/presentation/sign_in/widgets/input_field.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:jusicool_design_system/jusicool_design_system.dart'; - -class InputField extends StatelessWidget { - final String label; - final TextEditingController controller; - final String hint; - final bool hasError; - final String errorMessage; - final bool obscureText; - final Function(String) onChanged; - final double width; - final double height; - final InputDecoration Function(String, bool) getInputDecoration; - final bool showLoginError; - final VoidCallback clearLoginError; - - const InputField({ - super.key, - required this.label, - required this.controller, - required this.hint, - required this.hasError, - required this.errorMessage, - this.obscureText = false, - required this.onChanged, - required this.width, - required this.height, - required this.getInputDecoration, - required this.showLoginError, - required this.clearLoginError, - }); - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: JusicoolTypography.bodySmall.copyWith( - fontSize: 16.sp, - color: hasError ? JusicoolColor.error : JusicoolColor.black, - ), - ), - SizedBox(height: 8.h), - SizedBox( - width: width, - height: height, - child: TextFormField( - controller: controller, - obscureText: obscureText, - onChanged: (value) { - onChanged(value); - if (showLoginError) { - clearLoginError(); - } - }, - decoration: getInputDecoration(hint, hasError || showLoginError), - ), - ), - if (hasError && errorMessage.isNotEmpty) - Padding( - padding: EdgeInsets.only(top: 4.h), - child: SizedBox( - width: width, - child: Text( - errorMessage, - textAlign: TextAlign.right, - style: JusicoolTypography.bodySmall.copyWith( - color: JusicoolColor.error, - fontSize: 12.sp, - ), - ), - ), - ), - ], - ); - } -} From 23559cee6d868b0e2d7faf53e6ab8b4b2033687d Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:03:06 +0900 Subject: [PATCH 41/54] :sparkles: :: Creates reusable input field component Implements a reusable input field widget with error handling and styling. This component includes customizable hints, labels, error messages, and different border styles based on focus and error state, leveraging the Jusicool design system for consistent styling. --- .../sign_in/screens/widgets/input_field.dart | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 lib/presentation/sign_in/screens/widgets/input_field.dart diff --git a/lib/presentation/sign_in/screens/widgets/input_field.dart b/lib/presentation/sign_in/screens/widgets/input_field.dart new file mode 100644 index 0000000..17ee6da --- /dev/null +++ b/lib/presentation/sign_in/screens/widgets/input_field.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:jusicool_design_system/jusicool_design_system.dart'; + +class InputField extends StatelessWidget { + final String label; + final TextEditingController controller; + final String hint; + final String errorMessage; + final bool hasError; + final bool obscureText; + + const InputField({ + super.key, + required this.label, + required this.controller, + required this.hint, + this.errorMessage = "", + this.hasError = false, + this.obscureText = false, + }); + + @override + Widget build(BuildContext context) { + return Column( + spacing: 4.h, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: JusicoolTypography.bodySmall.copyWith( + fontSize: 16.sp, + color: hasError ? JusicoolColor.error : JusicoolColor.black, + ), + ), + SizedBox.shrink(), + TextFormField( + controller: controller, + obscureText: obscureText, + cursorColor: JusicoolColor.main, + cursorErrorColor: JusicoolColor.error, + decoration: getInputDecoration(hint, hasError), + ), + if (hasError && errorMessage.isNotEmpty) + Text( + errorMessage, + textAlign: TextAlign.right, + style: JusicoolTypography.bodySmall.copyWith( + color: JusicoolColor.error, + fontSize: 12.sp, + ), + ), + ], + ); + } + + InputDecoration getInputDecoration(String hint, bool hasError) { + return InputDecoration( + hintText: hint, + hintStyle: JusicoolTypography.bodySmall.copyWith( + color: hasError ? JusicoolColor.error : JusicoolColor.gray500, + ), + contentPadding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 18.h), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.r), + borderSide: BorderSide( + color: hasError ? JusicoolColor.error : JusicoolColor.gray300, + width: 1.w, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.r), + borderSide: BorderSide( + color: hasError ? JusicoolColor.error : JusicoolColor.main, + width: 2.w, + ), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.r), + borderSide: BorderSide(color: JusicoolColor.error, width: 1.w), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.r), + borderSide: BorderSide(color: JusicoolColor.error, width: 2.w), + ), + ); + } +} From e15dc6ceea4748a141cec067b6d1404a3d465f91 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:03:12 +0900 Subject: [PATCH 42/54] :sparkles: :: Creates sign in state Creates the `SignInState` using Freezed to manage the state of the sign-in screen. This state includes fields for email, password, a flag to enable the button, a flag to indicate if there is an error, and an error message. --- lib/presentation/sign_in/state/sign_in_state.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 lib/presentation/sign_in/state/sign_in_state.dart diff --git a/lib/presentation/sign_in/state/sign_in_state.dart b/lib/presentation/sign_in/state/sign_in_state.dart new file mode 100644 index 0000000..ffeface --- /dev/null +++ b/lib/presentation/sign_in/state/sign_in_state.dart @@ -0,0 +1,14 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'sign_in_state.freezed.dart'; + +@freezed +abstract class SignInState with _$SignInState { + factory SignInState({ + @Default("") String email, + @Default("") String password, + @Default(false) bool enableButton, + @Default(false) bool hasError, + @Default("") String errorMessage, + }) = _SignInState; +} From 12341d5e9b7a986ea56532f78eb17a62e11cf616 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:03:17 +0900 Subject: [PATCH 43/54] :sparkles: :: Adds email signup controller Implements the email signup controller, managing email sending, verification, and navigation to the password creation screen. It handles email validation, sends verification codes, and manages timers for code expiration. --- .../controller/sign_up_email_controller.dart | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 lib/presentation/sign_up/controller/sign_up_email_controller.dart diff --git a/lib/presentation/sign_up/controller/sign_up_email_controller.dart b/lib/presentation/sign_up/controller/sign_up_email_controller.dart new file mode 100644 index 0000000..5a9e194 --- /dev/null +++ b/lib/presentation/sign_up/controller/sign_up_email_controller.dart @@ -0,0 +1,158 @@ +import 'dart:async'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:email_validator/email_validator.dart'; +import 'package:go_router/go_router.dart'; +import 'package:jusicool_ios/domain/sign_up/entity/sign_up_email_entity.dart'; +import 'package:jusicool_ios/domain/sign_up/usecase/sign_up_usecase.dart'; +import 'package:jusicool_ios/presentation/sign_up/mapper/sign_up_email_mapper.dart'; +import 'package:jusicool_ios/presentation/sign_up/state/sign_up_email_state.dart'; + +import '../../../core/config/di/dependencies.dart'; +import '../../../core/config/router/router.dart'; + +final emailAuthControllerProvider = + StateNotifierProvider( + (ref) => EmailAuthController(di.get()), + ); + +class EmailAuthController extends StateNotifier { + final SignUpUseCase _signUpUseCase; + + EmailAuthController(this._signUpUseCase) : super(SignUpEmailState()) { + emailController.addListener(() => _onEmailChanged()); + codeController.addListener(() => _onCodeChanged()); + } + + final TextEditingController emailController = TextEditingController(); + final TextEditingController codeController = TextEditingController(); + + static const codeExpirationDuration = Duration(minutes: 5); + Timer? _codeExpirationTimer; + Timer? _countdownTimer; + + void disposeResources() { + _codeExpirationTimer?.cancel(); + _countdownTimer?.cancel(); + emailController.dispose(); + codeController.dispose(); + } + + void _onEmailChanged() { + final email = emailController.text.trim(); + state = state.copyWith( + email: email, + errorMessage: null, + enableButton: true, + isEmailValid: true, + ); + } + + void _onCodeChanged() { + final code = codeController.text.trim(); + state = state.copyWith( + verify: code, + isCodeMatched: true, + enableButton: true, + ); + } + + void sendEmail() { + final email = state.email; + if (email.isEmpty) { + state = state.copyWith( + isEmailValid: false, + enableButton: false, + errorMessage: "이메일을 입력해주세요.", + ); + return; + } + + if (EmailValidator.validate(email)) { + state = state.copyWith( + isEmailValid: true, + errorMessage: null, + isSendingCode: true, + ); + final SignUpEmailEntity request = SignUpEmailMapper.toEntity(state); + _signUpUseCase + .sendEmail(request) + .then((_) { + _startVerificationTimers(); + }) + .catchError((error) { + state = state.copyWith( + isEmailValid: false, + enableButton: false, + isSendingCode: false, + errorMessage: "이메일 전송에 실패했습니다. 잠시 후에 시도해주세요.", + ); + }); + } else { + state = state.copyWith( + isEmailValid: false, + enableButton: false, + isSendingCode: false, + errorMessage: "올바른 이메일 형식이 아닙니다.", + ); + } + } + + void sendVerificationCode(BuildContext context) { + final code = state.verify; + if (code.isEmpty || code.length != 6) { + state = state.copyWith( + isCodeMatched: false, + enableButton: false, + errorMessage: "인증번호를 입력해주세요.", + ); + return; + } + + state = state.copyWith( + isEmailValid: true, + errorMessage: null, + isSendingCode: true, + ); + final request = SignUpEmailMapper.toEntity(state); + + state = state.copyWith(isCodeMatched: true, isSendingCode: false); + _signUpUseCase + .verifyEmail(request) + .then((_) { + context.push(RoutePaths.passwordCreate); + state = state.copyWith(isCodeMatched: true, isSendingCode: false); + }) + .catchError((error) { + state = state.copyWith(isCodeMatched: false, isSendingCode: false); + }); + } + + void _startVerificationTimers() { + _codeExpirationTimer?.cancel(); + _countdownTimer?.cancel(); + + state = state.copyWith( + codeSent: true, + isSendingCode: false, + isCodeMatched: true, + timeRemaining: codeExpirationDuration, + ); + + codeController.clear(); + + _codeExpirationTimer = Timer(codeExpirationDuration, () { + _countdownTimer?.cancel(); + state = state.copyWith(codeSent: false); + }); + + _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + final seconds = state.timeRemaining.inSeconds; + if (seconds > 0) { + state = state.copyWith(timeRemaining: Duration(seconds: seconds - 1)); + } else { + timer.cancel(); + } + }); + } +} From 24b5af6554943efb6b00fd4889001e6556abcbcb Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:03:23 +0900 Subject: [PATCH 44/54] :sparkles: :: Adds sign-up name input controller Implements a controller to handle user name input during the sign-up process. The controller manages the text input, validates the username format (Korean characters only, minimum length of 2), and enables/disables a button based on the validation result. It also provides appropriate error messages if the validation fails. --- .../controller/sign_up_name_controller.dart | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 lib/presentation/sign_up/controller/sign_up_name_controller.dart diff --git a/lib/presentation/sign_up/controller/sign_up_name_controller.dart b/lib/presentation/sign_up/controller/sign_up_name_controller.dart new file mode 100644 index 0000000..d17ec1f --- /dev/null +++ b/lib/presentation/sign_up/controller/sign_up_name_controller.dart @@ -0,0 +1,52 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../state/sign_up_name_state.dart'; + +final nameControllerProvider = + StateNotifierProvider( + (ref) => SignUpNameController(), + ); + +class SignUpNameController extends StateNotifier { + SignUpNameController() : super(SignUpNameState()) { + _controller.addListener(() => _setUsername(_controller.text)); + } + + final TextEditingController _controller = TextEditingController(); + + TextEditingController get controller => _controller; + + void _setUsername(String username) { + state = state.copyWith(username: username); + if (username.length >= 2) { + _setEnableButton(true); + } else { + _setEnableButton(false); + } + } + + void _setEnableButton(bool enable) { + state = state.copyWith(enableButton: enable); + } + + bool validateUsername() { + final username = _controller.text; + + if (username.isEmpty) { + state = state.copyWith(errorMessage: "이름을 입력해주세요.", enableButton: false); + return false; + } + + if (RegExp(r'^[가-힣]{2,}$').hasMatch(username)) { + state = state.copyWith(errorMessage: null, enableButton: true); + return true; + } else { + state = state.copyWith( + errorMessage: "올바른 이름 형식이 아닙니다.", + enableButton: false, + ); + return false; + } + } +} From 59badd6edb8d160cfd061d3f900c81b749d40421 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:03:28 +0900 Subject: [PATCH 45/54] :sparkles: :: Adds signup password controller Implements the signup password controller using Riverpod. This controller manages the state and logic for the password and confirm password fields in the signup process. It includes validation for password strength and matching confirmation password. It also handles navigation to the next step in the signup flow. --- .../sign_up_password_controller.dart | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 lib/presentation/sign_up/controller/sign_up_password_controller.dart diff --git a/lib/presentation/sign_up/controller/sign_up_password_controller.dart b/lib/presentation/sign_up/controller/sign_up_password_controller.dart new file mode 100644 index 0000000..7346154 --- /dev/null +++ b/lib/presentation/sign_up/controller/sign_up_password_controller.dart @@ -0,0 +1,78 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:jusicool_ios/presentation/sign_up/state/sign_up_password_state.dart'; + +import '../../../core/config/router/router.dart'; + +final signupPasswordControllerProvider = + StateNotifierProvider( + (ref) => SignupPasswordController(), + ); + +class SignupPasswordController extends StateNotifier { + SignupPasswordController() : super(SignUpPasswordState()) { + _passwordController.addListener(_onPasswordChanged); + _confirmPasswordController.addListener(_onConfirmPasswordChanged); + } + + final TextEditingController _passwordController = TextEditingController(); + final TextEditingController _confirmPasswordController = + TextEditingController(); + + TextEditingController get passwordController => _passwordController; + + TextEditingController get confirmPasswordController => + _confirmPasswordController; + + void _onPasswordChanged() { + final password = _passwordController.text.trim(); + state = state.copyWith(password: password, isPasswordValid: true); + } + + void _onConfirmPasswordChanged() { + _updateFormState(); + checkPasswordsMatch(); + } + + void _updateFormState() { + state = state.copyWith( + isFormFilled: + _passwordController.text.isNotEmpty && + _confirmPasswordController.text.isNotEmpty, + ); + } + + void validatePassword(String password) { + state = state.copyWith(isPasswordValid: isValidPassword(password)); + } + + void checkPasswordsMatch() { + state = state.copyWith( + isPasswordMatched: + _passwordController.text == _confirmPasswordController.text, + ); + } + + bool isValidPassword(String password) { + if (password.length < 8 || password.length > 13) return false; + + final hasLetter = RegExp(r'[A-Za-z]').hasMatch(password); + final hasNumber = RegExp(r'\d').hasMatch(password); + final hasSpecial = RegExp(r'[@$!%*?&]').hasMatch(password); + + final count = [hasLetter, hasNumber, hasSpecial].where((e) => e).length; + return count >= 2; + } + + void onNextButtonPressed(BuildContext context) { + context.push(RoutePaths.findSchool); + } + + @override + void dispose() { + _passwordController.dispose(); + _confirmPasswordController.dispose(); + super.dispose(); + } +} From 2c4ff484b238f3621a72b2943faf100fc515fb83 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:03:32 +0900 Subject: [PATCH 46/54] :sparkles: :: Adds school signup controller Creates the `SignupSchoolController` to handle school search and signup logic. This controller manages the school name input, searches for schools based on user input using a debounce mechanism, and handles the signup process with the selected school. It also manages the state of the school search and selection. --- .../controller/sign_up_school_controller.dart | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 lib/presentation/sign_up/controller/sign_up_school_controller.dart diff --git a/lib/presentation/sign_up/controller/sign_up_school_controller.dart b/lib/presentation/sign_up/controller/sign_up_school_controller.dart new file mode 100644 index 0000000..504436a --- /dev/null +++ b/lib/presentation/sign_up/controller/sign_up_school_controller.dart @@ -0,0 +1,92 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:jusicool_ios/core/config/router/router.dart'; +import 'package:jusicool_ios/domain/sign_up/entity/sign_up_entity.dart'; +import 'package:jusicool_ios/domain/sign_up/usecase/sign_up_usecase.dart'; +import 'package:jusicool_ios/presentation/sign_up/mapper/sign_up_search_school_mapper.dart'; +import 'package:rxdart/rxdart.dart'; +import '../../../core/config/di/dependencies.dart'; +import '../state/sign_up_school_state.dart'; + +final signupSchoolControllerProvider = + StateNotifierProvider( + (ref) => SignupSchoolController(di.get()), + ); + +class SignupSchoolController extends StateNotifier { + SignUpUseCase _signUpUseCase; + final _schoolNameSubject = PublishSubject(); + + SignupSchoolController(this._signUpUseCase) : super(SignUpSchoolState()) { + _schoolNameController.addListener(() { + _schoolNameSubject.add(_schoolNameController.text); + }); + + _schoolNameSubject.debounceTime(const Duration(milliseconds: 500)).listen(( + _, + ) { + searchSchool(); + }); + } + + final TextEditingController _schoolNameController = TextEditingController(); + + TextEditingController get schoolNameController => _schoolNameController; + + void searchSchool() { + print(state.toString()); + _signUpUseCase + .searchSchool(_schoolNameController.text) + .then((result) { + if (result.schoolInfo.isNotEmpty) { + state = state.copyWith( + filteredSchools: SignUpSearchSchoolMapper.toState(result), + ); + print(state.toString()); + } else { + state = state.copyWith(filteredSchools: []); + } + }) + .catchError((error) { + state = state.copyWith(filteredSchools: []); + }); + } + + void selectSchool(SchoolInfoState school) { + state = state.copyWith(selectedSchool: school); + } + + void start({ + required BuildContext context, + required String email, + required String password, + required String name, + }) { + final String? schoolName = state.selectedSchool?.schoolName; + if (schoolName == null) { + return; + } else { + _signUpUseCase + .signUp( + SignUpEntity( + email: email, + password: password, + name: name, + school: schoolName, + ), + ) + .then((value) { + context.pushReplacement(RoutePaths.main); + }) + .catchError((error) {}); + } + } + + @override + void dispose() { + _schoolNameSubject.close(); + _schoolNameController.dispose(); + super.dispose(); + } +} From 9501123317c47c51e3496d939a238e6fbb3dbb6b Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:03:38 +0900 Subject: [PATCH 47/54] :sparkles: :: Adds data mappers for sign up feature Implements mappers to transform data between the presentation layer (states) and the domain layer (entities) for the sign-up feature. Specifically, it creates mappers for email verification and school search functionalities. --- .../sign_up/mapper/sign_up_email_mapper.dart | 7 +++++++ .../mapper/sign_up_search_school_mapper.dart | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 lib/presentation/sign_up/mapper/sign_up_email_mapper.dart create mode 100644 lib/presentation/sign_up/mapper/sign_up_search_school_mapper.dart diff --git a/lib/presentation/sign_up/mapper/sign_up_email_mapper.dart b/lib/presentation/sign_up/mapper/sign_up_email_mapper.dart new file mode 100644 index 0000000..cfe0a32 --- /dev/null +++ b/lib/presentation/sign_up/mapper/sign_up_email_mapper.dart @@ -0,0 +1,7 @@ +import 'package:jusicool_ios/domain/sign_up/entity/sign_up_email_entity.dart'; +import 'package:jusicool_ios/presentation/sign_up/state/sign_up_email_state.dart'; + +class SignUpEmailMapper { + static SignUpEmailEntity toEntity(SignUpEmailState state) => + SignUpEmailEntity(email: state.email, verifyCode: state.verify); +} diff --git a/lib/presentation/sign_up/mapper/sign_up_search_school_mapper.dart b/lib/presentation/sign_up/mapper/sign_up_search_school_mapper.dart new file mode 100644 index 0000000..c9c9012 --- /dev/null +++ b/lib/presentation/sign_up/mapper/sign_up_search_school_mapper.dart @@ -0,0 +1,18 @@ +import '../../../domain/sign_up/entity/sign_up_search_school_entity.dart'; +import '../state/sign_up_school_state.dart'; + +class SignUpSearchSchoolMapper { + static List toState( + SignUpSearchSchoolResponseEntity entity, + ) { + return entity.schoolInfo + .expand((info) => info.row ?? []) + .map( + (row) => SchoolInfoState( + schoolName: row.schoolName, + schoolAddress: row.schoolAddress, + ), + ) + .toList(); + } +} From 71f7a3ec7fc51a643ed45dd60084388805651d8c Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:03:44 +0900 Subject: [PATCH 48/54] :sparkles: :: Adds sign up states using freezed Implements states for different parts of the sign-up process, including email, name, password, and school information, using the Freezed package for immutable data classes. This provides a structured way to manage and update the UI state during the registration flow. --- .../sign_up/state/sign_up_email_state.dart | 20 +++++++++++++++++++ .../sign_up/state/sign_up_name_state.dart | 12 +++++++++++ .../sign_up/state/sign_up_password_state.dart | 13 ++++++++++++ .../sign_up/state/sign_up_school_state.dart | 19 ++++++++++++++++++ 4 files changed, 64 insertions(+) create mode 100644 lib/presentation/sign_up/state/sign_up_email_state.dart create mode 100644 lib/presentation/sign_up/state/sign_up_name_state.dart create mode 100644 lib/presentation/sign_up/state/sign_up_password_state.dart create mode 100644 lib/presentation/sign_up/state/sign_up_school_state.dart diff --git a/lib/presentation/sign_up/state/sign_up_email_state.dart b/lib/presentation/sign_up/state/sign_up_email_state.dart new file mode 100644 index 0000000..f9546e6 --- /dev/null +++ b/lib/presentation/sign_up/state/sign_up_email_state.dart @@ -0,0 +1,20 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'sign_up_email_state.freezed.dart'; + +@freezed +abstract class SignUpEmailState with _$SignUpEmailState { + factory SignUpEmailState({ + @Default("") String email, + @Default("") String verify, + + @Default(null) String? errorMessage, + @Default(false) bool enableButton, + + @Default(true) bool isEmailValid, + @Default(false) bool codeSent, + @Default(true) bool isCodeMatched, + @Default(false) bool isSendingCode, + @Default(Duration(minutes: 10)) Duration timeRemaining, + }) = _SignUpEmailState; +} diff --git a/lib/presentation/sign_up/state/sign_up_name_state.dart b/lib/presentation/sign_up/state/sign_up_name_state.dart new file mode 100644 index 0000000..d6fc5e4 --- /dev/null +++ b/lib/presentation/sign_up/state/sign_up_name_state.dart @@ -0,0 +1,12 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'sign_up_name_state.freezed.dart'; + +@freezed +abstract class SignUpNameState with _$SignUpNameState { + factory SignUpNameState({ + @Default("") String username, + @Default(null) String? errorMessage, + @Default(false) bool enableButton, + }) = _SignUpNameState; +} diff --git a/lib/presentation/sign_up/state/sign_up_password_state.dart b/lib/presentation/sign_up/state/sign_up_password_state.dart new file mode 100644 index 0000000..593e1c2 --- /dev/null +++ b/lib/presentation/sign_up/state/sign_up_password_state.dart @@ -0,0 +1,13 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'sign_up_password_state.freezed.dart'; + +@freezed +abstract class SignUpPasswordState with _$SignUpPasswordState { + const factory SignUpPasswordState({ + @Default('') String password, + @Default(true) bool isPasswordValid, + @Default(true) bool isPasswordMatched, + @Default(true) bool isFormFilled, + }) = _SignUpPasswordState; +} diff --git a/lib/presentation/sign_up/state/sign_up_school_state.dart b/lib/presentation/sign_up/state/sign_up_school_state.dart new file mode 100644 index 0000000..2013518 --- /dev/null +++ b/lib/presentation/sign_up/state/sign_up_school_state.dart @@ -0,0 +1,19 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'sign_up_school_state.freezed.dart'; + +@freezed +abstract class SignUpSchoolState with _$SignUpSchoolState { + const factory SignUpSchoolState({ + @Default(null) SchoolInfoState? selectedSchool, + @Default([]) List filteredSchools, + }) = _SignUpSchoolState; +} + +@freezed +abstract class SchoolInfoState with _$SchoolInfoState { + const factory SchoolInfoState({ + @Default('') String schoolName, + @Default('') String schoolAddress, + }) = _SchoolInfoState; +} From 4869195e1242c89b895f50e4151ea62084769a0c Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:03:50 +0900 Subject: [PATCH 49/54] :sparkles: :: Implements password creation screen Adds a screen for creating a password during signup. Includes password validation and confirmation. Navigates to the next screen upon successful password creation. --- .../sign_up/screens/email_auth_screen.dart | 434 ++++++------------ .../sign_up/screens/find_school_screen.dart | 183 +++----- .../sign_up/screens/name_input_screen.dart | 98 +--- .../screens/password_create_screen.dart | 170 ++----- 4 files changed, 275 insertions(+), 610 deletions(-) diff --git a/lib/presentation/sign_up/screens/email_auth_screen.dart b/lib/presentation/sign_up/screens/email_auth_screen.dart index 005de17..dc68eb7 100644 --- a/lib/presentation/sign_up/screens/email_auth_screen.dart +++ b/lib/presentation/sign_up/screens/email_auth_screen.dart @@ -1,187 +1,18 @@ -import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:jusicool_design_system/src/core/theme/colors/color_palette.dart'; import 'package:jusicool_design_system/src/core/theme/texts/typography.dart'; -import 'package:email_validator/email_validator.dart'; -import 'package:go_router/go_router.dart'; +import '../controller/sign_up_email_controller.dart'; -class AppStrings { - static const emailLabel = '이메일'; - static const emailHint = '이메일을 입력해주세요'; - static const emailInvalidFormat = '이메일 형식을 다시 확인해주세요'; - static const codeLabel = '인증번호'; - static const codeHint = '인증번호를 입력해주세요'; - static const codeInvalid = '인증번호가 일치하지 않습니다'; - static const resendCodeButton = '인증번호 재전송'; - static const nextButton = '다음'; - static const codeSentMessage = '인증번호가 전송되었습니다'; - static const codeExpiredMessage = '인증번호가 만료되었습니다'; - static const networkErrorMessage = '인증번호 전송에 실패했습니다'; - static const verifyEmailTitle = '이메일을 인증해주세요'; -} - -class EmailAuthScreen extends StatefulWidget { - final String username; - - const EmailAuthScreen({super.key, required this.username}); - - @override - State createState() => _EmailAuthScreenState(); -} - -class _EmailAuthScreenState extends State { - final TextEditingController emailController = TextEditingController(); - final TextEditingController codeController = TextEditingController(); - final FocusNode _codeFocusNode = FocusNode(); - Timer? _codeExpirationTimer; - Timer? _countdownTimer; - static const codeExpirationDuration = Duration(minutes: 5); - - bool isEmailValid = true; - bool codeSent = false; - bool isCodeMatched = true; - bool isSendingCode = false; - Duration timeRemaining = codeExpirationDuration; - - String get timerText => - '${(timeRemaining.inSeconds ~/ 60).toString().padLeft(1, '0')}:${(timeRemaining.inSeconds % 60).toString().padLeft(2, '0')}'; - - static final TextStyle LABEL_STYLE = JusicoolTypography.bodySmall.copyWith( - fontSize: 16.sp, - color: JusicoolColor.black, - ); - static final TextStyle ERROR_STYLE = JusicoolTypography.bodySmall.copyWith( - fontSize: 12.sp, - color: JusicoolColor.error, - ); - static final TextStyle HINT_STYLE = JusicoolTypography.bodySmall.copyWith( - fontSize: 16.sp, - color: JusicoolColor.gray300, - ); - static final TextStyle TIMER_STYLE = JusicoolTypography.bodySmall.copyWith( - fontSize: 14.sp, - color: JusicoolColor.black, - ); - static final TextStyle RESEND_STYLE = JusicoolTypography.bodySmall.copyWith( - fontSize: 14.sp, - color: JusicoolColor.main, - decoration: TextDecoration.underline, - ); - - @override - void initState() { - super.initState(); - emailController.addListener(onEmailChanged); - } - - @override - void dispose() { - _codeExpirationTimer?.cancel(); - _countdownTimer?.cancel(); - _codeFocusNode.dispose(); - emailController.dispose(); - codeController.dispose(); - super.dispose(); - } +class EmailAuthScreen extends ConsumerWidget { + const EmailAuthScreen({super.key}); - void onEmailChanged() { - final email = emailController.text.trim(); - setState(() { - isEmailValid = email.isEmpty || EmailValidator.validate(email); - }); - } - - void sendVerificationCode() { - if (!isEmailValid || isSendingCode) return; - - setState(() => isSendingCode = true); - try { - setState(() { - codeSent = true; - isCodeMatched = true; - codeController.clear(); - timeRemaining = codeExpirationDuration; - }); - - _codeExpirationTimer?.cancel(); - _countdownTimer?.cancel(); - _codeExpirationTimer = Timer(codeExpirationDuration, () { - setState(() { - codeSent = false; - _countdownTimer?.cancel(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text(AppStrings.codeExpiredMessage)), - ); - }); - }); - - _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { - setState(() { - if (timeRemaining.inSeconds > 0) { - timeRemaining = timeRemaining - const Duration(seconds: 1); - } else { - timer.cancel(); - } - }); - }); - - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text(AppStrings.codeSentMessage))); - FocusScope.of(context).requestFocus(_codeFocusNode); - } finally { - setState(() => isSendingCode = false); - } - } - - void checkCodeMatch() { - if (isSendingCode) return; - - setState(() => isSendingCode = true); - try { - if (codeController.text == "1234") { - setState(() { - isCodeMatched = true; - _codeExpirationTimer?.cancel(); - _countdownTimer?.cancel(); - }); - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('인증번호가 확인되었습니다'))); - context.push( - '/password-create', - extra: { - 'username': widget.username, - 'email': emailController.text.trim(), - }, - ); - return; - } - - setState(() => isCodeMatched = false); - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text(AppStrings.codeInvalid))); - } finally { - setState(() => isSendingCode = false); - } - } - - bool get isNextEnabled { - final canSendCode = - isEmailValid && emailController.text.isNotEmpty && !codeSent; - final canProceed = codeSent && codeController.text.length == 4; - return canSendCode || canProceed; - } - - void handleNextButton() { - if (!codeSent && isEmailValid) { - sendVerificationCode(); - } else if (codeSent && codeController.text.length == 4) { - checkCodeMatch(); - } + String formatTimer(Duration duration) { + final minutes = duration.inSeconds ~/ 60; + final seconds = duration.inSeconds % 60; + return '${minutes.toString().padLeft(1, '0')}:${seconds.toString().padLeft(2, '0')}'; } Widget buildTextField({ @@ -189,49 +20,48 @@ class _EmailAuthScreenState extends State { required String hintText, required bool isValid, TextInputType? keyboardType, - FocusNode? focusNode, int? maxLength, List? inputFormatters, }) { final borderSide = BorderSide( - color: - controller.text.isNotEmpty - ? (controller == emailController - ? (isEmailValid ? JusicoolColor.main : JusicoolColor.error) - : (isValid ? JusicoolColor.main : JusicoolColor.error)) - : JusicoolColor.gray300, - width: 1.w, + color: isValid ? JusicoolColor.main : JusicoolColor.error, + width: 2.w, ); - return Container( - width: 312.w, - height: 58.h, - child: TextField( - controller: controller, - keyboardType: keyboardType, - focusNode: focusNode, - maxLength: maxLength, - inputFormatters: inputFormatters, - decoration: InputDecoration( - hintText: hintText, - hintStyle: HINT_STYLE, - contentPadding: EdgeInsets.all(16.w), - filled: true, - fillColor: JusicoolColor.white, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.r), - borderSide: borderSide, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.r), - borderSide: borderSide, + return TextField( + controller: controller, + keyboardType: keyboardType, + maxLength: maxLength, + inputFormatters: inputFormatters, + decoration: InputDecoration( + hintText: hintText, + hintStyle: JusicoolTypography.bodySmall.copyWith( + fontSize: 16.sp, + color: JusicoolColor.gray300, + ), + contentPadding: EdgeInsets.all(16.w), + filled: true, + fillColor: JusicoolColor.white, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.r), + borderSide: BorderSide( + color: isValid ? JusicoolColor.gray200 : JusicoolColor.error, + width: 2.w, ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.r), - borderSide: borderSide, + ), + disabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.r), + borderSide: BorderSide( + color: isValid ? JusicoolColor.gray200 : JusicoolColor.error, + width: 2.w, ), - counterText: '', ), + + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.r), + borderSide: borderSide, + ), + counterText: '', ), ); } @@ -277,7 +107,10 @@ class _EmailAuthScreenState extends State { } @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final controller = ref.read(emailAuthControllerProvider.notifier); + final state = ref.watch(emailAuthControllerProvider); + return Scaffold( appBar: AppBar( leading: Padding( @@ -292,103 +125,120 @@ class _EmailAuthScreenState extends State { child: Column( children: [ Expanded( - child: Column( - children: [ - SingleChildScrollView( - padding: EdgeInsets.fromLTRB(24.w, 16.h, 24.w, 56.h), - child: Column( - spacing: 40.h, + child: SingleChildScrollView( + padding: EdgeInsets.fromLTRB(24.w, 16.h, 24.w, 56.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(top: 8.h), + child: Text( + '이메일을 인증해주세요', + style: JusicoolTypography.subTitle, + ), + ), + SizedBox(height: 40.h), + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 타이틀 - Padding( - padding: EdgeInsets.only(top: 8.h), - child: Text( - AppStrings.verifyEmailTitle, - style: JusicoolTypography.subTitle, + Text( + '이메일', + style: JusicoolTypography.bodySmall.copyWith( + fontSize: 16.sp, + color: JusicoolColor.black, ), ), - - // 이메일 입력 - Column( - spacing: 4.h, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(AppStrings.emailLabel, style: LABEL_STYLE), - buildTextField( - controller: emailController, - hintText: AppStrings.emailHint, - isValid: isEmailValid, - keyboardType: TextInputType.emailAddress, + buildTextField( + controller: controller.emailController, + hintText: '이메일을 입력해주세요', + isValid: state.isEmailValid, + keyboardType: TextInputType.emailAddress, + ), + if (controller.emailController.text.isNotEmpty && + !state.isEmailValid) + Text( + state.errorMessage ?? '올바른 이메일 형식이 아닙니다', + style: JusicoolTypography.bodySmall.copyWith( + fontSize: 12.sp, + color: JusicoolColor.error, ), - if (emailController.text.isNotEmpty && - !isEmailValid) + ), + ], + ), + SizedBox(height: 32.h), + if (state.codeSent) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ Text( - AppStrings.emailInvalidFormat, - style: ERROR_STYLE, + '인증번호', + style: JusicoolTypography.bodySmall.copyWith( + fontSize: 16.sp, + color: JusicoolColor.black, + ), ), - ], - ), - - // 인증번호 입력 (codeSent 상태에서만 표시) - if (codeSent) - Column( - spacing: 4.h, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - spacing: 8.w, - children: [ - Text( - AppStrings.codeLabel, - style: LABEL_STYLE, - ), - Text(timerText, style: TIMER_STYLE), - ], + SizedBox(width: 8.w), + Text( + formatTimer(state.timeRemaining), + style: JusicoolTypography.bodySmall.copyWith( + fontSize: 14.sp, + color: JusicoolColor.black, + ), ), - buildTextField( - controller: codeController, - hintText: AppStrings.codeHint, - isValid: isCodeMatched, - keyboardType: TextInputType.number, - focusNode: _codeFocusNode, - maxLength: 4, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - ], + ], + ), + buildTextField( + controller: controller.codeController, + hintText: '인증번호를 입력해주세요', + isValid: state.isCodeMatched, + keyboardType: TextInputType.number, + maxLength: 6, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + ), + if (controller.codeController.text.isNotEmpty && + !state.isCodeMatched) + Text( + '인증번호가 일치하지 않습니다', + style: JusicoolTypography.bodySmall.copyWith( + fontSize: 12.sp, + color: JusicoolColor.error, ), - if (codeController.text.isNotEmpty && - !isCodeMatched) - Text( - AppStrings.codeInvalid, - style: ERROR_STYLE, - ), - Align( - alignment: Alignment.centerLeft, - child: TextButton( - onPressed: sendVerificationCode, - child: Text( - AppStrings.resendCodeButton, - style: RESEND_STYLE, - ), + ), + Align( + alignment: Alignment.centerLeft, + child: TextButton( + onPressed: controller.sendEmail, + child: Text( + '인증번호 재전송', + style: JusicoolTypography.bodySmall.copyWith( + fontSize: 14.sp, + color: JusicoolColor.main, + decoration: TextDecoration.underline, ), ), - ], + ), ), - ], - ), - ), - ], + ], + ), + ], + ), ), ), - - // 하단 버튼 Padding( padding: EdgeInsets.fromLTRB(24.w, 0, 24.w, 24.h), child: buildButton( - label: AppStrings.nextButton, - onPressed: isNextEnabled ? handleNextButton : null, - isLoading: isSendingCode, + label: '다음', + onPressed: + state.enableButton + ? state.codeSent + ? () => controller.sendVerificationCode(context) + : controller.sendEmail + : null, + isLoading: state.isSendingCode, ), ), ], diff --git a/lib/presentation/sign_up/screens/find_school_screen.dart b/lib/presentation/sign_up/screens/find_school_screen.dart index 3efcbcc..e1bc1d6 100644 --- a/lib/presentation/sign_up/screens/find_school_screen.dart +++ b/lib/presentation/sign_up/screens/find_school_screen.dart @@ -1,88 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:jusicool_design_system/jusicool_design_system.dart'; -import 'package:go_router/go_router.dart'; +import 'package:jusicool_ios/presentation/sign_up/controller/sign_up_email_controller.dart'; +import '../controller/sign_up_name_controller.dart'; +import '../controller/sign_up_password_controller.dart'; +import '../controller/sign_up_school_controller.dart'; +import '../state/sign_up_school_state.dart'; -class SchoolInfo { - final String name; - final String address; - - SchoolInfo({required this.name, required this.address}); - - Map toMap() => {"name": name, "address": address}; -} - -class FindSchoolScreen extends StatefulWidget { - final String username; - final String email; - final String password; - - const FindSchoolScreen({ - super.key, - required this.username, - required this.email, - required this.password, - }); - - @override - State createState() => _FindSchoolScreenState(); -} - -class _FindSchoolScreenState extends State { - final TextEditingController schoolNameController = TextEditingController(); - - bool isSearchButtonPressed = false; - List filteredSchools = []; - SchoolInfo? selectedSchool; - //========== - final List schools = [ - SchoolInfo(name: "대충중학교", address: "대충남도 대충시 대충면 대충로 1-2"), - SchoolInfo(name: "대충고등학교", address: "대충남도 대충시 대충면 대충로 3-4"), - SchoolInfo(name: "가나초등학교", address: "대충남도 대충시 가나동 가나로 5-6"), - SchoolInfo(name: "다라중학교", address: "대충남도 대충시 다라동 다라로 7-8"), - ]; - //========== - - @override - void initState() { - super.initState(); - SystemChrome.setSystemUIOverlayStyle( - const SystemUiOverlayStyle( - statusBarColor: JusicoolColor.white, - statusBarIconBrightness: Brightness.dark, - systemNavigationBarColor: JusicoolColor.white, - systemNavigationBarIconBrightness: Brightness.dark, - ), - ); - } - - @override - void dispose() { - schoolNameController.dispose(); - super.dispose(); - } - - void onSearch() { - final q = schoolNameController.text.trim(); - setState(() { - if (q.isEmpty) { - filteredSchools = []; - selectedSchool = null; - } else { - filteredSchools = - schools - .where((s) => s.name.toLowerCase().contains(q.toLowerCase())) - .toList(); - } - }); - } - - void onStart() { - if (selectedSchool != null) { - context.go('/main-capital'); - } - } +class FindSchoolScreen extends ConsumerWidget { + const FindSchoolScreen({super.key}); Widget _labelChip(String text) => Container( padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 2.h), @@ -99,13 +27,14 @@ class _FindSchoolScreenState extends State { ), ); - Widget _schoolCard(SchoolInfo school) { - final isSelected = selectedSchool?.name == school.name; + Widget _schoolCard( + SchoolInfoState school, + SchoolInfoState? selectedSchool, + Function onTap, + ) { + final isSelected = selectedSchool?.schoolName == school.schoolName; return GestureDetector( - onTap: - () => setState(() { - selectedSchool = isSelected ? null : school; - }), + onTap: () => onTap(), child: Container( margin: EdgeInsets.only(bottom: 12.h), padding: EdgeInsets.all(16.w), @@ -126,7 +55,7 @@ class _FindSchoolScreenState extends State { Padding( padding: EdgeInsets.only(left: 12.w), child: Text( - school.name, + school.schoolName, style: JusicoolTypography.bodySmall.copyWith( fontSize: 12.sp, color: JusicoolColor.black, @@ -143,7 +72,7 @@ class _FindSchoolScreenState extends State { Padding( padding: EdgeInsets.only(left: 12.w), child: Text( - school.address, + school.schoolAddress, style: JusicoolTypography.bodySmall.copyWith( fontSize: 12.sp, color: JusicoolColor.black, @@ -159,12 +88,15 @@ class _FindSchoolScreenState extends State { ); } - Widget _searchRow() => Row( + Widget _searchRow( + TextEditingController schoolNameController, + bool isSelect, + Function onTap, + ) => Row( children: [ Expanded( child: TextField( controller: schoolNameController, - onChanged: (_) => onSearch(), decoration: InputDecoration( hintText: '학교명을 입력해주세요', hintStyle: JusicoolTypography.bodySmall.copyWith( @@ -194,33 +126,39 @@ class _FindSchoolScreenState extends State { width: 54.w, height: 54.h, child: GestureDetector( - onTapDown: (_) => setState(() => isSearchButtonPressed = true), - onTapUp: (_) { - setState(() => isSearchButtonPressed = false); - onSearch(); - }, - onTapCancel: () => setState(() => isSearchButtonPressed = false), + onTap: () => onTap(), child: AnimatedContainer( duration: const Duration(milliseconds: 100), decoration: BoxDecoration( - color: - isSearchButtonPressed - ? JusicoolColor.gray100 - : JusicoolColor.white, + color: isSelect ? JusicoolColor.gray100 : JusicoolColor.white, borderRadius: BorderRadius.circular(8.r), ), - child: JusicoolIcon.search(), + child: Container( + padding: EdgeInsets.all(15), + decoration: BoxDecoration( + border: Border.all( + color: JusicoolColor.main.withOpacity(0.5), + width: 1.sp, + ), + borderRadius: BorderRadius.circular(8), + ), + child: JusicoolIcon.search( + height: 24.h, + width: 24.w, + color: JusicoolColor.main.withOpacity(0.5), + ), + ), ), ), ), ], ); - Widget _startButton(bool enabled) => SizedBox( + Widget _startButton(bool enabled, Function onTap) => SizedBox( width: double.infinity, height: 54.h, child: ElevatedButton( - onPressed: enabled ? onStart : null, + onPressed: () => onTap(), style: ElevatedButton.styleFrom( backgroundColor: enabled ? JusicoolColor.main : JusicoolColor.gray300, foregroundColor: enabled ? JusicoolColor.white : JusicoolColor.gray600, @@ -239,8 +177,10 @@ class _FindSchoolScreenState extends State { ); @override - Widget build(BuildContext context) { - final isSchoolSelected = selectedSchool != null; + Widget build(BuildContext context, WidgetRef ref) { + final provider = ref.watch(signupSchoolControllerProvider.notifier); + final state = ref.watch(signupSchoolControllerProvider); + final isSchoolSelected = state.selectedSchool != null; return Scaffold( backgroundColor: JusicoolColor.white, @@ -289,7 +229,11 @@ class _FindSchoolScreenState extends State { color: JusicoolColor.black, ), ), - _searchRow(), + _searchRow( + provider.schoolNameController, + state.selectedSchool != null, + provider.searchSchool, + ), ], ), ], @@ -298,7 +242,7 @@ class _FindSchoolScreenState extends State { ), Expanded( child: - filteredSchools.isEmpty + state.filteredSchools.isEmpty ? Center( child: Text( '검색 결과가 없습니다.', @@ -310,12 +254,31 @@ class _FindSchoolScreenState extends State { ) : ListView.builder( padding: EdgeInsets.only(top: 2.h), - itemCount: filteredSchools.length, + itemCount: state.filteredSchools.length, itemBuilder: - (_, index) => _schoolCard(filteredSchools[index]), + (_, index) => _schoolCard( + state.filteredSchools[index], + state.selectedSchool, + () => provider.selectSchool( + state.filteredSchools[index], + ), + ), ), ), - Column(children: [_startButton(isSchoolSelected)]), + Column( + children: [ + _startButton( + isSchoolSelected, + () => provider.start( + context: context, + email: ref.watch(emailAuthControllerProvider).email, + password: + ref.watch(signupPasswordControllerProvider).password, + name: ref.watch(nameControllerProvider).username, + ), + ), + ], + ), ], ), ), diff --git a/lib/presentation/sign_up/screens/name_input_screen.dart b/lib/presentation/sign_up/screens/name_input_screen.dart index 7993a80..770d2a8 100644 --- a/lib/presentation/sign_up/screens/name_input_screen.dart +++ b/lib/presentation/sign_up/screens/name_input_screen.dart @@ -1,70 +1,17 @@ +import 'package:cookie_jar/cookie_jar.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:jusicool_design_system/jusicool_design_system.dart'; import 'package:go_router/go_router.dart'; +import 'package:jusicool_ios/presentation/sign_up/controller/sign_up_name_controller.dart'; -const double BUTTON_HEIGHT = 54; +import '../../../core/config/di/dependencies.dart'; +import '../../../core/config/router/router.dart'; -class NameInputScreen extends StatefulWidget { +class NameInputScreen extends ConsumerWidget { const NameInputScreen({super.key}); - @override - State createState() => _NameInputScreenState(); -} - -class _NameInputScreenState extends State { - final TextEditingController _controller = TextEditingController(); - String? _errorMessage; - - bool get _isButtonEnabled => _controller.text.trim().isNotEmpty; - - @override - void initState() { - super.initState(); - _controller.addListener(() { - _clearErrorOnTextChange(); - setState(() {}); - }); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - void _clearErrorOnTextChange() { - if (_errorMessage != null) { - setState(() { - _errorMessage = null; - }); - } - } - - void _handleNext() { - final name = _controller.text.trim(); - - if (name.isEmpty) { - setState(() { - _errorMessage = '필수 입력 항목입니다.'; - }); - return; - } - - if (!_isValidKoreanName(name)) { - setState(() { - _errorMessage = '한글 이름을 2자 이상 입력해주세요.'; - }); - return; - } - - context.push('/email-auth', extra: name); - } - - bool _isValidKoreanName(String name) { - return RegExp(r'^[가-힣]{2,}$').hasMatch(name); - } - Widget buildButton({ required String label, required VoidCallback? onPressed, @@ -77,11 +24,11 @@ class _NameInputScreenState extends State { label: label, child: Container( width: double.infinity, - height: BUTTON_HEIGHT.h, decoration: BoxDecoration(borderRadius: BorderRadius.circular(12.r)), child: ElevatedButton( onPressed: isLoading ? null : onPressed, style: ElevatedButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 16.h), backgroundColor: isEnabled ? JusicoolColor.main : JusicoolColor.gray300, foregroundColor: @@ -109,7 +56,11 @@ class _NameInputScreenState extends State { } @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final CookieJar cookieJar = di.get(); + cookieJar.deleteAll(); // 쿠키 삭제 + final provider = ref.watch(nameControllerProvider.notifier); + final state = ref.watch(nameControllerProvider); return Scaffold( backgroundColor: JusicoolColor.white, appBar: AppBar( @@ -134,27 +85,26 @@ class _NameInputScreenState extends State { children: [ Text('이름', style: JusicoolTypography.bodySmall), DefaultTextField( - controller: _controller, + controller: provider.controller, hintText: '실명을 적어주세요', - validator: (value) { - final name = value?.trim() ?? ''; - if (name.isEmpty) { - _errorMessage = '이름을 입력해주세요'; - } else if (!RegExp(r'^[가-힣]{2,}$').hasMatch(name)) { - _errorMessage = '2자 이상 한글로 입력해주세요'; - } else { - _errorMessage = null; - } - return _errorMessage; + errorText: state.errorMessage, + validator: (String) { + return null; }, - errorText: _errorMessage, ), ], ), const Spacer(), buildButton( label: '다음', - onPressed: _isButtonEnabled ? _handleNext : null, + onPressed: + state.enableButton + ? () { + if (provider.validateUsername()) { + context.push(RoutePaths.emailAuth); + } + } + : null, ), ], ), diff --git a/lib/presentation/sign_up/screens/password_create_screen.dart b/lib/presentation/sign_up/screens/password_create_screen.dart index 15b2a13..8334191 100644 --- a/lib/presentation/sign_up/screens/password_create_screen.dart +++ b/lib/presentation/sign_up/screens/password_create_screen.dart @@ -1,31 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:jusicool_design_system/jusicool_design_system.dart'; -import 'package:go_router/go_router.dart'; +import '../controller/sign_up_password_controller.dart'; -class PasswordCreateScreen extends StatefulWidget { - final String username; - final String email; - - const PasswordCreateScreen({ - super.key, - required this.username, - required this.email, - }); - - @override - State createState() => _PasswordCreateScreenState(); -} - -class _PasswordCreateScreenState extends State { - final TextEditingController _passwordController = TextEditingController(); - final TextEditingController _confirmPasswordController = - TextEditingController(); - - bool _isFormFilled = false; - bool _isPasswordValid = true; - bool _isPasswordMatched = true; +class PasswordCreateScreen extends ConsumerWidget { + const PasswordCreateScreen({super.key}); static final TextStyle _titleStyle = JusicoolTypography.bodyMedium.copyWith( fontSize: 18.sp, @@ -37,87 +17,11 @@ class _PasswordCreateScreenState extends State { ); @override - void initState() { - super.initState(); - - _passwordController.addListener(_onPasswordChanged); - _confirmPasswordController.addListener(_onConfirmPasswordChanged); - - SystemChrome.setSystemUIOverlayStyle( - const SystemUiOverlayStyle( - statusBarColor: JusicoolColor.white, - statusBarIconBrightness: Brightness.dark, - systemNavigationBarColor: JusicoolColor.white, - systemNavigationBarIconBrightness: Brightness.dark, - ), - ); - } - - @override - void dispose() { - _passwordController.dispose(); - _confirmPasswordController.dispose(); - super.dispose(); - } - - void _onPasswordChanged() { - _validatePassword(_passwordController.text); - _updateFormState(); - _checkPasswordsMatch(); - } - - void _onConfirmPasswordChanged() { - _updateFormState(); - _checkPasswordsMatch(); - } - - void _updateFormState() { - setState(() { - _isFormFilled = - _passwordController.text.isNotEmpty && - _confirmPasswordController.text.isNotEmpty; - }); - } - - void _validatePassword(String password) { - setState(() { - _isPasswordValid = _isValidPassword(password); - }); - } - - void _checkPasswordsMatch() { - setState(() { - _isPasswordMatched = - _passwordController.text == _confirmPasswordController.text; - }); - } - - bool _isValidPassword(String password) { - if (password.length < 8 || password.length > 13) return false; - - final hasLetter = RegExp(r'[A-Za-z]').hasMatch(password); - final hasNumber = RegExp(r'\d').hasMatch(password); - final hasSpecial = RegExp(r'[@$!%*?&]').hasMatch(password); - - final count = [hasLetter, hasNumber, hasSpecial].where((e) => e).length; - return count >= 2; - } - - void _onNextButtonPressed() { - context.push( - '/find-school', - extra: { - 'username': widget.username, - 'email': widget.email, - 'password': _passwordController.text, - }, - ); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(signupPasswordControllerProvider); + final provider = ref.watch(signupPasswordControllerProvider.notifier); final isButtonEnabled = - _isFormFilled && _isPasswordValid && _isPasswordMatched; + state.isFormFilled && state.isPasswordValid && state.isPasswordMatched; return Scaffold( appBar: AppBar( @@ -145,29 +49,20 @@ class _PasswordCreateScreenState extends State { '비밀번호', style: _labelStyle.copyWith( color: - _isPasswordValid + state.isPasswordValid ? JusicoolColor.black : JusicoolColor.error, ), ), DefaultTextField( - controller: _passwordController, + controller: provider.passwordController, hintText: '비밀번호를 입력해주세요', obscureText: true, errorText: - _isPasswordValid - ? null - : '영문, 숫자, 특수문자 중 2개 이상 조합으로 8~13자', - validator: (value) { - final pwd = value ?? ''; - if (pwd.isEmpty) { - return '비밀번호를 입력해주세요'; - } - if (!_isValidPassword(pwd)) { - return '영문, 숫자, 특수문자 중 2개 이상 조합으로 8~13자'; - } - return null; - }, + !state.isPasswordValid + ? '영문, 숫자, 특수문자 중 2개 이상 조합으로 8~13자' + : null, + validator: (String) => null, ), ], ), @@ -176,29 +71,21 @@ class _PasswordCreateScreenState extends State { spacing: 4.h, children: [ Text( - '비밀번호 재 입력', + '비밀번호 재입력', style: _labelStyle.copyWith( color: - _isPasswordMatched + state.isPasswordMatched ? JusicoolColor.black : JusicoolColor.error, ), ), DefaultTextField( - controller: _confirmPasswordController, + controller: provider.confirmPasswordController, hintText: '비밀번호를 다시 입력해주세요', obscureText: true, - errorText: _isPasswordMatched ? null : '비밀번호가 일치하지 않아요', - validator: (value) { - final confirmPwd = value ?? ''; - if (confirmPwd.isEmpty) { - return '비밀번호를 다시 입력해주세요'; - } - if (confirmPwd != _passwordController.text) { - return '비밀번호가 일치하지 않아요'; - } - return null; - }, + errorText: + !state.isPasswordMatched ? '비밀번호가 일치하지 않아요' : null, + validator: (String) => null, ), ], ), @@ -207,7 +94,22 @@ class _PasswordCreateScreenState extends State { width: double.infinity, height: 54.h, child: ElevatedButton( - onPressed: isButtonEnabled ? _onNextButtonPressed : null, + onPressed: () { + final password = provider.passwordController.text.trim(); + + provider.validatePassword(password); + provider.checkPasswordsMatch(); + + final newState = ref.read(signupPasswordControllerProvider); + final isValid = + newState.isFormFilled && + newState.isPasswordValid && + newState.isPasswordMatched; + + if (isValid) { + provider.onNextButtonPressed(context); + } + }, style: ElevatedButton.styleFrom( backgroundColor: isButtonEnabled From 9171d355e9129399b63abcf0d83b9d277c887e90 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:46:40 +0900 Subject: [PATCH 50/54] :fire: :: Removes unnecessary imports Removes unused imports from data layer files. Updates color definition to use withValues method. --- analysis_options.yml => analysis_options.yaml | 0 lib/data/user/repositories/user_repository_impl.dart | 1 - lib/data/user/service/neis_api.dart | 3 +-- lib/data/user/service/user_api.dart | 5 ++--- lib/presentation/sign_up/screens/find_school_screen.dart | 4 ++-- 5 files changed, 5 insertions(+), 8 deletions(-) rename analysis_options.yml => analysis_options.yaml (100%) diff --git a/analysis_options.yml b/analysis_options.yaml similarity index 100% rename from analysis_options.yml rename to analysis_options.yaml diff --git a/lib/data/user/repositories/user_repository_impl.dart b/lib/data/user/repositories/user_repository_impl.dart index e8128c6..8368023 100644 --- a/lib/data/user/repositories/user_repository_impl.dart +++ b/lib/data/user/repositories/user_repository_impl.dart @@ -3,7 +3,6 @@ import 'package:jusicool_ios/data/user/dto/remote/request/sign_in_request_dto.da import 'package:jusicool_ios/data/user/dto/remote/request/sign_up_request_dto.dart'; import 'package:jusicool_ios/data/user/dto/remote/request/sign_up_search_school_request_dto.dart'; import 'package:jusicool_ios/data/user/dto/remote/request/sign_up_verify_email_request_dto.dart'; -import 'package:jusicool_ios/data/user/dto/remote/response/sign_up_search_school_response_dto.dart'; import 'package:jusicool_ios/data/user/mappers/remote/request/sign_in_request_mapper.dart'; import 'package:jusicool_ios/data/user/mappers/remote/request/sign_up_request_mapper.dart'; import 'package:jusicool_ios/data/user/mappers/remote/request/sign_up_send_email_request_mapper.dart'; diff --git a/lib/data/user/service/neis_api.dart b/lib/data/user/service/neis_api.dart index 2ab3a8b..e034596 100644 --- a/lib/data/user/service/neis_api.dart +++ b/lib/data/user/service/neis_api.dart @@ -1,6 +1,5 @@ import 'package:dio/dio.dart'; -import 'package:retrofit/error_logger.dart'; -import 'package:retrofit/http.dart'; +import 'package:retrofit/retrofit.dart'; import '../dto/remote/response/sign_up_search_school_response_dto.dart'; part 'neis_api.g.dart'; diff --git a/lib/data/user/service/user_api.dart b/lib/data/user/service/user_api.dart index 6c0944b..f2dece3 100644 --- a/lib/data/user/service/user_api.dart +++ b/lib/data/user/service/user_api.dart @@ -1,7 +1,6 @@ import 'package:dio/dio.dart'; import 'package:jusicool_ios/data/user/dto/remote/request/sign_up_verify_email_request_dto.dart'; -import 'package:retrofit/error_logger.dart'; -import 'package:retrofit/http.dart'; +import 'package:retrofit/retrofit.dart'; import '../dto/remote/request/sign_in_request_dto.dart'; import '../dto/remote/request/sign_up_request_dto.dart'; import '../dto/remote/request/sign_up_send_email_request_dto.dart'; @@ -23,4 +22,4 @@ abstract class UserApi { @POST('/user/email/verify') Future verifyEmail(@Body() SignUpVerifyEmailRequestDto body); -} \ No newline at end of file +} diff --git a/lib/presentation/sign_up/screens/find_school_screen.dart b/lib/presentation/sign_up/screens/find_school_screen.dart index e1bc1d6..1c1e4a2 100644 --- a/lib/presentation/sign_up/screens/find_school_screen.dart +++ b/lib/presentation/sign_up/screens/find_school_screen.dart @@ -137,7 +137,7 @@ class FindSchoolScreen extends ConsumerWidget { padding: EdgeInsets.all(15), decoration: BoxDecoration( border: Border.all( - color: JusicoolColor.main.withOpacity(0.5), + color: JusicoolColor.main.withValues(alpha:0.5), width: 1.sp, ), borderRadius: BorderRadius.circular(8), @@ -145,7 +145,7 @@ class FindSchoolScreen extends ConsumerWidget { child: JusicoolIcon.search( height: 24.h, width: 24.w, - color: JusicoolColor.main.withOpacity(0.5), + color: JusicoolColor.main.withValues(alpha: 0.5), ), ), ), From 52e954b1f30f2efdf28fd4372ee8e81bd3247dbd Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 16:52:24 +0900 Subject: [PATCH 51/54] :green_heart: :: Adds code generation to CI/CD workflows Ensures generated code is up-to-date during CI/CD by incorporating the `build_runner` step. This helps prevent analysis errors and ensures consistency. --- .github/workflows/jusicool_cd.yml | 3 +++ .github/workflows/jusicool_ci.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/jusicool_cd.yml b/.github/workflows/jusicool_cd.yml index b86dcd2..b1008b8 100644 --- a/.github/workflows/jusicool_cd.yml +++ b/.github/workflows/jusicool_cd.yml @@ -37,6 +37,9 @@ jobs: - name: Install Dependencies run: flutter pub get + - name: Run Code Generation + run: flutter pub run build_runner build --delete-conflicting-outputs + - name: Analyze project source run: flutter analyze diff --git a/.github/workflows/jusicool_ci.yml b/.github/workflows/jusicool_ci.yml index 93fc108..d6b92f9 100644 --- a/.github/workflows/jusicool_ci.yml +++ b/.github/workflows/jusicool_ci.yml @@ -36,6 +36,9 @@ jobs: - name: Install Dependencies run: flutter pub get + - name: Run Code Generation + run: flutter pub run build_runner build --delete-conflicting-outputs + - name: Analyze project source run: flutter analyze From 1ac82108216788cef200bb41ff2d9795462dc5c5 Mon Sep 17 00:00:00 2001 From: parkyuhyeon <129172396+iloveuhyeon@users.noreply.github.com> Date: Tue, 8 Jul 2025 16:53:58 +0900 Subject: [PATCH 52/54] Update lib/presentation/sign_up/screens/find_school_screen.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/presentation/sign_up/screens/find_school_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/sign_up/screens/find_school_screen.dart b/lib/presentation/sign_up/screens/find_school_screen.dart index 1c1e4a2..bc433d0 100644 --- a/lib/presentation/sign_up/screens/find_school_screen.dart +++ b/lib/presentation/sign_up/screens/find_school_screen.dart @@ -30,7 +30,7 @@ class FindSchoolScreen extends ConsumerWidget { Widget _schoolCard( SchoolInfoState school, SchoolInfoState? selectedSchool, - Function onTap, + VoidCallback onTap, ) { final isSelected = selectedSchool?.schoolName == school.schoolName; return GestureDetector( From be59042f4d25bf5603f94ecd4a66e2b8ce9c20b6 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 17:23:45 +0900 Subject: [PATCH 53/54] :memo: :: Updates .gitignore and project settings Adds the .fvm/ directory to .gitignore to prevent accidental commits of the FVM version cache. Updates the .idea/Jusicool-iOS.iml file, likely due to IDE configuration changes. --- .gitignore | 6 +- .idea/Jusicool-iOS.iml | 789 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 793 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 488203a..59e0586 100644 --- a/.gitignore +++ b/.gitignore @@ -42,7 +42,6 @@ doc/api/ ### Flutter ### # Flutter/Dart/Pub related **/doc/api/ -.fvm/flutter_sdk .pub-cache/ .pub/ coverage/ @@ -322,4 +321,7 @@ iOSInjectionProject/ /*.gcno **/xcshareddata/WorkspaceSettings.xcsettings -# End of https://www.toptal.com/developers/gitignore/api/dotenv,flutter,xcode,swift,intellij,dart,git \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/dotenv,flutter,xcode,swift,intellij,dart,git + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/.idea/Jusicool-iOS.iml b/.idea/Jusicool-iOS.iml index c55092e..d9e12e4 100644 --- a/.idea/Jusicool-iOS.iml +++ b/.idea/Jusicool-iOS.iml @@ -18,6 +18,795 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 0e3083c8ff82363e7d1986b5a12c6272a1f08013 Mon Sep 17 00:00:00 2001 From: parkyuhyeon Date: Tue, 8 Jul 2025 17:24:28 +0900 Subject: [PATCH 54/54] =?UTF-8?q?:heavy=5Fplus=5Fsign:=20::=20.fvmrc=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .fvmrc | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .fvmrc diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 0000000..c300356 --- /dev/null +++ b/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "stable" +} \ No newline at end of file