📌 앱 소개: 네이버 지역 검색 API와 현재 위치 기반 장소 검색 기능을 제공하는 앱
🕒 기간: 2025.04.20 ~ 2025.04.22 (3일)  
📱 플랫폼: Flutter 크로스 플랫폼 앱 (iOS, Android)  
👥 개발 인원: 1명 (개인 프로젝트)  
💼 역할: 앱 전체 개발 및 UI/UX 설계, API 연동
🛠️ 주요 사용 기술: Flutter Dart Naver Local API VWorld API Riverpod Geolocator InAppWebView URL Launcher Dio  
🔗 GitHub: daehan-lim/flutter-place-finder
  
  
  
  
  
  
  
  
  
어디든GO는 네이버 지역 검색 API와 VWorld API를 활용하여 사용자가 장소명이나 주소로 검색할 수 있는 위치 기반 검색 애플리케이션입니다. GPS를 통한 현재 위치 기반 검색과 다양한 지도 앱 연동을 지원하며, 사용자가 직관적으로 주변 장소를 탐색하고 상세 정보에 접근할 수 있는 모바일 환경을 제공합니다.
├── app/                              # 애플리케이션 설정 및 구성 관련 파일
│   ├── constants/                    # 앱 전체에서 사용되는 상수 정의
│   │   ├── app_colors.dart           # 앱의 색상 테마 및 색상 상수
│   │   ├── app_constants.dart        # 앱에서 사용되는 일반 상수값 (문자열, 숫자 등)
│   │   └── app_styles.dart           # 앱의 텍스트 스타일, 여백 등 스타일 상수
│   ├── app_providers.dart            # Riverpod 프로바이더 설정 및 전역 상태 정의
│   └── theme.dart                    # 앱의 MaterialApp 테마 설정
│
├── core/                             # 핵심 기능 및 공통 유틸리티 클래스
│   ├── exceptions/                   # 앱 전체에서 사용되는 예외 클래스
│   │   └── data_exceptions.dart      # API 및 데이터 관련 예외 정의
│   ├── services/                     # 비즈니스 로직 및 외부 서비스 연동
│   │   └── map_launcher_service.dart
│   └── utils/                        # 헬퍼 함수 및 유틸리티 클래스
│       ├── geolocator_util.dart
│       ├── snackbar_util.dart
│       └── string_format_utils.dart
│
├── data/                             # 데이터 관련 클래스 및 데이터 액세스 계층
│   ├── dto/                          # 데이터 전송 객체 (API 응답 직접 매핑용)
│   │   ├── naver_place_dto.dart
│   │   └── vworld_district_dto.dart
│   ├── model/                        # 앱 내에서 사용되는 데이터 모델
│   │   └── place.dart                # 장소 정보를 나타내는 모델 클래스
│   ├── network/                      # 네트워크 통신 관련 클래스
│   │   └── dio_clients.dart
│   └── repository/                   # 데이터 접근 및 비즈니스 로직 구현
│       └── location_repository.dart
│
├── ui/                               # 사용자 인터페이스 관련 코드
│   ├── pages/                        # 앱의 주요 화면들
│   │   ├── home/                     # 홈 화면 관련 파일
│   │   │   ├── home_page.dart
│   │   │   ├── home_view_model.dart
│   │   │   └── widgets/              # 홈 화면 전용 위젯
│   │   │       └── home_list_item.dart
│   │   └── web/                      # 웹뷰 화면 관련 파일
│   │       ├── place_web_page.dart
│   │       └── place_web_page_view_model.dart
│   └── widgets/                      # 앱 전체에서 재사용 가능한 공통 위젯
│       └── error_layout.dart
│
└── main.dart                         # 앱의 진입점
Geolocator를 활용한 GPS 좌표 획득 및 VWorld API 연동으로 사용자 현재 위치의 행정구역 자동 인식Bearer Token 기반 인증과 Dio HTTP 클라이언트를 통한 안정적인 API 통신 구현VWorld API의 좌표 기반 행정구역 정보 조회로 정확한 위치 기반 검색 지원BaseOptions를 통한 일관된 HTTP 설정 관리Dio 클라이언트 10초 연결/수신 타임아웃 설정으로 응답성 보장LogInterceptor 활용한 개발 효율성 향상Geo URI를 통해 사용자가 기기에 설치된 지도 앱(구글 맵, 네이버 지도, 카카오맵 등) 중 선택하여 장소를 열 수 있는 시스템 구현Apple Maps 대체 실행URL Launcher를 활용한 외부 앱 연동으로 끊김 없는 사용자 워크플로우 제공Custom User Agent 설정으로 모바일 최적화된 웹 페이지 로딩 구현InAppBrowserView 모드 활용으로 앱 내 통합 경험 제공ApiException, NetworkException, EnvFileException 등 상황별 맞춤형 예외 클래스 구현Tooltip 제공으로 가독성 향상InkWell 효과와 그림자를 활용한 카드 형태 리스트 아이템 구현GestureDetector를 통한 화면 터치 시 키보드 자동 숨김 기능으로 사용자 편의성 향상Provider 패턴과 의존성 주입을 통한 테스트 가능한 아키텍처 구현AsyncValue를 활용한 로딩, 에러, 데이터 상태의 일관된 관리DTO와 Model 분리를 통한 외부 API 의존성 최소화 및 데이터 무결성 보장flutter_dotenv를 활용한 API 키 보안 처리 및 환경별 설정 분리.env.example 파일 제공으로 개발 환경 설정 가이드 제공HomeListItem, MessageLayout 등 독립적인 위젯 컴포넌트 구현StringFormatUtils, SnackbarUtil 등 공통 유틸리티 클래스로 코드 중복 제거iOS 지도 앱 연동 Silent Failure 문제
문제 상황
iOS에서 네이버 지도가 설치되지 않은 상태에서 launchUrl()을 통해 커스텀 스킴(nmap://)을 실행하면 아무런 반응 없이 조용히 실패. try/catch로 설정한 Apple Maps fallback도 실행되지 않아 사용자가 아무 피드백이나 대안도 받지 못하는 상황
canLaunchUrl()의 신뢰성 문제를 경험함. 실행 가능한 geo: URI에 대해서도 false를 반환하는 현상 발생:if (await canLaunchUrl(Uri.parse('geo:0,0?q=$encoded'))) {
  // 실행 가능한 URI임에도 불구하고 false를 반환
}
canLaunchUrl()이 신뢰하기 어려울 것이라 판단하여 양쪽 플랫폼 모두 try/catch 방식으로 실패 처리 선택
static Future<void> openInMap(String queryAddress) async {
  if (Platform.isIOS) { 
    try {
      await launchUrl(naverUri, mode: LaunchMode.externalApplication);
    } catch (e) {
      // iOS에서는 이 catch 블록이 실행되지 않음
      final appleUri = Uri.parse('http://maps.apple.com/?q=$encoded');
      await launchUrl(appleUri, mode: LaunchMode.externalApplication);
    }
  }
}
launchUrl()은 Naver Map이 설치되어 있지 않더라도 예외를 던지지 않음catch 블록이 실행되지 않음Apple Maps 실행도 무시됨
canLaunchUrl()로 사전 검증 필요geo:와 같은 표준 스킴에 대해 예외 기반 처리가 안정적으로 작동최종 해결 방법
각 OS의 특성에 맞는 플랫폼별 처리 구현:
static Future<void> openInMap(String queryAddress) async {
  if (Platform.isIOS) { 
    final naverUri = Uri.parse('nmap://search?query=$encoded&appname=$appName');
    if (await canLaunchUrl(naverUri)) {
      await launchUrl(naverUri, mode: LaunchMode.externalApplication);
    } else {
      // 네이버 지도 사용 불가, Apple Maps 사용
      final appleUri = Uri.parse('http://maps.apple.com/?q=$encoded');
      if (await canLaunchUrl(appleUri)) {
        await launchUrl(appleUri, mode: LaunchMode.externalApplication);
      }
    }
  } else {
    // Android: geo URI에 대한 try/catch 방식 유지
    try {
      await launchUrl(geoUri, mode: LaunchMode.externalApplication);
    } catch (e) {
      log('Could not launch map: $e');
    }
  }
}