Files
lunchpick/lib/data/datasources/remote/naver_html_parser.dart
2025-11-19 16:36:39 +09:00

306 lines
8.9 KiB
Dart

import 'package:html/dom.dart';
import 'package:flutter/foundation.dart';
/// 네이버 지도 HTML 파서
///
/// 네이버 지도 페이지의 HTML에서 식당 정보를 추출합니다.
class NaverHtmlParser {
// CSS 셀렉터 상수
static const List<String> _nameSelectors = [
'span.GHAhO',
'h1.Qpe7b',
'span.Fc1rA',
'[class*="place_name"]',
'meta[property="og:title"]',
];
static const List<String> _categorySelectors = [
'span.DJJvD',
'span.lnJFt',
'[class*="category"]',
];
static const List<String> _descriptionSelectors = [
'span.IH7VW',
'div.vV_z_',
'meta[name="description"]',
];
static const List<String> _phoneSelectors = [
'span.xlx7Q',
'a[href^="tel:"]',
'[class*="phone"]',
];
static const List<String> _addressSelectors = [
'span.IH7VW',
'span.jWDO_',
'[class*="address"]',
];
static const List<String> _businessHoursSelectors = [
'time.aT6WB',
'div.O8qbU',
'[class*="business"]',
'[class*="hours"]',
];
/// HTML 문서에서 식당 정보 추출
Map<String, dynamic> parseRestaurantInfo(Document document) {
return {
'name': extractName(document),
'category': extractCategory(document),
'subCategory': extractSubCategory(document),
'description': extractDescription(document),
'phone': extractPhoneNumber(document),
'roadAddress': extractRoadAddress(document),
'address': extractJibunAddress(document),
'latitude': extractLatitude(document),
'longitude': extractLongitude(document),
'businessHours': extractBusinessHours(document),
};
}
/// 식당 이름 추출
String? extractName(Document document) {
try {
for (final selector in _nameSelectors) {
final element = document.querySelector(selector);
if (element != null) {
if (element.localName == 'meta') {
return element.attributes['content'];
}
final text = element.text.trim();
if (text.isNotEmpty) {
return text;
}
}
}
return null;
} catch (e) {
debugPrint('NaverHtmlParser: 이름 추출 실패 - $e');
return null;
}
}
/// 카테고리 추출
String? extractCategory(Document document) {
try {
for (final selector in _categorySelectors) {
final element = document.querySelector(selector);
if (element != null) {
final text = element.text.trim();
if (text.isNotEmpty) {
// 첫 번째 카테고리만 추출 (예: "한식 > 국밥" -> "한식")
return text.split('>').first.trim();
}
}
}
return null;
} catch (e) {
debugPrint('NaverHtmlParser: 카테고리 추출 실패 - $e');
return null;
}
}
/// 서브 카테고리 추출
String? extractSubCategory(Document document) {
try {
final element = document.querySelector('span.DJJvD, span.lnJFt');
if (element != null) {
final text = element.text.trim();
if (text.contains('>')) {
// 두 번째 카테고리 반환 (예: "한식 > 국밥" -> "국밥")
return text.split('>').last.trim();
}
}
return null;
} catch (e) {
debugPrint('NaverHtmlParser: 서브 카테고리 추출 실패 - $e');
return null;
}
}
/// 설명 추출
String? extractDescription(Document document) {
try {
for (final selector in _descriptionSelectors) {
final element = document.querySelector(selector);
if (element != null) {
if (element.localName == 'meta') {
return element.attributes['content'];
}
final text = element.text.trim();
if (text.isNotEmpty) {
return text;
}
}
}
return null;
} catch (e) {
debugPrint('NaverHtmlParser: 설명 추출 실패 - $e');
return null;
}
}
/// 전화번호 추출
String? extractPhoneNumber(Document document) {
try {
for (final selector in _phoneSelectors) {
final element = document.querySelector(selector);
if (element != null) {
if (element.localName == 'a' && element.attributes['href'] != null) {
return element.attributes['href']!.replaceFirst('tel:', '');
}
final text = element.text.trim();
if (text.isNotEmpty && RegExp(r'[\d\-\+\(\)]+').hasMatch(text)) {
return text;
}
}
}
return null;
} catch (e) {
debugPrint('NaverHtmlParser: 전화번호 추출 실패 - $e');
return null;
}
}
/// 도로명 주소 추출
String? extractRoadAddress(Document document) {
try {
for (final selector in _addressSelectors) {
final elements = document.querySelectorAll(selector);
for (final element in elements) {
final text = element.text.trim();
// 도로명 주소 패턴 확인
if (text.contains('') || text.contains('')) {
return text;
}
}
}
return null;
} catch (e) {
debugPrint('NaverHtmlParser: 도로명 주소 추출 실패 - $e');
return null;
}
}
/// 지번 주소 추출
String? extractJibunAddress(Document document) {
try {
for (final selector in _addressSelectors) {
final elements = document.querySelectorAll(selector);
for (final element in elements) {
final text = element.text.trim();
// 지번 주소 패턴 확인 (숫자-숫자 형식 포함)
if (RegExp(r'\d+\-\d+').hasMatch(text) &&
!text.contains('') &&
!text.contains('')) {
return text;
}
}
}
return null;
} catch (e) {
debugPrint('NaverHtmlParser: 지번 주소 추출 실패 - $e');
return null;
}
}
/// 위도 추출
double? extractLatitude(Document document) {
try {
// 메타 태그에서 좌표 정보 찾기
final metaElement = document.querySelector('meta[property="og:url"]');
if (metaElement != null) {
final content = metaElement.attributes['content'];
if (content != null) {
// URL에서 좌표 파라미터 추출 (예: ?y=37.5666805)
final RegExp latRegex = RegExp(r'[?&]y=(\d+\.\d+)');
final match = latRegex.firstMatch(content);
if (match != null) {
return double.tryParse(match.group(1)!);
}
}
}
// 자바스크립트 변수에서 추출 시도
final scripts = document.querySelectorAll('script');
for (final script in scripts) {
final content = script.text;
if (content.contains('latitude') || content.contains('lat')) {
final RegExp latRegex = RegExp(r'(?:latitude|lat)["\s:]+(\d+\.\d+)');
final match = latRegex.firstMatch(content);
if (match != null) {
return double.tryParse(match.group(1)!);
}
}
}
return null;
} catch (e) {
debugPrint('NaverHtmlParser: 위도 추출 실패 - $e');
return null;
}
}
/// 경도 추출
double? extractLongitude(Document document) {
try {
// 메타 태그에서 좌표 정보 찾기
final metaElement = document.querySelector('meta[property="og:url"]');
if (metaElement != null) {
final content = metaElement.attributes['content'];
if (content != null) {
// URL에서 좌표 파라미터 추출 (예: ?x=126.9784147)
final RegExp lonRegex = RegExp(r'[?&]x=(\d+\.\d+)');
final match = lonRegex.firstMatch(content);
if (match != null) {
return double.tryParse(match.group(1)!);
}
}
}
// 자바스크립트 변수에서 추출 시도
final scripts = document.querySelectorAll('script');
for (final script in scripts) {
final content = script.text;
if (content.contains('longitude') || content.contains('lng')) {
final RegExp lonRegex = RegExp(r'(?:longitude|lng)["\s:]+(\d+\.\d+)');
final match = lonRegex.firstMatch(content);
if (match != null) {
return double.tryParse(match.group(1)!);
}
}
}
return null;
} catch (e) {
debugPrint('NaverHtmlParser: 경도 추출 실패 - $e');
return null;
}
}
/// 영업시간 추출
String? extractBusinessHours(Document document) {
try {
for (final selector in _businessHoursSelectors) {
final elements = document.querySelectorAll(selector);
for (final element in elements) {
final text = element.text.trim();
if (text.isNotEmpty &&
(text.contains('') ||
text.contains(':') ||
text.contains('영업'))) {
return text;
}
}
}
return null;
} catch (e) {
debugPrint('NaverHtmlParser: 영업시간 추출 실패 - $e');
return null;
}
}
}