본문 바로가기

프로젝트

TourAPI 대전 전체 장소 FB에 저장하는 코드

areaCode = 3; 은 대전을 특정한다.

totalCount는 751 였다.

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:dotenv/dotenv.dart' as dotenv;
import 'package:my_place_data_uploader/tag_map.dart';

Future<void> main() async {
  final env = dotenv.DotEnv()..load();

  final tourApiKey = env['TOUR_API_KEY'];
  final projectId = env['FIREBASE_PROJECT_ID'];
  final firebaseApiKey = env['FIREBASE_API_KEY'];
  final areaCode = 3;
  const pageSize = 100;

  // ✅ 먼저 totalCount 확인
  final initUrl = Uri.parse(
    'https://apis.data.go.kr/B551011/KorService1/areaBasedList1'
    '?serviceKey=$tourApiKey'
    '&numOfRows=1'
    '&pageNo=1'
    '&MobileOS=ETC'
    '&MobileApp=AITrip'
    '&_type=json'
    '&areaCode=$areaCode',
  );

  final initResponse = await http.get(initUrl);
  if (initResponse.statusCode != 200) {
    print('❌ 초기 요청 실패: ${initResponse.statusCode}');
    return;
  }

  final decodedInit = utf8.decode(initResponse.bodyBytes);
  final initJson = json.decode(decodedInit);
  final totalCount = initJson['response']['body']['totalCount'];
  final totalPages = (totalCount / pageSize).ceil();

  print('📦 총 장소 수: $totalCount → 총 페이지 수: $totalPages');

  for (int page = 1; page <= totalPages; page++) {
    final tourUrl = Uri.parse(
      'https://apis.data.go.kr/B551011/KorService1/areaBasedList1'
      '?serviceKey=$tourApiKey'
      '&numOfRows=$pageSize'
      '&pageNo=$page'
      '&MobileOS=ETC'
      '&MobileApp=AITrip'
      '&_type=json'
      '&areaCode=$areaCode',
    );

    final response = await http.get(tourUrl);
    if (response.statusCode != 200) {
      print('❌ 페이지 $page 요청 실패: ${response.statusCode}');
      continue;
    }

    final decoded = utf8.decode(response.bodyBytes);
    final jsonData = json.decode(decoded);
    final items = jsonData['response']['body']['items']['item'];
    final places = items is List ? items : [items];

    for (final item in places) {
      final title = item['title'] ?? '제목 없음';
      final address = item['addr1'] ?? '';
      final image = item['firstimage'] ?? '';
      final lat = double.tryParse(item['mapy']?.toString() ?? '') ?? 0.0;
      final lng = double.tryParse(item['mapx']?.toString() ?? '') ?? 0.0;

      final contentTypeId = item['contenttypeid']?.toString();
      final cat1 = item['cat1']?.toString();
      final cat2 = item['cat2']?.toString();
      final cat3 = item['cat3']?.toString();

      final tags = <String>{};
      if (contentTypeId != null && num_tagmap.containsKey(contentTypeId)) {
        tags.addAll(num_tagmap[contentTypeId]!);
      }
      if (cat1 != null && major_tagmap.containsKey(cat1)) {
        tags.addAll(major_tagmap[cat1]!);
      }
      if (cat2 != null && middle_tagmap.containsKey(cat2)) {
        tags.addAll(middle_tagmap[cat2]!);
      }
      if (cat3 != null && sub_tagmap.containsKey(cat3)) {
        tags.addAll(sub_tagmap[cat3]!);
      }

      // ✅ 중복 체크 (pname + address)
      final queryUrl = Uri.parse(
        'https://firestore.googleapis.com/v1/projects/$projectId/databases/(default)/documents:runQuery?key=$firebaseApiKey',
      );

      final queryBody = {
        "structuredQuery": {
          "from": [{"collectionId": "places"}],
          "where": {
            "compositeFilter": {
              "op": "AND",
              "filters": [
                {
                  "fieldFilter": {
                    "field": {"fieldPath": "pname"},
                    "op": "EQUAL",
                    "value": {"stringValue": title}
                  }
                },
                {
                  "fieldFilter": {
                    "field": {"fieldPath": "address"},
                    "op": "EQUAL",
                    "value": {"stringValue": address}
                  }
                }
              ]
            }
          },
          "limit": 1
        }
      };

      final checkResponse = await http.post(
        queryUrl,
        headers: {'Content-Type': 'application/json'},
        body: jsonEncode(queryBody),
      );

      final checkData = json.decode(checkResponse.body);
      final alreadyExists = checkData.any((doc) => doc['document'] != null);

      if (alreadyExists) {
        print('⛔ 중복: $title');
        continue;
      }

      // ✅ 저장할 데이터 구조
      final firestoreData = {
        "fields": {
          "pname": {"stringValue": title},
          "address": {"stringValue": address},
          "place_photo": {"stringValue": image},
          "location": {
            "mapValue": {
              "fields": {
                "lat": {"doubleValue": lat},
                "lng": {"doubleValue": lng}
              }
            }
          },
          "ptags": {
            "arrayValue": {
              "values": tags.map((tag) => {"stringValue": tag}).toList()
            }
          },
          "created_at": {
            "timestampValue": DateTime.now().toUtc().toIso8601String()
          }
        }
      };

      final firestoreUrl = Uri.parse(
        'https://firestore.googleapis.com/v1/projects/$projectId/databases/(default)/documents/places?key=$firebaseApiKey',
      );

      final saveResponse = await http.post(
        firestoreUrl,
        headers: {'Content-Type': 'application/json'},
        body: jsonEncode(firestoreData),
      );

      if (saveResponse.statusCode == 200) {
        print('✅ 저장 완료: $title');
      } else {
        print('❌ 저장 실패 (${saveResponse.statusCode}): $title');
        print(saveResponse.body);
      }
    }
  }

  print('🎉 전체 저장 완료!');
}

지난번에는 numrows 5개까지만 저장하는 코드였고 이번에는 TourAPI에 있는 대전에 해당하는 모든 장소들의 총 개수와 페이지 수를 불러와서 검색된 전체 장소들을 저장하는 코드이다.

 

참고로 .env에는 여러 API key들이 모여있다.

보안상 민감한 정보이므로 따로 올리지는 않겠다.