Skip to content
@Gourmet-HSU

Gourmet

🍽️ 구르맛 (Gourmet)

대학생을 위한 로컬 맛집 추천 플랫폼


🎬 프로젝트 시연 영상

구르맛 시연 영상

📺 클릭하여 전체 시연 영상 보기


📖 프로젝트 개요

**구르맛(Gourmet)**은 대학생들이 쉽게 주변 맛집을 찾고 공유할 수 있는 위치 기반 맛집 추천 앱입니다.
대학교를 중심으로 3km 반경 내 맛집 정보를 제공하며, Thread 스타일의 실시간 리뷰 피드를 통해 생생한 맛집 정보를 공유할 수 있습니다.

📅 개발 기간

2024년 9월 2일 ~ 2024년 12월 20일 (15주)

👥 팀 구성 및 역할

이름 역할
최은서 (팀장) Design & Frontend 개발
손주완 Frontend,Backend 개발
전상우 Frontend,Backend 개발
홍혜창 Frontend,Backend 개발

✨ 주요 기능

🗺️ 1. 맛집 지도

  • 위치 기반 서비스: 대학교 중심 3km 반경 내 맛집 표시
  • 대학교별 필터링: 3개 주요 대학교 지원 (서울 소재)
  • 실시간 위치 추적: 사용자의 현재 위치 표시
  • 지도 인터랙션: 마커 클릭 시 음식점 간단 정보 표시

📝 2. 실시간 리뷰 게시물

  • Thread 스타일 피드: SNS 형태의 직관적인 리뷰 피드
  • 이미지 업로드: 최대 5장의 음식 사진 업로드
  • 별점 평가: 1~5점 별점 시스템
  • 좋아요/싫어요: 리뷰에 대한 피드백 기능
  • 리뷰 작성 제한: 대학생 인증 완료 시에만 작성 가능

👤 3. 타임라인 & 마이페이지

  • 리뷰 타임라인: 내가 작성한 리뷰 시간순 정렬
  • 프로필 관리: 닉네임, 이메일, 소속 정보 표시
  • 닉네임 수정: 실시간 닉네임 변경 기능
  • 리뷰 삭제: 작성한 리뷰 관리

🎓 대학생 인증 시스템

인증 프로세스

  1. 이메일 도메인 검증: 학교 공식 이메일 주소 확인
  2. 이메일 인증: Firebase Authentication을 통한 이메일 인증 링크 발송
  3. 60초 제한 시간: 제한 시간 내 이메일 인증 완료 필요

권한 관리

  • 대학생: 리뷰 작성, 좋아요/싫어요, 음식점 조회
  • 일반 회원: 음식점 조회만 가능 (리뷰 작성 불가)

🛠️ 기술 스택

Frontend

  • Flutter
  • Dart

Backend & Database

  • Firebase
    • Firebase Authentication: 이메일 기반 회원 인증
    • Cloud Firestore: NoSQL 실시간 데이터베이스
    • Firebase Storage: 이미지 파일 저장

주요 패키지

패키지 버전 용도
google_maps_flutter ^2.5.0 Google Maps 지도 표시
geolocator ^13.0.1 GPS 위치 추적
cloud_firestore ^5.4.4 Firestore 데이터베이스 연동
firebase_auth ^5.3.1 Firebase 인증
firebase_storage ^12.3.4 이미지 업로드/다운로드
image_picker ^1.1.2 갤러리에서 이미지 선택
cached_network_image ^3.3.0 네트워크 이미지 캐싱
google_fonts ^6.2.1 커스텀 폰트 적용

🏗️ 시스템 설계

📊 데이터 흐름도

데이터 흐름도

🗂️ 데이터베이스 구조

ERD (Entity Relationship Diagram)

데이터베이스 ERD

테이블 상세 정보

데이터베이스 테이블

주요 컬렉션 (Firestore)

  • users: 사용자 정보 (이메일, 닉네임, 대학교, 학생 여부)
  • Restaurant: 음식점 정보 (이름, 위치, 카테고리, 영업시간, 메뉴)
  • Review: 리뷰 정보 (별점, 내용, 이미지, 좋아요/싫어요 수)
  • Menu: 메뉴 정보 (이름, 가격, 이미지)
  • University: 대학교 정보 (이름, 이메일 도메인)

🧩 메뉴 구조도

메뉴 구조도


📱 화면 구성

🔐 인증 화면

스플래시 & 로그인

회원가입 - 대학생

대학생 회원가입 절차: 대학생/일반인 선택 → 학교 이메일 인증 → 개인정보 입력

회원가입 - 일반 회원

일반 회원가입 절차: 대학생/일반인 선택 → 직업 입력 → 개인정보 입력


🗺️ 맛집 지도

지도 메인 화면

기능: 대학교 선택 필터, 3km 반경 표시, 현재 위치 추적, 하단 음식점 리스트

음식점 클릭

기능: 지도 마커 클릭 시 음식점 간단 정보 (이름, 카테고리, 평점, 영업시간)


🍴 음식점 상세

메뉴 탭 & 리뷰 탭

메뉴 탭: 음식 사진, 메뉴명, 가격
리뷰 탭: 별점, 리뷰 내용, 사진, 작성일


📝 리뷰 작성 & 피드

리뷰 작성 화면

기능: 별점 선택, 리뷰 작성, 사진 추가 (최대 5장), 음식점 검색

리뷰 피드

기능: Thread 스타일 피드, 좋아요/싫어요, 실시간 업데이트


👤 마이페이지

타임라인 & 프로필

타임라인: 내가 작성한 리뷰 시간순 정렬, 리뷰 삭제
프로필: 이메일, 이름, 소속, 닉네임 수정


🚀 주요 구현 사항

1. 위치 기반 서비스

  • geolocator 패키지를 활용한 실시간 GPS 추적
  • Google Maps API를 통한 지도 표시
  • 대학교 중심 좌표 기준 3km 반경 Circle 표시
  • Firestore의 Schools 배열 필드를 활용한 음식점 필터링

2. Firebase 통합

  • Authentication: 이메일 인증 기반 회원가입/로그인
  • Firestore: 실시간 데이터 동기화 (StreamBuilder + snapshots())
  • Storage: 이미지 업로드 후 URL 저장

3. 실시간 업데이트

  • Firestore의 snapshots() 메서드를 통한 실시간 데이터 스트림
  • FieldValue.increment()를 활용한 좋아요/싫어요 카운터
  • arrayUnion/arrayRemove를 통한 중복 방지

4. 이미지 처리

  • image_picker로 갤러리에서 다중 이미지 선택
  • Firebase Storage에 업로드 후 Firestore에 URL 저장
  • cached_network_image로 이미지 로딩 최적화

🎯 프로젝트 목표 및 성과

목표

  • ✅ 대학생 맞춤형 위치 기반 맛집 추천 시스템 구축
  • ✅ 실시간 리뷰 공유를 통한 커뮤니티 활성화
  • ✅ Firebase를 활용한 서버리스 백엔드 구현
  • ✅ Flutter로 크로스 플랫폼 앱 개발

성과

  • 3개 대학교 주변 맛집 데이터베이스 구축
  • Thread 스타일의 직관적인 리뷰 피드 구현
  • 대학생 인증 시스템을 통한 신뢰성 있는 리뷰 관리
  • 실시간 데이터 동기화를 통한 사용자 경험 향상

🔧 트러블슈팅

리뷰 좋아요/싫어요 기능 구현 시 데이터베이스 설계 문제

문제 상황: 단순 카운트만으로는 부족했던 초기 설계

초기 데이터베이스 설계

처음에는 리뷰에 대한 좋아요/싫어요 기능을 단순하게 생각하여 개수만 저장하는 방식으로 설계했습니다.

Review 컬렉션 (초기 설계)
├── Review_number (Key, AUTO_INCREMENT)
├── Review_content (Field2, Domain)
├── Rating (Field3, Domain)
├── Date (Field6, Domain)
├── Good_rate (Field7, Domain)      ← 좋아요 개수만 저장
├── Bad_rate (Field8, Domain)       ← 싫어요 개수만 저장
├── User_id (Key2, Domain)
└── Restaurant_id (Key3, Domain)

발생한 문제

실제로 UI를 구현하다 보니 개수만으로는 해결할 수 없는 문제들이 발생했습니다:

  1. 사용자별 상태 표시 불가능

    • 현재 사용자가 좋아요를 눌렀는지 안 눌렀는지 알 수 없음
    • 아이콘을 채워진 상태(👍)로 표시할지, 빈 상태(👍🏻)로 표시할지 판단 불가
  2. 중복 방지 불가능

    • 한 사용자가 좋아요를 여러 번 누를 수 있음
  3. 상태 유지 불가능

    • 사용자가 화면을 나갔다가 다시 들어와도 자신이 눌렀던 버튼을 기억하지 못함
    • 매번 초기 상태로 표시됨

예시: 부족했던 정보

// ❌ 초기 방식: 누가 눌렀는지 알 수 없음
Good_rate: 15    // 15명이 좋아요를 눌렀다는 것만 알 수 있음
Bad_rate: 3      // 하지만 "내가" 눌렀는지는 알 수 없음!

해결 방법: 사용자 목록 추적 배열 추가

개선된 데이터베이스 설계

좋아요/싫어요를 누른 사용자의 UID를 배열로 저장하는 필드를 추가했습니다.

Review 컬렉션 (개선된 설계)
├── Review_number (Key, AUTO_INCREMENT)
├── Review_content (string)
├── Rating (number)
├── Date (timestamp)
├── Good_rate (number)              ← 좋아요 개수
├── Good_users (array)              ← ✨ 좋아요 누른 사용자 UID 배열 (신규)
├── Bad_rate (number)               ← 싫어요 개수
├── Bad_users (array)               ← ✨ 싫어요 누른 사용자 UID 배열 (신규)
├── Content (string)
├── Images (array)
├── Nickname (string)
├── Restaurant_name (string)
└── UserId (string)

실제 Firestore 문서 예시

{
  Bad_rate: 0,
  Bad_users: [],                    // 싫어요 누른 사용자 목록
  
  Good_rate: 1,
  Good_users: [                     // 좋아요 누른 사용자 목록
    "m8exH7avJ3fAwmBpBO0fjuqwQR73"  // 이 사용자가 좋아요를 눌렀음
  ],
  
  Content: "여비는 진짜 레전드..맛집입니다. 아침부터 오프런했어요!!!",
  Date: "2024년 12월 3일 PM 2시 58분 3초 UTC+9",
  Images: [
    "https://firebasestorage.googleapis.com/.../image1.jpg",
    "https://firebasestorage.googleapis.com/.../image2.jpg"
  ],
  Nickname: "전상우입니다",
  Rating: 5,
  Restaurant_name: "삼방매",
  UserId: "OIuoJe03BkOjNU4ZLeHFiCjHiYW2"
}

구현 코드: 사용자별 상태 관리

1. 현재 사용자의 좋아요/싫어요 상태 확인

// threadScreen.dart의 likeDislikeContainer 위젯

StreamBuilder(
  stream: FirebaseFirestore.instance
      .collection("Review")
      .doc(widget.documentId)
      .snapshots(),
  builder: (context, snapshot) {
    final doc = snapshot.data!.data();
    
    // ✅ Good_users 배열에 현재 사용자 UID가 있는지 확인
    isliked = doc!["Good_users"].contains(
      FirebaseAuth.instance.currentUser!.uid
    );
    
    // ✅ Bad_users 배열에 현재 사용자 UID가 있는지 확인
    isdisliked = doc!["Bad_users"].contains(
      FirebaseAuth.instance.currentUser!.uid
    );
    
    return Container(
      child: Row(
        children: [
          // 좋아요 버튼
          IconButton(
            onPressed: () async {
              // 좋아요 토글
            },
            icon: Icon(
              isliked ? Icons.thumb_up : Icons.thumb_up_outlined,
              color: isliked ? Colors.blue : Colors.grey,
            ),
          ),
          Text("${doc["Good_rate"]}"),
          
          // 싫어요 버튼
          IconButton(
            onPressed: () async {
              // 싫어요 토글
            },
            icon: Icon(
              isdisliked ? Icons.thumb_down : Icons.thumb_down_outlined,
              color: isdisliked ? Colors.red : Colors.grey,
            ),
          ),
          Text("${doc["Bad_rate"]}"),
        ],
      ),
    );
  }
)

2. 좋아요/싫어요 토글 로직

// 좋아요 버튼 클릭 시
IconButton(
  onPressed: () async {
    isliked = !isliked;  // 상태 토글
    
    await FirebaseFirestore.instance
        .collection("Review")
        .doc(widget.documentId)
        .update({
      // ✅ 카운트 증가/감소
      "Good_rate": FieldValue.increment(isliked ? 1 : -1),
      
      // ✅ 사용자 UID를 배열에 추가/제거
      "Good_users": isliked
          ? FieldValue.arrayUnion([FirebaseAuth.instance.currentUser!.uid])
          : FieldValue.arrayRemove([FirebaseAuth.instance.currentUser!.uid])
    });
  },
  icon: Icon(
    isliked ? Icons.thumb_up : Icons.thumb_up_outlined,
    color: isliked ? Colors.blue : Colors.grey,
  ),
)

// 싫어요 버튼도 동일한 방식
IconButton(
  onPressed: () async {
    isdisliked = !isdisliked;
    
    await FirebaseFirestore.instance
        .collection("Review")
        .doc(widget.documentId)
        .update({
      "Bad_rate": FieldValue.increment(isdisliked ? 1 : -1),
      "Bad_users": isdisliked
          ? FieldValue.arrayUnion([FirebaseAuth.instance.currentUser!.uid])
          : FieldValue.arrayRemove([FirebaseAuth.instance.currentUser!.uid])
    });
  },
  icon: Icon(
    isdisliked ? Icons.thumb_down : Icons.thumb_down_outlined,
    color: isdisliked ? Colors.red : Colors.grey,
  ),
)

개선 효과

Before (문제 있는 방식)

사용자 A가 리뷰 화면 진입
↓
좋아요 개수: 15개 표시
↓
❌ 내가 눌렀는지 모름 → 빈 아이콘(👍🏻) 표시
↓
좋아요 버튼 클릭
↓
❌ 중복 체크 불가 → 개수만 증가 (16개)
↓
화면 나갔다가 다시 진입
↓
❌ 상태 초기화 → 또 빈 아이콘 표시

After (개선된 방식)

사용자 A가 리뷰 화면 진입
↓
Good_users 배열 확인
↓
✅ 내 UID가 있음 → 채워진 아이콘(👍) 표시
↓
좋아요 버튼 클릭 (토글)
↓
✅ arrayRemove로 UID 제거 → 중복 방지
✅ increment(-1)로 개수 감소
↓
화면 나갔다가 다시 진입
↓
✅ Good_users 배열 확인 → 상태 유지

핵심 개선 사항

기능 Before After
사용자별 상태 표시 ❌ 불가능 (개수만 저장) ✅ 가능 (Good_users 배열 확인)
중복 방지 ❌ 불가능 (여러 번 클릭 가능) arrayUnion/arrayRemove로 자동 방지
상태 유지 ❌ 화면 재진입 시 초기화 ✅ 배열에 UID 저장으로 영구 유지
아이콘 표시 ❌ 항상 빈 아이콘 ✅ 눌렀으면 채워진 아이콘
동시 클릭 방지 ❌ 좋아요+싫어요 동시 가능 ✅ 배열 기반 상태 관리로 방지 가능

NoSQL의 유연성 경험

스키마리스(Schemaless) 구조의 장점

이번 문제를 해결하면서 Firebase Firestore의 NoSQL 특성을 제대로 활용할 수 있었습니다.

  1. 필드 추가의 유연성
   기존 문서에 새 필드 추가 시:
   
   RDB (MySQL, PostgreSQL)
   ❌ ALTER TABLE 필요
   ❌ 기존 데이터 마이그레이션 필요
   ❌ 스키마 변경 시간 소요
   
   NoSQL (Firestore)
   ✅ 그냥 필드 추가하면 끝
   ✅ 기존 데이터 영향 없음
   ✅ 즉시 반영
  1. 점진적 마이그레이션

    • 기존에 생성된 리뷰 문서: Good_users, Bad_users 필드 없음
    • 새로 생성되는 리뷰 문서: 해당 필드 포함
    • 둘 다 정상 작동 → 필드 없으면 빈 배열로 처리
  2. 실제 적용 과정

   // ✅ 필드가 없어도 안전하게 처리
   isliked = (doc["Good_users"] ?? []).contains(
     FirebaseAuth.instance.currentUser!.uid
   );
   
   // ✅ 새 문서 생성 시 필드 추가
   await FirebaseFirestore.instance
       .collection("Review")
       .add({
     "Good_rate": 0,
     "Good_users": [],  // 신규 필드
     "Bad_rate": 0,
     "Bad_users": [],   // 신규 필드
     // ... 기타 필드
   });

느낀 점

"처음에는 단순하게 좋아요 개수만 세면 된다고 생각했는데, 실제 구현하다 보니 **'누가 눌렀는지'**를 추적해야 한다는 걸 깨달았습니다.

만약 RDB였다면 테이블 구조를 변경하고 마이그레이션 스크립트를 작성해야 했겠지만, Firestore의 스키마리스 특성 덕분에 기존 데이터에 영향 없이 새로운 필드를 추가할 수 있었습니다.

이번 경험을 통해 NoSQL의 유연성과 arrayUnion/arrayRemove 같은 Firestore의 강력한 배열 연산 기능을 제대로 활용할 수 있었습니다."

📂 프로젝트 구조

gourmet_app/
├── lib/
│   ├── main.dart                           # 앱 진입점
│   ├── firebase_options.dart               # Firebase 설정
│   ├── screens/
│   │   ├── loginScreen.dart                # 로그인 화면
│   │   ├── signupScreen.dart               # 회원가입 화면
│   │   ├── mapScreen.dart                  # 지도 메인 화면
│   │   ├── restaurantDetailsScreen.dart    # 음식점 상세 화면
│   │   ├── uploadScreen.dart               # 리뷰 작성 화면
│   │   ├── threadScreen.dart               # 리뷰 피드 화면
│   │   └── myPageScreen.dart               # 마이페이지 화면
│   └── components/
│       └── restourantInfo.dart             # 음식점 정보 컴포넌트
├── docs/                                   # 문서 및 이미지
│   ├── data-flow-diagram.png
│   ├── database-erd.png
│   ├── database-table.png
│   ├── menu-structure.png
│   └── screen-*.png
├── images/                                 # 앱 내 리소스
│   └── loading.png
└── pubspec.yaml                            # 패키지 의존성 관리

🔧 개발 환경 설정

요구 사항

  • Flutter SDK 3.24 이상
  • Dart 3.5.3 이상
  • Firebase 프로젝트 설정 (Authentication, Firestore, Storage 활성화)
  • Google Maps API 키 발급

🍽️ 대학생을 위한 맛집 추천 플랫폼, 구르맛 🍽️

Popular repositories Loading

  1. .github .github Public

    1

  2. flutter-app flutter-app Public

    flutter

    Dart

  3. flutter-test flutter-test Public

    github 통합 테스트

    Dart

Repositories

Showing 3 of 3 repositories

Top languages

Loading…

Most used topics

Loading…