Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

🍽️ 구르맛 (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

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

Loading
Type
Select type
Language
Select language
Sort
Select order
Showing 3 of 3 repositories

Top languages

Loading...

Most used topics

Loading...

AltStyle によって変換されたページ (->オリジナル) /