개요
다시봄 앱은 Flutter로 개발된 시니어 라이프케어 메신저입니다. 현재 소스코드에는 모의(mock) 구현이 포함되어 있으며, 이 가이드를 따라 실제 Firebase 서비스와 연동하면 프로덕션 수준의 앱을 완성할 수 있습니다.
사전 준비
연동 전 아래 항목을 모두 준비해 주세요.
flutter --versiondart --versionnpm install -g firebase-toolsFirebase 프로젝트 생성
Firebase 콘솔에서 새 프로젝트를 만들고 앱을 등록합니다.
- 1Firebase 콘솔에 접속하여 프로젝트 추가를 클릭합니다.
- 2프로젝트 이름을
dacibom으로 입력하고 Google 애널리틱스를 활성화합니다. - 3프로젝트 생성 후 Android 앱 추가를 클릭합니다. 패키지명은
com.dacibom.app으로 설정합니다. - 4iOS 앱도 동일하게 추가합니다. 번들 ID는
com.dacibom.app을 사용합니다.
applicationId와 Firebase에 등록한 패키지명이 정확히 일치해야 합니다.android/app/build.gradle에서 확인하세요.FlutterFire CLI 설치 및 설정
FlutterFire CLI를 사용하면 Firebase 설정 파일을 자동으로 생성할 수 있습니다.
프로젝트 루트 디렉터리(dacibom/)에서 아래 명령어를 순서대로 실행합니다.
# 1. FlutterFire CLI 전역 설치 dart pub global activate flutterfire_cli # 2. Firebase CLI 로그인 firebase login # 3. FlutterFire 설정 (대화형 프롬프트 진행) flutterfire configure # → 위에서 생성한 class="token-string">'dacibom' 프로젝트를 선택 # → Android, iOS 플랫폼 모두 선택 # → lib/firebase_options.dart 파일이 자동 생성됩니다
flutterfire configure 실행 시 google-services.json (Android)과GoogleService-Info.plist (iOS)가 자동으로 올바른 위치에 배치됩니다.생성된 lib/firebase_options.dart를 사용하여 lib/main.dart를 다음과 같이 수정합니다.
import class="token-string">'package:firebase_core/firebase_core.dart'; import class="token-string">'firebase_options.dart'; class=class="token-string">"token-comment">// 자동 생성된 파일 void main() async { WidgetsFlutterBinding.ensureInitialized(); class=class="token-string">"token-comment">// Firebase 초기화 (반드시 runApp 전에 호출) await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => AuthService()), ChangeNotifierProvider(create: (_) => AiChatService()), ], child: const DacibomApp(), ), ); }
전화번호 인증 설정
Firebase 콘솔에서 전화번호 인증을 활성화하고 플랫폼별 설정을 완료합니다.
Firebase 콘솔 설정
- 1Firebase 콘솔 → Authentication → Sign-in method 탭으로 이동
- 2전화 항목을 클릭하여 사용 설정으로 변경 후 저장
- 3테스트 전화번호 추가 (개발 중 실제 SMS 발송 없이 테스트 가능):
+82 10-0000-0000→ 코드123456
Android 설정
android/app/build.gradle에 SHA-1 인증서 지문을 추가해야 합니다.
# SHA-1 지문 확인 (디버그 키스토어) cd android && ./gradlew signingReport # 출력된 SHA-1 값을 Firebase 콘솔에 등록: # 프로젝트 설정 → 앱 → SHA 인증서 지문 추가
iOS 설정
ios/Runner/Info.plist에 URL Scheme을 추가합니다.
CFBundleURLTypes CFBundleTypeRole Editor CFBundleURLSchemes com.googleusercontent.apps.YOUR_CLIENT_ID
auth_service.dart 실제 연동
모의 구현을 Firebase Auth 실제 호출로 교체합니다.
lib/services/auth_service.dart의 두 메서드를 아래 코드로 교체합니다.
import class="token-string">'package:firebase_auth/firebase_auth.dart'; import class="token-string">'package:flutter/material.dart'; class AuthService extends ChangeNotifier { final FirebaseAuth _auth = FirebaseAuth.instance; bool _isAuthenticated = false; String? _verificationId; String? _phoneNumber; bool get isAuthenticated => _isAuthenticated; String? get phoneNumber => _phoneNumber; class=class="token-string">"token-comment">// ① SMS 인증 요청 Future<void> requestSmsCode(String phone) async { class=class="token-string">"token-comment">// 한국 번호 형식 변환: 01012345678 → +821012345678 final formatted = class="token-string">'+82${phone.substring(1)}'; _phoneNumber = phone; await _auth.verifyPhoneNumber( phoneNumber: formatted, timeout: const Duration(seconds: 60), verificationCompleted: (PhoneAuthCredential credential) async { class=class="token-string">"token-comment">// Android 자동 인증 처리 await _auth.signInWithCredential(credential); _isAuthenticated = true; notifyListeners(); }, verificationFailed: (FirebaseAuthException e) { debugPrint(class="token-string">'인증 실패: ${e.message}'); }, codeSent: (String verificationId, int? resendToken) { _verificationId = verificationId; notifyListeners(); }, codeAutoRetrievalTimeout: (String verificationId) { _verificationId = verificationId; }, ); } class=class="token-string">"token-comment">// ② SMS 코드 확인 및 로그인 FutureverifySmsCode(String code) async { if (_verificationId == null) return false; try { final credential = PhoneAuthProvider.credential( verificationId: _verificationId!, smsCode: code, ); final result = await _auth.signInWithCredential(credential); _isAuthenticated = result.user != null; notifyListeners(); return _isAuthenticated; } on FirebaseAuthException catch (e) { debugPrint(class="token-string">'코드 확인 실패: ${e.message}'); return false; } } class=class="token-string">"token-comment">// ③ 로그아웃 Future<void> signOut() async { await _auth.signOut(); _isAuthenticated = false; _phoneNumber = null; _verificationId = null; notifyListeners(); } }
main.dart에서 Firebase.initializeApp()이 완료된 후에AuthService가 초기화되어야 합니다. await Firebase.initializeApp()은 반드시runApp() 이전에 호출하세요.Firestore 데이터베이스 설정
채팅 메시지와 사용자 정보를 저장할 Firestore를 설정합니다.
Firestore 활성화
- 1Firebase 콘솔 → Firestore Database → 데이터베이스 만들기
- 2프로덕션 모드 또는 테스트 모드 선택 (개발 중에는 테스트 모드 권장)
- 3리전 선택: asia-northeast3 (서울)
보안 규칙 설정
rules_version = class="token-string">'2'; service cloud.firestore { match /databases/{database}/documents { class=class="token-string">"token-comment">// 사용자 정보: 본인만 읽기/쓰기 match /users/{userId} { allow read, write: if request.auth != null && request.auth.uid == userId; } class=class="token-string">"token-comment">// 채팅방: 참여자만 접근 가능 match /chatRooms/{roomId} { allow read, write: if request.auth != null && request.auth.uid in resource.data.participants; class=class="token-string">"token-comment">// 메시지: 채팅방 참여자만 읽기/쓰기 match /messages/{messageId} { allow read, write: if request.auth != null && request.auth.uid in get(/databases/$(database)/documents/chatRooms/$(roomId)).data.participants; } } } }
컬렉션 구조
OpenAI AI 친구 실제 연동
철수/영희 AI 친구를 OpenAI GPT API와 연결합니다.
lib/services/ai_chat_service.dart의 sendMessage 메서드에서 시뮬레이션 코드를 아래 실제 API 호출로 교체합니다.
class=class="token-string">"token-comment">// pubspec.yaml에 추가: dart_openai: ^4.1.0 import class="token-string">'package:dart_openai/dart_openai.dart'; class=class="token-string">"token-comment">// main.dart에서 초기화 (Firebase 초기화 이후) OpenAI.apiKey = const String.fromEnvironment(class="token-string">'OPENAI_API_KEY'); class=class="token-string">"token-comment">// ai_chat_service.dart - sendMessage 메서드 교체 Future<void> sendMessage( String roomId, String text, String currentUserId, String aiType, String aiName, ) async { class=class="token-string">"token-comment">// 1. 사용자 메시지 추가 final userMsg = ChatMessage( id: DateTime.now().millisecondsSinceEpoch.toString(), roomId: roomId, senderId: currentUserId, text: text, timestamp: DateTime.now(), isMe: true, ); _messages.add(userMsg); notifyListeners(); try { class=class="token-string">"token-comment">// 2. 대화 히스토리 구성 (최근 10개 메시지) final history = _messages.takeLast(10).map((msg) => OpenAIChatCompletionChoiceMessageModel( role: msg.isMe ? OpenAIChatMessageRole.user : OpenAIChatMessageRole.assistant, content: [ OpenAIChatCompletionChoiceMessageContentItemModel.text(msg.text) ], ) ).toList(); class=class="token-string">"token-comment">// 3. OpenAI API 호출 final response = await OpenAI.instance.chat.create( model: class="token-string">'gpt-4o-mini', messages: [ class=class="token-string">"token-comment">// 시스템 프롬프트 (페르소나 설정) OpenAIChatCompletionChoiceMessageModel( role: OpenAIChatMessageRole.system, content: [ OpenAIChatCompletionChoiceMessageContentItemModel.text( getPersonaPrompt(aiName, aiType) ) ], ), ...history, ], maxTokens: 300, temperature: 0.8, ); final aiText = response.choices.first.message.content?.first.text ?? class="token-string">''; class=class="token-string">"token-comment">// 4. AI 응답 메시지 추가 _messages.add(ChatMessage( id: (DateTime.now().millisecondsSinceEpoch + 1).toString(), roomId: roomId, senderId: class="token-string">'ai_$aiType', text: aiText, timestamp: DateTime.now(), isMe: false, )); notifyListeners(); } catch (e) { debugPrint(class="token-string">'OpenAI API 오류: $e'); class=class="token-string">"token-comment">// 오류 시 fallback 메시지 _messages.add(ChatMessage( id: (DateTime.now().millisecondsSinceEpoch + 1).toString(), roomId: roomId, senderId: class="token-string">'ai_$aiType', text: class="token-string">'잠시 연결이 불안정합니다. 조금 후에 다시 말씀해 주세요.', timestamp: DateTime.now(), isMe: false, )); notifyListeners(); } }
API 키 환경 변수 설정 (--dart-define 방식)
# 개발 실행 시 flutter run --dart-define=OPENAI_API_KEY=sk-your-key-here # 빌드 시 flutter build apk --dart-define=OPENAI_API_KEY=sk-your-key-here
GPS 위치 서비스 설정
원터치 GPS 동네 설정 기능을 위한 플랫폼별 권한 설정입니다.
Android 권한 설정
class="token-string">"http:class="token-commentclass="token-string">">//schemas.android.com/apk/res/android"> class="token-string">"android.permission.ACCESS_FINE_LOCATION" /> class="token-string">"android.permission.ACCESS_COARSE_LOCATION" /> ...
iOS 권한 설정
NSLocationWhenInUseUsageDescription 동네 설정을 위해 현재 위치를 사용합니다. NSLocationAlwaysAndWhenInUseUsageDescription 동네 설정을 위해 현재 위치를 사용합니다.
mypage_screen.dart 위치 서비스 연동
class=class="token-string">"token-comment">// pubspec.yaml에 추가: class=class="token-string">"token-comment">// geolocator: ^11.1.0 class=class="token-string">"token-comment">// geocoding: ^3.0.0 import class="token-string">'package:geolocator/geolocator.dart'; import class="token-string">'package:geocoding/geocoding.dart'; Future<void> _setLocation() async { class=class="token-string">"token-comment">// 1. 권한 확인 LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) return; } class=class="token-string">"token-comment">// 2. 현재 위치 획득 final position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.medium, ); class=class="token-string">"token-comment">// 3. 좌표 → 행정구역 변환 final placemarks = await placemarkFromCoordinates( position.latitude, position.longitude, ); if (placemarks.isNotEmpty) { final place = placemarks.first; class=class="token-string">"token-comment">// 예: class="token-string">"포항시 북구" final location = class="token-string">'${place.locality} ${place.subLocality}'; class=class="token-string">"token-comment">// 4. Firestore에 저장 await FirebaseFirestore.instance .collection(class="token-string">'users') .doc(currentUserId) .update({class="token-string">'location': location}); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(class="token-string">'동네가 "$location"으로 설정되었습니다.')), ); } }
FCM 푸시 알림 설정
새 메시지 수신 시 푸시 알림을 전송하는 FCM을 설정합니다.
FCM 초기화 코드
class=class="token-string">"token-comment">// lib/services/notification_service.dart(신규 파일 생성) import class="token-string">'package:firebase_messaging/firebase_messaging.dart'; import class="token-string">'package:flutter_local_notifications/flutter_local_notifications.dart'; class NotificationService { final FirebaseMessaging _messaging = FirebaseMessaging.instance; Future<void> initialize() async { class=class="token-string">"token-comment">// 1. 알림 권한 요청 await _messaging.requestPermission( alert: true, badge: true, sound: true, ); class=class="token-string">"token-comment">// 2. FCM 토큰 획득 (Firestore에 저장) final token = await _messaging.getToken(); if (token != null) { await _saveTokenToFirestore(token); } class=class="token-string">"token-comment">// 3. 포그라운드 메시지 처리 FirebaseMessaging.onMessage.listen((RemoteMessage message) { _showLocalNotification(message); }); class=class="token-string">"token-comment">// 4. 백그라운드 메시지 핸들러 등록 FirebaseMessaging.onBackgroundMessage(_backgroundHandler); } Future<void> _saveTokenToFirestore(String token) async { final uid = FirebaseAuth.instance.currentUser?.uid; if (uid == null) return; await FirebaseFirestore.instance .collection(class="token-string">'users') .doc(uid) .update({class="token-string">'fcmToken': token}); } } class=class="token-string">"token-comment">// 백그라운드 핸들러 (최상위 함수로 선언 필수) @pragma(class="token-string">'vm:entry-point') Future<void> _backgroundHandler(RemoteMessage message) async { await Firebase.initializeApp(); debugPrint(class="token-string">'백그라운드 메시지: ${message.messageId}'); }
Android 설정
class="token-string">"com.google.firebase.messaging.FirebaseMessagingService" android:exported=class="token-string">"false"> class="token-string">"com.google.firebase.MESSAGING_EVENT" />
최종 체크리스트
모든 단계를 완료하면 전화번호 인증, AI 친구 채팅, GPS 동네 설정, 푸시 알림이
실제 Firebase 서비스와 연동된 프로덕션 앱이 완성됩니다.