그룹 여행을 더 쉽고 재미있게! 일정 관리부터 비용 정산, 추억 공유까지 한 번에.
여기가는 친구, 동료, 가족과 함께하는 여행의 모든 과정을 체계적으로 관리하는 종합 여행 동반자 앱입니다.
- 총 코드 라인: 27,834 줄
- Dart 파일: 174개
- 기능 모듈: 11개 독립 모듈
- API 엔드포인트: 65개
- 화면 수: 24개
- 상태 관리 Provider: 58개 (StateNotifier 18개, Future 15개, Provider 25개)
- 팀원 가능 날짜 입력: 각자 여행 가능한 날짜 범위 선택
- 시각적 겹침 표시: 여러 명의 일정이 겹치는 날짜를 색상 농도로 표시
- 최적 날짜 선택: 모두가 가능한 날짜를 한눈에 파악
- 제안 단계 (Pending): 멤버들이 가고 싶은 장소를 자유롭게 제안
- 확정 단계 (Confirmed): 리더가 최종 일정 확정 및 순서 설정
- 완료 단계 (Completed): 방문 완료 체크 및 사진 자동 매칭
- 네이버 지도 연동: 장소 검색 및 상세 정보 제공
- 드래그 앤 드롭: 직관적인 일정 순서 변경
- EXIF 메타데이터 분석: 사진의 GPS 좌표 자동 추출
- 100m 반경 자동 매칭: 확정된 장소와 사진을 자동으로 연결
- 장소별 앨범: 방문한 장소마다 사진이 자동으로 정리
- 즐겨찾기: 마음에 드는 사진에 별표 표시
- 재매칭 기능: 일정 수정 후 사진 위치 재계산
- FCM 백그라운드 핸들러: 앱이 비활성 상태여도 5분마다 위치 자동 전송
- 실시간 지도: 여행 중 멤버들의 현재 위치 표시
- 경로 기록: 방장의 이동 경로 자동 저장
- 여행 복기: 완료된 여행의 전체 경로를 지도에 표시
- 1/n 자동 계산: 참여 인원에 따라 비용 자동 분할
- 일자별 정리: 여행 날짜별로 정산 내역 그룹화
- 체크리스트: 각 멤버의 정산 완료 여부 추적
- 정산 타입: 더치페이(Dutch Pay) 및 커스텀 분담 지원
- 리더 공지사항: 전체 멤버에게 중요 메시지 전달 (완료 체크)
- 집결지 (Ping): 만날 장소와 시간 공유 (도착 확인)
- URL 스킴:
yeogiga://trip/invite/:tripId - 원클릭 참가: 링크 클릭만으로 여행 자동 참가
- 인증 후 복귀: 비로그인 시 로그인 후 자동으로 원래 초대 링크로 이동
- Flutter: 3.35.4 (크로스플랫폼 UI)
- Dart: 3.9.2
- flutter_riverpod 2.6.1
- StateNotifierProvider: 복잡한 비즈니스 로직
- FutureProvider: 비동기 데이터 페칭
- Provider: 의존성 주입
- dio 5.8.0: HTTP 클라이언트
- retrofit 4.4.2: 타입 안전 REST API 클라이언트
- json_serializable: JSON 직렬화
- go_router 7.0.0: 선언적 라우팅 및 딥링크
- Firebase
firebase_core: 초기화firebase_messaging: FCM 푸시 알림
- Kakao
kakao_flutter_sdk_user: 소셜 로그인kakao_flutter_sdk_auth: 인증
- Naver
flutter_naver_map: 지도 SDK- Naver Place Search API: 장소 검색
- geolocator: GPS 위치 (실시간 추적)
- file_picker: 파일 선택
- exif: EXIF 메타데이터 (GPS 좌표 추출)
- image_gallery_saver_plus: 갤러리 저장
- share_plus: 공유 기능
- flutter_secure_storage: JWT 토큰 암호화 저장
- flutter_screenutil: 반응형 디자인 (393×852 기준)
- flutter_svg: SVG 아이콘
- skeletons: 로딩 스켈레톤
- liquid_pull_to_refresh: 물방울 효과 새로고침
- flutter_slidable: 스와이프 제스처
11개의 독립적인 기능 모듈로 구성되어 있으며, 각 모듈은 Model-Repository-Provider-View 계층으로 명확히 분리되어 있습니다.
lib/
├── common/ # 공통 컴포넌트, 상수, 유틸리티 (41개 파일)
│ ├── component/ # 재사용 UI 컴포넌트
│ ├── const/ # 상수 (색상, API URL)
│ ├── dio/ # CustomInterceptor (JWT 자동 갱신)
│ ├── provider/ # 핵심 Provider (Dio, GoRouter)
│ └── service/ # FCM 백그라운드 핸들러
│
├── trip/ # 여행 생성/관리/초대 (31개 파일)
├── main_trip/ # 홈 화면 (4개 파일)
├── trip_list/ # 여행 목록 (3개 파일)
├── schedule/ # 일정 관리 (14개 파일)
├── settlement/ # 비용 정산 (10개 파일)
├── trip_image/ # 사진 관리 (10개 파일)
├── user/ # 사용자 인증 (15개 파일)
├── notice/ # 공지사항/집결지 (10개 파일)
├── naver/ # 네이버 API (3개 파일)
└── w2m/ # When2Meet (6개 파일)
각 기능 모듈은 다음 구조를 따릅니다:
model/- 데이터 모델 (@JsonSerializable)repository/- API 통신 레이어 (Retrofit/Dio)provider/- Riverpod 상태 관리view/orscreen/- UI 화면component/- 재사용 가능한 UI 컴포넌트
Riverpod 3가지 주요 패턴:
// 1. StateNotifierProvider - 복잡한 상태 관리
final userMeProvider = StateNotifierProvider<UserMeStateNotifier, UserModelBase?>
// 2. FutureProvider - 비동기 데이터 페칭 (auto-dispose)
final mainTripFutureProvider = FutureProvider.autoDispose<MainTripModel?>
// 3. Provider - 의존성 주입
final dioProvider = Provider<Dio>주요 패턴:
AsyncValue<T>: loading/error/data 자동 처리.when(): 상태별 UI 분기ref.watch(): build 메서드에서 상태 구독ref.read(): 이벤트 핸들러에서 상태 읽기- Optimistic UI: 네트워크 응답 전 UI 즉시 업데이트, 실패 시 롤백
2가지 접근 방식:
@RestApi()
abstract class UserMeRepository {
@GET('/users/my')
@Headers({'accessToken': 'true'}) // 자동 토큰 주입
Future<UserResponseModel> getMe();
}Future<PostTripResponse> postTrip({required String title}) async {
final response = await dio.post(
'$baseUrl/trip',
options: Options(headers: {"accessToken": 'true'}),
data: {"title": title},
);
return PostTripResponse.fromJson(response.data);
}CustomInterceptor 기능:
- ✅ JWT 토큰 자동 주입 (
'accessToken': 'true'헤더 감지) - ✅ 401 에러 시 Refresh Token으로 자동 재발급
- ✅ 원본 요청 자동 재시도 (새 토큰 사용)
- ✅ Refresh Token 만료 시 자동 로그아웃
GoRouter 주요 기능:
- 인증 상태 기반 route guard (
authProvider.redirectLogic) - 비인증 사용자의 딥링크 URL 보존 (로그인 후 복귀)
- 커스텀 페이지 전환 애니메이션 (fade + slide, 150ms)
- Route observer로 생명주기 관리
딥링크 처리 흐름:
yeogiga://trip/invite/:tripId클릭- 비로그인 →
/login?redirect=%2Ftrip%2Finvite%2F123 - 로그인 성공 → 자동으로
/trip/invite/123로 이동 TripInviteHandler에서 자동으로 여행 참가 API 호출- 성공 시 여행 상세 화면으로 이동
- Flutter SDK: 3.7.0 이상
- Dart SDK: 3.9.0 이상
- iOS: Xcode 14 이상, CocoaPods
- Android: Android Studio, Gradle 7.0+
-
저장소 클론
git clone [repository-url] cd yeogiga-app -
의존성 설치
flutter pub get
-
환경 변수 설정
.env.example파일을 복사하여.env파일 생성:cp .env.example .env
그 후 실제 API 키 값들을 입력:
# API 서버 (프로덕션) API_BASE_URL=https://api.yeogiga.com/api/v1 # 네이버 지도 NAVER_MAP_API_CLIENT_ID=your_naver_map_key # 네이버 장소 검색 NAVER_PLACE_SEARCH_CLIENT_ID=your_naver_client_id NAVER_PLACE_SEARCH_CLIENT_SECRET=your_naver_client_secret # 카카오 로그인 KAKAO_NATIVE_APP_KEY=your_kakao_key
로컬 개발 시 API 서버 변경:
# Android Emulator (로컬 네트워크 IP 필요) API_BASE_URL=http://192.168.0.18:3000/api/v1 # iOS Simulator API_BASE_URL=http://127.0.0.1:3000/api/v1
-
코드 생성
JSON 직렬화 및 Retrofit API 클라이언트 생성:
dart run build_runner build --delete-conflicting-outputs
중요: 모델이나 Repository를 수정할 때마다 이 명령어를 실행해야 합니다.
개발 중 자동 생성 (watch 모드):
dart run build_runner watch --delete-conflicting-outputs
-
앱 실행
# 기본 실행 flutter run # 특정 디바이스 선택 flutter devices flutter run -d <device-id>
# 정적 분석
flutter analyze
# 코드 포맷팅
dart format .
# 테스트 실행
flutter test
# 테스트 커버리지
flutter test --coverage# Android APK
flutter build apk --release
# Android App Bundle (Google Play용)
flutter build appbundle --release
# iOS
flutter build ios --release# 빌드 캐시 삭제 및 재설치
flutter clean && flutter pub get
# 전체 재빌드 (코드 생성 포함)
flutter clean && flutter pub get && dart run build_runner build --delete-conflicting-outputs# 앱 아이콘 생성
flutter pub run flutter_launcher_icons
# 스플래시 스크린 생성
flutter pub run flutter_native_splash:create- 카카오 소셜 로그인 → 임시 토큰 발급
- 닉네임 설정 (신규 사용자)
- 정식 JWT 토큰 발급 (Access + Refresh Token)
- 자동 토큰 갱신 (401 에러 시 Refresh Token으로 재발급)
- flutter_secure_storage 사용
- iOS: Keychain
- Android: EncryptedSharedPreferences
- 저장 항목:
ACCESS_TOKEN,REFRESH_TOKEN
// CustomInterceptor에서 자동 처리
onError(DioException err) {
if (err.response?.statusCode == 401) {
// 1. Refresh Token으로 새 토큰 발급
// 2. Secure Storage에 저장
// 3. 원본 요청 헤더 업데이트
// 4. 원본 요청 재시도
}
}- 서버에서 여행 진행 중인 사용자에게 5분마다 FCM 데이터 메시지 전송
@pragma('vm:entry-point')백그라운드 핸들러 실행 (별도 isolate)- GPS 위치 획득 (
geolocator) - 여행 시작일 기준 현재 일차(day) 계산
- 2개 API 동시 호출:
/trip/:tripId/members/location(멤버 위치)/trip/:tripId/days/:day/routes(호스트 경로)
- Android:
ACCESS_FINE_LOCATION,ACCESS_BACKGROUND_LOCATION - iOS:
NSLocationAlwaysUsageDescription - 사용자 설정: "항상 허용" 위치 권한 필요
- 폰트: Pretendard (9가지 Weight: 100~900)
- 기본 크기: 14sp ~ 25sp
디자인 기준: 393×852 (iPhone 13/14 기준)
ScreenUtilInit(designSize: const Size(393, 852))
// 사용 예시
Text('Hello', style: TextStyle(fontSize: 16.sp)) // 화면 비례 폰트
Container(width: 100.w, height: 50.h) // 비례 너비/높이
SizedBox(height: 20.h) // 비례 간격
BorderRadius.circular(12.r) // 비례 반경ConfirmationDialog: 확인/취소 다이얼로그 (삭제, 로그아웃 등)CustomTextFormField: 통일된 텍스트 입력 필드PrimaryButton: 메인 액션 버튼SettingTripCard/TripCard: 여행 카드 UIDaySelector: 일차 선택 컴포넌트
- SVG 형식 (22개 커스텀 아이콘)
- 위치:
asset/icon/
트리거: main 브랜치 push 시 자동 실행
빌드 파이프라인:
- ✅ Android Keystore 디코드
- ✅ Flutter 의존성 설치 (
flutter pub get) - ✅ 정적 분석 (
flutter analyze) - ✅ 테스트 실행 (
flutter test) - ✅ AAB 빌드 (
flutter build appbundle --release) - ✅ Google Play Internal Test Track 자동 업로드
환경 변수 (Codemagic 설정):
CM_KEYSTORE: Base64 인코딩된 KeystoreCM_KEYSTORE_PASSWORD,CM_KEY_PASSWORD,CM_KEY_ALIASGCLOUD_SERVICE_ACCOUNT_CREDENTIALS: Google Play 인증 JSON
버전 관리:
- Format:
major.minor.patch+buildNumber pubspec.yaml에서 관리- 현재 버전: 1.0.0+3
- main: 프로덕션 브랜치 (안정 버전)
- Feature(*): 기능별 브랜치
- 예:
Feature(IL-362)-딥링크(여행초대) - 예:
Feature-사진-크게보기-UI-및-로직-구현
- 예:
Feature: 새로운 기능 추가
UI: UI/UX 개선
Fix: 버그 수정
Refactor: 코드 리팩토링
Docs: 문서 수정
Test: 테스트 추가/수정
Chore: 빌드 설정, 의존성 업데이트
// EXIF 메타데이터에서 GPS 좌표 추출
final exifData = await readExifFromBytes(imageBytes);
final latitude = exifData['GPS GPSLatitude'];
final longitude = exifData['GPS GPSLongitude'];
// 확정된 장소와 거리 계산 (100m 반경 내 자동 매칭)
final distance = Geolocator.distanceBetween(
photoLat, photoLng, placeLat, placeLng
);
if (distance <= 100) {
// 자동 매칭
}// 즉시 UI 반영 (네트워크 응답 대기 안함)
state = AsyncValue.data(updatedState);
// 서버 요청
final success = await repository.update(...);
// 실패 시 롤백
if (!success) {
state = previousState;
}사용자는 토큰 만료를 전혀 인지하지 못함. 모든 API 요청에 자동 적용.
비로그인 사용자가 초대 링크 클릭 → 로그인 후 자동으로 원래 초대 링크로 복귀
- iOS 시뮬레이터에서 네이버 지도 로딩이 느릴 수 있습니다 (실제 기기에서는 정상)
- FCM 백그라운드 핸들러는 위치 권한이 "항상 허용"이어야 정상 동작합니다
- Android Emulator에서 로컬 API 서버 접속 시
10.0.2.2또는 로컬 네트워크 IP 사용 필요
- 기능 브랜치 생성 (
Feature(ISSUE-번호)-기능명) - 변경사항 커밋
- 브랜치 푸시
- Pull Request 생성 (템플릿 참조:
.github/PULL_REQUEST_TEMPLATE.md)
- CLAUDE.md: 프로젝트 가이드 (아키텍처, 패턴, 주의사항)
- CI.md: CI 파이프라인 상세 문서
- CD.md: CD 배포 프로세스 문서
Private project - All rights reserved
Team Ilmansa
버전: 1.0.0+3 마지막 업데이트: 2025년 1월
Made with ❤️ using Flutter