From 620a271ee17910c02875afc374ceafb6b018aade Mon Sep 17 00:00:00 2001 From: Dev Ttangkong Date: Sun, 12 Jul 2026 14:31:42 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=8B=9C=EC=8A=A4?= =?UTF-8?q?=ED=85=9C=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sign_up/provider/sign_up_provider.dart | 37 ++++-- lib/presentation/sign_up/sign_up_page.dart | 8 +- .../sign_up/view/sign_up_nickname_view.dart | 59 ++++----- .../sign_up/view/sign_up_url_view.dart | 57 +++++---- .../sign_up/view/sign_up_view.dart | 103 ++++++++------- .../sign_up/view/sign_up_welcome_view.dart | 39 ++++++ .../sign_up/widget/sign_up_app_bar.dart | 31 ++--- .../sign_up/widget/sign_up_button.dart | 118 ++++++++++++++++++ .../widget/sign_up_check_nick_button.dart | 28 ----- .../widget/sign_up_check_url_button.dart | 47 ------- .../widget/sign_up_nickname_text_field.dart | 29 +++-- .../sign_up/widget/sign_up_subtitle.dart | 33 +++++ .../sign_up/widget/sign_up_term_agree.dart | 67 ++++++++++ .../widget/sign_up_term_agree_widget.dart | 53 -------- .../widget/sign_up_url_text_field.dart | 26 ++-- .../splash/provider/splash_provider.dart | 3 + 16 files changed, 445 insertions(+), 293 deletions(-) create mode 100644 lib/presentation/sign_up/view/sign_up_welcome_view.dart create mode 100644 lib/presentation/sign_up/widget/sign_up_button.dart delete mode 100644 lib/presentation/sign_up/widget/sign_up_check_nick_button.dart delete mode 100644 lib/presentation/sign_up/widget/sign_up_check_url_button.dart create mode 100644 lib/presentation/sign_up/widget/sign_up_subtitle.dart create mode 100644 lib/presentation/sign_up/widget/sign_up_term_agree.dart delete mode 100644 lib/presentation/sign_up/widget/sign_up_term_agree_widget.dart diff --git a/lib/presentation/sign_up/provider/sign_up_provider.dart b/lib/presentation/sign_up/provider/sign_up_provider.dart index 06a3195b..d9f992da 100644 --- a/lib/presentation/sign_up/provider/sign_up_provider.dart +++ b/lib/presentation/sign_up/provider/sign_up_provider.dart @@ -1,19 +1,19 @@ -import 'package:grimity/app/base/result.dart'; import 'package:grimity/app/enum/grimity.enum.dart'; import 'package:grimity/app/extension/string_extension.dart'; +import 'package:grimity/app/service/toast_service.dart'; import 'package:grimity/app/util/device_info_util.dart'; import 'package:grimity/app/util/validator_util.dart'; import 'package:grimity/domain/dto/auth_request_params.dart'; -import 'package:grimity/domain/entity/user.dart'; import 'package:grimity/domain/usecase/auth_usecases.dart'; import 'package:grimity/domain/usecase/users_usecase.dart'; import 'package:grimity/presentation/common/provider/auth_credential_provider.dart'; +import 'package:grimity/presentation/common/provider/user_auth_provider.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'sign_up_provider.g.dart'; -enum SignUpViewState { nickname, url } +enum SignUpViewState { nickname, url, welcome } /// 회원가입 상태를 관리하는 프로바이더 @riverpod @@ -51,7 +51,7 @@ class SignUp extends _$SignUp { state = state.copyWith( isNicknameChecking: false, signUpViewState: SignUpViewState.url, - nicknameState: GrimityTextFieldState.normal, + nicknameState: GrimityTextFieldState.success, ); }, onFailure: (error) { @@ -82,7 +82,10 @@ class SignUp extends _$SignUp { return; } - state = state.copyWith(isUrlChecking: false, urlState: GrimityTextFieldState.normal); + state = state.copyWith( + isUrlChecking: false, + urlState: GrimityTextFieldState.success, + ); } catch (e) { state = state.copyWith( isUrlChecking: false, @@ -93,14 +96,14 @@ class SignUp extends _$SignUp { } /// 회원가입 진행 - Future> signUp(WidgetRef ref) async { + Future signUp(WidgetRef ref) async { if (!isInformationValid()) { - return Result.failure(Exception('회원가입 정보가 유효하지 않습니다.')); + return ToastService.showFailure('회원가입 정보가 유효하지 않아요'); } final authCredential = ref.read(authCredentialProvider); if (authCredential.provider == null) { - return Result.failure(Exception('OAuth 인증 정보가 없습니다.')); + return ToastService.showFailure('OAuth 인증 정보가 없어요'); } final param = RegisterRequestParam( @@ -111,7 +114,19 @@ class SignUp extends _$SignUp { url: state.url, ); - return await completeRegisterUseCase.execute(param, this.ref); + final result = await completeRegisterUseCase.execute(param, this.ref); + + result.fold( + onSuccess: (user) { + // 회원 가입 이후 UserAuthProvider User 갱신 + ref.read(userAuthProvider.notifier).setUser(user); + + state = state.copyWith(signUpViewState: SignUpViewState.welcome); + }, + onFailure: (error) { + ToastService.showFailure('회원가입에 실패했어요'); + }, + ); } /// 유효성 검사 @@ -120,6 +135,10 @@ class SignUp extends _$SignUp { ValidatorUtil.isAvailableUrl(state.url) && state.isTermsAgreed; } + + void setSignUpState(SignUpViewState newViewState) { + state = state.copyWith(signUpViewState: newViewState); + } } /// 회원가입 상태 클래스 diff --git a/lib/presentation/sign_up/sign_up_page.dart b/lib/presentation/sign_up/sign_up_page.dart index af6ea673..14275e73 100644 --- a/lib/presentation/sign_up/sign_up_page.dart +++ b/lib/presentation/sign_up/sign_up_page.dart @@ -1,9 +1,7 @@ import 'package:flutter/material.dart'; import 'package:grimity/presentation/sign_up/view/sign_up_view.dart'; -import 'package:grimity/presentation/sign_up/widget/sign_up_check_nick_button.dart'; -import 'package:grimity/presentation/sign_up/widget/sign_up_check_url_button.dart'; import 'package:grimity/presentation/sign_up/widget/sign_up_nickname_text_field.dart'; -import 'package:grimity/presentation/sign_up/widget/sign_up_term_agree_widget.dart'; +import 'package:grimity/presentation/sign_up/widget/sign_up_term_agree.dart'; import 'package:grimity/presentation/sign_up/widget/sign_up_url_text_field.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -15,9 +13,7 @@ class SignUpPage extends ConsumerWidget { return const SignUpView( nicknameTextField: SignUpNicknameTextField(), urlTextField: SignUpUrlTextField(), - termAgreeWidget: SignUpTermAgreeWidget(), - checkNicknameButton: SignUpCheckNickButton(), - registerButton: SignUpCheckUrlButton(), + termAgreeWidget: SignUpTermAgree(), ); } } diff --git a/lib/presentation/sign_up/view/sign_up_nickname_view.dart b/lib/presentation/sign_up/view/sign_up_nickname_view.dart index 5adf5fc5..9487578f 100644 --- a/lib/presentation/sign_up/view/sign_up_nickname_view.dart +++ b/lib/presentation/sign_up/view/sign_up_nickname_view.dart @@ -1,50 +1,45 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:gap/gap.dart'; -import 'package:grimity/app/config/app_color.dart'; -import 'package:grimity/app/config/app_typeface.dart'; +import 'package:gds/gds.dart'; +import 'package:grimity/presentation/sign_up/widget/sign_up_subtitle.dart'; class SignUpNicknameView extends ConsumerWidget { const SignUpNicknameView({ super.key, required this.nicknameTextField, required this.termAgreeWidget, - required this.checkNicknameButton, }); final Widget nicknameTextField; final Widget termAgreeWidget; - final Widget checkNicknameButton; @override Widget build(BuildContext context, WidgetRef ref) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _SignUpDescription(), - const Gap(24), - nicknameTextField, - const Gap(15), - termAgreeWidget, - const Spacer(), - checkNicknameButton, - ], - ); - } -} - -class _SignUpDescription extends StatelessWidget { - const _SignUpDescription(); - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('활동명을 정해주세요', style: AppTypeface.title3), - Gap(8), - Text('가입 후 활동명 변경이 가능해요', style: AppTypeface.label2.copyWith(color: AppColor.gray700)), - ], + return SingleChildScrollView( + padding: EdgeInsets.only( + top: context.isMobile ? GdsSpacing.spacing40 : GdsSpacing.spacing48, + left: context.isMobile ? GdsSpacing.spacing16 : GdsSpacing.spacing40, + right: context.isMobile ? GdsSpacing.spacing16 : GdsSpacing.spacing40, + bottom: context.isMobile ? GdsSpacing.spacing16 : GdsSpacing.spacing40, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: GdsSpacing.spacing40, + children: [ + SignUpSubtitle( + subtitle: '활동명을 정해주세요', + description: '작품과 함께 그리미티에서 기억될 이름이에요', + ), + Column( + mainAxisSize: MainAxisSize.min, + spacing: GdsSpacing.spacing12, + children: [ + nicknameTextField, + termAgreeWidget, + ], + ), + ], + ), ); } } diff --git a/lib/presentation/sign_up/view/sign_up_url_view.dart b/lib/presentation/sign_up/view/sign_up_url_view.dart index 0249c427..ff24e6a2 100644 --- a/lib/presentation/sign_up/view/sign_up_url_view.dart +++ b/lib/presentation/sign_up/view/sign_up_url_view.dart @@ -1,40 +1,45 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; -import 'package:grimity/app/config/app_color.dart'; -import 'package:grimity/app/config/app_typeface.dart'; +import 'package:gds/gds.dart'; +import 'package:grimity/presentation/sign_up/widget/sign_up_subtitle.dart'; class SignUpUrlView extends StatelessWidget { - const SignUpUrlView({super.key, required this.nickname, required this.urlTextField, required this.registerButton}); + const SignUpUrlView({ + super.key, + required this.nickname, + required this.urlTextField, + }); final String nickname; final Widget urlTextField; - final Widget registerButton; @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [_SignUpDescription(nickname: nickname), const Gap(4), urlTextField, const Spacer(), registerButton], - ); - } -} - -class _SignUpDescription extends StatelessWidget { - const _SignUpDescription({required this.nickname}); + final colors = context.gdsColors; - final String nickname; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('$nickname님,\n프로필 URL을 설정해주세요', style: AppTypeface.title3), - Gap(8), - Text('단 하나뿐인 프로필 주소로 사용되어요', style: AppTypeface.label2.copyWith(color: AppColor.gray700)), - Gap(24), - Text('www.grimity.com/', style: AppTypeface.label2.copyWith(color: AppColor.gray600)), - ], + return SingleChildScrollView( + padding: EdgeInsets.only( + top: context.isMobile ? GdsSpacing.spacing40 : GdsSpacing.spacing48, + left: context.isMobile ? GdsSpacing.spacing16 : GdsSpacing.spacing40, + right: context.isMobile ? GdsSpacing.spacing16 : GdsSpacing.spacing40, + bottom: context.isMobile ? GdsSpacing.spacing16 : GdsSpacing.spacing40, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SignUpSubtitle( + subtitle: '프로필 URL을 정해주세요', + description: '단 하나뿐인 프로필 주소로 사용돼요', + ), + Gap(GdsSpacing.spacing40), + Text( + 'www.grimity.com/', + style: GdsTypography.title2.copyWith(color: colors.text.grayBold), + ), + Gap(GdsSpacing.spacing12), + urlTextField, + ], + ), ); } } diff --git a/lib/presentation/sign_up/view/sign_up_view.dart b/lib/presentation/sign_up/view/sign_up_view.dart index 45ed2ded..bd6ca6eb 100644 --- a/lib/presentation/sign_up/view/sign_up_view.dart +++ b/lib/presentation/sign_up/view/sign_up_view.dart @@ -1,75 +1,84 @@ -import 'package:animations/animations.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:gap/gap.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gds/gds.dart'; +import 'package:go_router/go_router.dart'; +import 'package:grimity/app/config/app_router.dart'; import 'package:grimity/presentation/sign_up/provider/sign_up_provider.dart'; import 'package:grimity/presentation/sign_up/view/sign_up_nickname_view.dart'; import 'package:grimity/presentation/sign_up/view/sign_up_url_view.dart'; +import 'package:grimity/presentation/sign_up/view/sign_up_welcome_view.dart'; import 'package:grimity/presentation/sign_up/widget/sign_up_app_bar.dart'; +import 'package:grimity/presentation/sign_up/widget/sign_up_button.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; -class SignUpView extends ConsumerWidget { +class SignUpView extends HookConsumerWidget { const SignUpView({ super.key, required this.nicknameTextField, required this.urlTextField, required this.termAgreeWidget, - required this.checkNicknameButton, - required this.registerButton, }); final Widget nicknameTextField; final Widget urlTextField; final Widget termAgreeWidget; - final Widget checkNicknameButton; - final Widget registerButton; @override Widget build(BuildContext context, WidgetRef ref) { - final signUpRef = ref.watch(signUpProvider); + final pageController = usePageController(); + final signUp = ref.watch(signUpProvider.notifier); + final state = ref.watch(signUpProvider); - return Scaffold( + WidgetsBinding.instance.addPostFrameCallback((_) { + final pageIndex = switch (state.signUpViewState) { + SignUpViewState.nickname => 0, + SignUpViewState.url => 1, + SignUpViewState.welcome => 2, + }; + + if (pageController.page?.toInt() != pageIndex) { + pageController.animateToPage(pageIndex, duration: Duration(milliseconds: 300), curve: Curves.ease); + } + }); + + return GdsScaffold( appBar: SignUpAppBar(), - body: Padding( - padding: EdgeInsets.symmetric(horizontal: 16), + body: PopScope( + canPop: state.signUpViewState == SignUpViewState.nickname, + onPopInvoked: (didPop) { + switch (state.signUpViewState) { + case SignUpViewState.nickname: + return context.pop(); + case SignUpViewState.url: + signUp.setSignUpState(SignUpViewState.nickname); + case SignUpViewState.welcome: + return HomeRoute().go(context); + } + }, child: Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Gap(40), Expanded( - child: PageTransitionSwitcher( - duration: const Duration(milliseconds: 200), - reverse: signUpRef.signUpViewState == SignUpViewState.nickname, - transitionBuilder: ( - Widget child, - Animation primaryAnimation, - Animation secondaryAnimation, - ) { - return SharedAxisTransition( - animation: primaryAnimation, - secondaryAnimation: secondaryAnimation, - transitionType: SharedAxisTransitionType.vertical, - fillColor: Colors.transparent, - child: child, - ); - }, - child: SafeArea( - child: - signUpRef.signUpViewState == SignUpViewState.nickname - ? SignUpNicknameView( - key: const ValueKey('nickname'), - nicknameTextField: nicknameTextField, - termAgreeWidget: termAgreeWidget, - checkNicknameButton: checkNicknameButton, - ) - : SignUpUrlView( - key: const ValueKey('url'), - nickname: signUpRef.nickname, - urlTextField: urlTextField, - registerButton: registerButton, - ), - ), + child: PageView( + physics: NeverScrollableScrollPhysics(), + controller: pageController, + children: [ + SignUpNicknameView( + key: const ValueKey('nickname'), + nicknameTextField: nicknameTextField, + termAgreeWidget: termAgreeWidget, + ), + SignUpUrlView( + key: const ValueKey('url'), + nickname: state.nickname, + urlTextField: urlTextField, + ), + SignUpWelcomeView( + key: const ValueKey('welcome'), + ), + ], ), ), + SignUpButton(), ], ), ), diff --git a/lib/presentation/sign_up/view/sign_up_welcome_view.dart b/lib/presentation/sign_up/view/sign_up_welcome_view.dart new file mode 100644 index 00000000..981afde6 --- /dev/null +++ b/lib/presentation/sign_up/view/sign_up_welcome_view.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:gap/gap.dart'; +import 'package:gds/gds.dart'; + +class SignUpWelcomeView extends ConsumerWidget { + const SignUpWelcomeView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final colors = context.gdsColors; + + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + GdsIcon.uploadSuccess.build( + width: 120, + height: 120, + ), + Gap(GdsSpacing.spacing24), + Text( + '가입을 축하드려요!', + style: + context.isMobile + ? GdsTypography.title2.copyWith(color: colors.text.grayBold) + : GdsTypography.title1.copyWith(color: colors.text.grayBold), + ), + Gap(GdsSpacing.spacing12), + Text( + '마음껏 그림을 공유하고\n자유롭게 이야기를 나눠보세요', + textAlign: TextAlign.center, + style: GdsTypography.body1R.copyWith(color: colors.text.grayBold), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/sign_up/widget/sign_up_app_bar.dart b/lib/presentation/sign_up/widget/sign_up_app_bar.dart index cdecb09e..93a4fa6d 100644 --- a/lib/presentation/sign_up/widget/sign_up_app_bar.dart +++ b/lib/presentation/sign_up/widget/sign_up_app_bar.dart @@ -1,30 +1,19 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:gds/gds.dart'; import 'package:go_router/go_router.dart'; -import 'package:grimity/app/config/app_color.dart'; -import 'package:grimity/app/config/app_theme.dart'; -import 'package:grimity/gen/assets.gen.dart'; -import 'package:grimity/presentation/common/widget/grimity_gesture.dart'; -class SignUpAppBar extends StatelessWidget implements PreferredSizeWidget { +class SignUpAppBar extends StatelessWidget { const SignUpAppBar({super.key}); @override Widget build(BuildContext context) { - return AppBar( - toolbarHeight: AppTheme.kToolbarHeight.height, - leading: Center( - child: GrimityGesture( - onTap: () => context.pop(), - child: Assets.icons.icon.close.svg(width: 24, height: 24), - ), - ), - bottom: const PreferredSize( - preferredSize: Size.fromHeight(1), - child: Divider(height: 1, color: AppColor.gray300), - ), + return GdsTopNavigation.iconButton( + title: '', + onBack: context.pop, + icons: [], + onIconTap: [], + showTitle: false, + showIcons: false, ); } - - @override - Size get preferredSize => AppTheme.kToolbarHeight; } diff --git a/lib/presentation/sign_up/widget/sign_up_button.dart b/lib/presentation/sign_up/widget/sign_up_button.dart new file mode 100644 index 00000000..02873a94 --- /dev/null +++ b/lib/presentation/sign_up/widget/sign_up_button.dart @@ -0,0 +1,118 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:gds/gds.dart'; +import 'package:grimity/app/config/app_router.dart'; +import 'package:grimity/app/enum/grimity.enum.dart'; +import 'package:grimity/app/util/validator_util.dart'; +import 'package:grimity/presentation/sign_up/provider/sign_up_provider.dart'; + +class SignUpButton extends ConsumerWidget { + const SignUpButton({super.key}); + + static const mobilePadding = EdgeInsets.only( + left: GdsSpacing.spacing16, + right: GdsSpacing.spacing16, + bottom: GdsSpacing.spacing24, + ); + + static const tabletPadding = EdgeInsets.only( + left: GdsSpacing.spacing40, + right: GdsSpacing.spacing40, + bottom: GdsSpacing.spacing40, + ); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final SignUpState state = ref.watch(signUpProvider); + final bool enabled = getEnabled(ref, state.signUpViewState); + final bool loading = getLoading(ref, state.signUpViewState); + + return Padding( + padding: context.isMobile ? mobilePadding : tabletPadding, + child: GdsGesture( + onTap: + enabled + ? () async { + if (state.signUpViewState == SignUpViewState.nickname) { + await ref.read(signUpProvider.notifier).checkNicknameDuplicate(); + } + + if (state.signUpViewState == SignUpViewState.url) { + // URL 유효성 검증 + await ref.read(signUpProvider.notifier).checkUrlValidity(); + + // 회원가입 진행 + if (context.mounted) { + await ref.read(signUpProvider.notifier).signUp(ref); + } + } + + if (state.signUpViewState == SignUpViewState.welcome) { + if (context.mounted) HomeRoute().go(context); + } + } + : null, + child: IgnorePointer( + child: buildRawButton(context, state.signUpViewState, enabled, loading), + ), + ), + ); + } + + bool getEnabled(WidgetRef ref, SignUpViewState viewState) { + if (viewState == SignUpViewState.nickname) { + return ref.watch(signUpProvider).nickname.length >= 2 && + ref.watch(signUpProvider).isTermsAgreed && + ref.watch(signUpProvider).nicknameState != GrimityTextFieldState.error; + } else if (viewState == SignUpViewState.url) { + return ValidatorUtil.isAvailableUrl(ref.watch(signUpProvider).url) && + ref.watch(signUpProvider).urlState != GrimityTextFieldState.error; + } + + return true; + } + + bool getLoading(WidgetRef ref, SignUpViewState viewState) { + if (viewState == SignUpViewState.nickname) { + return ref.watch(signUpProvider).isNicknameChecking; + } else if (viewState == SignUpViewState.url) { + return ref.watch(signUpProvider).isUrlChecking; + } + + return false; + } + + Widget buildRawButton( + BuildContext context, + SignUpViewState viewState, + bool enabled, + bool loading, + ) { + if (viewState == SignUpViewState.nickname) { + return GdsSolidButton( + size: GdsSolidButtonSize.large, + text: '다음', + enabled: enabled, + loading: loading, + expanded: true, + onPressed: () {}, + ); + } else if (viewState == SignUpViewState.url) { + return GdsSolidButton( + size: GdsSolidButtonSize.large, + text: '완료', + enabled: enabled, + loading: loading, + expanded: true, + onPressed: () {}, + ); + } + + return GdsSolidButton( + size: GdsSolidButtonSize.large, + text: '그리미티 시작하기', + expanded: true, + onPressed: () {}, + ); + } +} diff --git a/lib/presentation/sign_up/widget/sign_up_check_nick_button.dart b/lib/presentation/sign_up/widget/sign_up_check_nick_button.dart deleted file mode 100644 index 8bfd1c73..00000000 --- a/lib/presentation/sign_up/widget/sign_up_check_nick_button.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:grimity/app/enum/grimity.enum.dart'; -import 'package:grimity/presentation/common/widget/button/grimity_button.dart'; -import 'package:grimity/presentation/sign_up/provider/sign_up_provider.dart'; - -class SignUpCheckNickButton extends ConsumerWidget { - const SignUpCheckNickButton({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isEnabled = - ref.watch(signUpProvider).nickname.length >= 2 && - ref.watch(signUpProvider).isTermsAgreed && - ref.watch(signUpProvider).nicknameState != GrimityTextFieldState.error; - - return Padding( - padding: EdgeInsets.only(bottom: 24), - child: GrimityButton.large( - text: '다음', - onTap: () async { - await ref.read(signUpProvider.notifier).checkNicknameDuplicate(); - }, - status: isEnabled ? ButtonStatus.on : ButtonStatus.off, - ), - ); - } -} diff --git a/lib/presentation/sign_up/widget/sign_up_check_url_button.dart b/lib/presentation/sign_up/widget/sign_up_check_url_button.dart deleted file mode 100644 index 13587566..00000000 --- a/lib/presentation/sign_up/widget/sign_up_check_url_button.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:grimity/app/config/app_router.dart'; -import 'package:grimity/app/enum/grimity.enum.dart'; -import 'package:grimity/app/service/toast_service.dart'; -import 'package:grimity/app/util/validator_util.dart'; -import 'package:grimity/presentation/common/provider/user_auth_provider.dart'; -import 'package:grimity/presentation/common/widget/button/grimity_button.dart'; -import 'package:grimity/presentation/sign_up/provider/sign_up_provider.dart'; - -class SignUpCheckUrlButton extends ConsumerWidget { - const SignUpCheckUrlButton({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isEnabled = - ValidatorUtil.isAvailableUrl(ref.watch(signUpProvider).url) && - ref.watch(signUpProvider).urlState != GrimityTextFieldState.error; - - return Padding( - padding: EdgeInsets.only(bottom: 24), - child: GrimityButton.large( - text: '시작하기', - onTap: () async { - // URL 유효성 검증 - await ref.read(signUpProvider.notifier).checkUrlValidity(); - - // 회원가입 진행 - final result = await ref.read(signUpProvider.notifier).signUp(ref); - result.fold( - onSuccess: (user) { - // 회원 가입 이후 UserAuthProvider User 갱신 - ref.read(userAuthProvider.notifier).setUser(user); - - if (!context.mounted) return; - HomeRoute().go(context); - }, - onFailure: (error) { - ToastService.showFailure('회원가입에 실패했어요.'); - }, - ); - }, - status: isEnabled ? ButtonStatus.on : ButtonStatus.off, - ), - ); - } -} diff --git a/lib/presentation/sign_up/widget/sign_up_nickname_text_field.dart b/lib/presentation/sign_up/widget/sign_up_nickname_text_field.dart index 2bec3a51..1c8059b1 100644 --- a/lib/presentation/sign_up/widget/sign_up_nickname_text_field.dart +++ b/lib/presentation/sign_up/widget/sign_up_nickname_text_field.dart @@ -1,6 +1,7 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:grimity/presentation/common/widget/text_field/grimity_underline_text_field.dart'; +import 'package:gds/gds.dart'; +import 'package:grimity/app/enum/grimity.enum.dart'; import 'package:grimity/presentation/sign_up/provider/sign_up_provider.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -10,16 +11,22 @@ class SignUpNicknameTextField extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final textController = useTextEditingController(); + final state = ref.watch(signUpProvider); + final errorMessage = state.nicknameCheckMessage; + final nicknameStatus = state.nicknameState; - return GrimityUnderlineTextField.normal( - controller: textController, - state: ref.watch(signUpProvider).nicknameState, - maxLength: 12, - hintText: '프로필에 노출될 닉네임을 입력해주세요', - errorText: ref.watch(signUpProvider).nicknameCheckMessage, - onChanged: (value) { - ref.read(signUpProvider.notifier).updateNickname(value); - }, + return GdsInput.custom( + helperText: nicknameStatus == GrimityTextFieldState.error ? errorMessage : null, + error: true, + child: GdsTextField.count( + placeholder: '닉네임을 입력해주세요', + controller: textController, + maxLength: 12, + error: nicknameStatus == GrimityTextFieldState.error, + success: nicknameStatus == GrimityTextFieldState.success, + onChanged: ref.read(signUpProvider.notifier).updateNickname, + onEditingComplete: ref.read(signUpProvider.notifier).checkNicknameDuplicate, + ), ); } } diff --git a/lib/presentation/sign_up/widget/sign_up_subtitle.dart b/lib/presentation/sign_up/widget/sign_up_subtitle.dart new file mode 100644 index 00000000..4fff0ba3 --- /dev/null +++ b/lib/presentation/sign_up/widget/sign_up_subtitle.dart @@ -0,0 +1,33 @@ +import 'package:flutter/widgets.dart'; +import 'package:gds/gds.dart'; + +class SignUpSubtitle extends StatelessWidget { + const SignUpSubtitle({super.key, required this.subtitle, required this.description}); + + final String subtitle; + final String description; + + @override + Widget build(BuildContext context) { + final colors = context.gdsColors; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + spacing: GdsSpacing.spacing12, + children: [ + Text( + subtitle, + style: + context.isMobile + ? GdsTypography.title2.copyWith(color: colors.text.grayBold) + : GdsTypography.title1.copyWith(color: colors.text.grayBold), + ), + Text( + description, + style: GdsTypography.body1R.copyWith(color: colors.text.grayBold), + ), + ], + ); + } +} diff --git a/lib/presentation/sign_up/widget/sign_up_term_agree.dart b/lib/presentation/sign_up/widget/sign_up_term_agree.dart new file mode 100644 index 00000000..dd0e8849 --- /dev/null +++ b/lib/presentation/sign_up/widget/sign_up_term_agree.dart @@ -0,0 +1,67 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:gds/gds.dart'; +import 'package:grimity/app/config/app_const.dart'; +import 'package:grimity/presentation/sign_up/provider/sign_up_provider.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class SignUpTermAgree extends ConsumerWidget { + const SignUpTermAgree({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isTermsAgreed = ref.watch(signUpProvider).isTermsAgreed; + final colors = context.gdsColors; + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: GdsSpacing.spacing6, + children: [ + GdsCheckbox( + isChecked: isTermsAgreed, + onTap: () { + ref.read(signUpProvider.notifier).updateTermsAgreement(!isTermsAgreed); + }, + ), + Expanded( + child: Wrap( + children: [ + GdsGesture( + onTap: () async { + await launchUrl(Uri.parse(AppConst.serviceTermsUrl)); + }, + child: Text( + '서비스이용약관', + style: GdsTypography.body1SB.copyWith( + color: colors.text.grayBold, + decoration: TextDecoration.underline, + ), + ), + ), + Text( + '과 ', + style: GdsTypography.body1R.copyWith(color: colors.text.grayBold), + ), + GdsGesture( + onTap: () async { + await launchUrl(Uri.parse(AppConst.privacyPolicyUrl)); + }, + child: Text( + '개인정보처리방침', + style: GdsTypography.body1SB.copyWith( + color: colors.text.grayBold, + decoration: TextDecoration.underline, + ), + ), + ), + Text( + '에 동의합니다.', + style: GdsTypography.body1R.copyWith(color: colors.text.grayBold), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/presentation/sign_up/widget/sign_up_term_agree_widget.dart b/lib/presentation/sign_up/widget/sign_up_term_agree_widget.dart deleted file mode 100644 index 088f7569..00000000 --- a/lib/presentation/sign_up/widget/sign_up_term_agree_widget.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:gap/gap.dart'; -import 'package:grimity/app/config/app_color.dart'; -import 'package:grimity/app/config/app_const.dart'; -import 'package:grimity/app/config/app_typeface.dart'; -import 'package:grimity/presentation/common/widget/grimity_gesture.dart'; -import 'package:grimity/presentation/common/widget/system/check/grimity_check_box.dart'; -import 'package:grimity/presentation/sign_up/provider/sign_up_provider.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class SignUpTermAgreeWidget extends ConsumerWidget { - const SignUpTermAgreeWidget({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Row( - children: [ - GrimityCheckBox( - isChecked: ref.watch(signUpProvider).isTermsAgreed, - onChanged: (value) { - ref.read(signUpProvider.notifier).updateTermsAgreement(value ?? false); - }, - ), - const Gap(8), - Row( - children: [ - GrimityGesture( - onTap: () async { - await launchUrl(Uri.parse(AppConst.serviceTermsUrl)); - }, - child: Text( - '서비스이용약관', - style: AppTypeface.caption1.copyWith(color: AppColor.gray700, decoration: TextDecoration.underline), - ), - ), - Text(' 과 ', style: AppTypeface.caption1.copyWith(color: AppColor.gray600)), - GrimityGesture( - onTap: () async { - await launchUrl(Uri.parse(AppConst.privacyPolicyUrl)); - }, - child: Text( - '개인정보처리방침', - style: AppTypeface.caption1.copyWith(color: AppColor.gray700, decoration: TextDecoration.underline), - ), - ), - Text(' 에 동의합니다.', style: AppTypeface.caption1.copyWith(color: AppColor.gray600)), - ], - ), - ], - ); - } -} diff --git a/lib/presentation/sign_up/widget/sign_up_url_text_field.dart b/lib/presentation/sign_up/widget/sign_up_url_text_field.dart index bd3fc13c..2005903d 100644 --- a/lib/presentation/sign_up/widget/sign_up_url_text_field.dart +++ b/lib/presentation/sign_up/widget/sign_up_url_text_field.dart @@ -1,6 +1,7 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:grimity/presentation/common/widget/text_field/grimity_underline_text_field.dart'; +import 'package:gds/gds.dart'; +import 'package:grimity/app/enum/grimity.enum.dart'; import 'package:grimity/presentation/sign_up/provider/sign_up_provider.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -11,6 +12,9 @@ class SignUpUrlTextField extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final textController = useTextEditingController(); final focusNode = useFocusNode(); + final state = ref.watch(signUpProvider); + final urlStatus = state.urlState; + final errorMessage = state.urlCheckMessage; // 첫 Build 시 포커스 요청 useEffect(() { @@ -18,19 +22,15 @@ class SignUpUrlTextField extends HookConsumerWidget { return null; }, []); - return GrimityUnderlineTextField.normal( + return GdsInput( + placeholder: '숫자, 영문(소문자), 언더바(_)', controller: textController, - state: ref.watch(signUpProvider).urlState, - autoFocus: true, focusNode: focusNode, - hintText: '숫자, 영문(소문자), 언더바(_)', - errorText: ref.watch(signUpProvider).urlCheckMessage, - onChanged: (value) { - ref.read(signUpProvider.notifier).updateUrl(value); - }, - onSubmitted: (value) { - ref.read(signUpProvider.notifier).updateUrl(value); - }, + error: urlStatus == GrimityTextFieldState.error, + success: urlStatus == GrimityTextFieldState.success, + helperText: urlStatus == GrimityTextFieldState.error ? errorMessage : null, + onChanged: ref.read(signUpProvider.notifier).updateUrl, + onEditingComplete: ref.read(signUpProvider.notifier).checkUrlValidity, ); } } diff --git a/lib/presentation/splash/provider/splash_provider.dart b/lib/presentation/splash/provider/splash_provider.dart index d8d4f0dd..285791ee 100644 --- a/lib/presentation/splash/provider/splash_provider.dart +++ b/lib/presentation/splash/provider/splash_provider.dart @@ -26,6 +26,8 @@ class Splash extends _$Splash { // 유저 정보 조회 시도 // 조회 실패 시 로그인 화면으로 이동 await ref.read(userAuthProvider.notifier).getUser(); + + // 기존 사용자 정보가 없으면 로그인 페이지로 이동 if (ref.read(userAuthProvider) == null) { if (!ref.context.mounted) return false; @@ -38,6 +40,7 @@ class Splash extends _$Splash { // 유저 정보 로그인 시도 후 구독 여부 조회 ref.read(userSubscribeProvider.notifier).getSubscription(); + HomeRoute().go(ref.context); /// [ColdStart] 딥링크 처리 From fc16e58fa27428bc93ebead82b80bb93657adefe Mon Sep 17 00:00:00 2001 From: Dev Ttangkong Date: Sun, 12 Jul 2026 15:00:31 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85?= =?UTF-8?q?=20=EB=8B=89=EB=84=A4=EC=9E=84=20=EC=9E=85=EB=A0=A5=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=97=90=EB=9F=AC=20=EC=83=81=ED=83=9C=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sign_up/widget/sign_up_button.dart | 23 ++++++++++--------- .../widget/sign_up_nickname_text_field.dart | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/presentation/sign_up/widget/sign_up_button.dart b/lib/presentation/sign_up/widget/sign_up_button.dart index 02873a94..5cfc3fa5 100644 --- a/lib/presentation/sign_up/widget/sign_up_button.dart +++ b/lib/presentation/sign_up/widget/sign_up_button.dart @@ -24,8 +24,8 @@ class SignUpButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final SignUpState state = ref.watch(signUpProvider); - final bool enabled = getEnabled(ref, state.signUpViewState); - final bool loading = getLoading(ref, state.signUpViewState); + final bool enabled = getEnabled(state); + final bool loading = getLoading(state); return Padding( padding: context.isMobile ? mobilePadding : tabletPadding, @@ -59,24 +59,25 @@ class SignUpButton extends ConsumerWidget { ); } - bool getEnabled(WidgetRef ref, SignUpViewState viewState) { + bool getEnabled(SignUpState state) { + final viewState = state.signUpViewState; + if (viewState == SignUpViewState.nickname) { - return ref.watch(signUpProvider).nickname.length >= 2 && - ref.watch(signUpProvider).isTermsAgreed && - ref.watch(signUpProvider).nicknameState != GrimityTextFieldState.error; + return state.nickname.length >= 2 && state.isTermsAgreed && state.nicknameState != GrimityTextFieldState.error; } else if (viewState == SignUpViewState.url) { - return ValidatorUtil.isAvailableUrl(ref.watch(signUpProvider).url) && - ref.watch(signUpProvider).urlState != GrimityTextFieldState.error; + return ValidatorUtil.isAvailableUrl(state.url) && state.urlState != GrimityTextFieldState.error; } return true; } - bool getLoading(WidgetRef ref, SignUpViewState viewState) { + bool getLoading(SignUpState state) { + final viewState = state.signUpViewState; + if (viewState == SignUpViewState.nickname) { - return ref.watch(signUpProvider).isNicknameChecking; + return state.isNicknameChecking; } else if (viewState == SignUpViewState.url) { - return ref.watch(signUpProvider).isUrlChecking; + return state.isUrlChecking; } return false; diff --git a/lib/presentation/sign_up/widget/sign_up_nickname_text_field.dart b/lib/presentation/sign_up/widget/sign_up_nickname_text_field.dart index 1c8059b1..9fd51b44 100644 --- a/lib/presentation/sign_up/widget/sign_up_nickname_text_field.dart +++ b/lib/presentation/sign_up/widget/sign_up_nickname_text_field.dart @@ -17,7 +17,7 @@ class SignUpNicknameTextField extends HookConsumerWidget { return GdsInput.custom( helperText: nicknameStatus == GrimityTextFieldState.error ? errorMessage : null, - error: true, + error: nicknameStatus == GrimityTextFieldState.error, child: GdsTextField.count( placeholder: '닉네임을 입력해주세요', controller: textController,