diff --git a/lib/services/auth/auth_service.dart b/lib/services/auth/auth_service.dart new file mode 100644 index 0000000..8a9cc9d --- /dev/null +++ b/lib/services/auth/auth_service.dart @@ -0,0 +1,38 @@ +// auth_service.dart + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:get/get.dart'; +import 'package:google_sign_in/google_sign_in.dart'; + +class AuthService { + final FirebaseAuth _auth = FirebaseAuth.instance; + + Future signInWithGoogle() async { + try { + // 구글 로그인 + final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn(); + + // 구글 로그인 인증 정보 + final GoogleSignInAuthentication? googleAuth = + await googleUser?.authentication; + + // 구글 로그인 자격 증명 + final credential = GoogleAuthProvider.credential( + accessToken: googleAuth?.accessToken, + idToken: googleAuth?.idToken, + ); + // 구글 로그인 성공시 UserCredential 반환 + await _auth.signInWithCredential(credential); + + // 홈화면으로 이동 + Get.offAllNamed('/'); + } catch (error) { + // 에러 발생시 에러 메시지 출력 + Get.snackbar( + "구글 로그인 실패", + "구글 로그인에 실패했습니다. 다시 시도해주세요.", + snackPosition: SnackPosition.TOP, + ); + } + } +} diff --git a/lib/utilities/style/color_styles.dart b/lib/utilities/style/color_styles.dart index 34027e7..e46d20a 100644 --- a/lib/utilities/style/color_styles.dart +++ b/lib/utilities/style/color_styles.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -class ColorStyles { +class ColorSystem { static const Color main = Color(0xFF1DA1FA); static const Color sub1 = Color(0xFF5588FD); static const Color sub2 = Color(0xFFC1D2FF); diff --git a/lib/utilities/validators/auth_validators.dart b/lib/utilities/validators/auth_validators.dart new file mode 100644 index 0000000..6e4315e --- /dev/null +++ b/lib/utilities/validators/auth_validators.dart @@ -0,0 +1,31 @@ +class AuthValidators { + // Email Validator + static String? emailValidator(String? value) { + if (value == null || value.isEmpty) { + return '이메일 주소를 입력해주세요'; + } + // Email 정규식 + final RegExp emailRegex = RegExp( + r'^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$', + ); + + if (!emailRegex.hasMatch(value)) { + return '올바른 이메일 주소를 입력해주세요'; + } + return null; + } + + // Password Validator + // 대충.. 10자리 이상, 문자와 숫자가 섞여있어야 함 + static String? passwordValidator(String? value) { + if (value == null || value.isEmpty) { + return '비밀번호를 입력해주세요'; + } else if (value.length < 10) { + return '비밀번호는 10자리 이상이어야 합니다'; + } else if (!RegExp(r'^(?=.*?[a-zA-Z])(?=.*?[0-9]).{10,}$') + .hasMatch(value)) { + return '비밀번호는 문자와 숫자가 섞여있어야 합니다'; + } + return null; + } +} diff --git a/lib/viewModels/auth/email_login_viewmodel.dart b/lib/viewModels/auth/email_login_viewmodel.dart new file mode 100644 index 0000000..3015105 --- /dev/null +++ b/lib/viewModels/auth/email_login_viewmodel.dart @@ -0,0 +1,39 @@ +// email_login_viewmodel.dart + +import 'package:earlips/utilities/validators/auth_validators.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class EmailLoginViewModel extends GetxController { + final formKey = GlobalKey(); + final emailController = TextEditingController(); + final passwordController = TextEditingController(); + + // Email Validator + String? emailValidator(String? value) { + return AuthValidators.emailValidator(value); + } + + // Password Validator + String? passwordValidator(String? value) { + return AuthValidators.passwordValidator(value); + } + + // 로그인 메소드 + Future signInWithEmailAndPassword() async { + if (formKey.currentState!.validate()) { + try { + await FirebaseAuth.instance.signInWithEmailAndPassword( + email: emailController.text.trim(), + password: passwordController.text.trim(), + ); + // 로그인 성공시 홈화면으로 이동 + Get.offAllNamed('/'); + } on FirebaseAuthException catch (_) { + Get.snackbar('로그인 실패', "로그인에 실패했습니다. 다시 시도해주세요.", + snackPosition: SnackPosition.TOP); + } + } + } +} diff --git a/lib/viewModels/auth/email_signup_viewmodel.dart b/lib/viewModels/auth/email_signup_viewmodel.dart new file mode 100644 index 0000000..2e2737d --- /dev/null +++ b/lib/viewModels/auth/email_signup_viewmodel.dart @@ -0,0 +1,36 @@ +import 'package:earlips/utilities/validators/auth_validators.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class EmailSignupViewModel extends GetxController { + final formKey = GlobalKey(); + final emailController = TextEditingController(); + final passwordController = TextEditingController(); + + // Email Validator + String? emailValidator(String? value) { + return AuthValidators.emailValidator(value); + } + + // Password Validator + String? passwordValidator(String? value) { + return AuthValidators.passwordValidator(value); + } + + Future registerWithEmailAndPassword() async { + if (formKey.currentState!.validate()) { + try { + await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: emailController.text.trim(), + password: passwordController.text.trim(), + ); + // 뒤로 + Get.back(); + } on FirebaseAuthException catch (_) { + Get.snackbar('회원가입 실패', '회원가입에 실패했습니다. 다시 시도해주세요.', + snackPosition: SnackPosition.TOP); + } + } + } +} diff --git a/lib/views/auth/email_login_screen.dart b/lib/views/auth/email_login_screen.dart new file mode 100644 index 0000000..cd69f59 --- /dev/null +++ b/lib/views/auth/email_login_screen.dart @@ -0,0 +1,54 @@ +import 'package:earlips/viewModels/auth/email_login_viewmodel.dart'; +import 'package:earlips/views/auth/email_signup_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class EmailLoginScreen extends StatelessWidget { + const EmailLoginScreen({super.key}); + + @override + Widget build(BuildContext context) { + // 이메일 로그인 뷰 모델을 가져옴 + final controller = Get.put(EmailLoginViewModel()); + + return Scaffold( + appBar: AppBar( + title: const Text("이메일 로그인"), + ), + body: Padding( + padding: const EdgeInsets.all(20.0), + child: Form( + // 키 값을 ViewModel에서 가져옴 + key: controller.formKey, + child: Column( + children: [ + TextFormField( + controller: controller.emailController, + decoration: const InputDecoration(hintText: '이메일'), + validator: (value) => controller.emailValidator(value), + ), + TextFormField( + controller: controller.passwordController, + obscureText: true, + decoration: const InputDecoration(hintText: '비밀번호'), + validator: (value) => controller.passwordValidator(value), + ), + const SizedBox(height: 20), + ElevatedButton( + // 로그인 메소드 호출 + onPressed: controller.signInWithEmailAndPassword, + child: const Text('Login'), + ), + const SizedBox(height: 10), + // 회원가입 화면으로 이동 + TextButton( + onPressed: () => Get.to(() => const EmailSignupScreen()), + child: const Text("회원가입 하러가기"), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/views/auth/email_signup_screen.dart b/lib/views/auth/email_signup_screen.dart new file mode 100644 index 0000000..aa2227f --- /dev/null +++ b/lib/views/auth/email_signup_screen.dart @@ -0,0 +1,49 @@ +import 'package:earlips/viewModels/auth/email_signup_viewmodel.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class EmailSignupScreen extends StatelessWidget { + const EmailSignupScreen({super.key}); + + @override + Widget build(BuildContext context) { + // 뷰 모델을 가져옴 + final controller = Get.put(EmailSignupViewModel()); + + return Scaffold( + appBar: AppBar( + title: const Text("이메일 회원가입"), + ), + body: Padding( + padding: const EdgeInsets.all(20.0), + child: Form( + // 키 값을 ViewModel에서 가져옴 + key: controller.formKey, + child: Column( + children: [ + TextFormField( + // 이메일 메소드 호출 + controller: controller.emailController, + decoration: const InputDecoration(hintText: '이메일'), + validator: (value) => controller.emailValidator(value), + ), + TextFormField( + // 비밀번호 메소드 호출 + controller: controller.passwordController, + obscureText: true, + decoration: const InputDecoration(hintText: '비밀번호'), + validator: (value) => controller.passwordValidator(value), + ), + const SizedBox(height: 20), + ElevatedButton( + // 회원가입 메소드 호출 + onPressed: controller.registerWithEmailAndPassword, + child: const Text('회원 가입'), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/views/auth/login.dart b/lib/views/auth/login_screen.dart similarity index 58% rename from lib/views/auth/login.dart rename to lib/views/auth/login_screen.dart index 39de736..f631a2d 100644 --- a/lib/views/auth/login.dart +++ b/lib/views/auth/login_screen.dart @@ -1,7 +1,8 @@ -import 'package:firebase_auth/firebase_auth.dart'; +import 'package:earlips/services/auth/auth_service.dart'; +import 'package:earlips/views/auth/email_login_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:google_sign_in/google_sign_in.dart'; +import 'package:get/get.dart'; class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @@ -11,6 +12,9 @@ class LoginScreen extends StatefulWidget { } class _LoginScreenState extends State { + // AuthService 인스턴스 생성 + final AuthService _authService = AuthService(); + @override Widget build(BuildContext context) { return Scaffold( @@ -18,14 +22,13 @@ class _LoginScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Expanded( - child: Column( + Column( mainAxisAlignment: MainAxisAlignment.center, children: [ + // --------------------- 구글 로그인 --------------------- InkWell( onTap: () { - // tap - signInWithGoogle(); + _authService.signInWithGoogle(); }, child: Card( margin: const EdgeInsets.fromLTRB(20, 20, 20, 0), @@ -47,39 +50,19 @@ class _LoginScreenState extends State { ], ), ), - ) + ), + const SizedBox(height: 20), + // --------------------- 이메일 로그인 --------------------- + ElevatedButton( + onPressed: () { + Get.to(() => const EmailLoginScreen()); + }, + child: const Text("이메일 로그인"), + ), ], - )) + ) ], )), ); } - - void signInWithGoogle() async { - // Trigger the authentication flow - final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn(); - - // Obtain the auth details from the request - final GoogleSignInAuthentication? googleAuth = - await googleUser?.authentication; - - // Create a new credential - final credential = GoogleAuthProvider.credential( - accessToken: googleAuth?.accessToken, - idToken: googleAuth?.idToken, - ); - - // Once signed in, return the UserCredential - return await FirebaseAuth.instance - .signInWithCredential(credential) - .then((value) { - print(value.user?.displayName); - print(value.user?.email); - print(value.user?.photoURL); - }).onError( - (error, stackTrace) { - print(error); - }, - ); - } } diff --git a/lib/views/auth/logout_dialog.dart b/lib/views/auth/logout_dialog.dart new file mode 100644 index 0000000..1427394 --- /dev/null +++ b/lib/views/auth/logout_dialog.dart @@ -0,0 +1,39 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +Future showLogoutDialog(BuildContext context) async { + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('로그아웃'), + content: const SingleChildScrollView( + child: ListBody( + children: [ + Text('정말 로그아웃하시겠습니까?'), + ], + ), + ), + actions: [ + TextButton( + child: const Text('취소'), + onPressed: () { + Get.back(); + }, + ), + TextButton( + child: const Text('로그아웃'), + onPressed: () async { + // 로그아웃 처리 + // FirebaseAuth.instance.signOut(); + await FirebaseAuth.instance.signOut(); + Get.back(); + }, + ), + ], + ); + }, + ); +} diff --git a/lib/views/profile/profile_divider_widget.dart b/lib/views/profile/profile_divider_widget.dart new file mode 100644 index 0000000..0c62334 --- /dev/null +++ b/lib/views/profile/profile_divider_widget.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +class ProfileDividerWidget extends StatelessWidget { + const ProfileDividerWidget({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Container( + // width 꽉 차게 + width: double.infinity, + height: 2, + decoration: const BoxDecoration(color: Color(0xffe9e9ee))); + } +} diff --git a/lib/views/profile/profile_header_widget.dart b/lib/views/profile/profile_header_widget.dart new file mode 100644 index 0000000..5744b49 --- /dev/null +++ b/lib/views/profile/profile_header_widget.dart @@ -0,0 +1,35 @@ +import 'package:earlips/views/auth/login_screen.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class ProfileHeader extends StatelessWidget { + final User? user; + + const ProfileHeader({super.key, this.user}); + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.white, + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 62, 20, 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // 유저 정보가 있으면 이메일을 보여주고, 없으면 로그인 버튼 보여줌 + user != null + ? Text("오늘도 이어립스 해볼까요, ${user!.email}") + : GestureDetector( + onTap: () => Get.to(() => const LoginScreen()), + child: const Text( + "로그인 하러가기", + style: TextStyle(color: Colors.blue), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/profile/profile_screen.dart b/lib/views/profile/profile_screen.dart new file mode 100644 index 0000000..7891e67 --- /dev/null +++ b/lib/views/profile/profile_screen.dart @@ -0,0 +1,49 @@ +import 'package:earlips/views/auth/logout_dialog.dart'; +import 'package:earlips/views/profile/profile_divider_widget.dart'; +import 'package:earlips/views/profile/profile_header_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:firebase_auth/firebase_auth.dart'; + +class ProfileScreen extends StatelessWidget { + const ProfileScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: StreamBuilder( + stream: + FirebaseAuth.instance.authStateChanges(), // Stream user changes + builder: (context, snapshot) { + // 상태 연결 확인 웨이팅 + if (snapshot.connectionState == ConnectionState.waiting) { + // 서클 프로그레스바 + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + // 에러 발생시 안내 + return const Center(child: Text('네트워크 상태를 확인해주세요.')); + } else if (snapshot.hasData) { + // --------------------- 로그인 된 상태 --------------------- + return Column( + children: [ + ProfileHeader(user: snapshot.hasData ? snapshot.data : null), + const ProfileDividerWidget(), // Modified version with 'r' removed + // 로그아웃 버튼 + ElevatedButton( + onPressed: () => showLogoutDialog(context), + child: const Text('로그아웃'), + ), + ], + ); + } else { + // --------------------- 로그인 안된 상태 --------------------- + return const Column( + children: [ProfileHeader(), ProfileDividerWidget()], + ); + } + }, + ), + ), + ); + } +} diff --git a/lib/views/root/custom_bottom_navigation_bar.dart b/lib/views/root/custom_bottom_navigation_bar.dart index 8b9891f..69e0ad2 100644 --- a/lib/views/root/custom_bottom_navigation_bar.dart +++ b/lib/views/root/custom_bottom_navigation_bar.dart @@ -22,12 +22,12 @@ class CustomBottomNavigationBar extends BaseWidget { onTap: viewModel.changeIndex, // 아이템의 색상 - unselectedItemColor: ColorStyles.gray3, - selectedItemColor: ColorStyles.main, + unselectedItemColor: ColorSystem.gray3, + selectedItemColor: ColorSystem.main, // 탭 애니메이션 변경 (fixed: 없음) type: BottomNavigationBarType.fixed, - backgroundColor: ColorStyles.white, + backgroundColor: ColorSystem.white, // Bar에 보여질 요소. icon과 label로 구성. items: [ @@ -37,9 +37,9 @@ class CustomBottomNavigationBar extends BaseWidget { height: 24, colorFilter: viewModel.selectedIndex == 0 ? const ColorFilter.mode( - ColorStyles.main, BlendMode.srcATop) + ColorSystem.main, BlendMode.srcATop) : const ColorFilter.mode( - ColorStyles.gray3, BlendMode.srcATop), + ColorSystem.gray3, BlendMode.srcATop), ), label: "홈"), BottomNavigationBarItem( @@ -48,9 +48,9 @@ class CustomBottomNavigationBar extends BaseWidget { height: 24, colorFilter: viewModel.selectedIndex == 1 ? const ColorFilter.mode( - ColorStyles.main, BlendMode.srcATop) + ColorSystem.main, BlendMode.srcATop) : const ColorFilter.mode( - ColorStyles.gray3, BlendMode.srcATop), + ColorSystem.gray3, BlendMode.srcATop), ), label: "내정보"), ], diff --git a/lib/views/root/root_screen.dart b/lib/views/root/root_screen.dart index 2bddf9c..13ce069 100644 --- a/lib/views/root/root_screen.dart +++ b/lib/views/root/root_screen.dart @@ -1,4 +1,5 @@ -import 'package:earlips/views/auth/login.dart'; +import 'package:earlips/views/auth/login_screen.dart'; +import 'package:earlips/views/profile/profile_screen.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:earlips/views/home/home_screen.dart'; @@ -20,7 +21,7 @@ class RootScreen extends BaseScreen { index: viewModel.selectedIndex, children: const [ HomeScreen(), - LoginScreen(), + ProfileScreen(), ], ), );