diff --git a/lib/services/subscription_url_matcher.dart b/lib/services/subscription_url_matcher.dart index a6c46eb..99bdd65 100644 --- a/lib/services/subscription_url_matcher.dart +++ b/lib/services/subscription_url_matcher.dart @@ -1,382 +1,17 @@ import 'dart:convert'; import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; +import 'url_matcher/models/service_info.dart'; +import 'url_matcher/data/legacy_service_data.dart'; -/// 서비스 정보를 담는 데이터 클래스 -class ServiceInfo { - final String serviceId; - final String serviceName; - final String? serviceUrl; - final String? cancellationUrl; - final String categoryId; - final String categoryNameKr; - final String categoryNameEn; - - ServiceInfo({ - required this.serviceId, - required this.serviceName, - this.serviceUrl, - this.cancellationUrl, - required this.categoryId, - required this.categoryNameKr, - required this.categoryNameEn, - }); -} +// ServiceInfo를 외부에서 접근 가능하도록 export +export 'url_matcher/models/service_info.dart'; /// 구독 서비스와 웹사이트 URL 매칭을 처리하는 서비스 클래스 class SubscriptionUrlMatcher { static Map? _servicesData; static bool _isInitialized = false; - // 레거시 데이터 (JSON 로드 실패시 폴백) - // OTT 서비스 - static final Map ottServices = { - 'netflix': 'https://www.netflix.com', - '넷플릭스': 'https://www.netflix.com', - 'disney+': 'https://www.disneyplus.com', - '디즈니플러스': 'https://www.disneyplus.com', - 'youtube premium': 'https://www.youtube.com/premium', - '유튜브 프리미엄': 'https://www.youtube.com/premium', - 'watcha': 'https://watcha.com', - '왓챠': 'https://watcha.com', - 'wavve': 'https://www.wavve.com', - '웨이브': 'https://www.wavve.com', - 'apple tv+': 'https://tv.apple.com', - '애플 티비플러스': 'https://tv.apple.com', - 'tving': 'https://www.tving.com', - '티빙': 'https://www.tving.com', - 'prime video': 'https://www.primevideo.com', - '프라임 비디오': 'https://www.primevideo.com', - 'amazon prime': 'https://www.amazon.com/prime', - '아마존 프라임': 'https://www.amazon.com/prime', - 'coupang play': 'https://play.coupangplay.com', - '쿠팡 플레이': 'https://play.coupangplay.com', - 'hulu': 'https://www.hulu.com', - '훌루': 'https://www.hulu.com', - }; - - // 음악 서비스 - static final Map musicServices = { - 'spotify': 'https://www.spotify.com', - '스포티파이': 'https://www.spotify.com', - 'apple music': 'https://music.apple.com', - '애플 뮤직': 'https://music.apple.com', - 'melon': 'https://www.melon.com', - '멜론': 'https://www.melon.com', - 'genie': 'https://www.genie.co.kr', - '지니': 'https://www.genie.co.kr', - 'youtube music': 'https://music.youtube.com', - '유튜브 뮤직': 'https://music.youtube.com', - 'bugs': 'https://music.bugs.co.kr', - '벅스': 'https://music.bugs.co.kr', - 'flo': 'https://www.music-flo.com', - '플로': 'https://www.music-flo.com', - 'vibe': 'https://vibe.naver.com', - '바이브': 'https://vibe.naver.com', - 'tidal': 'https://www.tidal.com', - '타이달': 'https://www.tidal.com', - }; - - // 저장 (클라우드/파일) 서비스 - static final Map storageServices = { - 'google drive': 'https://www.google.com/drive/', - '구글 드라이브': 'https://www.google.com/drive/', - 'dropbox': 'https://www.dropbox.com', - '드롭박스': 'https://www.dropbox.com', - 'onedrive': 'https://www.onedrive.com', - '원드라이브': 'https://www.onedrive.com', - 'icloud': 'https://www.icloud.com', - '아이클라우드': 'https://www.icloud.com', - 'box': 'https://www.box.com', - '박스': 'https://www.box.com', - 'pcloud': 'https://www.pcloud.com', - 'mega': 'https://mega.nz', - '메가': 'https://mega.nz', - 'naver mybox': 'https://mybox.naver.com', - '네이버 마이박스': 'https://mybox.naver.com', - }; - - // 통신 · 인터넷 · TV 서비스 - static final Map telecomServices = { - 'skt': 'https://www.sktelecom.com', - 'sk텔레콤': 'https://www.sktelecom.com', - 'kt': 'https://www.kt.com', - 'lgu+': 'https://www.lguplus.com', - 'lg유플러스': 'https://www.lguplus.com', - 'olleh tv': 'https://www.kt.com/olleh_tv', - '올레 tv': 'https://www.kt.com/olleh_tv', - 'b tv': 'https://www.skbroadband.com', - '비티비': 'https://www.skbroadband.com', - 'u+모바일tv': 'https://www.lguplus.com', - '유플러스모바일tv': 'https://www.lguplus.com', - }; - - // 생활/라이프스타일 서비스 - static final Map lifestyleServices = { - '네이버 플러스': 'https://plus.naver.com', - 'naver plus': 'https://plus.naver.com', - '카카오 구독': 'https://subscribe.kakao.com', - 'kakao subscribe': 'https://subscribe.kakao.com', - '쿠팡 와우': 'https://www.coupang.com/np/coupangplus', - 'coupang wow': 'https://www.coupang.com/np/coupangplus', - '스타벅스 버디': 'https://www.starbucks.co.kr', - 'starbucks buddy': 'https://www.starbucks.co.kr', - 'cu 구독': 'https://cu.bgfretail.com', - 'gs25 구독': 'https://gs25.gsretail.com', - '현대차 구독': 'https://www.hyundai.com/kr/ko/eco/vehicle-subscription', - 'lg전자 구독': 'https://www.lge.co.kr', - '삼성전자 구독': 'https://www.samsung.com/sec', - '다이슨 케어': 'https://www.dyson.co.kr', - 'dyson care': 'https://www.dyson.co.kr', - '마켓컬리': 'https://www.kurly.com', - 'kurly': 'https://www.kurly.com', - '헬로네이처': 'https://www.hellonature.com', - 'hello nature': 'https://www.hellonature.com', - '이마트 트레이더스': 'https://www.emarttraders.co.kr', - '홈플러스': 'https://www.homeplus.co.kr', - 'hellofresh': 'https://www.hellofresh.com', - '헬로프레시': 'https://www.hellofresh.com', - 'bespoke post': 'https://www.bespokepost.com', - }; - - // 쇼핑/이커머스 서비스 - static final Map shoppingServices = { - 'amazon prime': 'https://www.amazon.com/prime', - '아마존 프라임': 'https://www.amazon.com/prime', - 'walmart+': 'https://www.walmart.com/plus', - '월마트플러스': 'https://www.walmart.com/plus', - 'chewy': 'https://www.chewy.com', - '츄이': 'https://www.chewy.com', - 'dollar shave club': 'https://www.dollarshaveclub.com', - '달러셰이브클럽': 'https://www.dollarshaveclub.com', - 'instacart': 'https://www.instacart.com', - '인스타카트': 'https://www.instacart.com', - 'shipt': 'https://www.shipt.com', - '십트': 'https://www.shipt.com', - 'grove': 'https://grove.co', - '그로브': 'https://grove.co', - 'cratejoy': 'https://www.cratejoy.com', - 'shopify': 'https://www.shopify.com', - '쇼피파이': 'https://www.shopify.com', - }; - - // AI 서비스 - static final Map aiServices = { - 'chatgpt': 'https://chat.openai.com', - '챗GPT': 'https://chat.openai.com', - 'openai': 'https://openai.com', - '오픈AI': 'https://openai.com', - 'claude': 'https://claude.ai', - '클로드': 'https://claude.ai', - 'anthropic': 'https://www.anthropic.com', - '앤트로픽': 'https://www.anthropic.com', - 'midjourney': 'https://www.midjourney.com', - '미드저니': 'https://www.midjourney.com', - 'perplexity': 'https://www.perplexity.ai', - '퍼플렉시티': 'https://www.perplexity.ai', - 'copilot': 'https://copilot.microsoft.com', - '코파일럿': 'https://copilot.microsoft.com', - 'gemini': 'https://gemini.google.com', - '제미니': 'https://gemini.google.com', - 'google ai': 'https://ai.google', - '구글 AI': 'https://ai.google', - 'bard': 'https://bard.google.com', - '바드': 'https://bard.google.com', - 'dall-e': 'https://openai.com/dall-e', - '달리': 'https://openai.com/dall-e', - 'stable diffusion': 'https://stability.ai', - '스테이블 디퓨전': 'https://stability.ai', - }; - - // 프로그래밍 / 개발 서비스 - static final Map programmingServices = { - 'github': 'https://github.com', - '깃허브': 'https://github.com', - 'cursor': 'https://cursor.com', - '커서': 'https://cursor.com', - 'jetbrains': 'https://www.jetbrains.com', - '제트브레인스': 'https://www.jetbrains.com', - 'intellij': 'https://www.jetbrains.com/idea', - '인텔리제이': 'https://www.jetbrains.com/idea', - 'visual studio': 'https://visualstudio.microsoft.com', - '비주얼 스튜디오': 'https://visualstudio.microsoft.com', - 'aws': 'https://aws.amazon.com', - '아마존 웹서비스': 'https://aws.amazon.com', - 'azure': 'https://azure.microsoft.com', - '애저': 'https://azure.microsoft.com', - 'google cloud': 'https://cloud.google.com', - '구글 클라우드': 'https://cloud.google.com', - 'digitalocean': 'https://www.digitalocean.com', - '디지털오션': 'https://www.digitalocean.com', - 'heroku': 'https://www.heroku.com', - '헤로쿠': 'https://www.heroku.com', - 'codecademy': 'https://www.codecademy.com', - '코드아카데미': 'https://www.codecademy.com', - 'udemy': 'https://www.udemy.com', - '유데미': 'https://www.udemy.com', - 'coursera': 'https://www.coursera.org', - '코세라': 'https://www.coursera.org', - }; - - // 오피스 및 협업 툴 - static final Map officeTools = { - 'microsoft 365': 'https://www.microsoft.com/microsoft-365', - '마이크로소프트 365': 'https://www.microsoft.com/microsoft-365', - 'office 365': 'https://www.microsoft.com/microsoft-365', - '오피스 365': 'https://www.microsoft.com/microsoft-365', - 'google workspace': 'https://workspace.google.com', - '구글 워크스페이스': 'https://workspace.google.com', - 'slack': 'https://slack.com', - '슬랙': 'https://slack.com', - 'notion': 'https://www.notion.so', - '노션': 'https://www.notion.so', - 'trello': 'https://trello.com', - '트렐로': 'https://trello.com', - 'asana': 'https://asana.com', - '아사나': 'https://asana.com', - 'dropbox': 'https://www.dropbox.com', - '드롭박스': 'https://www.dropbox.com', - 'figma': 'https://www.figma.com', - '피그마': 'https://www.figma.com', - 'adobe creative cloud': 'https://www.adobe.com/creativecloud.html', - '어도비 크리에이티브 클라우드': 'https://www.adobe.com/creativecloud.html', - }; - - // 기타 유명 서비스 - static final Map otherServices = { - 'google one': 'https://one.google.com', - '구글 원': 'https://one.google.com', - 'icloud': 'https://www.icloud.com', - '아이클라우드': 'https://www.icloud.com', - 'nintendo switch online': 'https://www.nintendo.com/switch/online-service', - '닌텐도 스위치 온라인': 'https://www.nintendo.com/switch/online-service', - 'playstation plus': 'https://www.playstation.com/ps-plus', - '플레이스테이션 플러스': 'https://www.playstation.com/ps-plus', - 'xbox game pass': 'https://www.xbox.com/xbox-game-pass', - '엑스박스 게임 패스': 'https://www.xbox.com/xbox-game-pass', - 'ea play': 'https://www.ea.com/ea-play', - 'EA 플레이': 'https://www.ea.com/ea-play', - 'ubisoft+': 'https://ubisoft.com/plus', - '유비소프트+': 'https://ubisoft.com/plus', - 'epic games': 'https://www.epicgames.com', - '에픽 게임즈': 'https://www.epicgames.com', - 'steam': 'https://store.steampowered.com', - '스팀': 'https://store.steampowered.com', - }; - - // 해지 안내 페이지 URL 목록 (공식 해지 안내 페이지가 있는 서비스들) - static final Map cancellationUrls = { - // OTT 서비스 해지 안내 페이지 - 'netflix': 'https://help.netflix.com/ko/node/407', - '넷플릭스': 'https://help.netflix.com/ko/node/407', - 'disney+': - 'https://help.disneyplus.com/csp?id=csp_article_content&sys_kb_id=f0ddbe01db7601105d5e040ad3961979', - '디즈니플러스': - 'https://help.disneyplus.com/csp?id=csp_article_content&sys_kb_id=f0ddbe01db7601105d5e040ad3961979', - 'youtube premium': 'https://support.google.com/youtube/answer/6308278', - '유튜브 프리미엄': 'https://support.google.com/youtube/answer/6308278', - 'watcha': 'https://watcha.com/settings/payment', - '왓챠': 'https://watcha.com/settings/payment', - 'wavve': 'https://www.wavve.com/my', - '웨이브': 'https://www.wavve.com/my', - 'apple tv+': 'https://support.apple.com/ko-kr/HT202039', - '애플 티비플러스': 'https://support.apple.com/ko-kr/HT202039', - 'tving': 'https://www.tving.com/my/cancelMembership', - '티빙': 'https://www.tving.com/my/cancelMembership', - 'amazon prime': 'https://www.amazon.com/gp/primecentral/managemembership', - '아마존 프라임': 'https://www.amazon.com/gp/primecentral/managemembership', - - // 음악 서비스 해지 안내 페이지 - 'spotify': 'https://support.spotify.com/us/article/cancel-premium/', - '스포티파이': 'https://support.spotify.com/us/article/cancel-premium/', - 'apple music': 'https://support.apple.com/ko-kr/HT202039', - '애플 뮤직': 'https://support.apple.com/ko-kr/HT202039', - 'melon': - 'https://faqs2.melon.com/customer/faq/informFaq.htm?no=17&faqId=QUES20150209000002&orderChk=date&SEARCH_KEY=&SEARCH_PAR_CATEGORY=CATE20130909000006&SEARCH_CATEGORY=CATE20130909000021', - '멜론': - 'https://faqs2.melon.com/customer/faq/informFaq.htm?no=17&faqId=QUES20150209000002&orderChk=date&SEARCH_KEY=&SEARCH_PAR_CATEGORY=CATE20130909000006&SEARCH_CATEGORY=CATE20130909000021', - 'youtube music': 'https://support.google.com/youtubemusic/answer/6308278', - '유튜브 뮤직': 'https://support.google.com/youtubemusic/answer/6308278', - - // AI 서비스 해지 안내 페이지 - 'chatgpt': - 'https://help.openai.com/en/articles/7730211-manage-or-cancel-your-chatgpt-plus-subscription', - '챗GPT': - 'https://help.openai.com/en/articles/7730211-manage-or-cancel-your-chatgpt-plus-subscription', - 'claude': - 'https://help.anthropic.com/en/articles/8798313-how-do-i-cancel-my-claude-subscription', - '클로드': - 'https://help.anthropic.com/en/articles/8798313-how-do-i-cancel-my-claude-subscription', - 'midjourney': 'https://docs.midjourney.com/docs/manage-subscription', - '미드저니': 'https://docs.midjourney.com/docs/manage-subscription', - - // 프로그래밍 / 개발 서비스 해지 안내 페이지 - 'github': - 'https://docs.github.com/ko/billing/managing-billing-for-your-github-account/downgrading-your-github-subscription', - '깃허브': - 'https://docs.github.com/ko/billing/managing-billing-for-your-github-account/downgrading-your-github-subscription', - 'jetbrains': - 'https://sales.jetbrains.com/hc/en-gb/articles/207240845-How-to-cancel-an-auto-renewal-subscription-', - '제트브레인스': - 'https://sales.jetbrains.com/hc/en-gb/articles/207240845-How-to-cancel-an-auto-renewal-subscription-', - - // 오피스 및 협업 툴 해지 안내 페이지 - 'microsoft 365': - 'https://support.microsoft.com/en-us/office/cancel-a-microsoft-365-subscription-46e2634c-c64b-4c65-94b9-2cc9c960e91b', - '마이크로소프트 365': - 'https://support.microsoft.com/en-us/office/cancel-a-microsoft-365-subscription-46e2634c-c64b-4c65-94b9-2cc9c960e91b', - 'office 365': - 'https://support.microsoft.com/en-us/office/cancel-a-microsoft-365-subscription-46e2634c-c64b-4c65-94b9-2cc9c960e91b', - '오피스 365': - 'https://support.microsoft.com/en-us/office/cancel-a-microsoft-365-subscription-46e2634c-c64b-4c65-94b9-2cc9c960e91b', - 'slack': - 'https://slack.com/help/articles/360003378691-Cancel-your-Slack-subscription', - '슬랙': - 'https://slack.com/help/articles/360003378691-Cancel-your-Slack-subscription', - 'notion': - 'https://www.notion.so/help/billing-and-payment-settings#cancel-a-subscription', - '노션': - 'https://www.notion.so/help/billing-and-payment-settings#cancel-a-subscription', - 'dropbox': 'https://help.dropbox.com/accounts-billing/cancellation', - '드롭박스': 'https://help.dropbox.com/accounts-billing/cancellation', - 'adobe creative cloud': - 'https://helpx.adobe.com/manage-account/using/cancel-subscription.html', - '어도비 크리에이티브 클라우드': - 'https://helpx.adobe.com/manage-account/using/cancel-subscription.html', - - // 기타 유명 서비스 해지 안내 페이지 - 'google one': 'https://support.google.com/googleone/answer/9140429', - '구글 원': 'https://support.google.com/googleone/answer/9140429', - 'icloud': 'https://support.apple.com/ko-kr/HT207594', - '아이클라우드': 'https://support.apple.com/ko-kr/HT207594', - 'nintendo switch online': - 'https://en-americas-support.nintendo.com/app/answers/detail/a_id/41925/~/how-to-cancel-a-nintendo-switch-online-membership', - '닌텐도 스위치 온라인': - 'https://en-americas-support.nintendo.com/app/answers/detail/a_id/41925/~/how-to-cancel-a-nintendo-switch-online-membership', - 'playstation plus': - 'https://www.playstation.com/support/subscriptions/cancel-playstation-plus/', - '플레이스테이션 플러스': - 'https://www.playstation.com/support/subscriptions/cancel-playstation-plus/', - 'xbox game pass': - 'https://support.xbox.com/help/subscriptions-billing/manage-subscriptions/xbox-game-pass-how-to-cancel', - '엑스박스 게임 패스': - 'https://support.xbox.com/help/subscriptions-billing/manage-subscriptions/xbox-game-pass-how-to-cancel', - }; - - // 모든 서비스 매핑을 합친 맵 - static final Map allServices = { - ...ottServices, - ...musicServices, - ...storageServices, - ...aiServices, - ...programmingServices, - ...officeTools, - ...lifestyleServices, - ...shoppingServices, - ...telecomServices, - ...otherServices, - }; - /// JSON 데이터 초기화 static Future initialize() async { if (_isInitialized) return; @@ -466,7 +101,7 @@ class SubscriptionUrlMatcher { } // JSON에서 못 찾았으면 레거시 방식으로 찾기 - for (final entry in allServices.entries) { + for (final entry in LegacyServiceData.allServices.entries) { final serviceUrl = entry.value; final serviceDomain = extractDomain(serviceUrl); @@ -499,7 +134,7 @@ class SubscriptionUrlMatcher { try { // 정확한 매칭을 먼저 시도 - for (final entry in allServices.entries) { + for (final entry in LegacyServiceData.allServices.entries) { if (lowerName.contains(entry.key.toLowerCase())) { print('SubscriptionUrlMatcher: 정확한 매칭 - $lowerName -> ${entry.key}'); return entry.value; @@ -507,7 +142,7 @@ class SubscriptionUrlMatcher { } // OTT 서비스 검사 - for (final entry in ottServices.entries) { + for (final entry in LegacyServiceData.ottServices.entries) { if (lowerName.contains(entry.key.toLowerCase())) { print( 'SubscriptionUrlMatcher: OTT 서비스 매칭 - $lowerName -> ${entry.key}'); @@ -516,7 +151,7 @@ class SubscriptionUrlMatcher { } // 음악 서비스 검사 - for (final entry in musicServices.entries) { + for (final entry in LegacyServiceData.musicServices.entries) { if (lowerName.contains(entry.key.toLowerCase())) { print( 'SubscriptionUrlMatcher: 음악 서비스 매칭 - $lowerName -> ${entry.key}'); @@ -525,7 +160,7 @@ class SubscriptionUrlMatcher { } // AI 서비스 검사 - for (final entry in aiServices.entries) { + for (final entry in LegacyServiceData.aiServices.entries) { if (lowerName.contains(entry.key.toLowerCase())) { print( 'SubscriptionUrlMatcher: AI 서비스 매칭 - $lowerName -> ${entry.key}'); @@ -534,7 +169,7 @@ class SubscriptionUrlMatcher { } // 개발 서비스 검사 - for (final entry in programmingServices.entries) { + for (final entry in LegacyServiceData.programmingServices.entries) { if (lowerName.contains(entry.key.toLowerCase())) { print( 'SubscriptionUrlMatcher: 개발 서비스 매칭 - $lowerName -> ${entry.key}'); @@ -543,7 +178,7 @@ class SubscriptionUrlMatcher { } // 오피스 툴 검사 - for (final entry in officeTools.entries) { + for (final entry in LegacyServiceData.officeTools.entries) { if (lowerName.contains(entry.key.toLowerCase())) { print( 'SubscriptionUrlMatcher: 오피스 툴 매칭 - $lowerName -> ${entry.key}'); @@ -552,7 +187,7 @@ class SubscriptionUrlMatcher { } // 기타 서비스 검사 - for (final entry in otherServices.entries) { + for (final entry in LegacyServiceData.otherServices.entries) { if (lowerName.contains(entry.key.toLowerCase())) { print( 'SubscriptionUrlMatcher: 기타 서비스 매칭 - $lowerName -> ${entry.key}'); @@ -561,7 +196,7 @@ class SubscriptionUrlMatcher { } // 유사한 이름 검사 (퍼지 매칭) - 단어 기반으로 검색 - for (final entry in allServices.entries) { + for (final entry in LegacyServiceData.allServices.entries) { final serviceWords = lowerName.split(' '); final keyWords = entry.key.toLowerCase().split(' '); @@ -671,12 +306,12 @@ class SubscriptionUrlMatcher { final String lowerText = serviceNameOrUrl.toLowerCase().trim(); // 직접 서비스명으로 찾기 - if (cancellationUrls.containsKey(lowerText)) { - return cancellationUrls[lowerText]; + if (LegacyServiceData.cancellationUrls.containsKey(lowerText)) { + return LegacyServiceData.cancellationUrls[lowerText]; } // 서비스명에 부분 포함으로 찾기 - for (var entry in cancellationUrls.entries) { + for (var entry in LegacyServiceData.cancellationUrls.entries) { final String key = entry.key.toLowerCase(); if (lowerText.contains(key) || key.contains(lowerText)) { return entry.value; @@ -693,7 +328,7 @@ class SubscriptionUrlMatcher { final domain = match.group(1)?.toLowerCase() ?? ''; // 도메인으로 서비스명 찾기 - for (var entry in cancellationUrls.entries) { + for (var entry in LegacyServiceData.cancellationUrls.entries) { if (entry.key.toLowerCase().contains(domain)) { return entry.value; } @@ -835,15 +470,15 @@ class SubscriptionUrlMatcher { static String _getCategoryForLegacyService(String serviceName) { final lowerName = serviceName.toLowerCase(); - if (ottServices.containsKey(lowerName)) return 'ott_services'; - if (musicServices.containsKey(lowerName)) return 'music_streaming'; - if (storageServices.containsKey(lowerName)) return 'cloud_storage'; - if (aiServices.containsKey(lowerName)) return 'ai_services'; - if (programmingServices.containsKey(lowerName)) return 'dev_tools'; - if (officeTools.containsKey(lowerName)) return 'office_tools'; - if (lifestyleServices.containsKey(lowerName)) return 'lifestyle'; - if (shoppingServices.containsKey(lowerName)) return 'shopping'; - if (telecomServices.containsKey(lowerName)) return 'telecom'; + if (LegacyServiceData.ottServices.containsKey(lowerName)) return 'ott_services'; + if (LegacyServiceData.musicServices.containsKey(lowerName)) return 'music_streaming'; + if (LegacyServiceData.storageServices.containsKey(lowerName)) return 'cloud_storage'; + if (LegacyServiceData.aiServices.containsKey(lowerName)) return 'ai_services'; + if (LegacyServiceData.programmingServices.containsKey(lowerName)) return 'dev_tools'; + if (LegacyServiceData.officeTools.containsKey(lowerName)) return 'office_tools'; + if (LegacyServiceData.lifestyleServices.containsKey(lowerName)) return 'lifestyle'; + if (LegacyServiceData.shoppingServices.containsKey(lowerName)) return 'shopping'; + if (LegacyServiceData.telecomServices.containsKey(lowerName)) return 'telecom'; return 'other'; } @@ -874,7 +509,7 @@ class SubscriptionUrlMatcher { final lowerSms = smsText.toLowerCase(); // 모든 서비스명 검사 - for (final entry in allServices.entries) { + for (final entry in LegacyServiceData.allServices.entries) { if (lowerSms.contains(entry.key.toLowerCase())) { final categoryId = await findCategoryByServiceName(entry.key) ?? 'other'; @@ -910,8 +545,8 @@ class SubscriptionUrlMatcher { final String lowerText = text.toLowerCase().trim(); // 정확히 일치하는 경우 - if (allServices.containsKey(lowerText)) { - return allServices[lowerText]; + if (LegacyServiceData.allServices.containsKey(lowerText)) { + return LegacyServiceData.allServices[lowerText]; } // 부분 일치 검색이 활성화된 경우 @@ -920,7 +555,7 @@ class SubscriptionUrlMatcher { String? bestMatch; int maxLength = 0; - for (var entry in allServices.entries) { + for (var entry in LegacyServiceData.allServices.entries) { final String key = entry.key; // 입력된 텍스트에 서비스 키워드가 포함되어 있거나, 서비스 키워드에 입력된 텍스트가 포함된 경우 diff --git a/lib/services/subscription_url_matcher.dart.backup b/lib/services/subscription_url_matcher.dart.backup new file mode 100644 index 0000000..a6c46eb --- /dev/null +++ b/lib/services/subscription_url_matcher.dart.backup @@ -0,0 +1,941 @@ +import 'dart:convert'; +import 'package:flutter/services.dart'; +import 'package:flutter/material.dart'; + +/// 서비스 정보를 담는 데이터 클래스 +class ServiceInfo { + final String serviceId; + final String serviceName; + final String? serviceUrl; + final String? cancellationUrl; + final String categoryId; + final String categoryNameKr; + final String categoryNameEn; + + ServiceInfo({ + required this.serviceId, + required this.serviceName, + this.serviceUrl, + this.cancellationUrl, + required this.categoryId, + required this.categoryNameKr, + required this.categoryNameEn, + }); +} + +/// 구독 서비스와 웹사이트 URL 매칭을 처리하는 서비스 클래스 +class SubscriptionUrlMatcher { + static Map? _servicesData; + static bool _isInitialized = false; + + // 레거시 데이터 (JSON 로드 실패시 폴백) + // OTT 서비스 + static final Map ottServices = { + 'netflix': 'https://www.netflix.com', + '넷플릭스': 'https://www.netflix.com', + 'disney+': 'https://www.disneyplus.com', + '디즈니플러스': 'https://www.disneyplus.com', + 'youtube premium': 'https://www.youtube.com/premium', + '유튜브 프리미엄': 'https://www.youtube.com/premium', + 'watcha': 'https://watcha.com', + '왓챠': 'https://watcha.com', + 'wavve': 'https://www.wavve.com', + '웨이브': 'https://www.wavve.com', + 'apple tv+': 'https://tv.apple.com', + '애플 티비플러스': 'https://tv.apple.com', + 'tving': 'https://www.tving.com', + '티빙': 'https://www.tving.com', + 'prime video': 'https://www.primevideo.com', + '프라임 비디오': 'https://www.primevideo.com', + 'amazon prime': 'https://www.amazon.com/prime', + '아마존 프라임': 'https://www.amazon.com/prime', + 'coupang play': 'https://play.coupangplay.com', + '쿠팡 플레이': 'https://play.coupangplay.com', + 'hulu': 'https://www.hulu.com', + '훌루': 'https://www.hulu.com', + }; + + // 음악 서비스 + static final Map musicServices = { + 'spotify': 'https://www.spotify.com', + '스포티파이': 'https://www.spotify.com', + 'apple music': 'https://music.apple.com', + '애플 뮤직': 'https://music.apple.com', + 'melon': 'https://www.melon.com', + '멜론': 'https://www.melon.com', + 'genie': 'https://www.genie.co.kr', + '지니': 'https://www.genie.co.kr', + 'youtube music': 'https://music.youtube.com', + '유튜브 뮤직': 'https://music.youtube.com', + 'bugs': 'https://music.bugs.co.kr', + '벅스': 'https://music.bugs.co.kr', + 'flo': 'https://www.music-flo.com', + '플로': 'https://www.music-flo.com', + 'vibe': 'https://vibe.naver.com', + '바이브': 'https://vibe.naver.com', + 'tidal': 'https://www.tidal.com', + '타이달': 'https://www.tidal.com', + }; + + // 저장 (클라우드/파일) 서비스 + static final Map storageServices = { + 'google drive': 'https://www.google.com/drive/', + '구글 드라이브': 'https://www.google.com/drive/', + 'dropbox': 'https://www.dropbox.com', + '드롭박스': 'https://www.dropbox.com', + 'onedrive': 'https://www.onedrive.com', + '원드라이브': 'https://www.onedrive.com', + 'icloud': 'https://www.icloud.com', + '아이클라우드': 'https://www.icloud.com', + 'box': 'https://www.box.com', + '박스': 'https://www.box.com', + 'pcloud': 'https://www.pcloud.com', + 'mega': 'https://mega.nz', + '메가': 'https://mega.nz', + 'naver mybox': 'https://mybox.naver.com', + '네이버 마이박스': 'https://mybox.naver.com', + }; + + // 통신 · 인터넷 · TV 서비스 + static final Map telecomServices = { + 'skt': 'https://www.sktelecom.com', + 'sk텔레콤': 'https://www.sktelecom.com', + 'kt': 'https://www.kt.com', + 'lgu+': 'https://www.lguplus.com', + 'lg유플러스': 'https://www.lguplus.com', + 'olleh tv': 'https://www.kt.com/olleh_tv', + '올레 tv': 'https://www.kt.com/olleh_tv', + 'b tv': 'https://www.skbroadband.com', + '비티비': 'https://www.skbroadband.com', + 'u+모바일tv': 'https://www.lguplus.com', + '유플러스모바일tv': 'https://www.lguplus.com', + }; + + // 생활/라이프스타일 서비스 + static final Map lifestyleServices = { + '네이버 플러스': 'https://plus.naver.com', + 'naver plus': 'https://plus.naver.com', + '카카오 구독': 'https://subscribe.kakao.com', + 'kakao subscribe': 'https://subscribe.kakao.com', + '쿠팡 와우': 'https://www.coupang.com/np/coupangplus', + 'coupang wow': 'https://www.coupang.com/np/coupangplus', + '스타벅스 버디': 'https://www.starbucks.co.kr', + 'starbucks buddy': 'https://www.starbucks.co.kr', + 'cu 구독': 'https://cu.bgfretail.com', + 'gs25 구독': 'https://gs25.gsretail.com', + '현대차 구독': 'https://www.hyundai.com/kr/ko/eco/vehicle-subscription', + 'lg전자 구독': 'https://www.lge.co.kr', + '삼성전자 구독': 'https://www.samsung.com/sec', + '다이슨 케어': 'https://www.dyson.co.kr', + 'dyson care': 'https://www.dyson.co.kr', + '마켓컬리': 'https://www.kurly.com', + 'kurly': 'https://www.kurly.com', + '헬로네이처': 'https://www.hellonature.com', + 'hello nature': 'https://www.hellonature.com', + '이마트 트레이더스': 'https://www.emarttraders.co.kr', + '홈플러스': 'https://www.homeplus.co.kr', + 'hellofresh': 'https://www.hellofresh.com', + '헬로프레시': 'https://www.hellofresh.com', + 'bespoke post': 'https://www.bespokepost.com', + }; + + // 쇼핑/이커머스 서비스 + static final Map shoppingServices = { + 'amazon prime': 'https://www.amazon.com/prime', + '아마존 프라임': 'https://www.amazon.com/prime', + 'walmart+': 'https://www.walmart.com/plus', + '월마트플러스': 'https://www.walmart.com/plus', + 'chewy': 'https://www.chewy.com', + '츄이': 'https://www.chewy.com', + 'dollar shave club': 'https://www.dollarshaveclub.com', + '달러셰이브클럽': 'https://www.dollarshaveclub.com', + 'instacart': 'https://www.instacart.com', + '인스타카트': 'https://www.instacart.com', + 'shipt': 'https://www.shipt.com', + '십트': 'https://www.shipt.com', + 'grove': 'https://grove.co', + '그로브': 'https://grove.co', + 'cratejoy': 'https://www.cratejoy.com', + 'shopify': 'https://www.shopify.com', + '쇼피파이': 'https://www.shopify.com', + }; + + // AI 서비스 + static final Map aiServices = { + 'chatgpt': 'https://chat.openai.com', + '챗GPT': 'https://chat.openai.com', + 'openai': 'https://openai.com', + '오픈AI': 'https://openai.com', + 'claude': 'https://claude.ai', + '클로드': 'https://claude.ai', + 'anthropic': 'https://www.anthropic.com', + '앤트로픽': 'https://www.anthropic.com', + 'midjourney': 'https://www.midjourney.com', + '미드저니': 'https://www.midjourney.com', + 'perplexity': 'https://www.perplexity.ai', + '퍼플렉시티': 'https://www.perplexity.ai', + 'copilot': 'https://copilot.microsoft.com', + '코파일럿': 'https://copilot.microsoft.com', + 'gemini': 'https://gemini.google.com', + '제미니': 'https://gemini.google.com', + 'google ai': 'https://ai.google', + '구글 AI': 'https://ai.google', + 'bard': 'https://bard.google.com', + '바드': 'https://bard.google.com', + 'dall-e': 'https://openai.com/dall-e', + '달리': 'https://openai.com/dall-e', + 'stable diffusion': 'https://stability.ai', + '스테이블 디퓨전': 'https://stability.ai', + }; + + // 프로그래밍 / 개발 서비스 + static final Map programmingServices = { + 'github': 'https://github.com', + '깃허브': 'https://github.com', + 'cursor': 'https://cursor.com', + '커서': 'https://cursor.com', + 'jetbrains': 'https://www.jetbrains.com', + '제트브레인스': 'https://www.jetbrains.com', + 'intellij': 'https://www.jetbrains.com/idea', + '인텔리제이': 'https://www.jetbrains.com/idea', + 'visual studio': 'https://visualstudio.microsoft.com', + '비주얼 스튜디오': 'https://visualstudio.microsoft.com', + 'aws': 'https://aws.amazon.com', + '아마존 웹서비스': 'https://aws.amazon.com', + 'azure': 'https://azure.microsoft.com', + '애저': 'https://azure.microsoft.com', + 'google cloud': 'https://cloud.google.com', + '구글 클라우드': 'https://cloud.google.com', + 'digitalocean': 'https://www.digitalocean.com', + '디지털오션': 'https://www.digitalocean.com', + 'heroku': 'https://www.heroku.com', + '헤로쿠': 'https://www.heroku.com', + 'codecademy': 'https://www.codecademy.com', + '코드아카데미': 'https://www.codecademy.com', + 'udemy': 'https://www.udemy.com', + '유데미': 'https://www.udemy.com', + 'coursera': 'https://www.coursera.org', + '코세라': 'https://www.coursera.org', + }; + + // 오피스 및 협업 툴 + static final Map officeTools = { + 'microsoft 365': 'https://www.microsoft.com/microsoft-365', + '마이크로소프트 365': 'https://www.microsoft.com/microsoft-365', + 'office 365': 'https://www.microsoft.com/microsoft-365', + '오피스 365': 'https://www.microsoft.com/microsoft-365', + 'google workspace': 'https://workspace.google.com', + '구글 워크스페이스': 'https://workspace.google.com', + 'slack': 'https://slack.com', + '슬랙': 'https://slack.com', + 'notion': 'https://www.notion.so', + '노션': 'https://www.notion.so', + 'trello': 'https://trello.com', + '트렐로': 'https://trello.com', + 'asana': 'https://asana.com', + '아사나': 'https://asana.com', + 'dropbox': 'https://www.dropbox.com', + '드롭박스': 'https://www.dropbox.com', + 'figma': 'https://www.figma.com', + '피그마': 'https://www.figma.com', + 'adobe creative cloud': 'https://www.adobe.com/creativecloud.html', + '어도비 크리에이티브 클라우드': 'https://www.adobe.com/creativecloud.html', + }; + + // 기타 유명 서비스 + static final Map otherServices = { + 'google one': 'https://one.google.com', + '구글 원': 'https://one.google.com', + 'icloud': 'https://www.icloud.com', + '아이클라우드': 'https://www.icloud.com', + 'nintendo switch online': 'https://www.nintendo.com/switch/online-service', + '닌텐도 스위치 온라인': 'https://www.nintendo.com/switch/online-service', + 'playstation plus': 'https://www.playstation.com/ps-plus', + '플레이스테이션 플러스': 'https://www.playstation.com/ps-plus', + 'xbox game pass': 'https://www.xbox.com/xbox-game-pass', + '엑스박스 게임 패스': 'https://www.xbox.com/xbox-game-pass', + 'ea play': 'https://www.ea.com/ea-play', + 'EA 플레이': 'https://www.ea.com/ea-play', + 'ubisoft+': 'https://ubisoft.com/plus', + '유비소프트+': 'https://ubisoft.com/plus', + 'epic games': 'https://www.epicgames.com', + '에픽 게임즈': 'https://www.epicgames.com', + 'steam': 'https://store.steampowered.com', + '스팀': 'https://store.steampowered.com', + }; + + // 해지 안내 페이지 URL 목록 (공식 해지 안내 페이지가 있는 서비스들) + static final Map cancellationUrls = { + // OTT 서비스 해지 안내 페이지 + 'netflix': 'https://help.netflix.com/ko/node/407', + '넷플릭스': 'https://help.netflix.com/ko/node/407', + 'disney+': + 'https://help.disneyplus.com/csp?id=csp_article_content&sys_kb_id=f0ddbe01db7601105d5e040ad3961979', + '디즈니플러스': + 'https://help.disneyplus.com/csp?id=csp_article_content&sys_kb_id=f0ddbe01db7601105d5e040ad3961979', + 'youtube premium': 'https://support.google.com/youtube/answer/6308278', + '유튜브 프리미엄': 'https://support.google.com/youtube/answer/6308278', + 'watcha': 'https://watcha.com/settings/payment', + '왓챠': 'https://watcha.com/settings/payment', + 'wavve': 'https://www.wavve.com/my', + '웨이브': 'https://www.wavve.com/my', + 'apple tv+': 'https://support.apple.com/ko-kr/HT202039', + '애플 티비플러스': 'https://support.apple.com/ko-kr/HT202039', + 'tving': 'https://www.tving.com/my/cancelMembership', + '티빙': 'https://www.tving.com/my/cancelMembership', + 'amazon prime': 'https://www.amazon.com/gp/primecentral/managemembership', + '아마존 프라임': 'https://www.amazon.com/gp/primecentral/managemembership', + + // 음악 서비스 해지 안내 페이지 + 'spotify': 'https://support.spotify.com/us/article/cancel-premium/', + '스포티파이': 'https://support.spotify.com/us/article/cancel-premium/', + 'apple music': 'https://support.apple.com/ko-kr/HT202039', + '애플 뮤직': 'https://support.apple.com/ko-kr/HT202039', + 'melon': + 'https://faqs2.melon.com/customer/faq/informFaq.htm?no=17&faqId=QUES20150209000002&orderChk=date&SEARCH_KEY=&SEARCH_PAR_CATEGORY=CATE20130909000006&SEARCH_CATEGORY=CATE20130909000021', + '멜론': + 'https://faqs2.melon.com/customer/faq/informFaq.htm?no=17&faqId=QUES20150209000002&orderChk=date&SEARCH_KEY=&SEARCH_PAR_CATEGORY=CATE20130909000006&SEARCH_CATEGORY=CATE20130909000021', + 'youtube music': 'https://support.google.com/youtubemusic/answer/6308278', + '유튜브 뮤직': 'https://support.google.com/youtubemusic/answer/6308278', + + // AI 서비스 해지 안내 페이지 + 'chatgpt': + 'https://help.openai.com/en/articles/7730211-manage-or-cancel-your-chatgpt-plus-subscription', + '챗GPT': + 'https://help.openai.com/en/articles/7730211-manage-or-cancel-your-chatgpt-plus-subscription', + 'claude': + 'https://help.anthropic.com/en/articles/8798313-how-do-i-cancel-my-claude-subscription', + '클로드': + 'https://help.anthropic.com/en/articles/8798313-how-do-i-cancel-my-claude-subscription', + 'midjourney': 'https://docs.midjourney.com/docs/manage-subscription', + '미드저니': 'https://docs.midjourney.com/docs/manage-subscription', + + // 프로그래밍 / 개발 서비스 해지 안내 페이지 + 'github': + 'https://docs.github.com/ko/billing/managing-billing-for-your-github-account/downgrading-your-github-subscription', + '깃허브': + 'https://docs.github.com/ko/billing/managing-billing-for-your-github-account/downgrading-your-github-subscription', + 'jetbrains': + 'https://sales.jetbrains.com/hc/en-gb/articles/207240845-How-to-cancel-an-auto-renewal-subscription-', + '제트브레인스': + 'https://sales.jetbrains.com/hc/en-gb/articles/207240845-How-to-cancel-an-auto-renewal-subscription-', + + // 오피스 및 협업 툴 해지 안내 페이지 + 'microsoft 365': + 'https://support.microsoft.com/en-us/office/cancel-a-microsoft-365-subscription-46e2634c-c64b-4c65-94b9-2cc9c960e91b', + '마이크로소프트 365': + 'https://support.microsoft.com/en-us/office/cancel-a-microsoft-365-subscription-46e2634c-c64b-4c65-94b9-2cc9c960e91b', + 'office 365': + 'https://support.microsoft.com/en-us/office/cancel-a-microsoft-365-subscription-46e2634c-c64b-4c65-94b9-2cc9c960e91b', + '오피스 365': + 'https://support.microsoft.com/en-us/office/cancel-a-microsoft-365-subscription-46e2634c-c64b-4c65-94b9-2cc9c960e91b', + 'slack': + 'https://slack.com/help/articles/360003378691-Cancel-your-Slack-subscription', + '슬랙': + 'https://slack.com/help/articles/360003378691-Cancel-your-Slack-subscription', + 'notion': + 'https://www.notion.so/help/billing-and-payment-settings#cancel-a-subscription', + '노션': + 'https://www.notion.so/help/billing-and-payment-settings#cancel-a-subscription', + 'dropbox': 'https://help.dropbox.com/accounts-billing/cancellation', + '드롭박스': 'https://help.dropbox.com/accounts-billing/cancellation', + 'adobe creative cloud': + 'https://helpx.adobe.com/manage-account/using/cancel-subscription.html', + '어도비 크리에이티브 클라우드': + 'https://helpx.adobe.com/manage-account/using/cancel-subscription.html', + + // 기타 유명 서비스 해지 안내 페이지 + 'google one': 'https://support.google.com/googleone/answer/9140429', + '구글 원': 'https://support.google.com/googleone/answer/9140429', + 'icloud': 'https://support.apple.com/ko-kr/HT207594', + '아이클라우드': 'https://support.apple.com/ko-kr/HT207594', + 'nintendo switch online': + 'https://en-americas-support.nintendo.com/app/answers/detail/a_id/41925/~/how-to-cancel-a-nintendo-switch-online-membership', + '닌텐도 스위치 온라인': + 'https://en-americas-support.nintendo.com/app/answers/detail/a_id/41925/~/how-to-cancel-a-nintendo-switch-online-membership', + 'playstation plus': + 'https://www.playstation.com/support/subscriptions/cancel-playstation-plus/', + '플레이스테이션 플러스': + 'https://www.playstation.com/support/subscriptions/cancel-playstation-plus/', + 'xbox game pass': + 'https://support.xbox.com/help/subscriptions-billing/manage-subscriptions/xbox-game-pass-how-to-cancel', + '엑스박스 게임 패스': + 'https://support.xbox.com/help/subscriptions-billing/manage-subscriptions/xbox-game-pass-how-to-cancel', + }; + + // 모든 서비스 매핑을 합친 맵 + static final Map allServices = { + ...ottServices, + ...musicServices, + ...storageServices, + ...aiServices, + ...programmingServices, + ...officeTools, + ...lifestyleServices, + ...shoppingServices, + ...telecomServices, + ...otherServices, + }; + + /// JSON 데이터 초기화 + static Future initialize() async { + if (_isInitialized) return; + + try { + final jsonString = await rootBundle.loadString('assets/data/subscription_services.json'); + _servicesData = json.decode(jsonString); + _isInitialized = true; + print('SubscriptionUrlMatcher: JSON 데이터 로드 완료'); + } catch (e) { + print('SubscriptionUrlMatcher: JSON 로드 실패 - $e'); + // 로드 실패시 기존 하드코딩 데이터 사용 + _isInitialized = true; + } + } + + /// 도메인 추출 (www와 TLD 제외) + static String? extractDomain(String url) { + try { + final uri = Uri.parse(url); + final host = uri.host.toLowerCase(); + + // 도메인 부분 추출 + var parts = host.split('.'); + + // www 제거 + if (parts.isNotEmpty && parts[0] == 'www') { + parts = parts.sublist(1); + } + + // 서브도메인 처리 (예: music.youtube.com) + if (parts.length >= 3) { + // 서브도메인 포함 전체 도메인 반환 + return parts.sublist(0, parts.length - 1).join('.'); + } else if (parts.length >= 2) { + // 메인 도메인만 반환 + return parts[0]; + } + + return null; + } catch (e) { + print('SubscriptionUrlMatcher: 도메인 추출 실패 - $e'); + return null; + } + } + + /// URL로 서비스 찾기 + static Future findServiceByUrl(String url) async { + await initialize(); + + final domain = extractDomain(url); + if (domain == null) return null; + + // JSON 데이터가 있으면 JSON에서 찾기 + if (_servicesData != null) { + final categories = _servicesData!['categories'] as Map; + + for (final categoryEntry in categories.entries) { + final categoryId = categoryEntry.key; + final categoryData = categoryEntry.value as Map; + final services = categoryData['services'] as Map; + + for (final serviceEntry in services.entries) { + final serviceId = serviceEntry.key; + final serviceData = serviceEntry.value as Map; + final domains = List.from(serviceData['domains'] ?? []); + + // 도메인이 일치하는지 확인 + for (final serviceDomain in domains) { + if (domain.contains(serviceDomain) || serviceDomain.contains(domain)) { + final names = List.from(serviceData['names'] ?? []); + final urls = serviceData['urls'] as Map?; + + return ServiceInfo( + serviceId: serviceId, + serviceName: names.isNotEmpty ? names[0] : serviceId, + serviceUrl: urls?['kr'] ?? urls?['en'], + cancellationUrl: null, + categoryId: _getCategoryIdByKey(categoryId), + categoryNameKr: categoryData['nameKr'] ?? '', + categoryNameEn: categoryData['nameEn'] ?? '', + ); + } + } + } + } + } + + // JSON에서 못 찾았으면 레거시 방식으로 찾기 + for (final entry in allServices.entries) { + final serviceUrl = entry.value; + final serviceDomain = extractDomain(serviceUrl); + + if (serviceDomain != null && + (domain.contains(serviceDomain) || serviceDomain.contains(domain))) { + return ServiceInfo( + serviceId: entry.key, + serviceName: entry.key, + serviceUrl: serviceUrl, + cancellationUrl: null, + categoryId: _getCategoryForLegacyService(entry.key), + categoryNameKr: '', + categoryNameEn: '', + ); + } + } + + return null; + } + + /// 서비스명으로 URL 찾기 (기존 suggestUrl 메서드 유지) + static String? suggestUrl(String serviceName) { + if (serviceName.isEmpty) { + print('SubscriptionUrlMatcher: 빈 serviceName'); + return null; + } + + // 소문자로 변환하여 비교 + final lowerName = serviceName.toLowerCase().trim(); + + try { + // 정확한 매칭을 먼저 시도 + for (final entry in allServices.entries) { + if (lowerName.contains(entry.key.toLowerCase())) { + print('SubscriptionUrlMatcher: 정확한 매칭 - $lowerName -> ${entry.key}'); + return entry.value; + } + } + + // OTT 서비스 검사 + for (final entry in ottServices.entries) { + if (lowerName.contains(entry.key.toLowerCase())) { + print( + 'SubscriptionUrlMatcher: OTT 서비스 매칭 - $lowerName -> ${entry.key}'); + return entry.value; + } + } + + // 음악 서비스 검사 + for (final entry in musicServices.entries) { + if (lowerName.contains(entry.key.toLowerCase())) { + print( + 'SubscriptionUrlMatcher: 음악 서비스 매칭 - $lowerName -> ${entry.key}'); + return entry.value; + } + } + + // AI 서비스 검사 + for (final entry in aiServices.entries) { + if (lowerName.contains(entry.key.toLowerCase())) { + print( + 'SubscriptionUrlMatcher: AI 서비스 매칭 - $lowerName -> ${entry.key}'); + return entry.value; + } + } + + // 개발 서비스 검사 + for (final entry in programmingServices.entries) { + if (lowerName.contains(entry.key.toLowerCase())) { + print( + 'SubscriptionUrlMatcher: 개발 서비스 매칭 - $lowerName -> ${entry.key}'); + return entry.value; + } + } + + // 오피스 툴 검사 + for (final entry in officeTools.entries) { + if (lowerName.contains(entry.key.toLowerCase())) { + print( + 'SubscriptionUrlMatcher: 오피스 툴 매칭 - $lowerName -> ${entry.key}'); + return entry.value; + } + } + + // 기타 서비스 검사 + for (final entry in otherServices.entries) { + if (lowerName.contains(entry.key.toLowerCase())) { + print( + 'SubscriptionUrlMatcher: 기타 서비스 매칭 - $lowerName -> ${entry.key}'); + return entry.value; + } + } + + // 유사한 이름 검사 (퍼지 매칭) - 단어 기반으로 검색 + for (final entry in allServices.entries) { + final serviceWords = lowerName.split(' '); + final keyWords = entry.key.toLowerCase().split(' '); + + // 단어 단위로 일치하는지 확인 + for (final word in serviceWords) { + if (word.length > 2 && + keyWords.any((keyWord) => keyWord.contains(word))) { + print( + 'SubscriptionUrlMatcher: 단어 기반 매칭 - $word (in $lowerName) -> ${entry.key}'); + return entry.value; + } + } + } + + // 추출 가능한 도메인이 있는지 확인 + final domainMatch = RegExp(r'(\w+)').firstMatch(lowerName); + if (domainMatch != null && domainMatch.group(1)!.length > 2) { + final domain = domainMatch.group(1)!.trim(); + if (domain.length > 2 && + !['the', 'and', 'for', 'www'].contains(domain)) { + final url = 'https://www.$domain.com'; + print('SubscriptionUrlMatcher: 도메인 추출 - $lowerName -> $url'); + return url; + } + } + + print('SubscriptionUrlMatcher: 매칭 실패 - $lowerName'); + return null; + } catch (e) { + print('SubscriptionUrlMatcher: URL 매칭 중 오류 발생: $e'); + return null; + } + } + + /// 해지 안내 URL 찾기 (개선된 버전) + static Future findCancellationUrl({ + String? serviceName, + String? websiteUrl, + String locale = 'kr', + }) async { + await initialize(); + + // JSON 데이터가 있으면 JSON에서 찾기 + if (_servicesData != null) { + final categories = _servicesData!['categories'] as Map; + + // 1. 서비스명으로 찾기 + if (serviceName != null && serviceName.isNotEmpty) { + final lowerName = serviceName.toLowerCase().trim(); + + for (final categoryData in categories.values) { + final services = (categoryData as Map)['services'] as Map; + + for (final serviceData in services.values) { + final names = List.from((serviceData as Map)['names'] ?? []); + + for (final name in names) { + if (lowerName.contains(name.toLowerCase()) || name.toLowerCase().contains(lowerName)) { + final cancellationUrls = serviceData['cancellationUrls'] as Map?; + if (cancellationUrls != null) { + // 요청한 언어의 URL이 있으면 반환, 없으면 다른 언어 URL 반환 + return cancellationUrls[locale] ?? + cancellationUrls[locale == 'kr' ? 'en' : 'kr']; + } + } + } + } + } + } + + // 2. URL로 찾기 + if (websiteUrl != null && websiteUrl.isNotEmpty) { + final domain = extractDomain(websiteUrl); + if (domain != null) { + for (final categoryData in categories.values) { + final services = (categoryData as Map)['services'] as Map; + + for (final serviceData in services.values) { + final domains = List.from((serviceData as Map)['domains'] ?? []); + + for (final serviceDomain in domains) { + if (domain.contains(serviceDomain) || serviceDomain.contains(domain)) { + final cancellationUrls = serviceData['cancellationUrls'] as Map?; + if (cancellationUrls != null) { + return cancellationUrls[locale] ?? + cancellationUrls[locale == 'kr' ? 'en' : 'kr']; + } + } + } + } + } + } + } + } + + // JSON에서 못 찾았으면 레거시 방식으로 찾기 + return _findCancellationUrlLegacy(serviceName ?? websiteUrl ?? ''); + } + + /// 서비스명 또는 웹사이트 URL을 기반으로 해지 안내 페이지 URL 찾기 (레거시) + static String? _findCancellationUrlLegacy(String serviceNameOrUrl) { + if (serviceNameOrUrl.isEmpty) { + return null; + } + + // 소문자로 변환하여 처리 + final String lowerText = serviceNameOrUrl.toLowerCase().trim(); + + // 직접 서비스명으로 찾기 + if (cancellationUrls.containsKey(lowerText)) { + return cancellationUrls[lowerText]; + } + + // 서비스명에 부분 포함으로 찾기 + for (var entry in cancellationUrls.entries) { + final String key = entry.key.toLowerCase(); + if (lowerText.contains(key) || key.contains(lowerText)) { + return entry.value; + } + } + + // URL을 통해 서비스명 추출 후 찾기 + if (lowerText.startsWith('http')) { + // URL 도메인 추출 (https://www.netflix.com 에서 netflix 추출) + final domainRegex = RegExp(r'https?://(?:www\.)?([a-zA-Z0-9-]+)'); + final match = domainRegex.firstMatch(lowerText); + + if (match != null && match.groupCount >= 1) { + final domain = match.group(1)?.toLowerCase() ?? ''; + + // 도메인으로 서비스명 찾기 + for (var entry in cancellationUrls.entries) { + if (entry.key.toLowerCase().contains(domain)) { + return entry.value; + } + } + } + } + + // 해지 안내 페이지를 찾지 못함 + return null; + } + + /// 서비스에 공식 해지 안내 페이지가 있는지 확인 + static Future hasCancellationPage(String serviceNameOrUrl) async { + // 새로운 JSON 기반 방식으로 확인 + final cancellationUrl = await findCancellationUrl( + serviceName: serviceNameOrUrl, + websiteUrl: serviceNameOrUrl, + ); + return cancellationUrl != null; + } + + /// 서비스명으로 카테고리 찾기 + static Future findCategoryByServiceName(String serviceName) async { + await initialize(); + if (serviceName.isEmpty) return null; + + final lowerName = serviceName.toLowerCase().trim(); + + // JSON 데이터가 있으면 JSON에서 찾기 + if (_servicesData != null) { + final categories = _servicesData!['categories'] as Map; + + for (final categoryEntry in categories.entries) { + final categoryId = categoryEntry.key; + final categoryData = categoryEntry.value as Map; + final services = categoryData['services'] as Map; + + for (final serviceData in services.values) { + final names = List.from((serviceData as Map)['names'] ?? []); + + for (final name in names) { + if (lowerName.contains(name.toLowerCase()) || name.toLowerCase().contains(lowerName)) { + return _getCategoryIdByKey(categoryId); + } + } + } + } + } + + // JSON에서 못 찾았으면 레거시 방식으로 카테고리 추측 + return _getCategoryForLegacyService(serviceName); + } + + /// 현재 로케일에 따라 서비스 표시명 가져오기 + static Future getServiceDisplayName({ + required String serviceName, + required String locale, + }) async { + await initialize(); + + if (_servicesData == null) { + return serviceName; + } + + final lowerName = serviceName.toLowerCase().trim(); + final categories = _servicesData!['categories'] as Map; + + // JSON에서 서비스 찾기 + for (final categoryData in categories.values) { + final services = (categoryData as Map)['services'] as Map; + + for (final serviceData in services.values) { + final data = serviceData as Map; + final names = List.from(data['names'] ?? []); + + // names 배열에 있는지 확인 + for (final name in names) { + if (lowerName == name.toLowerCase() || + lowerName.contains(name.toLowerCase()) || + name.toLowerCase().contains(lowerName)) { + // 로케일에 따라 적절한 이름 반환 + if (locale == 'ko' || locale == 'kr') { + return data['nameKr'] ?? serviceName; + } else { + return data['nameEn'] ?? serviceName; + } + } + } + + // nameKr/nameEn에 직접 매칭 확인 + final nameKr = (data['nameKr'] ?? '').toString().toLowerCase(); + final nameEn = (data['nameEn'] ?? '').toString().toLowerCase(); + + if (lowerName == nameKr || lowerName == nameEn) { + if (locale == 'ko' || locale == 'kr') { + return data['nameKr'] ?? serviceName; + } else { + return data['nameEn'] ?? serviceName; + } + } + } + } + + // 찾지 못한 경우 원래 이름 반환 + return serviceName; + } + + /// 카테고리 키를 실제 카테고리 ID로 매핑 + static String _getCategoryIdByKey(String key) { + // 여기에 실제 앱의 카테고리 ID 매핑을 추가 + // 임시로 카테고리명 기반 매핑 + switch (key) { + case 'music': + return 'music_streaming'; + case 'ott': + return 'ott_services'; + case 'storage': + return 'cloud_storage'; + case 'ai': + return 'ai_services'; + case 'programming': + return 'dev_tools'; + case 'office': + return 'office_tools'; + case 'lifestyle': + return 'lifestyle'; + case 'shopping': + return 'shopping'; + case 'gaming': + return 'gaming'; + case 'telecom': + return 'telecom'; + default: + return 'other'; + } + } + + /// 레거시 서비스명으로 카테고리 추측 + static String _getCategoryForLegacyService(String serviceName) { + final lowerName = serviceName.toLowerCase(); + + if (ottServices.containsKey(lowerName)) return 'ott_services'; + if (musicServices.containsKey(lowerName)) return 'music_streaming'; + if (storageServices.containsKey(lowerName)) return 'cloud_storage'; + if (aiServices.containsKey(lowerName)) return 'ai_services'; + if (programmingServices.containsKey(lowerName)) return 'dev_tools'; + if (officeTools.containsKey(lowerName)) return 'office_tools'; + if (lifestyleServices.containsKey(lowerName)) return 'lifestyle'; + if (shoppingServices.containsKey(lowerName)) return 'shopping'; + if (telecomServices.containsKey(lowerName)) return 'telecom'; + + return 'other'; + } + + /// SMS에서 URL과 서비스 정보 추출 + static Future extractServiceFromSms(String smsText) async { + await initialize(); + + // URL 패턴 찾기 + final urlPattern = RegExp( + r'https?://(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*)', + caseSensitive: false, + ); + + final matches = urlPattern.allMatches(smsText); + + for (final match in matches) { + final url = match.group(0); + if (url != null) { + final serviceInfo = await findServiceByUrl(url); + if (serviceInfo != null) { + return serviceInfo; + } + } + } + + // URL로 못 찾았으면 서비스명으로 시도 + final lowerSms = smsText.toLowerCase(); + + // 모든 서비스명 검사 + for (final entry in allServices.entries) { + if (lowerSms.contains(entry.key.toLowerCase())) { + final categoryId = await findCategoryByServiceName(entry.key) ?? 'other'; + + return ServiceInfo( + serviceId: entry.key, + serviceName: entry.key, + serviceUrl: entry.value, + cancellationUrl: null, + categoryId: categoryId, + categoryNameKr: '', + categoryNameEn: '', + ); + } + } + + return null; + } + + /// URL이 알려진 서비스 URL인지 확인 + static Future isKnownServiceUrl(String url) async { + final serviceInfo = await findServiceByUrl(url); + return serviceInfo != null; + } + + /// 입력된 서비스 이름이나 문자열에서 매칭되는 URL을 찾아 반환 (레거시 호환성) + static String? findMatchingUrl(String text, {bool usePartialMatch = true}) { + // 입력 텍스트가 비어있거나 null인 경우 + if (text.isEmpty) { + return null; + } + + // 소문자로 변환하여 처리 + final String lowerText = text.toLowerCase().trim(); + + // 정확히 일치하는 경우 + if (allServices.containsKey(lowerText)) { + return allServices[lowerText]; + } + + // 부분 일치 검색이 활성화된 경우 + if (usePartialMatch) { + // 가장 긴 부분 매칭 찾기 + String? bestMatch; + int maxLength = 0; + + for (var entry in allServices.entries) { + final String key = entry.key; + + // 입력된 텍스트에 서비스 키워드가 포함되어 있거나, 서비스 키워드에 입력된 텍스트가 포함된 경우 + if (lowerText.contains(key) || key.contains(lowerText)) { + // 더 긴 매칭을 우선시 + if (key.length > maxLength) { + maxLength = key.length; + bestMatch = entry.value; + } + } + } + + return bestMatch; + } + + return null; + } +} \ No newline at end of file diff --git a/lib/services/url_matcher/data/legacy_service_data.dart b/lib/services/url_matcher/data/legacy_service_data.dart new file mode 100644 index 0000000..0e09225 --- /dev/null +++ b/lib/services/url_matcher/data/legacy_service_data.dart @@ -0,0 +1,368 @@ +/// 레거시 서비스 데이터 - 하드코딩된 서비스 URL 매핑 +class LegacyServiceData { + // OTT 서비스 + static final Map ottServices = { + 'netflix': 'https://www.netflix.com', + '넷플릭스': 'https://www.netflix.com', + 'disney+': 'https://www.disneyplus.com', + '디즈니플러스': 'https://www.disneyplus.com', + 'youtube premium': 'https://www.youtube.com/premium', + '유튜브 프리미엄': 'https://www.youtube.com/premium', + 'watcha': 'https://watcha.com', + '왓챠': 'https://watcha.com', + 'wavve': 'https://www.wavve.com', + '웨이브': 'https://www.wavve.com', + 'apple tv+': 'https://tv.apple.com', + '애플 티비플러스': 'https://tv.apple.com', + 'tving': 'https://www.tving.com', + '티빙': 'https://www.tving.com', + 'prime video': 'https://www.primevideo.com', + '프라임 비디오': 'https://www.primevideo.com', + 'amazon prime': 'https://www.amazon.com/prime', + '아마존 프라임': 'https://www.amazon.com/prime', + 'coupang play': 'https://play.coupangplay.com', + '쿠팡 플레이': 'https://play.coupangplay.com', + 'hulu': 'https://www.hulu.com', + '훌루': 'https://www.hulu.com', + }; + + // 음악 서비스 + static final Map musicServices = { + 'spotify': 'https://www.spotify.com', + '스포티파이': 'https://www.spotify.com', + 'apple music': 'https://music.apple.com', + '애플 뮤직': 'https://music.apple.com', + 'melon': 'https://www.melon.com', + '멜론': 'https://www.melon.com', + 'genie': 'https://www.genie.co.kr', + '지니': 'https://www.genie.co.kr', + 'youtube music': 'https://music.youtube.com', + '유튜브 뮤직': 'https://music.youtube.com', + 'bugs': 'https://music.bugs.co.kr', + '벅스': 'https://music.bugs.co.kr', + 'flo': 'https://www.music-flo.com', + '플로': 'https://www.music-flo.com', + 'vibe': 'https://vibe.naver.com', + '바이브': 'https://vibe.naver.com', + 'tidal': 'https://www.tidal.com', + '타이달': 'https://www.tidal.com', + }; + + // 저장 (클라우드/파일) 서비스 + static final Map storageServices = { + 'google drive': 'https://www.google.com/drive/', + '구글 드라이브': 'https://www.google.com/drive/', + 'dropbox': 'https://www.dropbox.com', + '드롭박스': 'https://www.dropbox.com', + 'onedrive': 'https://www.onedrive.com', + '원드라이브': 'https://www.onedrive.com', + 'icloud': 'https://www.icloud.com', + '아이클라우드': 'https://www.icloud.com', + 'box': 'https://www.box.com', + '박스': 'https://www.box.com', + 'pcloud': 'https://www.pcloud.com', + 'mega': 'https://mega.nz', + '메가': 'https://mega.nz', + 'naver mybox': 'https://mybox.naver.com', + '네이버 마이박스': 'https://mybox.naver.com', + }; + + // 통신 · 인터넷 · TV 서비스 + static final Map telecomServices = { + 'skt': 'https://www.sktelecom.com', + 'sk텔레콤': 'https://www.sktelecom.com', + 'kt': 'https://www.kt.com', + 'lgu+': 'https://www.lguplus.com', + 'lg유플러스': 'https://www.lguplus.com', + 'olleh tv': 'https://www.kt.com/olleh_tv', + '올레 tv': 'https://www.kt.com/olleh_tv', + 'b tv': 'https://www.skbroadband.com', + '비티비': 'https://www.skbroadband.com', + 'u+모바일tv': 'https://www.lguplus.com', + '유플러스모바일tv': 'https://www.lguplus.com', + }; + + // 생활/라이프스타일 서비스 + static final Map lifestyleServices = { + '네이버 플러스': 'https://plus.naver.com', + 'naver plus': 'https://plus.naver.com', + '카카오 구독': 'https://subscribe.kakao.com', + 'kakao subscribe': 'https://subscribe.kakao.com', + '쿠팡 와우': 'https://www.coupang.com/np/coupangplus', + 'coupang wow': 'https://www.coupang.com/np/coupangplus', + '스타벅스 버디': 'https://www.starbucks.co.kr', + 'starbucks buddy': 'https://www.starbucks.co.kr', + 'cu 구독': 'https://cu.bgfretail.com', + 'gs25 구독': 'https://gs25.gsretail.com', + '현대차 구독': 'https://www.hyundai.com/kr/ko/eco/vehicle-subscription', + 'lg전자 구독': 'https://www.lge.co.kr', + '삼성전자 구독': 'https://www.samsung.com/sec', + '다이슨 케어': 'https://www.dyson.co.kr', + 'dyson care': 'https://www.dyson.co.kr', + '마켓컬리': 'https://www.kurly.com', + 'kurly': 'https://www.kurly.com', + '헬로네이처': 'https://www.hellonature.com', + 'hello nature': 'https://www.hellonature.com', + '이마트 트레이더스': 'https://www.emarttraders.co.kr', + '홈플러스': 'https://www.homeplus.co.kr', + 'hellofresh': 'https://www.hellofresh.com', + '헬로프레시': 'https://www.hellofresh.com', + 'bespoke post': 'https://www.bespokepost.com', + }; + + // 쇼핑/이커머스 서비스 + static final Map shoppingServices = { + 'amazon prime': 'https://www.amazon.com/prime', + '아마존 프라임': 'https://www.amazon.com/prime', + 'walmart+': 'https://www.walmart.com/plus', + '월마트플러스': 'https://www.walmart.com/plus', + 'chewy': 'https://www.chewy.com', + '츄이': 'https://www.chewy.com', + 'dollar shave club': 'https://www.dollarshaveclub.com', + '달러셰이브클럽': 'https://www.dollarshaveclub.com', + 'instacart': 'https://www.instacart.com', + '인스타카트': 'https://www.instacart.com', + 'shipt': 'https://www.shipt.com', + '십트': 'https://www.shipt.com', + 'grove': 'https://grove.co', + '그로브': 'https://grove.co', + 'cratejoy': 'https://www.cratejoy.com', + 'shopify': 'https://www.shopify.com', + '쇼피파이': 'https://www.shopify.com', + }; + + // AI 서비스 + static final Map aiServices = { + 'chatgpt': 'https://chat.openai.com', + '챗GPT': 'https://chat.openai.com', + 'openai': 'https://openai.com', + '오픈AI': 'https://openai.com', + 'claude': 'https://claude.ai', + '클로드': 'https://claude.ai', + 'anthropic': 'https://www.anthropic.com', + '앤트로픽': 'https://www.anthropic.com', + 'midjourney': 'https://www.midjourney.com', + '미드저니': 'https://www.midjourney.com', + 'perplexity': 'https://www.perplexity.ai', + '퍼플렉시티': 'https://www.perplexity.ai', + 'copilot': 'https://copilot.microsoft.com', + '코파일럿': 'https://copilot.microsoft.com', + 'gemini': 'https://gemini.google.com', + '제미니': 'https://gemini.google.com', + 'google ai': 'https://ai.google', + '구글 AI': 'https://ai.google', + 'bard': 'https://bard.google.com', + '바드': 'https://bard.google.com', + 'dall-e': 'https://openai.com/dall-e', + '달리': 'https://openai.com/dall-e', + 'stable diffusion': 'https://stability.ai', + '스테이블 디퓨전': 'https://stability.ai', + }; + + // 프로그래밍 / 개발 서비스 + static final Map programmingServices = { + 'github': 'https://github.com', + '깃허브': 'https://github.com', + 'cursor': 'https://cursor.com', + '커서': 'https://cursor.com', + 'jetbrains': 'https://www.jetbrains.com', + '제트브레인스': 'https://www.jetbrains.com', + 'intellij': 'https://www.jetbrains.com/idea', + '인텔리제이': 'https://www.jetbrains.com/idea', + 'visual studio': 'https://visualstudio.microsoft.com', + '비주얼 스튜디오': 'https://visualstudio.microsoft.com', + 'aws': 'https://aws.amazon.com', + '아마존 웹서비스': 'https://aws.amazon.com', + 'azure': 'https://azure.microsoft.com', + '애저': 'https://azure.microsoft.com', + 'google cloud': 'https://cloud.google.com', + '구글 클라우드': 'https://cloud.google.com', + 'digitalocean': 'https://www.digitalocean.com', + '디지털오션': 'https://www.digitalocean.com', + 'heroku': 'https://www.heroku.com', + '헤로쿠': 'https://www.heroku.com', + 'codecademy': 'https://www.codecademy.com', + '코드아카데미': 'https://www.codecademy.com', + 'udemy': 'https://www.udemy.com', + '유데미': 'https://www.udemy.com', + 'coursera': 'https://www.coursera.org', + '코세라': 'https://www.coursera.org', + }; + + // 오피스 및 협업 툴 + static final Map officeTools = { + 'microsoft 365': 'https://www.microsoft.com/microsoft-365', + '마이크로소프트 365': 'https://www.microsoft.com/microsoft-365', + 'office 365': 'https://www.microsoft.com/microsoft-365', + '오피스 365': 'https://www.microsoft.com/microsoft-365', + 'google workspace': 'https://workspace.google.com', + '구글 워크스페이스': 'https://workspace.google.com', + 'slack': 'https://slack.com', + '슬랙': 'https://slack.com', + 'notion': 'https://www.notion.so', + '노션': 'https://www.notion.so', + 'trello': 'https://trello.com', + '트렐로': 'https://trello.com', + 'asana': 'https://asana.com', + '아사나': 'https://asana.com', + 'dropbox': 'https://www.dropbox.com', + '드롭박스': 'https://www.dropbox.com', + 'figma': 'https://www.figma.com', + '피그마': 'https://www.figma.com', + 'adobe creative cloud': 'https://www.adobe.com/creativecloud.html', + '어도비 크리에이티브 클라우드': 'https://www.adobe.com/creativecloud.html', + }; + + // 기타 유명 서비스 + static final Map otherServices = { + 'google one': 'https://one.google.com', + '구글 원': 'https://one.google.com', + 'icloud': 'https://www.icloud.com', + '아이클라우드': 'https://www.icloud.com', + 'nintendo switch online': 'https://www.nintendo.com/switch/online-service', + '닌텐도 스위치 온라인': 'https://www.nintendo.com/switch/online-service', + 'playstation plus': 'https://www.playstation.com/ps-plus', + '플레이스테이션 플러스': 'https://www.playstation.com/ps-plus', + 'xbox game pass': 'https://www.xbox.com/xbox-game-pass', + '엑스박스 게임 패스': 'https://www.xbox.com/xbox-game-pass', + 'ea play': 'https://www.ea.com/ea-play', + 'EA 플레이': 'https://www.ea.com/ea-play', + 'ubisoft+': 'https://ubisoft.com/plus', + '유비소프트+': 'https://ubisoft.com/plus', + 'epic games': 'https://www.epicgames.com', + '에픽 게임즈': 'https://www.epicgames.com', + 'steam': 'https://store.steampowered.com', + '스팀': 'https://store.steampowered.com', + }; + + // 해지 안내 페이지 URL 목록 (공식 해지 안내 페이지가 있는 서비스들) + static final Map cancellationUrls = { + // OTT 서비스 해지 안내 페이지 + 'netflix': 'https://help.netflix.com/ko/node/407', + '넷플릭스': 'https://help.netflix.com/ko/node/407', + 'disney+': + 'https://help.disneyplus.com/csp?id=csp_article_content&sys_kb_id=f0ddbe01db7601105d5e040ad3961979', + '디즈니플러스': + 'https://help.disneyplus.com/csp?id=csp_article_content&sys_kb_id=f0ddbe01db7601105d5e040ad3961979', + 'youtube premium': 'https://support.google.com/youtube/answer/6308278', + '유튜브 프리미엄': 'https://support.google.com/youtube/answer/6308278', + 'watcha': 'https://watcha.com/settings/payment', + '왓챠': 'https://watcha.com/settings/payment', + 'wavve': 'https://www.wavve.com/my', + '웨이브': 'https://www.wavve.com/my', + 'apple tv+': 'https://support.apple.com/ko-kr/HT202039', + '애플 티비플러스': 'https://support.apple.com/ko-kr/HT202039', + 'tving': 'https://www.tving.com/my/cancelMembership', + '티빙': 'https://www.tving.com/my/cancelMembership', + 'amazon prime': 'https://www.amazon.com/gp/primecentral/managemembership', + '아마존 프라임': 'https://www.amazon.com/gp/primecentral/managemembership', + + // 음악 서비스 해지 안내 페이지 + 'spotify': 'https://support.spotify.com/us/article/cancel-premium/', + '스포티파이': 'https://support.spotify.com/us/article/cancel-premium/', + 'apple music': 'https://support.apple.com/ko-kr/HT202039', + '애플 뮤직': 'https://support.apple.com/ko-kr/HT202039', + 'melon': + 'https://faqs2.melon.com/customer/faq/informFaq.htm?no=17&faqId=QUES20150209000002&orderChk=date&SEARCH_KEY=&SEARCH_PAR_CATEGORY=CATE20130909000006&SEARCH_CATEGORY=CATE20130909000021', + '멜론': + 'https://faqs2.melon.com/customer/faq/informFaq.htm?no=17&faqId=QUES20150209000002&orderChk=date&SEARCH_KEY=&SEARCH_PAR_CATEGORY=CATE20130909000006&SEARCH_CATEGORY=CATE20130909000021', + 'youtube music': 'https://support.google.com/youtubemusic/answer/6308278', + '유튜브 뮤직': 'https://support.google.com/youtubemusic/answer/6308278', + + // AI 서비스 해지 안내 페이지 + 'chatgpt': + 'https://help.openai.com/en/articles/7730211-manage-or-cancel-your-chatgpt-plus-subscription', + '챗GPT': + 'https://help.openai.com/en/articles/7730211-manage-or-cancel-your-chatgpt-plus-subscription', + 'claude': + 'https://help.anthropic.com/en/articles/8798313-how-do-i-cancel-my-claude-subscription', + '클로드': + 'https://help.anthropic.com/en/articles/8798313-how-do-i-cancel-my-claude-subscription', + 'midjourney': 'https://docs.midjourney.com/docs/manage-subscription', + '미드저니': 'https://docs.midjourney.com/docs/manage-subscription', + + // 프로그래밍 / 개발 서비스 해지 안내 페이지 + 'github': + 'https://docs.github.com/ko/billing/managing-billing-for-your-github-account/downgrading-your-github-subscription', + '깃허브': + 'https://docs.github.com/ko/billing/managing-billing-for-your-github-account/downgrading-your-github-subscription', + 'jetbrains': + 'https://sales.jetbrains.com/hc/en-gb/articles/207240845-How-to-cancel-an-auto-renewal-subscription-', + '제트브레인스': + 'https://sales.jetbrains.com/hc/en-gb/articles/207240845-How-to-cancel-an-auto-renewal-subscription-', + + // 오피스 및 협업 툴 해지 안내 페이지 + 'microsoft 365': + 'https://support.microsoft.com/en-us/office/cancel-a-microsoft-365-subscription-46e2634c-c64b-4c65-94b9-2cc9c960e91b', + '마이크로소프트 365': + 'https://support.microsoft.com/en-us/office/cancel-a-microsoft-365-subscription-46e2634c-c64b-4c65-94b9-2cc9c960e91b', + 'office 365': + 'https://support.microsoft.com/en-us/office/cancel-a-microsoft-365-subscription-46e2634c-c64b-4c65-94b9-2cc9c960e91b', + '오피스 365': + 'https://support.microsoft.com/en-us/office/cancel-a-microsoft-365-subscription-46e2634c-c64b-4c65-94b9-2cc9c960e91b', + 'slack': + 'https://slack.com/help/articles/360003378691-Cancel-your-Slack-subscription', + '슬랙': + 'https://slack.com/help/articles/360003378691-Cancel-your-Slack-subscription', + 'notion': + 'https://www.notion.so/help/billing-and-payment-settings#cancel-a-subscription', + '노션': + 'https://www.notion.so/help/billing-and-payment-settings#cancel-a-subscription', + 'dropbox': 'https://help.dropbox.com/accounts-billing/cancellation', + '드롭박스': 'https://help.dropbox.com/accounts-billing/cancellation', + 'adobe creative cloud': + 'https://helpx.adobe.com/manage-account/using/cancel-subscription.html', + '어도비 크리에이티브 클라우드': + 'https://helpx.adobe.com/manage-account/using/cancel-subscription.html', + + // 기타 유명 서비스 해지 안내 페이지 + 'google one': 'https://support.google.com/googleone/answer/9140429', + '구글 원': 'https://support.google.com/googleone/answer/9140429', + 'icloud': 'https://support.apple.com/ko-kr/HT207594', + '아이클라우드': 'https://support.apple.com/ko-kr/HT207594', + 'nintendo switch online': + 'https://en-americas-support.nintendo.com/app/answers/detail/a_id/41925/~/how-to-cancel-a-nintendo-switch-online-membership', + '닌텐도 스위치 온라인': + 'https://en-americas-support.nintendo.com/app/answers/detail/a_id/41925/~/how-to-cancel-a-nintendo-switch-online-membership', + 'playstation plus': + 'https://www.playstation.com/support/subscriptions/cancel-playstation-plus/', + '플레이스테이션 플러스': + 'https://www.playstation.com/support/subscriptions/cancel-playstation-plus/', + 'xbox game pass': + 'https://support.xbox.com/help/subscriptions-billing/manage-subscriptions/xbox-game-pass-how-to-cancel', + '엑스박스 게임 패스': + 'https://support.xbox.com/help/subscriptions-billing/manage-subscriptions/xbox-game-pass-how-to-cancel', + }; + + // 모든 서비스 매핑을 합친 맵 + static Map get allServices => { + ...ottServices, + ...musicServices, + ...storageServices, + ...aiServices, + ...programmingServices, + ...officeTools, + ...lifestyleServices, + ...shoppingServices, + ...telecomServices, + ...otherServices, + }; + + /// 서비스 카테고리 찾기 + static String? getCategoryForService(String serviceName) { + final lowerName = serviceName.toLowerCase(); + + if (ottServices.containsKey(lowerName)) return 'ott'; + if (musicServices.containsKey(lowerName)) return 'music'; + if (storageServices.containsKey(lowerName)) return 'storage'; + if (aiServices.containsKey(lowerName)) return 'ai'; + if (programmingServices.containsKey(lowerName)) return 'programming'; + if (officeTools.containsKey(lowerName)) return 'office'; + if (lifestyleServices.containsKey(lowerName)) return 'lifestyle'; + if (shoppingServices.containsKey(lowerName)) return 'shopping'; + if (telecomServices.containsKey(lowerName)) return 'telecom'; + if (otherServices.containsKey(lowerName)) return 'other'; + + return null; + } +} \ No newline at end of file diff --git a/lib/services/url_matcher/models/service_info.dart b/lib/services/url_matcher/models/service_info.dart new file mode 100644 index 0000000..c371af8 --- /dev/null +++ b/lib/services/url_matcher/models/service_info.dart @@ -0,0 +1,20 @@ +/// 서비스 정보를 담는 데이터 클래스 +class ServiceInfo { + final String serviceId; + final String serviceName; + final String? serviceUrl; + final String? cancellationUrl; + final String categoryId; + final String categoryNameKr; + final String categoryNameEn; + + ServiceInfo({ + required this.serviceId, + required this.serviceName, + this.serviceUrl, + this.cancellationUrl, + required this.categoryId, + required this.categoryNameKr, + required this.categoryNameEn, + }); +} \ No newline at end of file diff --git a/lib/services/url_matcher/url_matcher.dart b/lib/services/url_matcher/url_matcher.dart new file mode 100644 index 0000000..d2dbf4e --- /dev/null +++ b/lib/services/url_matcher/url_matcher.dart @@ -0,0 +1,2 @@ +/// URL Matcher 패키지의 export 파일 +export 'models/service_info.dart'; \ No newline at end of file diff --git a/lib/utils/subscription_category_helper.dart b/lib/utils/subscription_category_helper.dart index fff798e..55dda43 100644 --- a/lib/utils/subscription_category_helper.dart +++ b/lib/utils/subscription_category_helper.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../models/subscription_model.dart'; import '../providers/category_provider.dart'; import '../services/subscription_url_matcher.dart'; +import '../services/url_matcher/data/legacy_service_data.dart'; /// 구독 서비스를 카테고리별로 구분하는 도우미 클래스 class SubscriptionCategoryHelper { @@ -38,7 +39,7 @@ class SubscriptionCategoryHelper { // 카테고리 ID가 없거나 카테고리를 찾을 수 없는 경우 서비스 이름 기반 분류 // 음악 if (_isInCategory( - subscription.serviceName, SubscriptionUrlMatcher.musicServices)) { + subscription.serviceName, LegacyServiceData.musicServices)) { if (!categorizedSubscriptions.containsKey('music')) { categorizedSubscriptions['music'] = []; } @@ -46,7 +47,7 @@ class SubscriptionCategoryHelper { } // OTT(동영상) else if (_isInCategory( - subscription.serviceName, SubscriptionUrlMatcher.ottServices)) { + subscription.serviceName, LegacyServiceData.ottServices)) { if (!categorizedSubscriptions.containsKey('ottVideo')) { categorizedSubscriptions['ottVideo'] = []; } @@ -54,7 +55,7 @@ class SubscriptionCategoryHelper { } // 저장/클라우드 else if (_isInCategory( - subscription.serviceName, SubscriptionUrlMatcher.storageServices)) { + subscription.serviceName, LegacyServiceData.storageServices)) { if (!categorizedSubscriptions.containsKey('storageCloud')) { categorizedSubscriptions['storageCloud'] = []; } @@ -62,7 +63,7 @@ class SubscriptionCategoryHelper { } // 통신 · 인터넷 · TV else if (_isInCategory( - subscription.serviceName, SubscriptionUrlMatcher.telecomServices)) { + subscription.serviceName, LegacyServiceData.telecomServices)) { if (!categorizedSubscriptions.containsKey('telecomInternetTv')) { categorizedSubscriptions['telecomInternetTv'] = []; } @@ -70,7 +71,7 @@ class SubscriptionCategoryHelper { } // 생활/라이프스타일 else if (_isInCategory( - subscription.serviceName, SubscriptionUrlMatcher.lifestyleServices)) { + subscription.serviceName, LegacyServiceData.lifestyleServices)) { if (!categorizedSubscriptions.containsKey('lifestyle')) { categorizedSubscriptions['lifestyle'] = []; } @@ -78,7 +79,7 @@ class SubscriptionCategoryHelper { } // 쇼핑/이커머스 else if (_isInCategory( - subscription.serviceName, SubscriptionUrlMatcher.shoppingServices)) { + subscription.serviceName, LegacyServiceData.shoppingServices)) { if (!categorizedSubscriptions.containsKey('shoppingEcommerce')) { categorizedSubscriptions['shoppingEcommerce'] = []; } @@ -86,7 +87,7 @@ class SubscriptionCategoryHelper { } // 프로그래밍 else if (_isInCategory(subscription.serviceName, - SubscriptionUrlMatcher.programmingServices)) { + LegacyServiceData.programmingServices)) { if (!categorizedSubscriptions.containsKey('programming')) { categorizedSubscriptions['programming'] = []; } @@ -94,7 +95,7 @@ class SubscriptionCategoryHelper { } // 협업/오피스 else if (_isInCategory( - subscription.serviceName, SubscriptionUrlMatcher.officeTools)) { + subscription.serviceName, LegacyServiceData.officeTools)) { if (!categorizedSubscriptions.containsKey('collaborationOffice')) { categorizedSubscriptions['collaborationOffice'] = []; } @@ -102,7 +103,7 @@ class SubscriptionCategoryHelper { } // AI 서비스 else if (_isInCategory( - subscription.serviceName, SubscriptionUrlMatcher.aiServices)) { + subscription.serviceName, LegacyServiceData.aiServices)) { if (!categorizedSubscriptions.containsKey('aiService')) { categorizedSubscriptions['aiService'] = []; } @@ -110,7 +111,7 @@ class SubscriptionCategoryHelper { } // 기타 else if (_isInCategory( - subscription.serviceName, SubscriptionUrlMatcher.otherServices)) { + subscription.serviceName, LegacyServiceData.otherServices)) { if (!categorizedSubscriptions.containsKey('other')) { categorizedSubscriptions['other'] = []; }