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 0000000..3b7bfcd
Binary files /dev/null and b/app/assets/img/badicon.png differ
diff --git a/app/assets/img/goodicon.png b/app/assets/img/goodicon.png
new file mode 100644
index 0000000..6aca8e1
Binary files /dev/null and b/app/assets/img/goodicon.png differ
diff --git a/app/assets/img/profile.png b/app/assets/img/profile.png
new file mode 100644
index 0000000..091876e
Binary files /dev/null and b/app/assets/img/profile.png differ
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