프로젝트

지도에서 위치 범위 설정, 위치 안에 있는 장소 필터링

TMJeti 2025. 4. 17. 14:57

반경 설정하는 부분이 이상해서 잘 고친 부분 수정한 뒤 장소 필터링 기능 코드를 다시 올린다.

 

사용자가 UI에서 여행반경을 설정하면 FireStore에 있는 places/해당장소 문서/lng, lat 정보를 가져와서  장소를 필터링 해준다.

import 'dart:math';

// 좌표 간 거리 계산 함수 (단위: km)
double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
  const double earthRadius = 6371.0;
  final dLat = _degToRad(lat2 - lat1);
  final dLon = _degToRad(lon2 - lon1);

  final a = sin(dLat / 2) * sin(dLat / 2) +
      cos(_degToRad(lat1)) * cos(_degToRad(lat2)) *
      sin(dLon / 2) * sin(dLon / 2);
  final c = 2 * atan2(sqrt(a), sqrt(1 - a));

  return earthRadius * c;
}

double _degToRad(double degree) => degree * pi / 180.0;

좌표 간 거리 계산 함수를 만들었다. 함수를 사용할 클래스 안에 넣어준다.

Future<void> fetchPlacesAndAskGpt() async {
    final url = Uri.parse(
      'https://firestore.googleapis.com/v1/projects/$firebaseProjectId/databases/(default)/documents/places?key=$firebaseApiKey',
    );

    final response = await http.get(url);
    if (response.statusCode != 200) {
      setState(() {
        gptAnswer = '❌ Firestore 불러오기 실패';
        isLoading = false;
      });
      return;
    }

    final jsonData = jsonDecode(utf8.decode(response.bodyBytes));
    final documents = jsonData['documents'] ?? [];

    final List<Map<String, dynamic>> placeList = [];

    final centerLat = location['lat'] ?? 0.0;
    final centerLng = location['lng'] ?? 0.0;
    final radiusKm = location['radius_km']?.toDouble() ?? 10.0;
    debugPrint('반경: $radiusKm km, 위치: ($centerLat, $centerLng)');
    debugPrint('실제 반경: $radiusKm km');

    for (final doc in documents) {
  final fields = doc['fields'];
  final name = fields['pname']?['stringValue'] ?? '이름 없음';

  final lat = double.tryParse(fields['location']?['mapValue']?['fields']?['lat']?['doubleValue']?.toString() ?? '') ?? 0.0;
  final lng = double.tryParse(fields['location']?['mapValue']?['fields']?['lng']?['doubleValue']?.toString() ?? '') ?? 0.0;

  final distance = calculateDistance(centerLat, centerLng, lat, lng);
  print('📍 $name → 거리: \${distance.toStringAsFixed(2)} km');

  if (distance <= radiusKm) {
    print('✅ 포함됨');

    final ptags = fields['ptags']?['arrayValue']?['values'];
    final tagList = ptags is List ? ptags.map((e) => e['stringValue'] as String).toList() : [];

    final address = fields['address']?['stringValue'] ?? '주소 없음';

    placeList.add({
      'name': name,
      'address': address,
      'tags': tagList,
      'lat': lat,
      'lng': lng,
    });
  } else {
    print('❌ 제외됨');
  }
}

    // 더 엄격한 프롬프트 작성
    final prompt = StringBuffer()
      ..writeln("당신은 여행 플래너입니다. 아래 조건에 따라 하나의 종합적인 '${totalDays}일 일정' 코스를 JSON 배열로 2~3개 추천해주세요.")
      ..writeln("출력 형식은 반드시 다음과 같아야 합니다:")
      ..writeln("[")
      ..writeln("  { courseName: string, scores: {}, comparisons: [], places: [ { name, address, day, duration } ] }, ...")
      ..writeln("]")
      ..writeln("조건:")
      ..writeln("- 동행자 유형: ${companions.join(', ')}")
      ..writeln("- 활동: ${activities.join(', ')}")
      ..writeln("- 테마: ${themes.join(', ')}")
      ..writeln("- 장소 키워드: ${places.join(', ')}")
      ..writeln("- 여행 기간: ${startDate.year}.${startDate.month}.${startDate.day} ~ ${endDate.year}.${endDate.month}.${endDate.day} (${totalDays}일)")
      ..writeln()
      ..writeln("⚠️ 주의사항:")
      ..writeln("- 각 코스는 1일차부터 ${totalDays}일차까지 모든 날을 포함해야 하며, `day` 필드를 통해 명시해주세요.")
      ..writeln("- 장소 간 동선을 고려하여 가능한 한 효율적인 이동 경로로 배치해 주세요.")
      ..writeln("- 각 장소에 머무는 시간은 `duration` 필드로 1~4시간 사이로 적절히 조절해주세요.")
      ..writeln()
      ..writeln("📍 후보 장소 목록:");
      for (var p in placeList) {
        prompt.writeln("- ${p['name']} / ${p['address']} / ${p['tags'].join(', ')}");
      }

    for (var p in placeList) {
      prompt.writeln("- ${p['name']} / ${p['address']} / ${p['tags'].join(', ')}");
    }

    final result = await askGPT(prompt.toString());
    final jsonStr = extractJson(result);

    try {
      final parsed = jsonDecode(jsonStr);
      if (parsed is List) {
        final processedResults = validateAndFixDays(parsed);
        setState(() {
          travelResults = processedResults;
          isLoading = false;
          gptAnswer = '여행 코스가 준비되었습니다!';
        });
      } else {
        throw Exception("리스트 형태 아님");
      }
    } catch (e) {
      print("JSON 파싱 오류: $e\n응답 내용: $result");
      setState(() {
        travelResults = [];
        gptAnswer = '❌ 응답 형식 오류';
        isLoading = false;
      });
    }
  }

 

double.tryParse는 타입 에러 방지를 위해 사용한다.

final lat, lng 부분이 firestore에서 가져오는 장소의 위치 정보이다.

 

"location": {
  "lat": 36.35,
  "lng": 127.38
}
FireStore에 저장한 방식이다. 실제 저장은 places(엔드포인트)/장소 문서/location/lat, lng 이다.

 

코드 알고리즘은 맞아보이는데 범위 지정이 안되고 전체장소만 계속 가져왔다. 왜 이러는지는 flutter map 공부 후 후술해보겠다.

 

-> 해결 완료 했다. 반경 설정 부분에서 오류가 난 것이었다. 실행 부분은 반경 설정 포스트에 사진 올려놨다.