Daehan Lim

Header Project Icon

Cooki - AI 레시피 커뮤니티 앱

📝 개요

📌 앱 소개: 생성형 AI를 활용한 개인 맞춤형 레시피 생성 및 공유 플랫폼
🕒 기간: 2025.06.01 ~ 2025.07.04 (1개월)
📱 플랫폼: Flutter 크로스 플랫폼 앱 (iOS, Android)
👥 개발 인원: 3명
💼 역할: AI 레시피 생성, 레시피 수정, 리뷰 관련 기능, 신고 기능, 다국어 지원 개발, 등
🛠️ 주요 사용 기술: Flutter Dart Firebase Riverpod MVVM Gemini API Firestore Dio Cloud Functions Google Cloud Translation API
🔗 GitHub: flutter-fantastic-four/cooki-app
🔗 App Store: apps.apple.com/kr/app/cooki/id6747327839

1 ad screen 2 ad screen Recipe generation screen Recipe detail screen Community screen Review screen My recipes screen

📖 프로젝트 배경

🛠️ Tech Stack

Flutter Dart Riverpod MVVM Firebase Firestore Dio Cloud Functions Gemini AI Crashlytics Firebase Storage Google Translate API Cached Network Image Image Picker Image Compress Share Plus SharedPreferences i18n Speech Recognition Easy Image Viewer

🌟 수행 내용 및 성과

AI 레시피 생성 및 관리 시스템 구축

리뷰 시스템 및 번역 기능

다국어 지원 및 음성 인식

레시피 외부 공유 기능

UI/UX 최적화 및 성능 개선

아키텍처 및 예외 처리 시스템

🧭 기술적 의사결정

1. Gemini AI 모델 선택 및 2단계 검증 시스템 구축

// 검증 모델 설정
_validationModel = googleAI.generativeModel(
  model: 'gemini-1.5-flash',
  generationConfig: GenerationConfig(
    responseMimeType: 'application/json',
    responseSchema: Schema.object(
      properties: {'isValid': Schema.boolean()},
    ),
  ),
);

// 생성 모델 설정  
_recipeGenerationModel = googleAI.generativeModel(
  model: 'gemini-2.0-flash',
  generationConfig: GenerationConfig(
    responseMimeType: 'application/json',
    responseSchema: Schema.object(/* 레시피 구조 정의 */),
  ),
);

2. Firebase Cloud Functions 기반 번역 시스템

exports.translateText = onCall({ region: "asia-northeast3" }, async (request) => {
  try {
    const { text, targetLanguage, sourceLanguage } = request.data;
    
    const translationRequest = {
      parent: `projects/${projectId}/locations/global`,
      contents: [text],
      mimeType: 'text/plain',
      targetLanguageCode: targetLanguage,
      ...(sourceLanguage && { sourceLanguageCode: sourceLanguage }),
    };
    
    const [response] = await translationClient.translateText(translationRequest);
    
    return {
      success: true,
      translatedText: response.translations[0].translatedText,
      detectedSourceLanguage: response.translations[0].detectedLanguageCode || sourceLanguage
    };
  } catch (error) {
    throw new Error('Translation failed: ' + error.message);
  }
});

3. 통합 로깅 및 크래시 모니터링 유틸리티

void logError(
  dynamic error,
  StackTrace stack, {
  String? reason,
  bool fatal = false,
}) {
  final message = reason != null 
      ? '[EXCEPTION] $reason\n$error' 
      : '[EXCEPTION] $error';
  log(message, stackTrace: stack);

  FirebaseCrashlytics.instance.recordError(
    error,
    stack,
    reason: reason,
    fatal: fatal,
  );
}

// 사용 예시
try {
  final bytes = await imageDownloadRepository.downloadImage(
    recipe.imageUrl!,
  );
  ...
} catch (e, stack) {
  logError(e, stack, reason: 'Image download failed');
}

4. 멀티모달 프롬프트 엔지니어링 아키텍처

Future<String> _buildRecipePrompt({
  String? textInput,
  Set<String>? preferences,
  required bool hasImage,
  required String textOnlyRecipePromptPath,
  required String imageRecipePromptPath,
}) async {
  if (hasImage) {
    String imagePrompt = await rootBundle.loadString(
      'assets/prompts/$imageRecipePromptPath',
    );
    
    // 동적 섹션 구성
    String textContextSection = textInput?.isNotEmpty == true 
        ? await _buildTextContextSection(textInput!)
        : '';
    String preferencesSection = await _buildPreferencesSection(preferences);
    
    return imagePrompt
        .replaceAll(AppConstants.textContextSectionPlaceholder, textContextSection)
        .replaceAll(AppConstants.preferencesSectionPlaceholder, preferencesSection);
  }
  // 텍스트 전용 프롬프트 처리...
}

🌱 문제 해결

1. 레시피 생성과 이미지 업로드 병렬 처리 최적화

// 기존 순차 처리 방식
final generatedRecipe = await _generateRecipe(
  imageBytes: compressedImageBytes, // Uint8List 바이너리 데이터로 AI에 전송
  textInput: state.textInput,
  // ...
);
if (generatedRecipe != null) {
  final imageUrl = await _uploadImageToStorage(compressedImageBytes, user.id);
  final saved = await _saveRecipe(generatedRecipe, user, imageUrl);
}

// 개선된 병렬 처리 방식
final generationTask = _generateRecipe(...);
final imageUploadTask = _uploadImageIfNeeded(...);
final results = await Future.wait([generationTask, imageUploadTask]);

final generated = results[0] as GeneratedRecipe?;
final imageUrl = results[1] as String?;

2. 리뷰 언어 감지 최적화

// 기존 동기 처리 방식
await saveReview(review);
await detectAndUpdateLanguage(reviewId); // UI 블로킹

// 개선된 비동기 처리 방식
final reviewId = await saveReview(review);
detectAndUpdateLanguage(reviewId); // await 제거로 백그라운드 실행

3. 이미지 리사이징·압축 최적화를 통한 API 비용 절감 및 성능 향상

4. 다중 이미지 병렬 업로드 시 파일명 충돌 문제

🎞️ 시연 영상

Watch the Video