diff --git a/app/android/app/build.gradle.kts b/app/android/app/build.gradle.kts index cb28db6..ec2b21b 100644 --- a/app/android/app/build.gradle.kts +++ b/app/android/app/build.gradle.kts @@ -7,7 +7,7 @@ plugins { } android { - namespace = "com.example.app" + namespace = "com.daoblock.rup" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion @@ -22,7 +22,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "com.example.rup" + applicationId = "com.daoblock.rup" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion diff --git a/app/android/app/google-services.json b/app/android/app/google-services.json index 5e58445..5ad5a76 100644 --- a/app/android/app/google-services.json +++ b/app/android/app/google-services.json @@ -5,6 +5,43 @@ "storage_bucket": "rup-project-9d4c4.firebasestorage.app" }, "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:379988243470:android:348749e6ea9404d61cb85f", + "android_client_info": { + "package_name": "com.daoblock.rup" + } + }, + "oauth_client": [ + { + "client_id": "379988243470-gdevefapm92n317jo6rri67n83beql44.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.daoblock.rup", + "certificate_hash": "7bda16a71840f93ab7c41fecc24da2bde3702424" + } + }, + { + "client_id": "379988243470-g6490l8gucc3ljras93i28c3l4qlroi4.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyAql00w1JB6n5wb4StXi9eyixVyIuT3-Hg" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "379988243470-g6490l8gucc3ljras93i28c3l4qlroi4.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + }, { "client_info": { "mobilesdk_app_id": "1:379988243470:android:b4a4761b906eb44a1cb85f", diff --git a/app/android/app/src/main/kotlin/com/daoblock/rup/MainActivity.kt b/app/android/app/src/main/kotlin/com/daoblock/rup/MainActivity.kt new file mode 100644 index 0000000..5334889 --- /dev/null +++ b/app/android/app/src/main/kotlin/com/daoblock/rup/MainActivity.kt @@ -0,0 +1,5 @@ +package com.daoblock.rup + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/app/android/app/src/main/kotlin/com/example/app/MainActivity.kt b/app/android/app/src/main/kotlin/com/example/app/MainActivity.kt index 4c832cd..6411553 100644 --- a/app/android/app/src/main/kotlin/com/example/app/MainActivity.kt +++ b/app/android/app/src/main/kotlin/com/example/app/MainActivity.kt @@ -1,5 +1,3 @@ -package com.example.app - -import io.flutter.embedding.android.FlutterActivity - -class MainActivity : FlutterActivity() +// This file should be deleted. +// package com.example.app +// Moved to com.daoblock.rup diff --git a/app/assets/icons/appointmenticon.svg b/app/assets/icons/appointment_icon.svg similarity index 100% rename from app/assets/icons/appointmenticon.svg rename to app/assets/icons/appointment_icon.svg diff --git a/app/assets/icons/calendaricon.svg b/app/assets/icons/calendar_icon.svg similarity index 100% rename from app/assets/icons/calendaricon.svg rename to app/assets/icons/calendar_icon.svg diff --git a/app/assets/icons/catdogicon.svg b/app/assets/icons/catdog_icon.svg similarity index 100% rename from app/assets/icons/catdogicon.svg rename to app/assets/icons/catdog_icon.svg diff --git a/app/assets/icons/findicon.svg b/app/assets/icons/find_icon.svg similarity index 100% rename from app/assets/icons/findicon.svg rename to app/assets/icons/find_icon.svg diff --git a/app/assets/icons/flowericon.svg b/app/assets/icons/flower_icon.svg similarity index 100% rename from app/assets/icons/flowericon.svg rename to app/assets/icons/flower_icon.svg diff --git a/app/assets/icons/general_schedule_icon.svg b/app/assets/icons/general_schedule_icon.svg new file mode 100644 index 0000000..c6aed61 --- /dev/null +++ b/app/assets/icons/general_schedule_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/googleicon.svg b/app/assets/icons/google_icon.svg similarity index 100% rename from app/assets/icons/googleicon.svg rename to app/assets/icons/google_icon.svg diff --git a/app/assets/icons/homeicon.svg b/app/assets/icons/home_icon.svg similarity index 100% rename from app/assets/icons/homeicon.svg rename to app/assets/icons/home_icon.svg diff --git a/app/assets/icons/important_schedule_icon.svg b/app/assets/icons/important_schedule_icon.svg new file mode 100644 index 0000000..7cf74f1 --- /dev/null +++ b/app/assets/icons/important_schedule_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/incomplete_icon.svg b/app/assets/icons/incomplete_icon.svg new file mode 100644 index 0000000..57bb105 --- /dev/null +++ b/app/assets/icons/incomplete_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/kakaoicon.svg b/app/assets/icons/kakao_icon.svg similarity index 100% rename from app/assets/icons/kakaoicon.svg rename to app/assets/icons/kakao_icon.svg diff --git a/app/assets/icons/myicon.svg b/app/assets/icons/my_icon.svg similarity index 100% rename from app/assets/icons/myicon.svg rename to app/assets/icons/my_icon.svg diff --git a/app/assets/icons/navericon.svg b/app/assets/icons/naver_icon.svg similarity index 100% rename from app/assets/icons/navericon.svg rename to app/assets/icons/naver_icon.svg diff --git a/app/assets/icons/profileicon.svg b/app/assets/icons/profile_icon.svg similarity index 100% rename from app/assets/icons/profileicon.svg rename to app/assets/icons/profile_icon.svg diff --git a/app/assets/icons/shopicon.svg b/app/assets/icons/shop_icon.svg similarity index 100% rename from app/assets/icons/shopicon.svg rename to app/assets/icons/shop_icon.svg diff --git a/app/ios/Runner.xcodeproj/project.pbxproj b/app/ios/Runner.xcodeproj/project.pbxproj index 3995a47..148c971 100644 --- a/app/ios/Runner.xcodeproj/project.pbxproj +++ b/app/ios/Runner.xcodeproj/project.pbxproj @@ -368,7 +368,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.app; + PRODUCT_BUNDLE_IDENTIFIER = com.daoblock.rup; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -384,7 +384,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.app.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.daoblock.rup.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -401,7 +401,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.app.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.daoblock.rup.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -416,7 +416,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.app.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.daoblock.rup.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -547,7 +547,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.app; + PRODUCT_BUNDLE_IDENTIFIER = com.daoblock.rup; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -569,7 +569,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.app; + PRODUCT_BUNDLE_IDENTIFIER = com.daoblock.rup; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/app/lib/data/pet_data.dart b/app/lib/data/pet_data.dart new file mode 100644 index 0000000..1da7a11 --- /dev/null +++ b/app/lib/data/pet_data.dart @@ -0,0 +1,135 @@ +class PetData { + static const List diseaseList = [ + "피부질환", + "눈 질환", + "치아 / 구강 질환", + "뼈 / 관절 질환", + "생식기 / 비뇨기 질환", + "심장 / 혈관 질환", + "소화기 질환", + "호흡기 질환", + "내분비계 질환", + "뇌신경 질환", + "생식기 질환", + "귀 질환", + "코 질환", + "기타", + ]; + + static const Map>> breedsData = { + "포유류": { + "강아지": [ + "말티즈", + "푸들", + "포메라니안", + "믹스견", + "치와와", + "시츄", + "비숑 프리제", + "골든 리트리버", + "진돗개", + "웰시 코기", + "프렌치 불독", + "시바견", + "닥스후트", + "요크셔 테리어", + "보더 콜리", + "사모예드", + "허스키", + "말라뮤트", + "기타(직접 입력)", + ], + "고양이": [ + "코리안 숏헤어", + "브리티시 숏헤어", + "아메리칸 숏헤어", + "뱅갈", + "메인쿤", + "데본 렉스", + "페르시안", + "러시안 블루", + "샴", + "렉돌", + "스코티시 폴드", + "먼치킨", + "노르웨이 숲", + "믹스묘", + "기타(직접 입력)", + ], + "햄스터": ["정글리안", "펄", "푸딩", "골든 햄스터", "로보로브스키", "기타(직접 입력)"], + "토끼": ["롭이어", "더치", "라이언 헤드", "드워프", "렉스", "기타(직접 입력)"], + "기니피그": ["잉글리쉬", "아비시니안", "페루비안", "실키", "기타(직접 입력)"], + "고슴도치": ["플라티나", "화이트 초코", "알비노", "핀토", "기타(직접 입력)"], + "기타(직접 입력)": ["기타(직접 입력)"], + }, + "파충류": { + "거북이": ["커먼 머스크 터틀", "레이저백", "육지거북", "붉은귀거북", "남생이", "기타(직접 입력)"], + "도마뱀": [ + "크레스티드 게코", + "리키에너스 게코", + "가고일 게코", + "레오파드 게코", + "비어디 드래곤", + "블루텅 스킨크", + "이구아나", + "기타(직접 입력)", + ], + "뱀": [ + "볼 파이톤", + "가터 스네이크", + "호그노즈 스네이크", + "콘 스네이크", + "킹 스네이크", + "밀크 스네이크", + "기타(직접 입력)", + ], + "기타(직접 입력)": ["기타(직접 입력)"], + }, + "조류": { + "앵무새(소/중형)": [ + "사랑앵무(잉꼬)", + "코카티엘(왕관앵무)", + "모란앵무", + "코뉴어", + "퀘이커", + "카카리키", + "사자나미(빗금앵무)", + "유리앵무", + "기타(직접 입력)", + ], + "앵무새(대형)": [ + "뉴기니아", + "회색앵무", + "금강앵무(마카우)", + "유황앵무(코카투)", + "아마존앵무", + "대본영", + "기타(직접 입력)", + ], + "핀치/관상조": ["카나리아", "십자매", "문조", "금화조", "호금조", "백문조", "기타(직접 입력)"], + "비둘기/닭/메추리": [ + "애완용 비둘기", + "관상닭(실키 등)", + "메추리", + "미니메추리", + "오리/거위", + "기타(직접 입력)", + ], + "기타(직접 입력)": ["직접 입력"], + }, + "양서류": { + "개구리": ["청개구리", "팩맨", "다트 프록", "화이트 트리 프록", "기타(직접 입력)"], + "도룡뇽": ["우파루파", "파이어 벨리 뉴트", "타이거 살라만더", "기타(직접 입력)"], + "기타(직접 입력)": ["기타(직접 입력)"], + }, + "어류": { + "열대어": ["구피", "베타", "테트라", "디스커스", "엔젤피쉬", "기타(직접 입력)"], + "금붕어/잉어": ["금붕어", "비단잉어", "기타(직접 입력)"], + "해수어": ["크라운피쉬(니모)", "블루탱", "기타(직접 입력)"], + "기타(직접 입력)": ["기타(직접 입력)"], + }, + "기타(직접 입력)": { + "기타(직접 입력)": ["기타(직접 입력)"], + }, + }; +} diff --git a/app/lib/main.dart b/app/lib/main.dart index 99f3657..d3de19b 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:firebase_core/firebase_core.dart'; import 'dart:developer'; import 'screens/splash_screen.dart'; +import 'screens/register_complete_screen.dart'; import 'utils/log_manager.dart'; final GlobalKey navigatorKey = GlobalKey(); @@ -45,6 +46,9 @@ class RupApp extends StatelessWidget { navigatorKey: navigatorKey, debugShowCheckedModeBanner: false, home: const SplashScreen(), + routes: { + '/register_complete': (context) => const RegisterCompleteScreen(), + }, ); }, ); diff --git a/app/lib/models/pet_model.dart b/app/lib/models/pet_model.dart new file mode 100644 index 0000000..494e553 --- /dev/null +++ b/app/lib/models/pet_model.dart @@ -0,0 +1,79 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +class Pet { + final String id; + final String ownerId; + final String name; + final String species; // "강아지", "고양이", etc. + final String breed; // "말티즈", "코숏", etc. + final String gender; // "남아", "여아" + final bool isNeutered; + final DateTime? birthDate; + final bool isDateUnknown; + final String? registrationNumber; + final String? profileImageUrl; + final List diseases; + final List pastDiseases; + final List healthConcerns; + final DateTime createdAt; + + Pet({ + required this.id, + required this.ownerId, + required this.name, + required this.species, + required this.breed, + required this.gender, + required this.isNeutered, + this.birthDate, + required this.isDateUnknown, + this.registrationNumber, + this.profileImageUrl, + required this.diseases, + required this.pastDiseases, + required this.healthConcerns, + required this.createdAt, + }); + + Map toMap() { + return { + 'id': id, + 'ownerId': ownerId, + 'name': name, + 'species': species, + 'breed': breed, + 'gender': gender, + 'isNeutered': isNeutered, + 'birthDate': birthDate != null ? Timestamp.fromDate(birthDate!) : null, + 'isDateUnknown': isDateUnknown, + 'registrationNumber': registrationNumber, + 'profileImageUrl': profileImageUrl, + 'diseases': diseases, + 'pastDiseases': pastDiseases, + 'healthConcerns': healthConcerns, + 'createdAt': Timestamp.fromDate(createdAt), + }; + } + + factory Pet.fromMap(Map map) { + return Pet( + id: map['id'] ?? '', + ownerId: map['ownerId'] ?? '', + name: map['name'] ?? '', + species: map['species'] ?? '', + breed: map['breed'] ?? '', + gender: map['gender'] ?? '', + isNeutered: map['isNeutered'] ?? false, + birthDate: map['birthDate'] != null + ? (map['birthDate'] as Timestamp).toDate() + : null, + isDateUnknown: map['isDateUnknown'] ?? false, + registrationNumber: map['registrationNumber'], + profileImageUrl: map['profileImageUrl'], + diseases: List.from(map['diseases'] ?? []), + pastDiseases: List.from(map['pastDiseases'] ?? []), + healthConcerns: List.from(map['healthConcerns'] ?? []), + createdAt: (map['createdAt'] as Timestamp).toDate(), + ); + } +} diff --git a/app/lib/screens/home_screen.dart b/app/lib/screens/home_screen.dart index bfa43af..2f23f16 100644 --- a/app/lib/screens/home_screen.dart +++ b/app/lib/screens/home_screen.dart @@ -1,71 +1,320 @@ import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; // Import screenutil +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'pet_registration_screen.dart'; +import '../services/firestore_service.dart'; +import '../models/pet_model.dart'; +import '../theme/app_colors.dart'; -class HomeScreen extends StatelessWidget { +class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + final FirestoreService _firestoreService = FirestoreService(); + String? _userId; + Pet? _selectedPet; + + @override + void initState() { + super.initState(); + _userId = _firestoreService.getCurrentUserId(); + } + + // 반려동물 선택 시 호출 + void _selectPet(Pet pet) { + setState(() { + _selectedPet = pet; + }); + Navigator.pop(context); // 모달 닫기 + } + + // 반려동물 선택 모달 표시 + void _showPetSelectionModal(List pets) { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (context) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20.r)), + ), + 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), + // 반려동물 리스트 + Flexible( + child: ListView.builder( + shrinkWrap: true, + itemCount: pets.length, + itemBuilder: (context, index) { + final pet = pets[index]; + final isSelected = pet.id == _selectedPet?.id; + return ListTile( + leading: CircleAvatar( + radius: 24.r, + backgroundColor: Colors.grey[200], + backgroundImage: pet.profileImageUrl != null + ? NetworkImage(pet.profileImageUrl!) + : null, + child: pet.profileImageUrl == null + ? SvgPicture.asset( + 'assets/icons/profile_icon.svg', + width: 24.w, + colorFilter: ColorFilter.mode( + Colors.grey[400]!, + BlendMode.srcIn, + ), + ) + : null, + ), + title: Text( + pet.name, + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 16.sp, + fontWeight: isSelected + ? FontWeight.bold + : FontWeight.normal, + color: isSelected + ? AppColors.highlight + : Colors.black, + ), + ), + trailing: isSelected + ? const Icon(Icons.check, color: AppColors.highlight) + : null, + onTap: () => _selectPet(pet), + ); + }, + ), + ), + Divider(thickness: 1, color: Colors.grey[200]), + // 반려동물 추가 버튼 + ListTile( + leading: Container( + width: 48.r, + height: 48.r, + decoration: BoxDecoration( + color: Colors.grey[100], + shape: BoxShape.circle, + ), + child: const Icon(Icons.add, color: Colors.black54), + ), + title: Text( + '반려동물 추가하기', + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + ), + onTap: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const PetRegistrationScreen(), + ), + ); + }, + ), + SizedBox(height: 30.h), + ], + ), + ); + }, + ); + } + @override Widget build(BuildContext context) { + if (_userId == null) { + return const Scaffold(body: Center(child: Text('로그인이 필요합니다.'))); + } + return Scaffold( 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), + child: StreamBuilder>( + stream: _firestoreService.getPets(_userId!), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + if (snapshot.hasError) { + return Center(child: Text('오류가 발생했습니다: ${snapshot.error}')); + } + + final pets = snapshot.data ?? []; + + // 등록된 반려동물이 없을 때: 기존 UI 유지 (등록 버튼 강조) + if (pets.isEmpty) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric( + horizontal: 20.w, + vertical: 20.h, + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(8.r), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const PetRegistrationScreen(), + ), + ); + }, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 4.h), + child: Row( + mainAxisSize: MainAxisSize.min, + 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', + fontSize: 16.sp, + color: Colors.grey[600], + ), + ), + ), + ), + ], + ); + } + + // 등록된 반려동물이 있을 때 + // 선택된 펫이 없거나 리스트에 없으면 첫 번째 펫 선택 + if (_selectedPet == null || + !pets.any((p) => p.id == _selectedPet!.id)) { + // We shouldn't update state directly in build, but for initialization it's tricky. + // Using the first pet as default display. + // Better approach: use a local variable for display, update state in callbacks. + _selectedPet = pets.first; + } + + // To ensure _selectedPet is valid (e.g. after deletion), find it in the new list + final displayPet = pets.firstWhere( + (p) => p.id == _selectedPet?.id, + orElse: () => pets.first, + ); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric( + horizontal: 20.w, + vertical: 20.h, + ), + child: GestureDetector( + onTap: () => _showPetSelectionModal(pets), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // 프로필 이미지 + CircleAvatar( + radius: 20.r, + backgroundColor: Colors.grey[200], + backgroundImage: displayPet.profileImageUrl != null + ? NetworkImage(displayPet.profileImageUrl!) + : null, + child: displayPet.profileImageUrl == null + ? SvgPicture.asset( + 'assets/icons/profile_icon.svg', + width: 20.w, + colorFilter: ColorFilter.mode( + Colors.grey[400]!, + BlendMode.srcIn, + ), + ) + : null, + ), + SizedBox(width: 10.w), + // 이름 + Text( + displayPet.name, + style: TextStyle( + fontFamily: 'SCDream', + fontWeight: FontWeight.bold, + fontSize: 18.sp, + color: Colors.black, + ), + ), + SizedBox(width: 4.w), + // 드롭다운 화살표 + Icon( + Icons.keyboard_arrow_down, + size: 24.w, + color: Colors.black, + ), + ], + ), ), ), - ), - ), - Expanded( - child: Center( - child: Text( - '로그인 성공!\n여기는 메인 홈 화면입니다.', - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: 'SCDream', - fontWeight: FontWeight.w500, - fontSize: 18.sp, + Expanded( + child: Center( + child: Text( + '안녕, ${displayPet.name}!', + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 24.sp, + fontWeight: FontWeight.bold, + ), + ), ), ), - ), - ), - ], + ], + ); + }, ), ), ); diff --git a/app/lib/screens/login_screen.dart b/app/lib/screens/login_screen.dart index 8b74dd7..65f7b1a 100644 --- a/app/lib/screens/login_screen.dart +++ b/app/lib/screens/login_screen.dart @@ -156,7 +156,7 @@ class _LoginScreenState extends State { fontSize: 15.sp, backgroundColor: const Color(0xFF00D03F), onPressed: () {}, - iconPath: 'assets/icons/navericon.svg', + iconPath: 'assets/icons/naver_icon.svg', ), SizedBox(height: 15.h), // Kakao Login Button @@ -168,7 +168,7 @@ class _LoginScreenState extends State { fontSize: 15.sp, backgroundColor: const Color(0xFFFAE100), onPressed: () {}, - iconPath: 'assets/icons/kakaoicon.svg', + iconPath: 'assets/icons/kakao_icon.svg', ), SizedBox(height: 15.h), // Google Login Button @@ -180,7 +180,7 @@ class _LoginScreenState extends State { fontSize: 15.sp, backgroundColor: Colors.white, onPressed: _handleGoogleLogin, - iconPath: 'assets/icons/googleicon.svg', + iconPath: 'assets/icons/google_icon.svg', isBordered: true, ), ], diff --git a/app/lib/screens/main_screen.dart b/app/lib/screens/main_screen.dart index ca0d230..1752423 100644 --- a/app/lib/screens/main_screen.dart +++ b/app/lib/screens/main_screen.dart @@ -87,14 +87,14 @@ class _MainScreenState extends State { BottomNavigationBarItem( icon: Padding( padding: EdgeInsets.only(bottom: 10.h), - child: _buildSvgIcon('assets/icons/homeicon.svg', 0), + child: _buildSvgIcon('assets/icons/home_icon.svg', 0), ), label: '홈', ), BottomNavigationBarItem( icon: Padding( padding: EdgeInsets.only(bottom: 10.h), - child: _buildSvgIcon('assets/icons/appointmenticon.svg', 1), + child: _buildSvgIcon('assets/icons/appointment_icon.svg', 1), ), label: '예약/조회', ), @@ -115,14 +115,14 @@ class _MainScreenState extends State { BottomNavigationBarItem( icon: Padding( padding: EdgeInsets.only(bottom: 10.h), - child: _buildSvgIcon('assets/icons/shopicon.svg', 3), + child: _buildSvgIcon('assets/icons/shop_icon.svg', 3), ), label: '상점', ), BottomNavigationBarItem( icon: Padding( padding: EdgeInsets.only(bottom: 10.h), - child: _buildSvgIcon('assets/icons/myicon.svg', 4), + child: _buildSvgIcon('assets/icons/my_icon.svg', 4), ), label: '내 정보', ), diff --git a/app/lib/screens/pet_registration_screen.dart b/app/lib/screens/pet_registration_screen.dart index 4cf14ad..6d557bc 100644 --- a/app/lib/screens/pet_registration_screen.dart +++ b/app/lib/screens/pet_registration_screen.dart @@ -5,6 +5,11 @@ 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'; +import '../data/pet_data.dart'; // Import Data +import '../widgets/pet_registration/selection_modal.dart'; // Import SelectionModal +import '../widgets/pet_registration/input_formatters.dart'; // Import InputFormatters +import '../services/firestore_service.dart'; +import '../models/pet_model.dart'; class PetRegistrationScreen extends StatefulWidget { const PetRegistrationScreen({super.key}); @@ -17,27 +22,53 @@ class _PetRegistrationScreenState extends State { // 정확한 날짜를 몰라요 상태 bool _isDateUnknown = false; - final TextEditingController _yearController = TextEditingController(); final TextEditingController _monthController = TextEditingController(); + final TextEditingController _yearController = + TextEditingController(); // Added back final TextEditingController _dayController = TextEditingController(); + final TextEditingController _registrationNumberController = + TextEditingController(); // Added Registration Number Controller + final FocusNode _yearFocus = FocusNode(); + final FocusNode _monthFocus = FocusNode(); + final FocusNode _dayFocus = FocusNode(); - // 보유 질환 데이터 - final List _diseaseList = [ - "피부질환", - "눈 질환", - "치아 / 구강 질환", - "뼈 / 관절 질환", - "생식기 / 비뇨기 질환", - "심장 / 혈관 질환", - "소화기 질환", - "호흡기 질환", - "내분비계 질환", - "뇌신경 질환", - "생식기 질환", - "귀 질환", - "코 질환", - "기타", - ]; + bool get _isFormValid { + // 1. 이름 확인 + if (_nameController.text.trim().isEmpty) return false; + // 2. 종 확인 + if (_speciesController.text.trim().isEmpty) return false; + // 3. 품종 확인 + if (_breedController.text.trim().isEmpty) return false; + // 4. 성별 확인 + if (_selectedGender == null) return false; + // 5. 생년월일 확인 + if (!_isDateUnknown) { + if (_yearController.text.length != 4 || + _monthController.text.length != 2 || + _dayController.text.length != 2) { + return false; + } + } + return true; + } + + @override + void initState() { + super.initState(); + // 폼 상태 변경 감지를 위한 리스너 등록 + _nameController.addListener(_updateState); + _speciesController.addListener(_updateState); + _breedController.addListener(_updateState); + _yearController.addListener(_updateState); + _monthController.addListener(_updateState); + _dayController.addListener(_updateState); + } + + void _updateState() { + setState(() {}); + } + + // 보유 질환 데이터 (Removed: Use PetData.diseaseList) // 각 항목별 선택 상태 및 컨트롤러 final TextEditingController _nameController = @@ -49,85 +80,7 @@ class _PetRegistrationScreenState extends State { final TextEditingController _genderController = TextEditingController(); // 성별 컨트롤러 - // 종 데이터 (대분류 -> 중분류 -> 품종) - final Map>> _petData = { - "포유류": { - "강아지": [ - "말티즈", - "푸들", - "포메라니안", - "믹스견", - "치와와", - "시츄", - "비숑 프리제", - "골든 리트리버", - "진돗개", - "웰시 코기", - "기타(직접 입력)", - ], - "고양이": [ - "코리안 숏헤어", - "페르시안", - "러시안 블루", - "샴", - "렉돌", - "스코티시 폴드", - "먼치킨", - "노르웨이 숲", - "믹스묘", - "기타(직접 입력)", - ], - "햄스터": ["정글리안", "펄", "푸딩", "골든 햄스터", "로보로브스키", "기타(직접 입력)"], - "토끼": ["롭이어", "더치", "라이언 헤드", "드워프", "렉스", "기타(직접 입력)"], - "기니피그": ["잉글리쉬", "아비시니안", "페루비안", "실키", "기타(직접 입력)"], - "고슴도치": ["플라티나", "화이트 초코", "알비노", "핀토", "기타(직접 입력)"], - "기타": ["기타(직접 입력)"], - }, - "파충류": { - "거북이": ["커먼 머스크 터틀", "레이저백", "육지거북", "붉은귀거북", "남생이", "기타(직접 입력)"], - "도마뱀": ["크레스티드 게코", "레오파드 게코", "비어디 드래곤", "블루텅 스킨크", "이구아나", "기타(직접 입력)"], - "뱀": ["볼 파이톤", "콘 스네이크", "킹 스네이크", "밀크 스네이크", "기타(직접 입력)"], - "기타": ["기타(직접 입력)"], - }, - "조류": { - "앵무새": [ - "사랑앵무(잉꼬)", - "코카티엘(왕관앵무)", - "모란앵무", - "코뉴어", - "퀘이커", - "금강앵무", - "기타(직접 입력)", - ], - "카나리아": ["옐로우 카나리아", "레드 카나리아", "보더 카나리아", "기타(직접 입력)"], - "핀치": ["문조", "십자매", "금화조", "호금조", "기타(직접 입력)"], - "기타": ["기타(직접 입력)"], - }, - "어류": { - "금붕어": ["오란다", "유금", "단정", "진주린", "코메트", "기타(직접 입력)"], - "열대어": ["네온 테트라", "엔젤피쉬", "플래티", "몰리", "디스커스", "기타(직접 입력)"], - "구피": ["고정 구피", "막구피(믹스)", "기타(직접 입력)"], - "잉어": ["비단잉어", "향어", "기타(직접 입력)"], - "기타": ["기타(직접 입력)"], - }, - "곤충": { - "장수풍뎅이": ["국산 장수풍뎅이", "헤라클레스 장수풍뎅이", "코카서스 장수풍뎅이", "기타(직접 입력)"], - "사슴벌레": ["넓적사슴벌레", "왕사슴벌레", "톱사슴벌레", "애사슴벌레", "기타(직접 입력)"], - "나비/나방": ["배추흰나비", "호랑나비", "누에나방", "기타(직접 입력)"], - "사마귀": ["왕사마귀", "사마귀", "넓적배사마귀", "기타(직접 입력)"], - "기타": ["기타(직접 입력)"], - }, - "절지동물": { - "타란툴라(거미)": ["로즈헤어", "골든니", "화이트니", "핑크토", "기타(직접 입력)"], - "전갈": ["황제전갈", "극동전갈", "아시안 포레스트 전갈", "기타(직접 입력)"], - "지네": ["왕지네", "청지네", "기타(직접 입력)"], - "소라게": ["인도 소라게", "딸기 소라게", "바이오라센트", "기타(직접 입력)"], - "기타": ["기타(직접 입력)"], - }, - "기타": { - "기타(직접 입력)": ["기타(직접 입력)"], - }, - }; + // 종 데이터 (Removed: Use PetData.breedsData) // 선택된 종 정보 (품종 선택을 위해 필요) String? _currentMajorCategory; @@ -211,6 +164,27 @@ class _PetRegistrationScreenState extends State { } }, ), + ListTile( + leading: Icon( + Icons.delete_outline, + color: Colors.black, + size: 24.w, + ), + title: Text( + '기본 이미지로 변경', + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 16.sp, + color: Colors.black, + ), + ), + onTap: () { + Navigator.pop(context); + setState(() { + _profileImage = null; + }); + }, + ), SizedBox(height: 20.h), ], ), @@ -239,9 +213,98 @@ class _PetRegistrationScreenState extends State { _diseaseController.dispose(); _pastDiseaseController.dispose(); _healthConcernController.dispose(); + _yearFocus.dispose(); + _monthFocus.dispose(); + _dayFocus.dispose(); + _registrationNumberController.dispose(); super.dispose(); } + bool _isLoading = false; + + Future _registerPet() async { + setState(() { + _isLoading = true; + }); + + try { + final firestoreService = FirestoreService(); + final userId = firestoreService.getCurrentUserId(); + + if (userId == null) { + throw Exception('로그인이 필요합니다.'); + } + + // 날짜 처리 + DateTime? birthDate; + if (!_isDateUnknown) { + birthDate = DateTime( + int.parse(_yearController.text), + int.parse(_monthController.text), + int.parse(_dayController.text), + ); + } + + // 리스트 + 기타 텍스트 합치기 (보유 질환) + List finalDiseases = List.from(_selectedDiseases); + if (finalDiseases.contains('기타') && _otherDiseaseText.isNotEmpty) { + finalDiseases.remove('기타'); + finalDiseases.add('기타($_otherDiseaseText)'); + } + + // 리스트 + 기타 텍스트 합치기 (과거 질환) + List finalPastDiseases = List.from(_selectedPastDiseases); + if (finalPastDiseases.contains('기타') && + _otherPastDiseaseText.isNotEmpty) { + finalPastDiseases.remove('기타'); + finalPastDiseases.add('기타($_otherPastDiseaseText)'); + } + + // 리스트 + 기타 텍스트 합치기 (염려 건강) + List finalHealthConcerns = List.from(_selectedHealthConcerns); + if (finalHealthConcerns.contains('기타') && + _otherHealthConcernText.isNotEmpty) { + finalHealthConcerns.remove('기타'); + finalHealthConcerns.add('기타($_otherHealthConcernText)'); + } + + final newPet = Pet( + id: firestoreService.generatePetId(), + ownerId: userId, + name: _nameController.text, + species: _speciesController.text, // 중분류 or 직접입력 + breed: _breedController.text, + gender: _selectedGender!, + isNeutered: _isNeutered, + birthDate: birthDate, + isDateUnknown: _isDateUnknown, + registrationNumber: _registrationNumberController.text.isNotEmpty + ? _registrationNumberController.text + : null, + diseases: finalDiseases, + pastDiseases: finalPastDiseases, + healthConcerns: finalHealthConcerns, + createdAt: DateTime.now(), + ); + + await firestoreService.registerPet(newPet, _profileImage); + + if (!mounted) return; + Navigator.pushReplacementNamed(context, '/register_complete'); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('등록 실패: $e'))); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + void _toggleDateUnknown() { setState(() { _isDateUnknown = !_isDateUnknown; @@ -301,10 +364,10 @@ class _PetRegistrationScreenState extends State { // 리스트 영역 Expanded( child: ListView.builder( - itemCount: _diseaseList.length, + itemCount: PetData.diseaseList.length, padding: const EdgeInsets.symmetric(vertical: 10), itemBuilder: (context, index) { - final disease = _diseaseList[index]; + final disease = PetData.diseaseList[index]; final isSelected = tempSelected.contains(disease); return Column( @@ -523,7 +586,7 @@ class _PetRegistrationScreenState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - // 뒤로가기 버튼 (대분류 선택 상태이거나 입력창 상태일 때 표시) + // 뒤로가기 버튼 (selectedMajor != null || showInput) ? GestureDetector( onTap: () { @@ -644,11 +707,10 @@ class _PetRegistrationScreenState extends State { : (selectedMajor == null ? ListView.builder( // 대분류 리스트 - itemCount: _petData.keys.length, + itemCount: PetData.breedsData.keys.length, itemBuilder: (context, index) { - final major = _petData.keys.elementAt( - index, - ); + final major = PetData.breedsData.keys + .elementAt(index); return ListTile( title: Text( major, @@ -664,7 +726,7 @@ class _PetRegistrationScreenState extends State { ), onTap: () { setModalState(() { - if (major == '기타') { + if (major == '기타(직접 입력)') { showInput = true; } else { selectedMajor = major; @@ -676,9 +738,12 @@ class _PetRegistrationScreenState extends State { ) : ListView.builder( // 중분류 리스트 - itemCount: _petData[selectedMajor]!.length, + itemCount: + PetData.breedsData[selectedMajor]!.length, itemBuilder: (context, index) { - final minor = _petData[selectedMajor]!.keys + final minor = PetData + .breedsData[selectedMajor]! + .keys .elementAt(index); return ListTile( title: Text( @@ -746,10 +811,10 @@ class _PetRegistrationScreenState extends State { } // 3. 품종 리스트 가져오기 - final List originalList = - _petData[_currentMajorCategory]![_currentMinorCategory]! - .where((e) => e != '기타(직접 입력)') - .toList(); + final List originalList = PetData + .breedsData[_currentMajorCategory]![_currentMinorCategory]! + .where((e) => e != '기타(직접 입력)') + .toList(); // '기타(직접 입력)'은 리스트 마지막에 고정하거나 별도 처리, 여기서는 필터링 후 맨 뒤에 붙일 예정 showModalBottomSheet( @@ -1359,7 +1424,7 @@ class _PetRegistrationScreenState extends State { child: _profileImage == null ? Center( child: SvgPicture.asset( - 'assets/icons/profileicon.svg', + 'assets/icons/profile_icon.svg', width: 40.w, colorFilter: ColorFilter.mode( Colors.grey[400]!, @@ -1401,15 +1466,14 @@ class _PetRegistrationScreenState extends State { // 2. 반려동물 이름 입력 _buildLabel('반려동물 이름 입력', isRequired: true), - SizedBox(height: 8.h), _buildTextField( controller: _nameController, - hint: '이름 입력 (2~10글자/영문/숫자/한글)', + hint: '이름 입력 (2~10글자/한글/영문/숫자)', inputFormatters: [ LengthLimitingTextInputFormatter(10), // 최대 10글자 제한 ], ), - SizedBox(height: 24.h), + SizedBox(height: 20.h), // 3. 선택 박스들 (종, 품종, 성별) _buildSearchField( @@ -1417,31 +1481,34 @@ class _PetRegistrationScreenState extends State { controller: _speciesController, readOnly: true, onTap: _showSpeciesSelectionModal, + isRequired: true, ), - SizedBox(height: 12.h), + SizedBox(height: 20.h), _buildSearchField( '반려동물 품종 선택', controller: _breedController, readOnly: true, onTap: _showBreedSelectionModal, + isRequired: true, ), - const SizedBox(height: 12), + SizedBox(height: 20.h), _buildSearchField( '반려동물 성별', controller: _genderController, readOnly: true, onTap: _showGenderSelectionModal, + isRequired: true, ), - const SizedBox(height: 24), + SizedBox(height: 20.h), // 4. 생년월일 _buildLabel('반려동물 생년월일', isRequired: true), - const SizedBox(height: 8), Row( children: [ Expanded( child: _buildTextField( controller: _yearController, + focusNode: _yearFocus, hint: 'YYYY', textAlign: TextAlign.center, hintColor: _isDateUnknown @@ -1453,12 +1520,18 @@ class _PetRegistrationScreenState extends State { FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(4), ], + onChanged: (value) { + if (value.length == 4) { + FocusScope.of(context).requestFocus(_monthFocus); + } + }, ), ), SizedBox(width: 12.w), Expanded( child: _buildTextField( controller: _monthController, + focusNode: _monthFocus, hint: 'MM', textAlign: TextAlign.center, hintColor: _isDateUnknown @@ -1469,14 +1542,20 @@ class _PetRegistrationScreenState extends State { inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(2), - _DateRangeInputFormatter(min: 1, max: 12), + DateRangeInputFormatter(min: 1, max: 12), ], + onChanged: (value) { + if (value.length == 2) { + FocusScope.of(context).requestFocus(_dayFocus); + } + }, ), ), SizedBox(width: 12.w), Expanded( child: _buildTextField( controller: _dayController, + focusNode: _dayFocus, hint: 'DD', textAlign: TextAlign.center, hintColor: _isDateUnknown @@ -1487,7 +1566,7 @@ class _PetRegistrationScreenState extends State { inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(2), - _DayInputFormatter( + DayInputFormatter( monthController: _monthController, yearController: _yearController, ), @@ -1525,9 +1604,10 @@ class _PetRegistrationScreenState extends State { // 5. 동물 등록 번호 _buildLabel('동물 등록 번호', isRequired: false), - const SizedBox(height: 8), + // const SizedBox(height: 8), _buildTextField( - hint: '동물 등록 번호 입력', + controller: _registrationNumberController, + hint: '숫자만 입력', keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, @@ -1564,7 +1644,7 @@ class _PetRegistrationScreenState extends State { }, ), ), - const SizedBox(height: 24), + SizedBox(height: 20.h), _buildSearchField( '과거 진단받은 질병', controller: _pastDiseaseController, @@ -1591,7 +1671,7 @@ class _PetRegistrationScreenState extends State { }, ), ), - const SizedBox(height: 24), + SizedBox(height: 20.h), _buildSearchField( '염려되는 건강 문제', controller: _healthConcernController, @@ -1626,23 +1706,34 @@ class _PetRegistrationScreenState extends State { width: double.infinity, height: 52.h, child: ElevatedButton( - onPressed: () {}, + onPressed: (_isFormValid && !_isLoading) ? _registerPet : null, style: ElevatedButton.styleFrom( backgroundColor: AppColors.highlight, + disabledBackgroundColor: AppColors.inactive, + disabledForegroundColor: Colors.white, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(30.r), ), ), - child: Text( - '반려동물 등록', - style: TextStyle( - fontFamily: 'SCDream', - fontSize: 16.sp, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), + child: _isLoading + ? SizedBox( + width: 24.w, + height: 24.w, + child: const CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : Text( + '반려동물 등록', + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 16.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), ), ), SizedBox(height: 20.h), @@ -1655,19 +1746,27 @@ class _PetRegistrationScreenState extends State { // Helper Widget: 라벨 (필수 표시 포함) Widget _buildLabel(String text, {bool isRequired = false}) { return Row( + mainAxisSize: MainAxisSize.min, children: [ if (isRequired) Padding( padding: EdgeInsets.only(right: 4.w), - child: Icon(Icons.circle, size: 4.w, color: Colors.red), + child: Container( + width: 4.w, + height: 4.w, + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + ), ), Text( text, style: TextStyle( fontFamily: 'SCDream', - fontSize: 13.sp, - fontWeight: FontWeight.bold, - color: Colors.black87, + fontSize: 14.sp, + fontWeight: FontWeight.w500, + color: Colors.black, ), ), ], @@ -1683,9 +1782,13 @@ class _PetRegistrationScreenState extends State { TextEditingController? controller, TextInputType? keyboardType, List? inputFormatters, + FocusNode? focusNode, + ValueChanged? onChanged, }) { return TextField( controller: controller, + focusNode: focusNode, + onChanged: onChanged, enabled: enabled, textAlign: textAlign, keyboardType: keyboardType, @@ -1750,19 +1853,15 @@ class _PetRegistrationScreenState extends State { TextEditingController? controller, VoidCallback? onTap, bool readOnly = false, + bool isRequired = false, // Added isRequired param }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( + _buildLabel( label, - style: TextStyle( - fontFamily: 'SCDream', - fontSize: 13.sp, - fontWeight: FontWeight.bold, - color: Colors.black87, - ), - ), + isRequired: isRequired, + ), // Use _buildLabel to show label with red dot TextField( controller: controller, // 컨트롤러 연결 onTap: onTap, // 탭 이벤트 연결 @@ -1792,86 +1891,3 @@ class _PetRegistrationScreenState extends State { ); } } - -// 범위 제한 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/register_complete_screen.dart b/app/lib/screens/register_complete_screen.dart new file mode 100644 index 0000000..c91b27a --- /dev/null +++ b/app/lib/screens/register_complete_screen.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../theme/app_colors.dart'; +import 'main_screen.dart'; + +class RegisterCompleteScreen extends StatelessWidget { + const RegisterCompleteScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Spacer(), + // TODO: Add a success image/icon here if available + Icon( + Icons.check_circle_outline, + size: 100.w, + color: AppColors.highlight, + ), + SizedBox(height: 24.h), + Text( + '반려동물 등록 완료!', + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 24.sp, + fontWeight: FontWeight.bold, + color: const Color(0xFF1F1F1F), + ), + ), + SizedBox(height: 12.h), + Text( + '이제 RUP과 함께\n똑똑한 반려생활을 시작해보세요.', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 16.sp, + color: const Color(0xFF7D7C7C), + height: 1.5, + ), + ), + const Spacer(), + SizedBox( + width: double.infinity, + height: 52.h, + child: ElevatedButton( + onPressed: () { + // 메인 화면으로 이동하면서 스택 초기화 + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => const MainScreen(), + ), + (route) => false, + ); + }, + 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), + ], + ), + ), + ), + ); + } +} diff --git a/app/lib/services/auth_service.dart b/app/lib/services/auth_service.dart index ed38488..da2d455 100644 --- a/app/lib/services/auth_service.dart +++ b/app/lib/services/auth_service.dart @@ -193,11 +193,13 @@ class AuthService { await _storage.write(key: 'accessToken', value: accessToken); await _storage.write(key: 'refreshToken', value: refreshToken); - // Firebase 로그인 처리 (필요시) - idToken으로 credential 생성 가능하지만 access token이 없으므로 생략하거나 - // 이미 signInWithGoogle에서 받아온 credential을 재사용할 수 있으면 좋음. - // 하지만 여기서는 백엔드 세션이 중요하므로 일단 백엔드 토큰만 저장. - // (Firebase Auth와 Custom Backend Auth를 혼용 중이라 복잡함. - // 일단 백엔드 로직이 메인이므로 백엔드 토큰 처리에 집중) + // Firebase 로그인 처리 + // idToken으로 Credential 생성하여 로그인 + final OAuthCredential credential = GoogleAuthProvider.credential( + idToken: idToken, + accessToken: null, // accessToken은 필수가 아님 (idToken만으로 가능) + ); + await _auth.signInWithCredential(credential); return true; } diff --git a/app/lib/services/firestore_service.dart b/app/lib/services/firestore_service.dart new file mode 100644 index 0000000..c643a17 --- /dev/null +++ b/app/lib/services/firestore_service.dart @@ -0,0 +1,98 @@ +import 'dart:io'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:uuid/uuid.dart'; +import '../models/pet_model.dart'; +import '../utils/log_manager.dart'; + +class FirestoreService { + final FirebaseFirestore _db = FirebaseFirestore.instance; + final FirebaseStorage _storage = FirebaseStorage.instance; + final FirebaseAuth _auth = FirebaseAuth.instance; + + // 반려동물 등록 + Future registerPet(Pet pet, File? imageFile) async { + try { + String? imageUrl; + + // 1. 이미지 업로드 (이미지가 있는 경우) + if (imageFile != null) { + final String fileName = + '${pet.id}_${DateTime.now().millisecondsSinceEpoch}.jpg'; + final Reference storageRef = _storage + .ref() + .child('pet_images') + .child(fileName); + + LogManager().addLog( + '[Storage] Starting upload to ${storageRef.fullPath}', + ); + LogManager().addLog('[Storage] Bucket: ${_storage.bucket}'); + + final TaskSnapshot snapshot = await storageRef.putFile(imageFile); + + if (snapshot.state == TaskState.success) { + LogManager().addLog( + '[Storage] Upload success. Validating metadata...', + ); + // 잠시 대기 (consistency 이슈 방지) + await Future.delayed(const Duration(milliseconds: 500)); + imageUrl = await storageRef.getDownloadURL(); + LogManager().addLog('[Storage] Download URL retrieved: $imageUrl'); + } else { + throw Exception('이미지 업로드 실패 (상태: ${snapshot.state})'); + } + } + + // 2. Pet 객체에 이미지 URL 업데이트 (새로운 객체 생성) + Pet petWithImage = Pet( + id: pet.id, + ownerId: pet.ownerId, + name: pet.name, + species: pet.species, + breed: pet.breed, + gender: pet.gender, + isNeutered: pet.isNeutered, + birthDate: pet.birthDate, + isDateUnknown: pet.isDateUnknown, + registrationNumber: pet.registrationNumber, + profileImageUrl: imageUrl, // 이미지 URL 설정 + diseases: pet.diseases, + pastDiseases: pet.pastDiseases, + healthConcerns: pet.healthConcerns, + createdAt: pet.createdAt, + ); + + // 3. Firestore에 저장 + await _db.collection('pets').doc(pet.id).set(petWithImage.toMap()); + + LogManager().addLog('[Firestore] Pet registered successfully: ${pet.id}'); + } catch (e) { + LogManager().addLog('[Firestore] Error registering pet: $e'); + throw Exception('반려동물 등록 실패: $e'); + } + } + + // 현재 로그인한 사용자의 ID 가져오기 + String? getCurrentUserId() { + return _auth.currentUser?.uid; + } + + // 반려동물 리스트 스트림 가져오기 + Stream> getPets(String userId) { + return _db + .collection('pets') + .where('ownerId', isEqualTo: userId) + .orderBy('createdAt', descending: true) // 최신 등록순 + .snapshots() + .map((snapshot) { + return snapshot.docs.map((doc) => Pet.fromMap(doc.data())).toList(); + }); + } + + // 새 Pet ID 생성 + String generatePetId() { + return const Uuid().v4(); + } +} diff --git a/app/lib/widgets/pet_registration/input_formatters.dart b/app/lib/widgets/pet_registration/input_formatters.dart new file mode 100644 index 0000000..b22b33f --- /dev/null +++ b/app/lib/widgets/pet_registration/input_formatters.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +// 범위 제한 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 < min || value > max) { + // 범위를 벗어나면 이전 값 유지 (단, 입력 중인 상태 고려 - 예: 3을 입력하려는데 30이 되면 안됨) + // 여기서는 단순하게 max보다 크면 입력 불가 처리 (사용자 경험상 이게 나을 수 있음) + if (newValue.text.length > max.toString().length) { + return oldValue; + } + // 더 정교한 로직이 필요할 수 있으나, 기본적으로 입력 막음 + if (value > max) return oldValue; + } + + return newValue; + } +} + +// 일(Day) 입력 Formatter (월에 따라 28~31일 제한) +class DayInputFormatter extends TextInputFormatter { + final TextEditingController yearController; + final TextEditingController monthController; + + DayInputFormatter({ + required this.yearController, + required this.monthController, + }); + + @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 year = int.tryParse(yearController.text) ?? DateTime.now().year; + int month = int.tryParse(monthController.text) ?? 1; + + int maxDay = DateTime(year, month + 1, 0).day; // 해당 월의 마지막 날짜 계산 + + if (day < 1 || day > maxDay) { + if (newValue.text.length > maxDay.toString().length) { + return oldValue; + } + if (day > maxDay) return oldValue; + } + + return newValue; + } +} diff --git a/app/lib/widgets/pet_registration/selection_modal.dart b/app/lib/widgets/pet_registration/selection_modal.dart new file mode 100644 index 0000000..e03ec52 --- /dev/null +++ b/app/lib/widgets/pet_registration/selection_modal.dart @@ -0,0 +1,215 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class SelectionModal extends StatefulWidget { + final String title; + final List currentSelected; + final String currentOtherText; + final Function(List, String) onComplete; + final bool isSingleSelection; + final List itemList; + + const SelectionModal({ + super.key, + required this.title, + required this.currentSelected, + required this.currentOtherText, + required this.onComplete, + this.isSingleSelection = false, + required this.itemList, + }); + + @override + State createState() => _SelectionModalState(); +} + +class _SelectionModalState extends State { + late List _tempSelected; + final TextEditingController _otherController = TextEditingController(); + bool _isOtherSelected = false; + + @override + void initState() { + super.initState(); + _tempSelected = List.from(widget.currentSelected); + _otherController.text = widget.currentOtherText; + + // "기타"가 선택되어 있는지 확인 (여기서는 단순히 리스트에 있는지로 판단하지 않고, + // "기타"라는 항목 텍스트 자체를 체크하거나, 외부에서 넘겨받은 텍스트가 있으면 체크) + // *로직 수정*: itemList에 "기타"가 포함되어 있다면 그것을 기준으로 함. + if (_tempSelected.contains("기타")) { + _isOtherSelected = true; + } else if (widget.currentOtherText.isNotEmpty) { + // 기존 로직상 기타 텍스트가 있으면 기타가 선택된 것으로 간주할 수도 있음 + // 하지만 리스트 기반이므로 명시적으로 "기타" 아이템이 있어야 함. + // 여기서는 단순화하여 UI 상태만 초기화. + } + } + + @override + void dispose() { + _otherController.dispose(); + super.dispose(); + } + + void _onItemTap(String item) { + setState(() { + if (item == "기타") { + if (widget.isSingleSelection) { + _tempSelected.clear(); + _tempSelected.add(item); + _isOtherSelected = true; + } else { + if (_tempSelected.contains(item)) { + _tempSelected.remove(item); + _isOtherSelected = false; + } else { + _tempSelected.add(item); + _isOtherSelected = true; + } + } + } else { + if (widget.isSingleSelection) { + _tempSelected.clear(); + _tempSelected.add(item); + // 단일 선택에서 다른거 누르면 기타 해제 + _isOtherSelected = false; + } else { + if (_tempSelected.contains(item)) { + _tempSelected.remove(item); + } else { + _tempSelected.add(item); + } + } + } + }); + } + + @override + Widget build(BuildContext context) { + return Container( + height: MediaQuery.of(context).size.height * 0.7, + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + children: [ + SizedBox(height: 20.h), + Text( + widget.title, + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + fontFamily: 'SCDream', + ), + ), + SizedBox(height: 20.h), + Expanded( + child: ListView.builder( + padding: EdgeInsets.symmetric(horizontal: 20.w), + itemCount: widget.itemList.length, + itemBuilder: (context, index) { + final item = widget.itemList[index]; + final isSelected = _tempSelected.contains(item); + + return Column( + children: [ + ListTile( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.r), + side: BorderSide( + color: isSelected + ? const Color(0xFFFF7500) + : Colors.grey[300]!, + ), + ), + tileColor: isSelected + ? const Color(0xFFFF7500).withValues( + alpha: 0.1, + ) // Fixed deprecated + : Colors.white, + title: Text( + item, + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 15.sp, + color: isSelected + ? const Color(0xFFFF7500) + : Colors.black, + fontWeight: isSelected + ? FontWeight.bold + : FontWeight.normal, + ), + ), + trailing: isSelected + ? Icon( + Icons.check, + color: const Color(0xFFFF7500), + size: 20.w, + ) + : null, + onTap: () => _onItemTap(item), + ), + SizedBox(height: 10.h), + // 기타 선택 시 입력창 표시 + if (item == "기타" && isSelected) + Padding( + padding: EdgeInsets.only( + left: 10.w, + right: 10.w, + bottom: 20.h, + ), + child: TextField( + controller: _otherController, + decoration: const InputDecoration( + hintText: '내용을 입력해주세요', + hintStyle: TextStyle( + fontFamily: 'SCDream', + color: Colors.grey, + ), + border: UnderlineInputBorder(), + ), + ), + ), + ], + ); + }, + ), + ), + Padding( + padding: EdgeInsets.all(20.w), + child: SizedBox( + width: double.infinity, + height: 50.h, + child: ElevatedButton( + onPressed: () { + widget.onComplete( + _tempSelected, + _isOtherSelected ? _otherController.text : '', + ); + Navigator.pop(context); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFFF7500), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.r), + ), + ), + child: Text( + '선택 완료', + style: TextStyle( + fontFamily: 'SCDream', + fontWeight: FontWeight.bold, + fontSize: 16.sp, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/app/linux/CMakeLists.txt b/app/linux/CMakeLists.txt index 6aef9d4..8bc48a3 100644 --- a/app/linux/CMakeLists.txt +++ b/app/linux/CMakeLists.txt @@ -7,7 +7,7 @@ project(runner LANGUAGES CXX) set(BINARY_NAME "app") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "com.example.app") +set(APPLICATION_ID "com.daoblock.rup") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. diff --git a/app/macos/Flutter/GeneratedPluginRegistrant.swift b/app/macos/Flutter/GeneratedPluginRegistrant.swift index 5a57a16..299e803 100644 --- a/app/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,17 +5,21 @@ import FlutterMacOS import Foundation +import cloud_firestore import file_selector_macos import firebase_auth import firebase_core +import firebase_storage import flutter_secure_storage_macos import google_sign_in_ios import video_player_avfoundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) diff --git a/app/macos/Runner.xcodeproj/project.pbxproj b/app/macos/Runner.xcodeproj/project.pbxproj index 062b369..edc6d19 100644 --- a/app/macos/Runner.xcodeproj/project.pbxproj +++ b/app/macos/Runner.xcodeproj/project.pbxproj @@ -385,7 +385,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.app.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.daoblock.rup.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/app"; @@ -399,7 +399,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.app.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.daoblock.rup.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/app"; @@ -413,7 +413,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.app.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.daoblock.rup.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/app"; diff --git a/app/macos/Runner/Configs/AppInfo.xcconfig b/app/macos/Runner/Configs/AppInfo.xcconfig index 8530c6f..3f734e8 100644 --- a/app/macos/Runner/Configs/AppInfo.xcconfig +++ b/app/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = app // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.app +PRODUCT_BUNDLE_IDENTIFIER = com.daoblock.rup // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved. diff --git a/app/move_file.ps1 b/app/move_file.ps1 new file mode 100644 index 0000000..8edef86 --- /dev/null +++ b/app/move_file.ps1 @@ -0,0 +1,3 @@ +New-Item -ItemType Directory -Force -Path 'android/app/src/main/kotlin/com/daoblock/rup' +Move-Item -Path 'android/app/src/main/kotlin/com/example/app/MainActivity.kt' -Destination 'android/app/src/main/kotlin/com/daoblock/rup/MainActivity.kt' +Remove-Item -Recurse -Force 'android/app/src/main/kotlin/com/example/app' diff --git a/app/pubspec.lock b/app/pubspec.lock index aa1121c..2177a77 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -49,6 +49,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + cloud_firestore: + dependency: "direct main" + description: + name: cloud_firestore + sha256: "2d33da4465bdb81b6685c41b535895065adcb16261beb398f5f3bbc623979e9c" + url: "https://pub.dev" + source: hosted + version: "5.6.12" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + sha256: "413c4e01895cf9cb3de36fa5c219479e06cd4722876274ace5dfc9f13ab2e39b" + url: "https://pub.dev" + source: hosted + version: "6.6.12" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + sha256: c1e30fc4a0fcedb08723fb4b1f12ee4e56d937cbf9deae1bda43cbb6367bb4cf + url: "https://pub.dev" + source: hosted + version: "4.4.12" code_assets: dependency: transitive description: @@ -217,6 +241,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.24.1" + firebase_storage: + dependency: "direct main" + description: + name: firebase_storage + sha256: "958fc88a7ef0b103e694d30beed515c8f9472dde7e8459b029d0e32b8ff03463" + url: "https://pub.dev" + source: hosted + version: "12.4.10" + firebase_storage_platform_interface: + dependency: transitive + description: + name: firebase_storage_platform_interface + sha256: d2661c05293c2a940c8ea4bc0444e1b5566c79dd3202c2271140c082c8cd8dd4 + url: "https://pub.dev" + source: hosted + version: "5.2.10" + firebase_storage_web: + dependency: transitive + description: + name: firebase_storage_web + sha256: "629a557c5e1ddb97a3666cbf225e97daa0a66335dbbfdfdce113ef9f881e833f" + url: "https://pub.dev" + source: hosted + version: "3.10.17" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -717,6 +773,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + url: "https://pub.dev" + source: hosted + version: "4.5.2" vector_graphics: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 53b8463..1de4067 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -43,6 +43,9 @@ dependencies: flutter_secure_storage: ^9.0.0 image_picker: ^1.2.1 flutter_screenutil: ^5.9.3 + cloud_firestore: ^5.0.0 + firebase_storage: ^12.0.0 + uuid: ^4.0.0 dev_dependencies: flutter_test: diff --git a/app/windows/flutter/generated_plugin_registrant.cc b/app/windows/flutter/generated_plugin_registrant.cc index 5b9f218..d3eda85 100644 --- a/app/windows/flutter/generated_plugin_registrant.cc +++ b/app/windows/flutter/generated_plugin_registrant.cc @@ -6,18 +6,24 @@ #include "generated_plugin_registrant.h" +#include #include #include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + CloudFirestorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("CloudFirestorePluginCApi")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); FirebaseAuthPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); FirebaseCorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + FirebaseStoragePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseStoragePluginCApi")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); } diff --git a/app/windows/flutter/generated_plugins.cmake b/app/windows/flutter/generated_plugins.cmake index d959a1b..9d37e8b 100644 --- a/app/windows/flutter/generated_plugins.cmake +++ b/app/windows/flutter/generated_plugins.cmake @@ -3,9 +3,11 @@ # list(APPEND FLUTTER_PLUGIN_LIST + cloud_firestore file_selector_windows firebase_auth firebase_core + firebase_storage flutter_secure_storage_windows )