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
)