From c36fe45df254e644e1c47435af34a905da699cd3 Mon Sep 17 00:00:00 2001 From: youngbeom Date: Fri, 23 Jan 2026 15:29:20 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B0=98=EB=A0=A4=EB=8F=99=EB=AC=BC=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=ED=8E=98=EC=9D=B4=EC=A7=80=20ui=20/=20?= =?UTF-8?q?=EB=B0=98=EC=9D=91=ED=98=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/icons/calendaricon.svg | 12 + app/assets/icons/findicon.svg | 3 + app/assets/icons/flowericon.svg | 10 + app/assets/icons/profileicon.svg | 7 + app/assets/img/badicon.png | Bin 0 -> 703 bytes app/assets/img/goodicon.png | Bin 0 -> 862 bytes app/assets/img/profile.png | Bin 0 -> 971 bytes app/lib/main.dart | 16 +- app/lib/screens/home_screen.dart | 69 +- .../screens/identity_verification_screen.dart | 55 +- app/lib/screens/login_screen.dart | 69 +- app/lib/screens/main_screen.dart | 131 +- app/lib/screens/mungnyangz_screen.dart | 13 +- app/lib/screens/my_info_screen.dart | 55 +- app/lib/screens/notice_screen.dart | 30 +- app/lib/screens/pet_registration_screen.dart | 1877 +++++++++++++++++ app/lib/screens/reservation_screen.dart | 1 + app/lib/screens/shop_screen.dart | 6 +- app/lib/screens/signup_screen.dart | 259 --- app/lib/screens/splash_screen.dart | 29 +- app/lib/screens/terms_agreement_screen.dart | 124 +- app/lib/theme/app_colors.dart | 3 + app/lib/widgets/intro_body.dart | 28 +- app/lib/widgets/login_panel.dart | 41 +- .../flutter/generated_plugin_registrant.cc | 4 + app/linux/flutter/generated_plugins.cmake | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 2 + app/pubspec.lock | 120 ++ app/pubspec.yaml | 2 + .../flutter/generated_plugin_registrant.cc | 3 + app/windows/flutter/generated_plugins.cmake | 1 + 31 files changed, 2419 insertions(+), 552 deletions(-) create mode 100644 app/assets/icons/calendaricon.svg create mode 100644 app/assets/icons/findicon.svg create mode 100644 app/assets/icons/flowericon.svg create mode 100644 app/assets/icons/profileicon.svg create mode 100644 app/assets/img/badicon.png create mode 100644 app/assets/img/goodicon.png create mode 100644 app/assets/img/profile.png create mode 100644 app/lib/screens/pet_registration_screen.dart delete mode 100644 app/lib/screens/signup_screen.dart diff --git a/app/assets/icons/calendaricon.svg b/app/assets/icons/calendaricon.svg new file mode 100644 index 0000000..de904fd --- /dev/null +++ b/app/assets/icons/calendaricon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/app/assets/icons/findicon.svg b/app/assets/icons/findicon.svg new file mode 100644 index 0000000..6dcc317 --- /dev/null +++ b/app/assets/icons/findicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/flowericon.svg b/app/assets/icons/flowericon.svg new file mode 100644 index 0000000..d173c82 --- /dev/null +++ b/app/assets/icons/flowericon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/assets/icons/profileicon.svg b/app/assets/icons/profileicon.svg new file mode 100644 index 0000000..962f950 --- /dev/null +++ b/app/assets/icons/profileicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/assets/img/badicon.png b/app/assets/img/badicon.png new file mode 100644 index 0000000000000000000000000000000000000000..3b7bfcdd956618b9fc732ea6eace07f35ef66f8a GIT binary patch literal 703 zcmV;w0zmzVP)cMBSr zKF?;CEEIMjVEQDplbLzvnfH5zID$wdaulVm1deU` z{r>TGyM6Zge1^5f`BhOA-IkG3?ybK*{QS_Ts;U-4p^$C>mk@-*;T-ODaleiEAFjU& zi?o={W@V$%kem^qE`xOrDa3y9&Tu$12ZH&wI|R{VtJRXka=A1gpnw-SNehL7oX_Xd z1~bV%4`8h?77KF`3O8dAg}2_ zfT=Z`&Gxh@l}aR6`~ChsX7hrL)oLY1qmigoDtn~IOj4Qrbi=hsCX*~G(>euOGYiIp z!9ZLKT4V7Eg&+9k`OqDg!itlz)0iJvaZkTq1I8hf$*7ynrf6>9g&fo77O-;JY)lH8 zO);O(i(7yhgiOiN9SZ#W4JL?Nfayn%_eVpb?PNY&Px@b3?fpt5xwPum9AFGFn zKAB8TYUOt3gmR7$E3I0s3RI$}EOQyvY>@cfD~A~alAjX`i%VCKB~M;eM=dzo?Y65N zEkdEN2aWODQrg{a*RVU>G4PDqx@k-qK8(Qq+-)Md4{X@Z-g;?aSU-u!PcMR;zx=ma zt!4^(b-U?wDm$HyESJlywii~glh1cRu3^gI-W}%eOm=54woSI&_Rjdgl~3^L%b}t^ l;R5R+?O#B{A2n1VU^lAsGyXK(aK0pwJ;{OTjII&TaA+^tMU{lhHwFI_eTL z=~fqAd!o{%(9_Z`MPI+4!#ghKPVdYI9(V7(@B4nfzupfbk5CjPvbMJNB^(YPPmxQr z+%In5v4;GB7bsX*DwV|6)|OaZT|IJ<`?$BaC)(|{XfzrEggh1`uV63KOeQ0MYPAXl z-<*uu*w`>gOkfSKO?xJSVi1^sS5Q1=%!O%;iIGuPo=D(2EVTm)$HRxv+PwjKy`B+1 z%+Gt^imb1%-xxq&lB9^un8@<-va+(Wazic<2oR42Syq{e2nu=eD-IXBi)_3SjYb_8 zV{NzFjZ7sP4u`iah0=67t+2(%u)5#x)9&uB<9f^w27{nQ^dps0?h&1f;qdFy*_mpN zv*nvkAz>o&0?4wC;QY^2>Iy%CgwEqJX--!7YY?Hb5SH=K`2mO+!Ffp{g-I0413?v= z++jQ(zZJS!Aj^Vw1ktrMKy+&Uvo}XxLo}5rl*?&LpL4Of`wv3@1U*F`6WAx5bjzD^ zxlGAqlJ@uajenri=~!d=eBO}&g~HISVhE`!a->@p$TGraEkK^&-6-Z3oW|^cF|12- zoWyCsJKEUUcLfevY-rhui0eKE4@Pd!40zLQHg!$Y>W)NQ?lBgi?d@&KX0ujK{GwK? zp`eUHb0gZscXU=6WV3izEEbWUOIHD03&@eZji1cZ0C}9~rcT*~D>ObZsj{4avggR0 zP9$Q3>h?au=Z8pDRTRP{7s16uB93d_B*0!O6bj^rgqz0RyqPNObpLzFNg|OzUMjvG zBrJS_w}Jhw-~`O$Y9GCLTn|Rqi;pk7$cykZdjIwvEj|euN0514pwzJ$u}}E_n!{A# zrgBbn-$V@k@xujr{qlfzo=54!`6q1L3N|3;5DGPJl~${zlLts{6UY;LbrM*3bcp`% ozkgu)!6d8i1SEF+4L1w^1M3Ph^%ZO*0{{R307*qoM6N<$f=G;rcK`qY literal 0 HcmV?d00001 diff --git a/app/assets/img/profile.png b/app/assets/img/profile.png new file mode 100644 index 0000000000000000000000000000000000000000..091876e78fec72f3b2efc62d8faf43d171205ee0 GIT binary patch literal 971 zcmV;+12p`JP)v_N5=eWf@+uhyGeRu}0%|@fK z-syBs!1NT;b>&YroT*L|!U`^${!yYKnEl1Pj*<0xy*JI60=8PMioBO#|L2f-5Tx7N z+tn|Dzr4Jh11uHLdpuHrWy*sfbj0@o{9Bgjj)O^72wcF>@N92y?^4Y`6aoU=sewf% z3J_R;8AQi0SzXOR6d(-48}OuC^5x~_O;DltL*vC*YrF_x5;a{~S~?&wM$~S%FUDF~{Wo3m@=Q7RoIsz(_^#a1ZDL2*RSz-n>7p0aw%%&VJE1H=u)7Hlc7R;gW z2JpVq=h}a{@*o?44=@S9rFEG51TQZP-zifxlEYMUDMnVSgan0Dy;yM7NQ7BrEa%1;FbRT+)>4S+dxaWu>53aCxFJNW2A@K#g4PR&l?$W3 ty1H8P305y~lO)ea$|%3xrser0`T)TM8Rg&6KuiDt002ovPDHLkV1gy~u)F{O literal 0 HcmV?d00001 diff --git a/app/lib/main.dart b/app/lib/main.dart index 10958ef..99f3657 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -1,3 +1,4 @@ +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:firebase_core/firebase_core.dart'; @@ -35,10 +36,17 @@ class RupApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - navigatorKey: navigatorKey, - debugShowCheckedModeBanner: false, - home: const SplashScreen(), + return ScreenUtilInit( + designSize: const Size(375, 812), // 기준 디자인 해상도 set + minTextAdapt: true, + splitScreenMode: true, + builder: (context, child) { + return MaterialApp( + navigatorKey: navigatorKey, + debugShowCheckedModeBanner: false, + home: const SplashScreen(), + ); + }, ); } } diff --git a/app/lib/screens/home_screen.dart b/app/lib/screens/home_screen.dart index ebeec3f..bfa43af 100644 --- a/app/lib/screens/home_screen.dart +++ b/app/lib/screens/home_screen.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; // Import screenutil +import 'pet_registration_screen.dart'; class HomeScreen extends StatelessWidget { const HomeScreen({super.key}); @@ -6,17 +8,64 @@ class HomeScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - body: const SafeArea( - child: Center( - child: Text( - '로그인 성공!\n여기는 메인 홈 화면입니다.', - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: 'SCDream', - fontWeight: FontWeight.w500, - fontSize: 18, + backgroundColor: Colors.white, + body: SafeArea( + child: Column( + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 20.h), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular( + 8.r, + ), // Optional: Add radius + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const PetRegistrationScreen(), + ), + ); + }, + child: Row( + mainAxisSize: MainAxisSize.min, // Wrap content only + children: [ + Image.asset( + 'assets/img/profile.png', + width: 40.w, + height: 40.h, + ), + SizedBox(width: 10.w), + Text( + '반려동물 등록 +', + style: TextStyle( + fontFamily: 'SCDream', + fontWeight: FontWeight.w500, + fontSize: 15.sp, + letterSpacing: 0.45.sp, + color: const Color(0xFF1f1f1f), + ), + ), + ], + ), + ), + ), ), - ), + Expanded( + child: Center( + child: Text( + '로그인 성공!\n여기는 메인 홈 화면입니다.', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'SCDream', + fontWeight: FontWeight.w500, + fontSize: 18.sp, + ), + ), + ), + ), + ], ), ), ); diff --git a/app/lib/screens/identity_verification_screen.dart b/app/lib/screens/identity_verification_screen.dart index 049c6f3..d6a8d55 100644 --- a/app/lib/screens/identity_verification_screen.dart +++ b/app/lib/screens/identity_verification_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; // Import screenutil import 'main_screen.dart'; class IdentityVerificationScreen extends StatelessWidget { @@ -12,13 +13,13 @@ class IdentityVerificationScreen extends StatelessWidget { backgroundColor: Colors.white, elevation: 0, leading: IconButton( - icon: const Icon(Icons.arrow_back_ios, color: Colors.black, size: 20), + icon: Icon(Icons.arrow_back_ios, color: Colors.black, size: 20.w), onPressed: () => Navigator.pop(context), ), - title: const Text( + title: Text( '본인 인증', style: TextStyle( - fontSize: 15, + fontSize: 15.sp, fontFamily: 'SCDream', fontWeight: FontWeight.w500, color: Colors.black, @@ -27,52 +28,52 @@ class IdentityVerificationScreen extends StatelessWidget { centerTitle: true, ), body: Padding( - padding: const EdgeInsets.all(20.0), + padding: EdgeInsets.all(20.0.w), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const SizedBox(height: 20), - const Text( + SizedBox(height: 20.h), + Text( '더 안전한 서비스 이용을 위해\n본인 인증을 진행해 주세요.', style: TextStyle( - fontSize: 20, + fontSize: 20.sp, fontFamily: 'SCDream', fontWeight: FontWeight.bold, color: Colors.black, ), ), - const SizedBox(height: 40), + SizedBox(height: 40.h), // 본인 인증 UI (Placeholder) Container( - padding: const EdgeInsets.all(20), + padding: EdgeInsets.all(20.w), decoration: BoxDecoration( color: Colors.grey[100], - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(10.r), border: Border.all(color: Colors.grey[300]!), ), child: Column( children: [ - const Icon( + Icon( Icons.shield_outlined, - size: 50, - color: Color(0xFFFF7500), + size: 50.w, + color: const Color(0xFFFF7500), ), - const SizedBox(height: 10), - const Text( + SizedBox(height: 10.h), + Text( 'PASS / 문자 인증', style: TextStyle( fontFamily: 'SCDream', - fontSize: 16, + fontSize: 16.sp, fontWeight: FontWeight.w500, ), ), - const SizedBox(height: 10), - const Text( + SizedBox(height: 10.h), + Text( '(현재 UI만 구현된 상태입니다)', style: TextStyle( fontFamily: 'SCDream', - fontSize: 12, + fontSize: 12.sp, color: Colors.grey, ), ), @@ -92,22 +93,22 @@ class IdentityVerificationScreen extends StatelessWidget { (route) => false, ); }, - child: const Text( + child: Text( '다음에 하기 (건너뛰기)', style: TextStyle( fontFamily: 'SCDream', - fontSize: 14, + fontSize: 14.sp, fontWeight: FontWeight.w500, color: Colors.grey, decoration: TextDecoration.underline, ), ), ), - const SizedBox(height: 10), + SizedBox(height: 10.h), // 인증하기 버튼 (현재는 동작 X) SizedBox( - height: 50, + height: 50.h, child: ElevatedButton( onPressed: () { ScaffoldMessenger.of(context).showSnackBar( @@ -118,21 +119,21 @@ class IdentityVerificationScreen extends StatelessWidget { backgroundColor: const Color(0xFFFF7500), elevation: 0, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(8.r), ), ), - child: const Text( + child: Text( '인증하기', style: TextStyle( fontFamily: 'SCDream', - fontSize: 16, + fontSize: 16.sp, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), - const SizedBox(height: 10), + SizedBox(height: 10.h), ], ), ), diff --git a/app/lib/screens/login_screen.dart b/app/lib/screens/login_screen.dart index 67005be..8b74dd7 100644 --- a/app/lib/screens/login_screen.dart +++ b/app/lib/screens/login_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; // Import screenutil import '../services/auth_service.dart'; import 'main_screen.dart'; import 'terms_agreement_screen.dart'; // Import TermsAgreementScreen @@ -71,17 +72,17 @@ class _LoginScreenState extends State { backgroundColor: Colors.white, elevation: 0, leading: IconButton( - icon: const Icon( + icon: Icon( Icons.arrow_back_ios, color: Colors.black, - size: 20, + size: 16.w, // Responsive size ), onPressed: () => Navigator.pop(context), ), - title: const Text( - '로그인', + title: Text( + '간편 로그인', style: TextStyle( - fontSize: 15, + fontSize: 15.sp, // Responsive font size fontFamily: 'SCDream', fontWeight: FontWeight.w500, color: Colors.black, @@ -90,20 +91,28 @@ class _LoginScreenState extends State { centerTitle: true, ), body: Padding( - padding: const EdgeInsets.fromLTRB(20, 0, 20, 240), + padding: EdgeInsets.fromLTRB( + 20.w, + 0, + 20.w, + 240.h, + ), // Responsive padding child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 20), + SizedBox(height: 20.h), // Responsive height Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - Image.asset('assets/img/foot.png', width: 30), - SizedBox(width: 10), + Image.asset( + 'assets/img/foot.png', + width: 30.w, + ), // Responsive width + SizedBox(width: 10.w), // Responsive width Text( - '로그인', + '간편 로그인', style: TextStyle( - fontSize: 24, + fontSize: 24.sp, // Responsive font size fontFamily: 'SCDream', fontWeight: FontWeight.bold, color: Colors.black, @@ -111,29 +120,29 @@ class _LoginScreenState extends State { ), ], ), - SizedBox(height: 10), + SizedBox(height: 10.h), Text( - 'RUP에 어서오세요!\n지금 로그인하고 다양한 서비스를 이용해보세요.', + '똑똑한 반려생활을 위한 첫걸음,\nRUP에 오신것을 환영해요!', style: TextStyle( - fontSize: 14, + fontSize: 14.sp, fontFamily: 'SCDream', fontWeight: FontWeight.w500, color: Colors.black, ), textAlign: TextAlign.start, ), - Spacer(), + const Spacer(), Align( alignment: Alignment.centerRight, - child: Image.asset('assets/img/cat.png', height: 150), + child: Image.asset('assets/img/cat.png', height: 150.h), ), - SizedBox(height: 10), + SizedBox(height: 10.h), ], ), ), bottomSheet: Container( color: Colors.white, - padding: const EdgeInsets.fromLTRB(20, 20, 20, 40), + padding: EdgeInsets.fromLTRB(20.w, 20.h, 20.w, 40.h), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -144,31 +153,31 @@ class _LoginScreenState extends State { textColor: Colors.white, fontFamily: 'SCDream', fontWeight: FontWeight.bold, - fontSize: 15, + fontSize: 15.sp, backgroundColor: const Color(0xFF00D03F), onPressed: () {}, iconPath: 'assets/icons/navericon.svg', ), - const SizedBox(height: 15), + SizedBox(height: 15.h), // Kakao Login Button _SocialLoginButton( text: '카카오 로그인', textColor: const Color(0xFF212121), fontFamily: 'SCDream', fontWeight: FontWeight.bold, - fontSize: 15, + fontSize: 15.sp, backgroundColor: const Color(0xFFFAE100), onPressed: () {}, iconPath: 'assets/icons/kakaoicon.svg', ), - const SizedBox(height: 15), + SizedBox(height: 15.h), // Google Login Button _SocialLoginButton( text: '구글 로그인', textColor: const Color(0xFF17191A), fontFamily: 'SCDream', fontWeight: FontWeight.bold, - fontSize: 15, + fontSize: 15.sp, backgroundColor: Colors.white, onPressed: _handleGoogleLogin, iconPath: 'assets/icons/googleicon.svg', @@ -180,7 +189,7 @@ class _LoginScreenState extends State { ), if (_isLoading) Container( - color: Colors.black.withOpacity(0.5), + color: Colors.black.withValues(alpha: 0.5), child: const Center(child: CircularProgressIndicator()), ), ], @@ -208,13 +217,13 @@ class _SocialLoginButton extends StatelessWidget { this.isBordered = false, this.fontFamily = 'SCDream', this.fontWeight = FontWeight.w500, - this.fontSize = 16, + required this.fontSize, // Make required to ensure passed value is used }); @override Widget build(BuildContext context) { return SizedBox( - height: 50, + height: 50.h, child: ElevatedButton( onPressed: onPressed, style: ElevatedButton.styleFrom( @@ -226,14 +235,14 @@ class _SocialLoginButton extends StatelessWidget { ? BorderSide(color: Colors.grey[300]!) : BorderSide.none, ), - padding: const EdgeInsets.symmetric(horizontal: 20), + padding: EdgeInsets.symmetric(horizontal: 20.w), ), child: Row( children: [ SvgPicture.asset( iconPath, - width: 24, - height: 24, + width: 24.w, + height: 24.h, fit: BoxFit.contain, ), Expanded( @@ -247,7 +256,7 @@ class _SocialLoginButton extends StatelessWidget { textAlign: TextAlign.center, ), ), - const SizedBox(width: 24), + SizedBox(width: 24.w), ], ), ), diff --git a/app/lib/screens/main_screen.dart b/app/lib/screens/main_screen.dart index cfeab69..ca0d230 100644 --- a/app/lib/screens/main_screen.dart +++ b/app/lib/screens/main_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; // Import screenutil import 'home_screen.dart'; import 'reservation_screen.dart'; import 'mungnyangz_screen.dart'; @@ -37,8 +38,8 @@ class _MainScreenState extends State { Widget _buildSvgIcon(String assetName, int index) { return SvgPicture.asset( assetName, - width: 24, - height: 24, + width: 24.w, + height: 24.h, colorFilter: ColorFilter.mode( _selectedIndex == index ? AppColors.highlight @@ -52,66 +53,82 @@ class _MainScreenState extends State { Widget build(BuildContext context) { return Scaffold( body: IndexedStack(index: _selectedIndex, children: _screens), - bottomNavigationBar: BottomNavigationBar( - currentIndex: _selectedIndex, - onTap: _onItemTapped, - type: BottomNavigationBarType.fixed, - selectedItemColor: AppColors.highlight, - unselectedItemColor: AppColors.inactive, - selectedLabelStyle: TextStyle( - fontFamily: 'SCDream', - fontSize: 12, - fontWeight: FontWeight.w500, // Medium + bottomNavigationBar: Container( + padding: EdgeInsets.symmetric(vertical: 10.h), + decoration: const BoxDecoration( + color: Colors.white, + border: Border(top: BorderSide(color: Color(0xFFEEEEEE), width: 1.0)), ), - unselectedLabelStyle: TextStyle( - fontFamily: 'SCDream', - fontSize: 12, - fontWeight: FontWeight.w400, // Regular - ), - showUnselectedLabels: true, - items: [ - BottomNavigationBarItem( - icon: Padding( - padding: const EdgeInsets.only(bottom: 10), - child: _buildSvgIcon('assets/icons/homeicon.svg', 0), - ), - label: '홈', + child: Theme( + data: Theme.of(context).copyWith( + splashColor: Colors.transparent, + highlightColor: Colors.transparent, ), - BottomNavigationBarItem( - icon: Padding( - padding: const EdgeInsets.only(bottom: 10), - child: _buildSvgIcon('assets/icons/appointmenticon.svg', 1), + child: BottomNavigationBar( + backgroundColor: Colors.white, + elevation: 0, // Remove default shadow since we have a border + currentIndex: _selectedIndex, + onTap: _onItemTapped, + type: BottomNavigationBarType.fixed, + selectedItemColor: AppColors.highlight, + unselectedItemColor: AppColors.inactive, + selectedLabelStyle: TextStyle( + fontFamily: 'SCDream', + fontSize: 12.sp, + fontWeight: FontWeight.w500, // Medium ), - label: '예약/조회', - ), - BottomNavigationBarItem( - icon: Padding( - padding: const EdgeInsets.only(bottom: 10), - child: Image.asset( - _selectedIndex == 2 - ? 'assets/img/catdog_on.png' - : 'assets/img/catdog_off.png', - width: 24, - height: 24, + unselectedLabelStyle: TextStyle( + fontFamily: 'SCDream', + fontSize: 12.sp, + fontWeight: FontWeight.w400, // Regular + ), + showUnselectedLabels: true, + items: [ + BottomNavigationBarItem( + icon: Padding( + padding: EdgeInsets.only(bottom: 10.h), + child: _buildSvgIcon('assets/icons/homeicon.svg', 0), + ), + label: '홈', ), - ), - label: '멍냥즈', + BottomNavigationBarItem( + icon: Padding( + padding: EdgeInsets.only(bottom: 10.h), + child: _buildSvgIcon('assets/icons/appointmenticon.svg', 1), + ), + label: '예약/조회', + ), + BottomNavigationBarItem( + icon: Padding( + padding: EdgeInsets.only(bottom: 10.h), + child: Image.asset( + _selectedIndex == 2 + ? 'assets/img/catdog_on.png' + : 'assets/img/catdog_off.png', + width: 29.w, + height: 26.h, + fit: BoxFit.cover, + ), + ), + label: '멍냥즈', + ), + BottomNavigationBarItem( + icon: Padding( + padding: EdgeInsets.only(bottom: 10.h), + child: _buildSvgIcon('assets/icons/shopicon.svg', 3), + ), + label: '상점', + ), + BottomNavigationBarItem( + icon: Padding( + padding: EdgeInsets.only(bottom: 10.h), + child: _buildSvgIcon('assets/icons/myicon.svg', 4), + ), + label: '내 정보', + ), + ], ), - BottomNavigationBarItem( - icon: Padding( - padding: const EdgeInsets.only(bottom: 10), - child: _buildSvgIcon('assets/icons/shopicon.svg', 3), - ), - label: '상점', - ), - BottomNavigationBarItem( - icon: Padding( - padding: const EdgeInsets.only(bottom: 10), - child: _buildSvgIcon('assets/icons/myicon.svg', 4), - ), - label: '내 정보', - ), - ], + ), ), ); } diff --git a/app/lib/screens/mungnyangz_screen.dart b/app/lib/screens/mungnyangz_screen.dart index 2ddfb6f..1b776ba 100644 --- a/app/lib/screens/mungnyangz_screen.dart +++ b/app/lib/screens/mungnyangz_screen.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; - +import 'package:flutter_screenutil/flutter_screenutil.dart'; // Import screenutil import '../utils/log_manager.dart'; class MungNyangzScreen extends StatelessWidget { @@ -8,6 +8,7 @@ class MungNyangzScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Colors.white, body: SafeArea( child: ValueListenableBuilder>( valueListenable: LogManager().logs, @@ -18,19 +19,19 @@ class MungNyangzScreen extends StatelessWidget { ); } return ListView.builder( - padding: const EdgeInsets.all(10), + padding: EdgeInsets.all(10.w), itemCount: logs.length, itemBuilder: (context, index) { return Container( - margin: const EdgeInsets.only(bottom: 5), - padding: const EdgeInsets.all(8), + margin: EdgeInsets.only(bottom: 5.h), + padding: EdgeInsets.all(8.w), decoration: BoxDecoration( color: Colors.black12, - borderRadius: BorderRadius.circular(5), + borderRadius: BorderRadius.circular(5.r), ), child: Text( logs[index], - style: const TextStyle(fontSize: 12, fontFamily: 'SCDream'), + style: TextStyle(fontSize: 12.sp, fontFamily: 'SCDream'), ), ); }, diff --git a/app/lib/screens/my_info_screen.dart b/app/lib/screens/my_info_screen.dart index 36c2d1e..c995e3f 100644 --- a/app/lib/screens/my_info_screen.dart +++ b/app/lib/screens/my_info_screen.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import '../services/auth_service.dart'; import 'welcome_screen.dart'; -import 'notice_screen.dart'; // 공지사항 화면 임포트 -import '../data/terms_data.dart'; // 데이터 임포트 +import 'notice_screen.dart'; +import '../data/terms_data.dart'; class MyInfoScreen extends StatefulWidget { const MyInfoScreen({super.key}); @@ -97,10 +97,10 @@ class _MyInfoScreenState extends State { showModalBottomSheet( context: context, isScrollControlled: true, - backgroundColor: Colors.transparent, // 둥근 모서리 적용을 위해 투명 + backgroundColor: Colors.transparent, builder: (context) { return DraggableScrollableSheet( - initialChildSize: 0.85, // 화면의 85% 높이로 시작 + initialChildSize: 0.85, minChildSize: 0.5, maxChildSize: 0.95, builder: (_, controller) { @@ -228,10 +228,8 @@ class _MyInfoScreenState extends State { ? const Center(child: Text('정보를 불러올 수 없습니다.')) : SafeArea( child: Column( - // Column으로 변경하여 하단 고정 영역 확보 children: [ Expanded( - // 상단 스크롤 영역 child: SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( @@ -243,10 +241,10 @@ class _MyInfoScreenState extends State { color: Colors.grey[100], borderRadius: BorderRadius.circular(16), ), - child: Column( + child: Row( children: [ const CircleAvatar( - radius: 40, + radius: 35, backgroundColor: Colors.grey, child: Icon( Icons.person, @@ -254,23 +252,28 @@ class _MyInfoScreenState extends State { color: Colors.white, ), ), - const SizedBox(height: 16), - Text( - _userInfo!['nickname'] ?? '이름 없음', - style: const TextStyle( - fontFamily: 'SCDream', - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Text( - _userInfo!['email'] ?? '이메일 없음', - style: TextStyle( - fontFamily: 'SCDream', - fontSize: 14, - color: Colors.grey[600], - ), + const SizedBox(width: 20), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _userInfo!['nickname'] ?? '이름 없음', + style: const TextStyle( + fontFamily: 'SCDream', + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + _userInfo!['email'] ?? '이메일 없음', + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 14, + color: Colors.grey[600], + ), + ), + ], ), ], ), @@ -299,7 +302,7 @@ class _MyInfoScreenState extends State { title: '버전 정보', icon: Icons.info_outline, trailingText: '1.0.0', - onTap: () {}, // 클릭 효과를 위해 빈 함수 전달 + onTap: () {}, ), // 회원 탈퇴 버튼 removed from here ], diff --git a/app/lib/screens/notice_screen.dart b/app/lib/screens/notice_screen.dart index 318a0da..76e2821 100644 --- a/app/lib/screens/notice_screen.dart +++ b/app/lib/screens/notice_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; // Import screenutil class NoticeScreen extends StatelessWidget { const NoticeScreen({super.key}); @@ -26,49 +27,47 @@ class NoticeScreen extends StatelessWidget { return Scaffold( backgroundColor: Colors.white, appBar: AppBar( - title: const Text( + title: Text( '공지사항', style: TextStyle( fontFamily: 'SCDream', fontWeight: FontWeight.bold, color: Colors.black, + fontSize: 18.sp, // Added responsive font size ), ), centerTitle: true, backgroundColor: Colors.white, elevation: 0, leading: IconButton( - icon: const Icon(Icons.arrow_back_ios, color: Colors.black, size: 20), + icon: Icon(Icons.arrow_back_ios, color: Colors.black, size: 20.w), onPressed: () => Navigator.pop(context), ), ), body: ListView.separated( itemCount: notices.length, separatorBuilder: (context, index) => - const Divider(height: 1, color: Color(0xFFEEEEEE)), + Divider(height: 1.h, color: const Color(0xFFEEEEEE)), itemBuilder: (context, index) { final notice = notices[index]; return ExpansionTile( - tilePadding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 8, - ), + tilePadding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 8.h), title: Text( notice['title']!, - style: const TextStyle( + style: TextStyle( fontFamily: 'SCDream', - fontSize: 16, + fontSize: 16.sp, fontWeight: FontWeight.w500, color: Colors.black87, ), ), subtitle: Padding( - padding: const EdgeInsets.only(top: 4), + padding: EdgeInsets.only(top: 4.h), child: Text( notice['date']!, style: TextStyle( fontFamily: 'SCDream', - fontSize: 12, + fontSize: 12.sp, color: Colors.grey[500], ), ), @@ -76,16 +75,13 @@ class NoticeScreen extends StatelessWidget { children: [ Container( width: double.infinity, - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 20, - ), + padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 20.h), color: Colors.grey[50], child: Text( notice['content']!, - style: const TextStyle( + style: TextStyle( fontFamily: 'SCDream', - fontSize: 14, + fontSize: 14.sp, height: 1.5, color: Colors.black87, ), diff --git a/app/lib/screens/pet_registration_screen.dart b/app/lib/screens/pet_registration_screen.dart new file mode 100644 index 0000000..4cf14ad --- /dev/null +++ b/app/lib/screens/pet_registration_screen.dart @@ -0,0 +1,1877 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../theme/app_colors.dart'; + +class PetRegistrationScreen extends StatefulWidget { + const PetRegistrationScreen({super.key}); + + @override + State createState() => _PetRegistrationScreenState(); +} + +class _PetRegistrationScreenState extends State { + // 정확한 날짜를 몰라요 상태 + bool _isDateUnknown = false; + + final TextEditingController _yearController = TextEditingController(); + final TextEditingController _monthController = TextEditingController(); + final TextEditingController _dayController = TextEditingController(); + + // 보유 질환 데이터 + final List _diseaseList = [ + "피부질환", + "눈 질환", + "치아 / 구강 질환", + "뼈 / 관절 질환", + "생식기 / 비뇨기 질환", + "심장 / 혈관 질환", + "소화기 질환", + "호흡기 질환", + "내분비계 질환", + "뇌신경 질환", + "생식기 질환", + "귀 질환", + "코 질환", + "기타", + ]; + + // 각 항목별 선택 상태 및 컨트롤러 + final TextEditingController _nameController = + TextEditingController(); // 이름 컨트롤러 + final TextEditingController _speciesController = + TextEditingController(); // 종 컨트롤러 + final TextEditingController _breedController = + TextEditingController(); // 품종 컨트롤러 + final TextEditingController _genderController = + TextEditingController(); // 성별 컨트롤러 + + // 종 데이터 (대분류 -> 중분류 -> 품종) + final Map>> _petData = { + "포유류": { + "강아지": [ + "말티즈", + "푸들", + "포메라니안", + "믹스견", + "치와와", + "시츄", + "비숑 프리제", + "골든 리트리버", + "진돗개", + "웰시 코기", + "기타(직접 입력)", + ], + "고양이": [ + "코리안 숏헤어", + "페르시안", + "러시안 블루", + "샴", + "렉돌", + "스코티시 폴드", + "먼치킨", + "노르웨이 숲", + "믹스묘", + "기타(직접 입력)", + ], + "햄스터": ["정글리안", "펄", "푸딩", "골든 햄스터", "로보로브스키", "기타(직접 입력)"], + "토끼": ["롭이어", "더치", "라이언 헤드", "드워프", "렉스", "기타(직접 입력)"], + "기니피그": ["잉글리쉬", "아비시니안", "페루비안", "실키", "기타(직접 입력)"], + "고슴도치": ["플라티나", "화이트 초코", "알비노", "핀토", "기타(직접 입력)"], + "기타": ["기타(직접 입력)"], + }, + "파충류": { + "거북이": ["커먼 머스크 터틀", "레이저백", "육지거북", "붉은귀거북", "남생이", "기타(직접 입력)"], + "도마뱀": ["크레스티드 게코", "레오파드 게코", "비어디 드래곤", "블루텅 스킨크", "이구아나", "기타(직접 입력)"], + "뱀": ["볼 파이톤", "콘 스네이크", "킹 스네이크", "밀크 스네이크", "기타(직접 입력)"], + "기타": ["기타(직접 입력)"], + }, + "조류": { + "앵무새": [ + "사랑앵무(잉꼬)", + "코카티엘(왕관앵무)", + "모란앵무", + "코뉴어", + "퀘이커", + "금강앵무", + "기타(직접 입력)", + ], + "카나리아": ["옐로우 카나리아", "레드 카나리아", "보더 카나리아", "기타(직접 입력)"], + "핀치": ["문조", "십자매", "금화조", "호금조", "기타(직접 입력)"], + "기타": ["기타(직접 입력)"], + }, + "어류": { + "금붕어": ["오란다", "유금", "단정", "진주린", "코메트", "기타(직접 입력)"], + "열대어": ["네온 테트라", "엔젤피쉬", "플래티", "몰리", "디스커스", "기타(직접 입력)"], + "구피": ["고정 구피", "막구피(믹스)", "기타(직접 입력)"], + "잉어": ["비단잉어", "향어", "기타(직접 입력)"], + "기타": ["기타(직접 입력)"], + }, + "곤충": { + "장수풍뎅이": ["국산 장수풍뎅이", "헤라클레스 장수풍뎅이", "코카서스 장수풍뎅이", "기타(직접 입력)"], + "사슴벌레": ["넓적사슴벌레", "왕사슴벌레", "톱사슴벌레", "애사슴벌레", "기타(직접 입력)"], + "나비/나방": ["배추흰나비", "호랑나비", "누에나방", "기타(직접 입력)"], + "사마귀": ["왕사마귀", "사마귀", "넓적배사마귀", "기타(직접 입력)"], + "기타": ["기타(직접 입력)"], + }, + "절지동물": { + "타란툴라(거미)": ["로즈헤어", "골든니", "화이트니", "핑크토", "기타(직접 입력)"], + "전갈": ["황제전갈", "극동전갈", "아시안 포레스트 전갈", "기타(직접 입력)"], + "지네": ["왕지네", "청지네", "기타(직접 입력)"], + "소라게": ["인도 소라게", "딸기 소라게", "바이오라센트", "기타(직접 입력)"], + "기타": ["기타(직접 입력)"], + }, + "기타": { + "기타(직접 입력)": ["기타(직접 입력)"], + }, + }; + + // 선택된 종 정보 (품종 선택을 위해 필요) + String? _currentMajorCategory; + String? _currentMinorCategory; + + String? _selectedGender; // '남아', '여아' + bool _isNeutered = false; // 중성화 여부 + List _selectedDiseases = []; + String _otherDiseaseText = ''; // 보유 질환 기타 텍스트 + final TextEditingController _diseaseController = TextEditingController(); + + List _selectedPastDiseases = []; + File? _profileImage; // 프로필 이미지 + final ImagePicker _picker = ImagePicker(); // 이미지 피커 + + // 이미지 선택 모달 (카메라/갤러리) + void _pickImage() { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: (context) { + return Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 20.h), + Text( + '프로필 사진 설정', + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 18.sp, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 20.h), + ListTile( + leading: Icon( + Icons.camera_alt, + color: Colors.black, + size: 24.w, + ), + title: Text( + '카메라로 촬영', + style: TextStyle(fontFamily: 'SCDream', fontSize: 16.sp), + ), + onTap: () async { + Navigator.pop(context); + final XFile? image = await _picker.pickImage( + source: ImageSource.camera, + ); + if (image != null) { + setState(() { + _profileImage = File(image.path); + }); + } + }, + ), + ListTile( + leading: Icon( + Icons.photo_library, + color: Colors.black, + size: 24.w, + ), + title: Text( + '갤러리에서 선택', + style: TextStyle(fontFamily: 'SCDream', fontSize: 16.sp), + ), + onTap: () async { + Navigator.pop(context); + final XFile? image = await _picker.pickImage( + source: ImageSource.gallery, + ); + if (image != null) { + setState(() { + _profileImage = File(image.path); + }); + } + }, + ), + SizedBox(height: 20.h), + ], + ), + ); + }, + ); + } + + String _otherPastDiseaseText = ''; // 과거 진단 기타 텍스트 + final TextEditingController _pastDiseaseController = TextEditingController(); + + List _selectedHealthConcerns = []; + String _otherHealthConcernText = ''; // 염려 건강 기타 텍스트 + final TextEditingController _healthConcernController = + TextEditingController(); + + @override + void dispose() { + _nameController.dispose(); + _speciesController.dispose(); + _breedController.dispose(); + _genderController.dispose(); + _yearController.dispose(); + _monthController.dispose(); + _dayController.dispose(); + _diseaseController.dispose(); + _pastDiseaseController.dispose(); + _healthConcernController.dispose(); + super.dispose(); + } + + void _toggleDateUnknown() { + setState(() { + _isDateUnknown = !_isDateUnknown; + if (_isDateUnknown) { + _yearController.clear(); + _monthController.clear(); + _dayController.clear(); + } + }); + } + + // 공통 선택 모달 (보유 질환, 과거 진단, 염려 건강) - Generic Selection Modal + void _showSelectionModal({ + required String title, + required List currentSelected, + required String currentOtherText, + required Function(List, String) onComplete, + }) { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (context) { + // 모달 내부 임시 상태 + List tempSelected = List.from(currentSelected); + final TextEditingController otherInputController = + TextEditingController(text: currentOtherText); + + return StatefulBuilder( + builder: (BuildContext context, StateSetter setModalState) { + // 키보드가 올라왔을 때를 대비한 Padding 처리 + final bottomInset = MediaQuery.of(context).viewInsets.bottom; + + return Container( + height: 0.85.sh, + margin: EdgeInsets.only(top: 50.h), // 상단 여백 + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20.r)), + ), + child: Column( + children: [ + SizedBox(height: 20.h), + // 타이틀 + Text( + title, + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + SizedBox(height: 10.h), + Divider(color: const Color(0xFFEEEEEE), thickness: 1.h), + + // 리스트 영역 + Expanded( + child: ListView.builder( + itemCount: _diseaseList.length, + padding: const EdgeInsets.symmetric(vertical: 10), + itemBuilder: (context, index) { + final disease = _diseaseList[index]; + final isSelected = tempSelected.contains(disease); + + return Column( + children: [ + InkWell( + onTap: () { + setModalState(() { + if (isSelected) { + tempSelected.remove(disease); + } else { + tempSelected.add(disease); + } + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 16, + ), + child: Row( + children: [ + Icon( + Icons.check, + size: 20, + color: isSelected + ? AppColors.highlight + : Colors.grey[300], + ), + const SizedBox(width: 12), + Expanded( + child: Text( + disease, + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 16, + fontWeight: isSelected + ? FontWeight.bold + : FontWeight.normal, + color: isSelected + ? AppColors.highlight + : Colors.black, + ), + ), + ), + ], + ), + ), + ), + // 기타가 선택되었을 때 입력창 표시 + if (isSelected && disease == "기타") + Padding( + padding: EdgeInsets.fromLTRB( + 52.w, + 0, + 20.w, + 10.h, + ), + child: TextField( + key: const ValueKey('other_input'), + controller: otherInputController, + autofocus: true, // 입력창이 생기면 바로 포커스 + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 14.sp, + ), + decoration: InputDecoration( + hintText: '직접 입력해 주세요', + isDense: true, + contentPadding: EdgeInsets.symmetric( + vertical: 10.h, + horizontal: 10.w, + ), + border: const OutlineInputBorder( + borderSide: BorderSide( + color: Color(0xFFDDDDDD), + ), + ), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Color(0xFFDDDDDD), + ), + ), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: AppColors.highlight, + ), + ), + ), + ), + ), + ], + ); + }, + ), + ), + + // 하단 버튼 영역 + Padding( + padding: EdgeInsets.fromLTRB( + 20.w, + 20.h, + 20.w, + 20.h + bottomInset, + ), + child: Row( + children: [ + // 초기화 버튼 + InkWell( + onTap: () { + setModalState(() { + tempSelected.clear(); + otherInputController.clear(); + }); + }, + child: Container( + height: 52.h, + padding: EdgeInsets.symmetric(horizontal: 20.w), + decoration: BoxDecoration( + color: const Color(0xFF333333), + borderRadius: BorderRadius.circular(12.r), + ), + child: Row( + children: [ + Icon( + Icons.refresh, + color: Colors.white, + size: 20.w, + ), + SizedBox(width: 4.w), + const Text( + '초기화', + style: TextStyle( + fontFamily: 'SCDream', + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + SizedBox(width: 12.w), + // 선택 완료 버튼 + Expanded( + child: InkWell( + onTap: () { + onComplete( + tempSelected, + otherInputController.text, + ); + Navigator.pop(context); + }, + child: Container( + height: 52.h, + decoration: BoxDecoration( + color: AppColors.highlight, + borderRadius: BorderRadius.circular(12.r), + ), + child: Center( + child: Text( + '선택 완료', + style: TextStyle( + fontFamily: 'SCDream', + color: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); + }, + ); + } + + // 종 선택 모달 (대분류 -> 중분류 2단계) + void _showSpeciesSelectionModal() { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (context) { + String? selectedMajor; // 모달 내부 임시 상태 (대분류) + bool showInput = false; // 직접 입력 창 표시 여부 + final TextEditingController speciesInputController = + TextEditingController(); + + return StatefulBuilder( + builder: (BuildContext context, StateSetter setModalState) { + final bottomInset = MediaQuery.of(context).viewInsets.bottom; + + return Container( + height: 0.6.sh, // 높이 60%로 조정 + margin: EdgeInsets.only(top: 50.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20.r)), + ), + child: Column( + children: [ + // 상단 네비게이션바 (닫기 / 뒤로가기) + Padding( + padding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 12.h, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // 뒤로가기 버튼 (대분류 선택 상태이거나 입력창 상태일 때 표시) + (selectedMajor != null || showInput) + ? GestureDetector( + onTap: () { + setModalState(() { + if (showInput) { + showInput = false; + } else { + selectedMajor = null; + } + }); + }, + child: Icon( + Icons.arrow_back_ios, + size: 20.w, + color: Colors.black, + ), + ) + : SizedBox(width: 20.w), + // 타이틀 + Text( + showInput + ? '직접 입력' + : (selectedMajor == null ? '대분류' : '중분류'), + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + // 닫기 버튼 + GestureDetector( + onTap: () => Navigator.pop(context), + child: Icon( + Icons.close, + size: 24.w, + color: Colors.black, + ), + ), + ], + ), + ), + Divider(color: const Color(0xFFEEEEEE), thickness: 1.h), + + // 컨텐츠 영역 + Expanded( + child: showInput + ? Padding( + padding: EdgeInsets.all(20.w), + child: Column( + children: [ + Text( + '반려동물의 종을 직접 입력해주세요.', + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 16.sp, + color: Colors.black87, + ), + ), + SizedBox(height: 20.h), + TextField( + controller: speciesInputController, + autofocus: true, + decoration: const InputDecoration( + hintText: '예: 미어캣, 라쿤 등', + border: OutlineInputBorder(), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: AppColors.highlight, + ), + ), + ), + ), + const Spacer(), + SizedBox( + width: double.infinity, + height: 52.h, + child: ElevatedButton( + onPressed: () { + if (speciesInputController + .text + .isNotEmpty) { + setState(() { + _speciesController.text = + speciesInputController.text; + // 직접 입력 시 카테고리 정보 초기화 (품종 선택 불가 또는 직접 입력) + _currentMajorCategory = null; + _currentMinorCategory = null; + _breedController.clear(); + }); + Navigator.pop(context); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.highlight, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 12.r, + ), + ), + ), + child: Text( + '완료', + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 16.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + SizedBox(height: bottomInset), + ], + ), + ) + : (selectedMajor == null + ? ListView.builder( + // 대분류 리스트 + itemCount: _petData.keys.length, + itemBuilder: (context, index) { + final major = _petData.keys.elementAt( + index, + ); + return ListTile( + title: Text( + major, + style: const TextStyle( + fontFamily: 'SCDream', + fontSize: 16, + ), + ), + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + color: Colors.grey, + ), + onTap: () { + setModalState(() { + if (major == '기타') { + showInput = true; + } else { + selectedMajor = major; + } + }); + }, + ); + }, + ) + : ListView.builder( + // 중분류 리스트 + itemCount: _petData[selectedMajor]!.length, + itemBuilder: (context, index) { + final minor = _petData[selectedMajor]!.keys + .elementAt(index); + return ListTile( + title: Text( + minor, + style: const TextStyle( + fontFamily: 'SCDream', + fontSize: 16, + ), + ), + trailing: minor == '기타(직접 입력)' + ? const Icon( + Icons.arrow_forward_ios, + size: 16, + color: Colors.grey, + ) + : null, + onTap: () { + if (minor == '기타(직접 입력)') { + setModalState(() { + showInput = true; + }); + } else { + setState(() { + // 최종 선택 반영 + _currentMajorCategory = + selectedMajor; + _currentMinorCategory = minor; + _speciesController.text = minor; + _breedController + .clear(); // 종 변경 시 품종 초기화 + }); + Navigator.pop(context); + } + }, + ); + }, + )), + ), + ], + ), + ); + }, + ); + }, + ); + } + + // 품종 선택 모달 (검색 가능) + void _showBreedSelectionModal() { + // 1. 종 선택 선행 확인 + if (_speciesController.text.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('반려동물 종을 먼저 선택해주세요.'), + duration: Duration(seconds: 1), + ), + ); + return; + } + + // 2. 직접 입력 등 카테고리 정보가 없는 경우 -> 바로 직접 입력 모드로 + if (_currentMajorCategory == null || _currentMinorCategory == null) { + _showBreedDirectInputModal(); + return; + } + + // 3. 품종 리스트 가져오기 + final List originalList = + _petData[_currentMajorCategory]![_currentMinorCategory]! + .where((e) => e != '기타(직접 입력)') + .toList(); + // '기타(직접 입력)'은 리스트 마지막에 고정하거나 별도 처리, 여기서는 필터링 후 맨 뒤에 붙일 예정 + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) { + String searchText = ''; + List filteredList = List.from(originalList); + TextEditingController searchController = TextEditingController(); + bool showInput = false; + final TextEditingController manualInputController = + TextEditingController(); + + return StatefulBuilder( + builder: (BuildContext context, StateSetter setModalState) { + final bottomInset = MediaQuery.of(context).viewInsets.bottom; + + void filterList(String query) { + setModalState(() { + searchText = query; + if (query.isEmpty) { + filteredList = List.from(originalList); + } else { + filteredList = originalList + .where( + (breed) => + breed.toLowerCase().contains(query.toLowerCase()), + ) + .toList(); + } + }); + } + + return Container( + height: 0.85.sh, + margin: EdgeInsets.only(top: 50.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20.r)), + ), + child: Column( + children: [ + // 상단 네비게이션바 + Padding( + padding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 12.h, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + showInput + ? GestureDetector( + onTap: () { + setModalState(() { + showInput = false; + }); + }, + child: Icon( + Icons.arrow_back_ios, + size: 20.w, + color: Colors.black, + ), + ) + : const SizedBox(width: 20), + Text( + showInput ? '직접 입력' : '품종 선택', + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + GestureDetector( + onTap: () => Navigator.pop(context), + child: Icon( + Icons.close, + size: 24.w, + color: Colors.black, + ), + ), + ], + ), + ), + Divider(color: const Color(0xFFEEEEEE), thickness: 1.h), + + // 컨텐츠 + Expanded( + child: showInput + ? Padding( + padding: EdgeInsets.all(20.w), + child: Column( + children: [ + Text( + '반려동물의 품종을 직접 입력해주세요.', + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 16.sp, + color: Colors.black87, + ), + ), + SizedBox(height: 20.h), + TextField( + controller: manualInputController, + autofocus: true, + decoration: const InputDecoration( + hintText: '예: 믹스, 시고르자브종 등', + border: OutlineInputBorder(), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: AppColors.highlight, + ), + ), + ), + ), + const Spacer(), + SizedBox( + width: double.infinity, + height: 52.h, + child: ElevatedButton( + onPressed: () { + if (manualInputController + .text + .isNotEmpty) { + setState(() { + _breedController.text = + manualInputController.text; + }); + Navigator.pop(context); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.highlight, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 12.r, + ), + ), + ), + child: Text( + '완료', + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 16.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + SizedBox(height: bottomInset), + ], + ), + ) + : Column( + children: [ + // 검색창 + Padding( + padding: EdgeInsets.fromLTRB( + 20.w, + 10.h, + 20.w, + 10.h, + ), + child: TextField( + controller: searchController, + onChanged: filterList, + decoration: InputDecoration( + hintText: '품종 검색', + prefixIcon: const Icon( + Icons.search, + color: Colors.grey, + ), + filled: true, + fillColor: const Color(0xFFF5F5F5), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric( + vertical: 0, + horizontal: 16, + ), + ), + ), + ), + // 리스트 + Expanded( + child: ListView.builder( + itemCount: + filteredList.length + 1, // 목록 + 직접입력 + itemBuilder: (context, index) { + if (index == filteredList.length) { + // 마지막 아이템: 직접 입력 + return ListTile( + title: const Text( + '기타(직접 입력)', + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 16, + color: Colors.black87, + ), + ), + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + color: Colors.grey, + ), + onTap: () { + setModalState(() { + showInput = true; + }); + }, + ); + } + final breed = filteredList[index]; + return ListTile( + title: Text( + breed, + style: const TextStyle( + fontFamily: 'SCDream', + fontSize: 16, + ), + ), + onTap: () { + setState(() { + _breedController.text = breed; + }); + Navigator.pop(context); + }, + ); + }, + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); + }, + ); + } + + // 품종 직접 입력 모달 (카테고리 정보 없을 때) + void _showBreedDirectInputModal() { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (context) { + final TextEditingController manualInputController = + TextEditingController(); + return Container( + height: 0.85.sh, + margin: EdgeInsets.only(top: 50.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20.r)), + ), + child: Padding( + padding: EdgeInsets.all(20.w), + child: Column( + children: [ + // 네비게이션 + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + GestureDetector( + onTap: () => Navigator.pop(context), + child: Icon(Icons.close, size: 24.w, color: Colors.black), + ), + ], + ), + SizedBox(height: 20.h), + Text( + '반려동물의 품종을 직접 입력해주세요.', + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 16.sp, + color: Colors.black87, + ), + ), + SizedBox(height: 20.h), + TextField( + controller: manualInputController, + autofocus: true, + decoration: const InputDecoration( + hintText: '예: 믹스, 시고르자브종 등', + border: OutlineInputBorder(), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: AppColors.highlight), + ), + ), + ), + const Spacer(), + SizedBox( + width: double.infinity, + height: 52.h, + child: ElevatedButton( + onPressed: () { + if (manualInputController.text.isNotEmpty) { + setState(() { + _breedController.text = manualInputController.text; + }); + Navigator.pop(context); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.highlight, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + ), + child: Text( + '완료', + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 16.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + SizedBox(height: MediaQuery.of(context).viewInsets.bottom), + ], + ), + ), + ); + }, + ); + } + + // 성별 선택 모달 (남아/여아/기타 + 중성화) + void _showGenderSelectionModal() { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (context) { + // 모달 내부 임시 상태 + String? tempGender = _selectedGender; + bool tempNeutered = _isNeutered; + + return StatefulBuilder( + builder: (BuildContext context, StateSetter setModalState) { + return Container( + // height 제거 (내용물 크기에 맞춤) + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20.r)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, // 내용물만큼만 차지 + children: [ + // 상단 네비게이션바 + Padding( + padding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 12.h, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox(width: 24.w), // 닫기 버튼과 대칭을 위한 여백 + Text( + '성별 선택', + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + GestureDetector( + onTap: () => Navigator.pop(context), + child: Icon( + Icons.close, + size: 24.w, + color: Colors.black, + ), + ), + ], + ), + ), + Divider(color: const Color(0xFFEEEEEE), thickness: 1.h), + SizedBox(height: 30.h), + + // 성별 선택 버튼 영역 (3개) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + children: [ + Expanded( + child: _buildGenderCard( + '남아', + Icons.male, + tempGender == '남아', + (val) => setModalState(() => tempGender = val), + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildGenderCard( + '여아', + Icons.female, + tempGender == '여아', + (val) => setModalState(() => tempGender = val), + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildGenderCard( + '기타', + Icons.question_mark, + tempGender == '기타', + (val) => setModalState(() => tempGender = val), + ), + ), + ], + ), + ), + const SizedBox(height: 30), + + // 중성화 여부 체크박스 + GestureDetector( + onTap: () { + setModalState(() { + tempNeutered = !tempNeutered; + }); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + tempNeutered + ? Icons.check_box + : Icons.check_box_outline_blank, + color: tempNeutered + ? AppColors.highlight + : Colors.grey, + size: 24.w, + ), + SizedBox(width: 8.w), + Text( + '중성화를 했어요', + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 16.sp, + color: Colors.black87, + ), + ), + ], + ), + ), + SizedBox(height: 12.h), // 간격 축소 + // 완료 버튼 + Padding( + padding: EdgeInsets.fromLTRB(20.w, 0, 20.w, 20.h), + child: SizedBox( + width: double.infinity, + height: 52.h, + child: ElevatedButton( + onPressed: () { + setState(() { + _selectedGender = tempGender; + _isNeutered = tempNeutered; + if (_selectedGender != null) { + if (_selectedGender == '기타') { + _genderController.text = '기타'; + } else { + _genderController.text = + '$_selectedGender${_isNeutered ? '(중성화)' : ''}'; + } + } + }); + Navigator.pop(context); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.highlight, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + '선택 완료', + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ); + }, + ); + }, + ); + } + + Widget _buildGenderCard( + String gender, + IconData icon, + bool isSelected, + Function(String) onTap, + ) { + return GestureDetector( + onTap: () => onTap(gender), + child: Container( + width: 120.w, + height: 100.h, + decoration: BoxDecoration( + color: isSelected + ? AppColors.highlight.withOpacity(0.1) + : Colors.white, + borderRadius: BorderRadius.circular(16.r), + border: Border.all( + color: isSelected ? AppColors.highlight : const Color(0xFFEEEEEE), + width: 2.w, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + size: 40.w, + color: isSelected ? AppColors.highlight : Colors.grey, + ), + SizedBox(height: 12.h), + Text( + gender, + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 16.sp, + fontWeight: FontWeight.bold, + color: isSelected ? AppColors.highlight : Colors.grey, + ), + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + title: Text( + '반려동물 등록', + style: TextStyle( + color: Color(0xFF1f1f1f), + fontFamily: 'SCDream', + fontWeight: FontWeight.w500, + fontSize: 15.sp, + ), + ), + centerTitle: true, + backgroundColor: Colors.white, + scrolledUnderElevation: 0, + elevation: 0, + leading: IconButton( + icon: Icon(Icons.arrow_back_ios, color: Colors.black, size: 16.w), + onPressed: () => Navigator.pop(context), + ), + ), + body: SingleChildScrollView( + padding: EdgeInsets.all(20.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 1. 프로필 이미지 영역 + Center( + child: GestureDetector( + onTap: _pickImage, + child: Stack( + children: [ + Container( + width: 100.w, + height: 100.w, + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + shape: BoxShape.circle, + border: Border.all(color: const Color(0xFFEEEEEE)), + image: _profileImage != null + ? DecorationImage( + image: FileImage(_profileImage!), + fit: BoxFit.cover, + ) + : null, + ), + child: _profileImage == null + ? Center( + child: SvgPicture.asset( + 'assets/icons/profileicon.svg', + width: 40.w, + colorFilter: ColorFilter.mode( + Colors.grey[400]!, + BlendMode.srcIn, + ), + ), + ) + : null, + ), + Positioned( + bottom: 0, + right: 0, + child: Container( + padding: EdgeInsets.all(6.w), + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + border: Border.all(color: const Color(0xFFEEEEEE)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 4.r, + offset: Offset(0, 2.h), + ), + ], + ), + child: Icon( + Icons.camera_alt, + size: 16.w, + color: Colors.black87, + ), + ), + ), + ], + ), + ), + ), + SizedBox(height: 30.h), + + // 2. 반려동물 이름 입력 + _buildLabel('반려동물 이름 입력', isRequired: true), + SizedBox(height: 8.h), + _buildTextField( + controller: _nameController, + hint: '이름 입력 (2~10글자/영문/숫자/한글)', + inputFormatters: [ + LengthLimitingTextInputFormatter(10), // 최대 10글자 제한 + ], + ), + SizedBox(height: 24.h), + + // 3. 선택 박스들 (종, 품종, 성별) + _buildSearchField( + '반려동물 종 선택', + controller: _speciesController, + readOnly: true, + onTap: _showSpeciesSelectionModal, + ), + SizedBox(height: 12.h), + _buildSearchField( + '반려동물 품종 선택', + controller: _breedController, + readOnly: true, + onTap: _showBreedSelectionModal, + ), + const SizedBox(height: 12), + _buildSearchField( + '반려동물 성별', + controller: _genderController, + readOnly: true, + onTap: _showGenderSelectionModal, + ), + const SizedBox(height: 24), + + // 4. 생년월일 + _buildLabel('반려동물 생년월일', isRequired: true), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: _buildTextField( + controller: _yearController, + hint: 'YYYY', + textAlign: TextAlign.center, + hintColor: _isDateUnknown + ? const Color(0xFFC8C8C8) + : const Color(0xFF7D7C7C), + enabled: !_isDateUnknown, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(4), + ], + ), + ), + SizedBox(width: 12.w), + Expanded( + child: _buildTextField( + controller: _monthController, + hint: 'MM', + textAlign: TextAlign.center, + hintColor: _isDateUnknown + ? const Color(0xFFC8C8C8) + : const Color(0xFF7D7C7C), + enabled: !_isDateUnknown, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(2), + _DateRangeInputFormatter(min: 1, max: 12), + ], + ), + ), + SizedBox(width: 12.w), + Expanded( + child: _buildTextField( + controller: _dayController, + hint: 'DD', + textAlign: TextAlign.center, + hintColor: _isDateUnknown + ? const Color(0xFFC8C8C8) + : const Color(0xFF7D7C7C), + enabled: !_isDateUnknown, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(2), + _DayInputFormatter( + monthController: _monthController, + yearController: _yearController, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + GestureDetector( + onTap: _toggleDateUnknown, + child: Container( + width: double.infinity, + padding: EdgeInsets.symmetric(vertical: 14.h), + decoration: BoxDecoration( + color: _isDateUnknown + ? AppColors.subHighlight + : AppColors.inactive, + borderRadius: BorderRadius.circular(30.r), + ), + child: Center( + child: Text( + '정확한 날짜를 몰라요', + style: TextStyle( + fontFamily: 'SCDream', + color: Colors.white, + fontWeight: FontWeight.w500, + fontSize: 14.sp, + ), + ), + ), + ), + ), + const SizedBox(height: 24), + + // 5. 동물 등록 번호 + _buildLabel('동물 등록 번호', isRequired: false), + const SizedBox(height: 8), + _buildTextField( + hint: '동물 등록 번호 입력', + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(15), + ], + ), + const SizedBox(height: 24), + + // 6. 질환 정보 (검색 아이콘 포함) + _buildSearchField( + '보유 질환', + controller: _diseaseController, + readOnly: true, + onTap: () => _showSelectionModal( + title: '보유 질환 선택', + currentSelected: _selectedDiseases, + currentOtherText: _otherDiseaseText, + onComplete: (selected, otherText) { + setState(() { + _selectedDiseases = selected; + _otherDiseaseText = otherText; + + // 텍스트 필드 표시용 문자열 생성 + List displayList = selected + .where((e) => e != '기타') + .toList(); + if (selected.contains('기타') && otherText.isNotEmpty) { + displayList.add('기타($otherText)'); + } else if (selected.contains('기타')) { + displayList.add('기타'); + } + _diseaseController.text = displayList.join(', '); + }); + }, + ), + ), + const SizedBox(height: 24), + _buildSearchField( + '과거 진단받은 질병', + controller: _pastDiseaseController, + readOnly: true, + onTap: () => _showSelectionModal( + title: '과거 진단받은 질병 선택', + currentSelected: _selectedPastDiseases, + currentOtherText: _otherPastDiseaseText, + onComplete: (selected, otherText) { + setState(() { + _selectedPastDiseases = selected; + _otherPastDiseaseText = otherText; + + List displayList = selected + .where((e) => e != '기타') + .toList(); + if (selected.contains('기타') && otherText.isNotEmpty) { + displayList.add('기타($otherText)'); + } else if (selected.contains('기타')) { + displayList.add('기타'); + } + _pastDiseaseController.text = displayList.join(', '); + }); + }, + ), + ), + const SizedBox(height: 24), + _buildSearchField( + '염려되는 건강 문제', + controller: _healthConcernController, + readOnly: true, + onTap: () => _showSelectionModal( + title: '염려되는 건강 문제 선택', + currentSelected: _selectedHealthConcerns, + currentOtherText: _otherHealthConcernText, + onComplete: (selected, otherText) { + setState(() { + _selectedHealthConcerns = selected; + _otherHealthConcernText = otherText; + + List displayList = selected + .where((e) => e != '기타') + .toList(); + if (selected.contains('기타') && otherText.isNotEmpty) { + displayList.add('기타($otherText)'); + } else if (selected.contains('기타')) { + displayList.add('기타'); + } + _healthConcernController.text = displayList.join(', '); + }); + }, + ), + ), + + SizedBox(height: 40.h), + + // 7. 등록 버튼 + SizedBox( + width: double.infinity, + height: 52.h, + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.highlight, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30.r), + ), + ), + child: Text( + '반려동물 등록', + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 16.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + SizedBox(height: 20.h), + ], + ), + ), + ); + } + + // Helper Widget: 라벨 (필수 표시 포함) + Widget _buildLabel(String text, {bool isRequired = false}) { + return Row( + children: [ + if (isRequired) + Padding( + padding: EdgeInsets.only(right: 4.w), + child: Icon(Icons.circle, size: 4.w, color: Colors.red), + ), + Text( + text, + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 13.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ], + ); + } + + // Helper Widget: 텍스트 입력 필드 + Widget _buildTextField({ + required String hint, + TextAlign textAlign = TextAlign.start, + Color? hintColor, + bool enabled = true, + TextEditingController? controller, + TextInputType? keyboardType, + List? inputFormatters, + }) { + return TextField( + controller: controller, + enabled: enabled, + textAlign: textAlign, + keyboardType: keyboardType, + inputFormatters: inputFormatters, + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 14.sp, + color: AppColors.text, + ), + decoration: InputDecoration( + hintText: hint, + hintStyle: TextStyle( + fontFamily: 'SCDream', + fontSize: 14.sp, + color: hintColor ?? Colors.grey, + ), + enabledBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: Color(0xFFDDDDDD)), + ), + focusedBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: AppColors.highlight), + ), + contentPadding: EdgeInsets.symmetric(vertical: 8.h), + isDense: true, + ), + ); + } + + Widget _buildSelectionBox(String text, {required bool isRequired}) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 14.h), + decoration: BoxDecoration( + border: Border.all(color: const Color(0xFFDDDDDD)), + borderRadius: BorderRadius.circular(4.r), + ), + child: Row( + children: [ + if (isRequired) + Padding( + padding: EdgeInsets.only(right: 6.w), + child: Icon(Icons.circle, size: 4.w, color: Colors.red), + ), + Expanded( + child: Text( + text, + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 14.sp, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + ), + ), + Icon(Icons.arrow_forward_ios, size: 14.w, color: Colors.grey), + ], + ), + ); + } + + Widget _buildSearchField( + String label, { + TextEditingController? controller, + VoidCallback? onTap, + bool readOnly = false, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 13.sp, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + TextField( + controller: controller, // 컨트롤러 연결 + onTap: onTap, // 탭 이벤트 연결 + readOnly: readOnly, // 읽기 전용 여부 (키보드 방지) + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 14.sp, + overflow: TextOverflow.ellipsis, // ... 생략 표시 + ), + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric(vertical: 8.h), + enabledBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: Color(0xFFDDDDDD)), + ), + focusedBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: AppColors.highlight), + ), + suffixIcon: const Icon(Icons.search, color: Colors.black87), + suffixIconConstraints: BoxConstraints( + minWidth: 24.w, + minHeight: 24.w, + ), + ), + ), + ], + ); + } +} + +// 범위 제한 Formatter (월: 1~12) +class _DateRangeInputFormatter extends TextInputFormatter { + final int min; + final int max; + + _DateRangeInputFormatter({required this.min, required this.max}); + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + if (newValue.text.isEmpty) { + return newValue; + } + + final int? value = int.tryParse(newValue.text); + if (value == null) { + return oldValue; + } + + if (value < 0) { + return oldValue; + } + + // 입력 중에는 자릿수 제한이 있으므로, max값 초과 여부만 확인 + if (value > max) { + return oldValue; + } + + return newValue; + } +} + +// 일(Day) 입력 Formatter (월에 따라 28~31일 제한) +class _DayInputFormatter extends TextInputFormatter { + final TextEditingController monthController; + final TextEditingController yearController; + + _DayInputFormatter({ + required this.monthController, + required this.yearController, + }); + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + if (newValue.text.isEmpty) { + return newValue; + } + + final int? day = int.tryParse(newValue.text); + if (day == null) { + return oldValue; + } + + int maxDay = 31; + final int? month = int.tryParse(monthController.text); + + if (month != null) { + if (month == 2) { + maxDay = 29; // 기본 29일 + // 윤년 계산 + final int? year = int.tryParse(yearController.text); + if (year != null) { + bool isLeap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); + maxDay = isLeap ? 29 : 28; + } + } else if ([4, 6, 9, 11].contains(month)) { + maxDay = 30; + } + } + + if (day > maxDay) { + return oldValue; + } + + return newValue; + } +} diff --git a/app/lib/screens/reservation_screen.dart b/app/lib/screens/reservation_screen.dart index e79e429..9bb59f6 100644 --- a/app/lib/screens/reservation_screen.dart +++ b/app/lib/screens/reservation_screen.dart @@ -6,6 +6,7 @@ class ReservationScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Colors.white, body: const SafeArea( child: Center( child: Text( diff --git a/app/lib/screens/shop_screen.dart b/app/lib/screens/shop_screen.dart index f1f834c..4152071 100644 --- a/app/lib/screens/shop_screen.dart +++ b/app/lib/screens/shop_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; // Import screenutil class ShopScreen extends StatelessWidget { const ShopScreen({super.key}); @@ -6,11 +7,12 @@ class ShopScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - body: const SafeArea( + backgroundColor: Colors.white, + body: SafeArea( child: Center( child: Text( '상점 화면 준비 중입니다.', - style: TextStyle(fontFamily: 'SCDream'), + style: TextStyle(fontFamily: 'SCDream', fontSize: 14.sp), ), ), ), diff --git a/app/lib/screens/signup_screen.dart b/app/lib/screens/signup_screen.dart deleted file mode 100644 index 0cf1f7e..0000000 --- a/app/lib/screens/signup_screen.dart +++ /dev/null @@ -1,259 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import '../services/auth_service.dart'; -import 'main_screen.dart'; -import 'terms_agreement_screen.dart'; - -class SignupScreen extends StatefulWidget { - const SignupScreen({super.key}); - - @override - State createState() => _SignupScreenState(); -} - -class _SignupScreenState extends State { - bool _isLoading = false; - - Future _handleGoogleLogin() async { - setState(() { - _isLoading = true; - }); - - try { - final authService = AuthService(); - // Returns Map? { 'credential': ..., 'isNewUser': bool } - final result = await authService.signInWithGoogle(); - - if (!mounted) return; - - if (result != null) { - final isNewUser = result['isNewUser'] as bool; - - if (isNewUser) { - // 신규 가입자 -> 약관 동의 화면으로 이동 - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - TermsAgreementScreen(idToken: result['idToken']), - ), - ); - } else { - // 기존 가입자 -> 메인 화면으로 이동 - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const MainScreen()), - ); - } - } else { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('로그인이 취소되었습니다.'))); - } - } catch (e) { - if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('로그인 실패: $e'))); - } finally { - if (mounted) { - setState(() { - _isLoading = false; - }); - } - } - } - - @override - Widget build(BuildContext context) { - return Stack( - children: [ - Scaffold( - backgroundColor: Colors.white, - appBar: AppBar( - backgroundColor: Colors.white, - elevation: 0, - leading: IconButton( - icon: const Icon( - Icons.arrow_back_ios, - color: Colors.black, - size: 20, - ), - onPressed: () => Navigator.pop(context), - ), - title: const Text( - '회원가입', - style: TextStyle( - fontSize: 15, - fontFamily: 'SCDream', - fontWeight: FontWeight.w500, - color: Colors.black, - ), - ), - centerTitle: true, - ), - body: Padding( - padding: const EdgeInsets.fromLTRB(20, 0, 20, 240), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Image.asset('assets/img/foot.png', width: 30), - const SizedBox(width: 10), - const Text( - '간편 로그인', - style: TextStyle( - fontSize: 24, - fontFamily: 'SCDream', - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - ], - ), - const SizedBox(height: 10), - const Text( - '똑똑한 반려생활을 위한 첫걸음,\nRUP에 오신것을 환영해요!', - style: TextStyle( - fontSize: 14, - fontFamily: 'SCDream', - fontWeight: FontWeight.w500, - color: Colors.black, - ), - textAlign: TextAlign.start, - ), - const Spacer(), - Align( - alignment: Alignment.centerRight, - child: Image.asset('assets/img/cat.png', height: 150), - ), - const SizedBox(height: 10), - ], - ), - ), - bottomSheet: Container( - color: Colors.white, - padding: const EdgeInsets.fromLTRB(20, 20, 20, 40), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Naver Login Button - _SocialLoginButton( - text: '네이버 로그인', - textColor: Colors.white, - fontFamily: 'SCDream', - fontWeight: FontWeight.bold, - fontSize: 15, - backgroundColor: const Color(0xFF00D03F), - onPressed: () {}, - iconPath: 'assets/icons/navericon.svg', - ), - const SizedBox(height: 15), - // Kakao Login Button - _SocialLoginButton( - text: '카카오로 3초만에 시작하기', - textColor: const Color(0xFF212121), - fontFamily: 'SCDream', - fontWeight: FontWeight.bold, - fontSize: 15, - backgroundColor: const Color(0xFFFAE100), - onPressed: () {}, - iconPath: 'assets/icons/kakaoicon.svg', - ), - const SizedBox(height: 15), - // Google Login Button - _SocialLoginButton( - text: '구글 로그인', - textColor: const Color(0xFF17191A), - fontFamily: 'SCDream', - fontWeight: FontWeight.bold, - fontSize: 15, - backgroundColor: Colors.white, - onPressed: _handleGoogleLogin, - iconPath: 'assets/icons/googleicon.svg', - isBordered: true, - ), - ], - ), - ), - ), - if (_isLoading) - Container( - color: Colors.black.withOpacity(0.5), - child: const Center(child: CircularProgressIndicator()), - ), - ], - ); - } -} - -class _SocialLoginButton extends StatelessWidget { - final String text; - final Color textColor; - final Color backgroundColor; - final VoidCallback onPressed; - final String iconPath; - final bool isBordered; - final String fontFamily; - final double fontSize; - final FontWeight fontWeight; - - const _SocialLoginButton({ - required this.text, - required this.textColor, - required this.backgroundColor, - required this.onPressed, - required this.iconPath, - this.isBordered = false, - this.fontFamily = 'SCDream', - this.fontWeight = FontWeight.w500, - this.fontSize = 16, - }); - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 50, - child: ElevatedButton( - onPressed: onPressed, - style: ElevatedButton.styleFrom( - backgroundColor: backgroundColor, - foregroundColor: textColor, - elevation: 0, - shape: StadiumBorder( - side: isBordered - ? BorderSide(color: Colors.grey[300]!) - : BorderSide.none, - ), - padding: EdgeInsets.symmetric(horizontal: 20), - ), - child: Row( - children: [ - SvgPicture.asset( - iconPath, - width: 24, - height: 24, - fit: BoxFit.contain, - ), - Expanded( - child: Text( - text, - style: TextStyle( - fontFamily: fontFamily, - fontWeight: fontWeight, - fontSize: fontSize, - ), - textAlign: TextAlign.center, - ), - ), - SizedBox(width: 24), - ], - ), - ), - ); - } -} diff --git a/app/lib/screens/splash_screen.dart b/app/lib/screens/splash_screen.dart index c9bdb46..ad796fa 100644 --- a/app/lib/screens/splash_screen.dart +++ b/app/lib/screens/splash_screen.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; // Import screenutil import 'welcome_screen.dart'; import 'main_screen.dart'; import '../services/auth_service.dart'; // Import AuthService @@ -30,16 +31,20 @@ class _SplashScreenState extends State { if (token != null) { // 토큰이 있으면 메인 화면으로 (자동 로그인) - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const MainScreen()), - ); + if (mounted) { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => const MainScreen()), + ); + } } else { // 토큰이 없으면 웰컴 화면으로 - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const WelcomeScreen()), - ); + if (mounted) { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => const WelcomeScreen()), + ); + } } } @@ -51,14 +56,14 @@ class _SplashScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Image.asset('assets/img/maindog.png', width: 150, height: 150), - const SizedBox(height: 20), - const Text( + Image.asset('assets/img/maindog.png', width: 150.w, height: 150.h), + SizedBox(height: 20.h), + Text( 'RUP', style: TextStyle( fontFamily: 'SCDream', fontWeight: FontWeight.bold, - fontSize: 32, + fontSize: 32.sp, color: Colors.white, ), ), diff --git a/app/lib/screens/terms_agreement_screen.dart b/app/lib/screens/terms_agreement_screen.dart index c25e3db..46e1a23 100644 --- a/app/lib/screens/terms_agreement_screen.dart +++ b/app/lib/screens/terms_agreement_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; // Import screenutil import 'identity_verification_screen.dart'; import '../services/auth_service.dart'; // Import AuthService import '../data/terms_data.dart'; @@ -59,41 +60,41 @@ class _TermsAgreementScreenState extends State { maxChildSize: 0.9, builder: (_, controller) { return Container( - decoration: const BoxDecoration( + decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + borderRadius: BorderRadius.vertical(top: Radius.circular(20.r)), ), - padding: const EdgeInsets.all(20), + padding: EdgeInsets.all(20.w), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container( - width: 40, - height: 4, + width: 40.w, + height: 4.h, decoration: BoxDecoration( color: Colors.grey[300], - borderRadius: BorderRadius.circular(2), + borderRadius: BorderRadius.circular(2.r), ), ), ), - const SizedBox(height: 20), + SizedBox(height: 20.h), Text( title, - style: const TextStyle( - fontSize: 18, + style: TextStyle( + fontSize: 18.sp, fontWeight: FontWeight.bold, fontFamily: 'SCDream', ), ), - const SizedBox(height: 20), + SizedBox(height: 20.h), Expanded( child: SingleChildScrollView( controller: controller, child: Text( _getTermContent(index), - style: const TextStyle( - fontSize: 15, + style: TextStyle( + fontSize: 15.sp, height: 1.6, fontFamily: 'SCDream', color: Colors.black87, @@ -101,24 +102,24 @@ class _TermsAgreementScreenState extends State { ), ), ), - const SizedBox(height: 20), + SizedBox(height: 20.h), SizedBox( width: double.infinity, - height: 52, + height: 52.h, child: ElevatedButton( onPressed: () => Navigator.pop(context), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFFF7500), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30), + borderRadius: BorderRadius.circular(30.r), ), elevation: 0, ), - child: const Text( + child: Text( '확인', style: TextStyle( color: Colors.white, - fontSize: 16, + fontSize: 16.sp, fontWeight: FontWeight.bold, fontFamily: 'SCDream', ), @@ -142,14 +143,14 @@ class _TermsAgreementScreenState extends State { backgroundColor: Colors.white, elevation: 0, leading: IconButton( - icon: const Icon(Icons.arrow_back_ios, color: Colors.black, size: 20), + icon: Icon(Icons.arrow_back_ios, color: Colors.black, size: 20.w), onPressed: () => Navigator.pop(context), ), title: Text( widget.isViewOnly ? '이용 약관' : '회원가입', - style: const TextStyle( + style: TextStyle( color: Colors.black, - fontSize: 16, + fontSize: 16.sp, fontWeight: FontWeight.w600, fontFamily: 'SCDream', ), @@ -161,24 +162,24 @@ class _TermsAgreementScreenState extends State { children: [ Expanded( child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 20), + padding: EdgeInsets.symmetric(horizontal: 20.w), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(height: 20), + SizedBox(height: 20.h), // Header Area Row( children: [ Image.asset( 'assets/img/foot.png', - width: 24, - height: 24, + width: 24.w, + height: 24.h, ), - const SizedBox(width: 8), - const Text( + SizedBox(width: 8.w), + Text( '서비스 이용 약관', style: TextStyle( - fontSize: 22, + fontSize: 22.sp, fontWeight: FontWeight.bold, fontFamily: 'SCDream', color: Colors.black, @@ -186,32 +187,32 @@ class _TermsAgreementScreenState extends State { ), ], ), - const SizedBox(height: 8), + SizedBox(height: 8.h), Text( widget.isViewOnly ? 'RUP 서비스의 이용 약관 내용입니다.' : '서비스 이용을 위해 약관에 동의해주세요.', - style: const TextStyle( - fontSize: 15, + style: TextStyle( + fontSize: 15.sp, color: Colors.black54, fontFamily: 'SCDream', ), ), - const SizedBox(height: 30), + SizedBox(height: 30.h), // All Agree Box - *ViewOnly 모드에서는 숨김* if (!widget.isViewOnly) ...[ InkWell( onTap: () => _toggleAll(), - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(8.r), child: Container( - padding: const EdgeInsets.symmetric( - vertical: 16, - horizontal: 16, + padding: EdgeInsets.symmetric( + vertical: 16.h, + horizontal: 16.w, ), decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(8.r), ), child: Row( children: [ @@ -223,11 +224,11 @@ class _TermsAgreementScreenState extends State { ? const Color(0xFFFF7500) : Colors.grey, ), - const SizedBox(width: 12), - const Text( + SizedBox(width: 12.w), + Text( '모든 약관에 동의합니다.', style: TextStyle( - fontSize: 16, + fontSize: 16.sp, fontWeight: FontWeight.bold, fontFamily: 'SCDream', ), @@ -236,9 +237,9 @@ class _TermsAgreementScreenState extends State { ), ), ), - const SizedBox(height: 20), - const Divider(height: 1, color: Color(0xFFEEEEEE)), - const SizedBox(height: 10), + SizedBox(height: 20.h), + Divider(height: 1.h, color: const Color(0xFFEEEEEE)), + SizedBox(height: 10.h), ], // Individual Items @@ -255,10 +256,10 @@ class _TermsAgreementScreenState extends State { // Bottom Button Padding( - padding: const EdgeInsets.fromLTRB(20, 10, 20, 20), + padding: EdgeInsets.fromLTRB(20.w, 10.h, 20.w, 20.h), child: SizedBox( width: double.infinity, - height: 52, + height: 52.h, child: ElevatedButton( onPressed: widget.isViewOnly ? () => Navigator.pop(context) @@ -299,15 +300,15 @@ class _TermsAgreementScreenState extends State { backgroundColor: const Color(0xFFFF7500), disabledBackgroundColor: Colors.grey[300], shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30), + borderRadius: BorderRadius.circular(30.r), ), elevation: 0, ), child: Text( widget.isViewOnly ? '닫기' : '동의하고 본인 인증하기', - style: const TextStyle( + style: TextStyle( color: Colors.white, - fontSize: 16, + fontSize: 16.sp, fontWeight: FontWeight.bold, fontFamily: 'SCDream', ), @@ -326,7 +327,7 @@ class _TermsAgreementScreenState extends State { // ViewOnly 모드에서는 텍스트 클릭 시 모달 띄우기 (토글 X) return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), + padding: EdgeInsets.symmetric(vertical: 4.h), child: Row( children: [ Expanded( @@ -339,24 +340,21 @@ class _TermsAgreementScreenState extends State { _showTermDetail(context, title, index); } }, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(8.r), child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 8, - ), + padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 8.w), child: Row( children: [ // 체크 Icon - *ViewOnly 모드에서는 숨김* if (!widget.isViewOnly) ...[ Icon( Icons.check, - size: 20, + size: 20.w, color: _checks[index] ? const Color(0xFFFF7500) : Colors.grey[300], ), - const SizedBox(width: 12), + SizedBox(width: 12.w), ], Text( @@ -366,16 +364,16 @@ class _TermsAgreementScreenState extends State { ? const Color(0xFFFF7500) : Colors.grey, fontWeight: FontWeight.bold, - fontSize: 14, + fontSize: 14.sp, fontFamily: 'SCDream', ), ), - const SizedBox(width: 8), + SizedBox(width: 8.w), Expanded( child: Text( title, - style: const TextStyle( - fontSize: 15, + style: TextStyle( + fontSize: 15.sp, color: Colors.black87, fontFamily: 'SCDream', ), @@ -388,13 +386,13 @@ class _TermsAgreementScreenState extends State { ), IconButton( onPressed: () => _showTermDetail(context, title, index), - icon: const Icon( + icon: Icon( Icons.arrow_forward_ios, - size: 14, + size: 14.w, color: Colors.black, ), - splashRadius: 20, - padding: const EdgeInsets.all(12), + splashRadius: 20.r, + padding: EdgeInsets.all(12.w), constraints: const BoxConstraints(), ), ], diff --git a/app/lib/theme/app_colors.dart b/app/lib/theme/app_colors.dart index 21111c3..1be0994 100644 --- a/app/lib/theme/app_colors.dart +++ b/app/lib/theme/app_colors.dart @@ -12,4 +12,7 @@ class AppColors { // 강조색: #FF7500 (버튼, 호버 등 중요!) static const Color highlight = Color(0xFFFF7500); + + // 부강조색: #FBB800 + static const Color subHighlight = Color(0xFFFBB800); } diff --git a/app/lib/widgets/intro_body.dart b/app/lib/widgets/intro_body.dart index 7ad20bd..23d553d 100644 --- a/app/lib/widgets/intro_body.dart +++ b/app/lib/widgets/intro_body.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; // Import screenutil class IntroBody extends StatelessWidget { const IntroBody({super.key}); @@ -6,8 +7,8 @@ class IntroBody extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - padding: EdgeInsets.fromLTRB(40, 0, 40, 0), - margin: EdgeInsets.only(top: 156), + padding: EdgeInsets.fromLTRB(40.w, 0, 40.w, 0), + margin: EdgeInsets.only(top: 156.h), child: LayoutBuilder( builder: (context, constraints) { return FittedBox( @@ -28,7 +29,7 @@ class IntroBody extends StatelessWidget { Text( 'Right', style: TextStyle( - fontSize: 60, + fontSize: 60.sp, fontFamily: 'Ownglyph', color: Colors.white, ), @@ -36,17 +37,17 @@ class IntroBody extends StatelessWidget { Text( 'Use for', style: TextStyle( - fontSize: 60, + fontSize: 60.sp, fontFamily: 'Ownglyph', color: Colors.white, ), ), Padding( - padding: const EdgeInsets.only(bottom: 26), + padding: EdgeInsets.only(bottom: 26.h), child: Text( 'Pet', style: TextStyle( - fontSize: 60, + fontSize: 60.sp, fontFamily: 'Ownglyph', color: Colors.white, ), @@ -54,17 +55,26 @@ class IntroBody extends StatelessWidget { ), ], ), - Image.asset('assets/img/mainball.png'), + Image.asset( + 'assets/img/mainball.png', + width: 100.w, + ), // Added explicit width control ], ), Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ - Image.asset('assets/img/maindog.png'), + Image.asset( + 'assets/img/maindog.png', + width: 150.w, + ), // Added explicit width control Opacity( opacity: 0.0, - child: Image.asset('assets/img/mainball.png'), + child: Image.asset( + 'assets/img/mainball.png', + width: 100.w, + ), // Added explicit width control ), ], ), diff --git a/app/lib/widgets/login_panel.dart b/app/lib/widgets/login_panel.dart index 7481dfd..f3b6611 100644 --- a/app/lib/widgets/login_panel.dart +++ b/app/lib/widgets/login_panel.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import '../screens/signup_screen.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; // Import screenutil import '../screens/login_screen.dart'; class LoginPanel extends StatelessWidget { @@ -9,8 +9,8 @@ class LoginPanel extends StatelessWidget { Widget build(BuildContext context) { return Container( width: double.infinity, - color: Color(0xFFFF7500), - padding: EdgeInsets.fromLTRB(40, 0, 40, 40), + color: const Color(0xFFFF7500), + padding: EdgeInsets.fromLTRB(40.w, 0, 40.w, 40.h), child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, @@ -18,27 +18,27 @@ class LoginPanel extends StatelessWidget { Text( '간편한 반려생활을 위한 첫걸음,\nRUP에 오신것을 환영합니다!', style: TextStyle( - fontSize: 12, + fontSize: 12.sp, fontFamily: 'SCDream', fontWeight: FontWeight.w500, color: Colors.black, ), textAlign: TextAlign.center, ), - SizedBox(height: 30), + SizedBox(height: 30.h), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () { Navigator.push( context, - MaterialPageRoute(builder: (context) => SignupScreen()), + MaterialPageRoute(builder: (context) => const LoginScreen()), ); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.white, - shape: StadiumBorder(), - padding: EdgeInsets.all(15), + shape: const StadiumBorder(), + padding: EdgeInsets.all(15.w), ), child: Stack( alignment: Alignment.center, @@ -49,7 +49,9 @@ class LoginPanel extends StatelessWidget { '시작하기', style: TextStyle( fontFamily: 'SCDream', - fontWeight: FontWeight.bold, + fontWeight: FontWeight + .bold, // Fixed typo in previous code if any, bold is correct + fontSize: 14.sp, // Added font size color: Colors.black, ), ), @@ -58,27 +60,6 @@ class LoginPanel extends StatelessWidget { ), ), ), - - SizedBox(height: 5), - TextButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => LoginScreen()), - ); - }, - child: Text( - '기존 계정으로 로그인', - style: TextStyle( - fontFamily: 'SCDream', - fontWeight: FontWeight.w500, - fontSize: 12, - color: Colors.white, - decoration: TextDecoration.underline, - decorationColor: Colors.white, - ), - ), - ), ], ), ); diff --git a/app/linux/flutter/generated_plugin_registrant.cc b/app/linux/flutter/generated_plugin_registrant.cc index d0e7f79..85a2413 100644 --- a/app/linux/flutter/generated_plugin_registrant.cc +++ b/app/linux/flutter/generated_plugin_registrant.cc @@ -6,9 +6,13 @@ #include "generated_plugin_registrant.h" +#include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); diff --git a/app/linux/flutter/generated_plugins.cmake b/app/linux/flutter/generated_plugins.cmake index b29e9ba..62e3ed5 100644 --- a/app/linux/flutter/generated_plugins.cmake +++ b/app/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux flutter_secure_storage_linux ) diff --git a/app/macos/Flutter/GeneratedPluginRegistrant.swift b/app/macos/Flutter/GeneratedPluginRegistrant.swift index f824f2f..5a57a16 100644 --- a/app/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import file_selector_macos import firebase_auth import firebase_core import flutter_secure_storage_macos @@ -12,6 +13,7 @@ import google_sign_in_ios import video_player_avfoundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) diff --git a/app/pubspec.lock b/app/pubspec.lock index 3709a16..aa1121c 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -65,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608" + url: "https://pub.dev" + source: hosted + version: "0.3.5+1" crypto: dependency: transitive description: @@ -129,6 +137,38 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + url: "https://pub.dev" + source: hosted + version: "0.9.5" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://pub.dev" + source: hosted + version: "0.9.3+5" firebase_auth: dependency: "direct main" description: @@ -190,6 +230,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1 + url: "https://pub.dev" + source: hosted + version: "2.0.33" + flutter_screenutil: + dependency: "direct main" + description: + name: flutter_screenutil + sha256: "8239210dd68bee6b0577aa4a090890342d04a136ce1c81f98ee513fc0ce891de" + url: "https://pub.dev" + source: hosted + version: "5.9.3" flutter_secure_storage: dependency: "direct main" description: @@ -344,6 +400,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "297e42bd236c4ac4b091d4277292159b3280545e030cae2be3d503f9ecf7e6a1" + url: "https://pub.dev" + source: hosted + version: "0.8.13+12" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "956c16a42c0c708f914021666ffcd8265dde36e673c9fa68c81f7d085d9774ad" + url: "https://pub.dev" + source: hosted + version: "0.8.13+3" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" + url: "https://pub.dev" + source: hosted + version: "0.2.2+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" + url: "https://pub.dev" + source: hosted + version: "2.11.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" js: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 8a0bb07..53b8463 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -41,6 +41,8 @@ dependencies: google_sign_in: ^6.2.1 # 구글 로그인 UI/기능 (Updated) dio: ^5.4.0 flutter_secure_storage: ^9.0.0 + image_picker: ^1.2.1 + flutter_screenutil: ^5.9.3 dev_dependencies: flutter_test: diff --git a/app/windows/flutter/generated_plugin_registrant.cc b/app/windows/flutter/generated_plugin_registrant.cc index ea9741e..5b9f218 100644 --- a/app/windows/flutter/generated_plugin_registrant.cc +++ b/app/windows/flutter/generated_plugin_registrant.cc @@ -6,11 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); FirebaseAuthPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); FirebaseCorePluginCApiRegisterWithRegistrar( diff --git a/app/windows/flutter/generated_plugins.cmake b/app/windows/flutter/generated_plugins.cmake index b8ca912..d959a1b 100644 --- a/app/windows/flutter/generated_plugins.cmake +++ b/app/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows firebase_auth firebase_core flutter_secure_storage_windows