content-genie-mcp 2.5.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +137 -14
  2. package/dist/index.js +1033 -105
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import * as cheerio from "cheerio";
9
9
  // =============================================================================
10
10
  const server = new McpServer({
11
11
  name: "content-genie-mcp",
12
- version: "2.5.0",
12
+ version: "2.7.0",
13
13
  });
14
14
  // =============================================================================
15
15
  // 공통 스키마
@@ -469,6 +469,224 @@ server.tool("get_seasonal_content_guide", "다가오는 시즌/이벤트에 맞
469
469
  // =============================================================================
470
470
  // Helper Functions - 고도화
471
471
  // =============================================================================
472
+ // =============================================================================
473
+ // SEO 실시간 데이터 수집 함수들
474
+ // =============================================================================
475
+ // 네이버 자동완성 API (API 키 불필요)
476
+ async function getNaverAutocomplete(keyword) {
477
+ try {
478
+ const response = await axios.get(`https://ac.search.naver.com/nx/ac`, {
479
+ params: {
480
+ q: keyword,
481
+ con: 1,
482
+ frm: 'nv',
483
+ ans: 2,
484
+ r_format: 'json',
485
+ r_enc: 'UTF-8',
486
+ r_unicode: 0,
487
+ t_koreng: 1,
488
+ run: 2,
489
+ rev: 4,
490
+ q_enc: 'UTF-8'
491
+ },
492
+ headers: {
493
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
494
+ 'Accept': 'application/json',
495
+ },
496
+ timeout: 5000,
497
+ });
498
+ const suggestions = [];
499
+ const items = response.data?.items || [];
500
+ for (const group of items) {
501
+ if (Array.isArray(group)) {
502
+ for (const item of group) {
503
+ if (Array.isArray(item) && item[0]) {
504
+ suggestions.push(item[0]);
505
+ }
506
+ }
507
+ }
508
+ }
509
+ return suggestions.slice(0, 10);
510
+ }
511
+ catch {
512
+ return [];
513
+ }
514
+ }
515
+ // 구글 자동완성 API (API 키 불필요)
516
+ async function getGoogleAutocomplete(keyword) {
517
+ try {
518
+ const response = await axios.get(`https://suggestqueries.google.com/complete/search`, {
519
+ params: {
520
+ client: 'firefox',
521
+ q: keyword,
522
+ hl: 'ko',
523
+ },
524
+ headers: {
525
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
526
+ },
527
+ timeout: 5000,
528
+ });
529
+ // 응답 형식: ["keyword", ["suggestion1", "suggestion2", ...]]
530
+ if (Array.isArray(response.data) && Array.isArray(response.data[1])) {
531
+ return response.data[1].slice(0, 10);
532
+ }
533
+ return [];
534
+ }
535
+ catch {
536
+ return [];
537
+ }
538
+ }
539
+ // 네이버 연관검색어 스크래핑
540
+ async function getNaverRelatedKeywords(keyword) {
541
+ try {
542
+ const response = await axios.get(`https://search.naver.com/search.naver`, {
543
+ params: {
544
+ where: 'nexearch',
545
+ query: keyword,
546
+ },
547
+ headers: {
548
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
549
+ 'Accept-Language': 'ko-KR,ko;q=0.9',
550
+ },
551
+ timeout: 8000,
552
+ });
553
+ const $ = cheerio.load(response.data);
554
+ const relatedKeywords = [];
555
+ // 연관검색어 영역
556
+ $('.related_srch .keyword, .lst_related_srch .tit, [class*="related"] a').each((i, el) => {
557
+ const kw = $(el).text().trim();
558
+ if (kw && kw !== keyword && !relatedKeywords.find(r => r.keyword === kw)) {
559
+ relatedKeywords.push({
560
+ keyword: kw,
561
+ source: 'naver_related'
562
+ });
563
+ }
564
+ });
565
+ return relatedKeywords.slice(0, 10);
566
+ }
567
+ catch {
568
+ return [];
569
+ }
570
+ }
571
+ // 네이버 검색 결과 수 추정 (경쟁도 측정)
572
+ async function getNaverSearchResultCount(keyword) {
573
+ try {
574
+ const response = await axios.get(`https://search.naver.com/search.naver`, {
575
+ params: {
576
+ where: 'blog',
577
+ query: keyword,
578
+ },
579
+ headers: {
580
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
581
+ },
582
+ timeout: 5000,
583
+ });
584
+ const $ = cheerio.load(response.data);
585
+ // 검색 결과 수 추출 시도
586
+ const countText = $('.title_num, .result_num, [class*="count"]').first().text();
587
+ const match = countText.match(/[\d,]+/);
588
+ if (match) {
589
+ return parseInt(match[0].replace(/,/g, ''), 10);
590
+ }
591
+ // 대략적 추정: 검색 결과 아이템 수 기반
592
+ const itemCount = $('.lst_total li, .api_txt_lines').length;
593
+ return itemCount > 0 ? itemCount * 10000 : 50000;
594
+ }
595
+ catch {
596
+ return 50000; // 기본값
597
+ }
598
+ }
599
+ // 구글 검색 결과 수 추정
600
+ async function getGoogleSearchResultCount(keyword) {
601
+ try {
602
+ const response = await axios.get(`https://www.google.com/search`, {
603
+ params: {
604
+ q: keyword,
605
+ hl: 'ko',
606
+ gl: 'kr',
607
+ },
608
+ headers: {
609
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
610
+ },
611
+ timeout: 5000,
612
+ });
613
+ const $ = cheerio.load(response.data);
614
+ // "약 X개의 결과" 텍스트 추출
615
+ const resultStats = $('#result-stats').text();
616
+ const match = resultStats.match(/[\d,]+/);
617
+ if (match) {
618
+ return parseInt(match[0].replace(/,/g, ''), 10);
619
+ }
620
+ return 100000; // 기본값
621
+ }
622
+ catch {
623
+ return 100000;
624
+ }
625
+ }
626
+ // 검색량 레벨 추정 (자동완성 순위 기반)
627
+ function estimateSearchVolume(autocompleteRank, resultCount) {
628
+ // 자동완성 상위 + 결과 수 많음 = 검색량 높음
629
+ if (autocompleteRank <= 3 && resultCount > 100000)
630
+ return "매우 높음";
631
+ if (autocompleteRank <= 5 && resultCount > 50000)
632
+ return "높음";
633
+ if (autocompleteRank <= 8 && resultCount > 10000)
634
+ return "중간";
635
+ return "낮음";
636
+ }
637
+ // 경쟁도 레벨 추정
638
+ function estimateCompetition(resultCount) {
639
+ if (resultCount > 1000000)
640
+ return { level: "매우 높음", score: 90 };
641
+ if (resultCount > 500000)
642
+ return { level: "높음", score: 75 };
643
+ if (resultCount > 100000)
644
+ return { level: "중간", score: 55 };
645
+ if (resultCount > 10000)
646
+ return { level: "낮음", score: 35 };
647
+ return { level: "매우 낮음", score: 20 };
648
+ }
649
+ // SEO 난이도 계산
650
+ function calculateSEODifficulty(competition, resultCount) {
651
+ const baseScore = competition;
652
+ const resultFactor = Math.min(30, Math.log10(resultCount) * 5);
653
+ return Math.min(100, Math.round(baseScore + resultFactor));
654
+ }
655
+ // 콘텐츠 기회 점수 계산
656
+ function calculateOpportunityScore(searchVolume, competition) {
657
+ const volumeScores = { "매우 높음": 40, "높음": 30, "중간": 20, "낮음": 10 };
658
+ const competitionScores = { "매우 낮음": 40, "낮음": 30, "중간": 20, "높음": 10, "매우 높음": 5 };
659
+ return (volumeScores[searchVolume] || 20) + (competitionScores[competition] || 20);
660
+ }
661
+ // 키워드 카테고리 자동 분류
662
+ function categorizeKeyword(keyword) {
663
+ const text = keyword.toLowerCase();
664
+ if (/ai|gpt|인공지능|기술|테크|앱|소프트웨어|코딩|개발/.test(text))
665
+ return "tech";
666
+ if (/주식|코인|비트코인|투자|금리|경제|재테크|부동산|환율/.test(text))
667
+ return "finance";
668
+ if (/운동|헬스|다이어트|건강|병원|의료|영양/.test(text))
669
+ return "health";
670
+ if (/맛집|음식|요리|레시피|카페|먹방|배달/.test(text))
671
+ return "food";
672
+ if (/여행|호텔|관광|항공|휴가|리조트/.test(text))
673
+ return "travel";
674
+ if (/드라마|영화|연예|아이돌|방송|예능|넷플릭스|kpop/.test(text))
675
+ return "entertainment";
676
+ if (/게임|롤|배그|스팀|플스|닌텐도/.test(text))
677
+ return "gaming";
678
+ if (/뷰티|화장품|스킨케어|메이크업|패션|옷/.test(text))
679
+ return "beauty";
680
+ if (/축구|야구|농구|스포츠|올림픽|월드컵/.test(text))
681
+ return "sports";
682
+ if (/교육|공부|학교|시험|자격증|취업/.test(text))
683
+ return "education";
684
+ if (/쇼핑|할인|세일|구매|가격/.test(text))
685
+ return "shopping";
686
+ if (/뉴스|정치|사회|이슈/.test(text))
687
+ return "news";
688
+ return "general";
689
+ }
472
690
  // 네이버 트렌드 스크래핑
473
691
  async function scrapeNaverTrends() {
474
692
  try {
@@ -572,38 +790,391 @@ function getDaumFallbackTrends() {
572
790
  { keyword: "날씨 정보", platform: "daum", rank: 5, category: "general" },
573
791
  ];
574
792
  }
575
- // 구글 트렌드 코리아
793
+ // 구글 트렌드 코리아 - 실제 RSS 피드 스크래핑
576
794
  async function scrapeGoogleTrendsKorea() {
577
- return [
578
- { keyword: "생성형 AI", platform: "google", rank: 1, category: "tech", trend: "rising" },
579
- { keyword: "K-POP 신곡", platform: "google", rank: 2, category: "entertainment", trend: "stable" },
580
- { keyword: "미국 주식", platform: "google", rank: 3, category: "finance", trend: "rising" },
581
- { keyword: "비트코인 전망", platform: "google", rank: 4, category: "finance", trend: "volatile" },
582
- { keyword: "건강식품 추천", platform: "google", rank: 5, category: "health", trend: "rising" },
583
- { keyword: "원격 근무", platform: "google", rank: 6, category: "work", trend: "stable" },
584
- { keyword: "전기차 비교", platform: "google", rank: 7, category: "auto", trend: "rising" },
795
+ try {
796
+ // Google Trends Daily RSS Feed for Korea
797
+ const response = await axios.get('https://trends.google.com/trending/rss?geo=KR', {
798
+ headers: {
799
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
800
+ 'Accept': 'application/rss+xml, application/xml, text/xml'
801
+ },
802
+ timeout: 8000,
803
+ });
804
+ const $ = cheerio.load(response.data, { xmlMode: true });
805
+ const trends = [];
806
+ $('item').each((i, el) => {
807
+ if (i >= 15)
808
+ return false; // 최대 15개
809
+ const title = $(el).find('title').text().trim();
810
+ const traffic = $(el).find('ht\\:approx_traffic, approx_traffic').text().trim();
811
+ const newsItem = $(el).find('ht\\:news_item_title, news_item_title').first().text().trim();
812
+ if (title) {
813
+ trends.push({
814
+ keyword: title,
815
+ platform: "google",
816
+ rank: i + 1,
817
+ category: categorizeKeyword(title),
818
+ trend: "rising",
819
+ traffic: traffic || "10K+",
820
+ related_news: newsItem || null,
821
+ source: "google_trends_rss"
822
+ });
823
+ }
824
+ });
825
+ if (trends.length > 0) {
826
+ return trends;
827
+ }
828
+ // Fallback: 실시간 검색 트렌드 페이지 스크래핑 시도
829
+ return await scrapeGoogleTrendsFallback();
830
+ }
831
+ catch (error) {
832
+ return await scrapeGoogleTrendsFallback();
833
+ }
834
+ }
835
+ // Google Trends Fallback - 트렌드 페이지 스크래핑
836
+ async function scrapeGoogleTrendsFallback() {
837
+ try {
838
+ const response = await axios.get('https://trends.google.co.kr/trends/trendingsearches/daily?geo=KR', {
839
+ headers: {
840
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
841
+ 'Accept-Language': 'ko-KR,ko;q=0.9'
842
+ },
843
+ timeout: 8000,
844
+ });
845
+ const $ = cheerio.load(response.data);
846
+ const trends = [];
847
+ // 다양한 셀렉터 시도
848
+ $('[class*="feed-item"], [class*="trending"], .title').each((i, el) => {
849
+ if (i >= 10)
850
+ return false;
851
+ const keyword = $(el).text().trim();
852
+ if (keyword && keyword.length > 1 && keyword.length < 50) {
853
+ trends.push({
854
+ keyword,
855
+ platform: "google",
856
+ rank: i + 1,
857
+ category: categorizeKeyword(keyword),
858
+ trend: "rising",
859
+ source: "google_trends_page"
860
+ });
861
+ }
862
+ });
863
+ if (trends.length > 0)
864
+ return trends;
865
+ // 최종 Fallback: 시간대별 동적 데이터
866
+ return getGoogleFallbackTrends();
867
+ }
868
+ catch {
869
+ return getGoogleFallbackTrends();
870
+ }
871
+ }
872
+ function getGoogleFallbackTrends() {
873
+ const hour = new Date().getHours();
874
+ const dayOfWeek = new Date().getDay();
875
+ const baseTrends = [
876
+ { keyword: "AI 활용법", category: "tech" },
877
+ { keyword: "ChatGPT 팁", category: "tech" },
878
+ { keyword: "주식 시세", category: "finance" },
879
+ { keyword: "비트코인", category: "finance" },
880
+ { keyword: "다이어트", category: "health" },
585
881
  ];
882
+ // 시간대별 추가 트렌드
883
+ const timeTrends = hour >= 9 && hour <= 18
884
+ ? [{ keyword: "점심 메뉴", category: "food" }, { keyword: "카페 추천", category: "food" }]
885
+ : [{ keyword: "넷플릭스 추천", category: "entertainment" }, { keyword: "게임 추천", category: "entertainment" }];
886
+ // 요일별 추가 트렌드
887
+ const dayTrends = dayOfWeek === 0 || dayOfWeek === 6
888
+ ? [{ keyword: "주말 나들이", category: "travel" }, { keyword: "브런치 맛집", category: "food" }]
889
+ : [{ keyword: "재택근무 팁", category: "work" }, { keyword: "퇴근 후 취미", category: "lifestyle" }];
890
+ return [...baseTrends, ...timeTrends, ...dayTrends].map((item, i) => ({
891
+ ...item,
892
+ platform: "google",
893
+ rank: i + 1,
894
+ trend: "stable",
895
+ source: "fallback_dynamic"
896
+ }));
586
897
  }
587
- // 유튜브 트렌드 코리아
898
+ // 유튜브 트렌드 코리아 - 실제 스크래핑
588
899
  async function scrapeYoutubeTrendsKorea() {
589
- return [
590
- { keyword: "브이로그", platform: "youtube", rank: 1, category: "lifestyle", views: "1.5M", format: "vlog" },
591
- { keyword: "먹방", platform: "youtube", rank: 2, category: "food", views: "1.2M", format: "mukbang" },
592
- { keyword: "게임 실황", platform: "youtube", rank: 3, category: "gaming", views: "980K", format: "streaming" },
593
- { keyword: "뷰티 튜토리얼", platform: "youtube", rank: 4, category: "beauty", views: "850K", format: "tutorial" },
594
- { keyword: "운동 루틴", platform: "youtube", rank: 5, category: "fitness", views: "720K", format: "how-to" },
595
- { keyword: "코딩 강의", platform: "youtube", rank: 6, category: "education", views: "650K", format: "lecture" },
596
- { keyword: "여행 영상", platform: "youtube", rank: 7, category: "travel", views: "600K", format: "cinematic" },
597
- { keyword: "숏폼 콘텐츠", platform: "youtube", rank: 8, category: "shorts", views: "2.1M", format: "shorts" },
900
+ try {
901
+ // YouTube Korea Trending 페이지
902
+ const response = await axios.get('https://www.youtube.com/feed/trending?gl=KR&hl=ko', {
903
+ headers: {
904
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
905
+ 'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
906
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
907
+ },
908
+ timeout: 10000,
909
+ });
910
+ const trends = [];
911
+ const html = response.data;
912
+ // YouTube 초기 데이터 JSON 추출
913
+ const ytInitialDataMatch = html.match(/var ytInitialData = ({.*?});<\/script>/s);
914
+ if (ytInitialDataMatch) {
915
+ try {
916
+ const data = JSON.parse(ytInitialDataMatch[1]);
917
+ const tabs = data?.contents?.twoColumnBrowseResultsRenderer?.tabs || [];
918
+ for (const tab of tabs) {
919
+ const contents = tab?.tabRenderer?.content?.sectionListRenderer?.contents || [];
920
+ for (const section of contents) {
921
+ const items = section?.itemSectionRenderer?.contents || [];
922
+ for (const item of items) {
923
+ const video = item?.videoRenderer;
924
+ if (video && trends.length < 15) {
925
+ const title = video?.title?.runs?.[0]?.text || video?.title?.simpleText || '';
926
+ const viewCount = video?.viewCountText?.simpleText || video?.shortViewCountText?.simpleText || '';
927
+ const channel = video?.ownerText?.runs?.[0]?.text || '';
928
+ if (title) {
929
+ trends.push({
930
+ keyword: extractKeywordFromTitle(title),
931
+ title: title,
932
+ platform: "youtube",
933
+ rank: trends.length + 1,
934
+ category: categorizeYouTubeContent(title, channel),
935
+ views: viewCount,
936
+ channel: channel,
937
+ format: detectVideoFormat(title),
938
+ source: "youtube_trending"
939
+ });
940
+ }
941
+ }
942
+ }
943
+ }
944
+ }
945
+ }
946
+ catch (parseError) {
947
+ // JSON 파싱 실패 시 fallback
948
+ }
949
+ }
950
+ // HTML 파싱 시도 (JSON 실패 시)
951
+ if (trends.length === 0) {
952
+ const $ = cheerio.load(html);
953
+ $('a#video-title').each((i, el) => {
954
+ if (i >= 15)
955
+ return false;
956
+ const title = $(el).text().trim();
957
+ if (title) {
958
+ trends.push({
959
+ keyword: extractKeywordFromTitle(title),
960
+ title: title,
961
+ platform: "youtube",
962
+ rank: i + 1,
963
+ category: categorizeYouTubeContent(title, ''),
964
+ format: detectVideoFormat(title),
965
+ source: "youtube_html"
966
+ });
967
+ }
968
+ });
969
+ }
970
+ if (trends.length > 0)
971
+ return trends;
972
+ return getYoutubeFallbackTrends();
973
+ }
974
+ catch (error) {
975
+ return getYoutubeFallbackTrends();
976
+ }
977
+ }
978
+ // YouTube 제목에서 키워드 추출
979
+ function extractKeywordFromTitle(title) {
980
+ // 대괄호, 괄호 내용 제거
981
+ let keyword = title.replace(/[\[\(【].*?[\]\)】]/g, '').trim();
982
+ // 특수문자 제거
983
+ keyword = keyword.replace(/[^\w\sㄱ-ㅎㅏ-ㅣ가-힣]/g, ' ').trim();
984
+ // 너무 길면 자르기
985
+ if (keyword.length > 30) {
986
+ keyword = keyword.substring(0, 30) + '...';
987
+ }
988
+ return keyword || title.substring(0, 30);
989
+ }
990
+ // YouTube 콘텐츠 카테고리 분류
991
+ function categorizeYouTubeContent(title, channel) {
992
+ const text = (title + ' ' + channel).toLowerCase();
993
+ if (/먹방|mukbang|음식|요리|레시피|맛집/.test(text))
994
+ return "food";
995
+ if (/게임|gaming|롤|lol|배그|minecraft/.test(text))
996
+ return "gaming";
997
+ if (/뷰티|메이크업|화장|스킨케어|뷰스타/.test(text))
998
+ return "beauty";
999
+ if (/운동|헬스|다이어트|fitness|workout/.test(text))
1000
+ return "fitness";
1001
+ if (/브이로그|vlog|일상/.test(text))
1002
+ return "lifestyle";
1003
+ if (/여행|travel|trip/.test(text))
1004
+ return "travel";
1005
+ if (/음악|노래|커버|music|mv/.test(text))
1006
+ return "music";
1007
+ if (/드라마|예능|영화|movie/.test(text))
1008
+ return "entertainment";
1009
+ if (/공부|강의|교육|tutorial/.test(text))
1010
+ return "education";
1011
+ if (/테크|리뷰|tech|unboxing/.test(text))
1012
+ return "tech";
1013
+ if (/뉴스|이슈|news/.test(text))
1014
+ return "news";
1015
+ if (/shorts|쇼츠|숏/.test(text))
1016
+ return "shorts";
1017
+ return "general";
1018
+ }
1019
+ // 영상 포맷 감지
1020
+ function detectVideoFormat(title) {
1021
+ const text = title.toLowerCase();
1022
+ if (/shorts|쇼츠/.test(text))
1023
+ return "shorts";
1024
+ if (/vlog|브이로그|일상/.test(text))
1025
+ return "vlog";
1026
+ if (/먹방|mukbang/.test(text))
1027
+ return "mukbang";
1028
+ if (/asmr/.test(text))
1029
+ return "asmr";
1030
+ if (/리뷰|review|언박싱|unboxing/.test(text))
1031
+ return "review";
1032
+ if (/튜토리얼|tutorial|강의|하는 법/.test(text))
1033
+ return "tutorial";
1034
+ if (/live|라이브/.test(text))
1035
+ return "live";
1036
+ if (/mv|뮤비|music video/.test(text))
1037
+ return "music_video";
1038
+ return "standard";
1039
+ }
1040
+ function getYoutubeFallbackTrends() {
1041
+ const hour = new Date().getHours();
1042
+ // 시간대별 인기 카테고리
1043
+ const popularCategories = hour >= 18 || hour <= 2
1044
+ ? ["entertainment", "gaming", "music"] // 저녁/밤
1045
+ : hour >= 6 && hour <= 9
1046
+ ? ["news", "education", "lifestyle"] // 아침
1047
+ : ["food", "lifestyle", "tech"]; // 낮
1048
+ const trends = [
1049
+ { keyword: "유튜브 인기 급상승", category: popularCategories[0], format: "trending" },
1050
+ { keyword: "쇼츠 챌린지", category: "shorts", format: "shorts", views: "500K+" },
1051
+ { keyword: "브이로그", category: "lifestyle", format: "vlog", views: "300K+" },
1052
+ { keyword: "먹방 ASMR", category: "food", format: "mukbang", views: "400K+" },
1053
+ { keyword: "게임 실황", category: "gaming", format: "streaming", views: "350K+" },
1054
+ { keyword: "K-POP 커버", category: "music", format: "cover", views: "600K+" },
1055
+ { keyword: "메이크업 튜토리얼", category: "beauty", format: "tutorial", views: "250K+" },
1056
+ { keyword: "IT 리뷰", category: "tech", format: "review", views: "200K+" },
598
1057
  ];
1058
+ return trends.map((item, i) => ({
1059
+ ...item,
1060
+ platform: "youtube",
1061
+ rank: i + 1,
1062
+ source: "fallback_dynamic"
1063
+ }));
599
1064
  }
600
- // 줌 트렌드
1065
+ // 줌 트렌드 - 실제 스크래핑
601
1066
  async function scrapeZumTrends() {
602
- return [
603
- { keyword: "오늘의 뉴스", platform: "zum", rank: 1, category: "news" },
604
- { keyword: "핫이슈", platform: "zum", rank: 2, category: "general" },
605
- { keyword: "연예 화제", platform: "zum", rank: 3, category: "entertainment" },
1067
+ try {
1068
+ // Zum 메인 페이지 실시간 검색어
1069
+ const response = await axios.get('https://zum.com/', {
1070
+ headers: {
1071
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
1072
+ 'Accept-Language': 'ko-KR,ko;q=0.9',
1073
+ },
1074
+ timeout: 8000,
1075
+ });
1076
+ const $ = cheerio.load(response.data);
1077
+ const trends = [];
1078
+ // 줌 실시간 검색어 셀렉터들
1079
+ const selectors = [
1080
+ '.realtime_keyword_list li',
1081
+ '.issue_keyword li',
1082
+ '[class*="ranking"] li',
1083
+ '[class*="keyword"] a',
1084
+ '.hot_keyword li'
1085
+ ];
1086
+ for (const selector of selectors) {
1087
+ if (trends.length >= 10)
1088
+ break;
1089
+ $(selector).each((i, el) => {
1090
+ if (trends.length >= 10)
1091
+ return false;
1092
+ const keyword = $(el).text().trim()
1093
+ .replace(/^\d+\.?\s*/, '') // 순위 번호 제거
1094
+ .replace(/new|↑|↓|─/gi, '') // 변동 표시 제거
1095
+ .trim();
1096
+ if (keyword && keyword.length > 1 && keyword.length < 30 && !trends.find(t => t.keyword === keyword)) {
1097
+ trends.push({
1098
+ keyword,
1099
+ platform: "zum",
1100
+ rank: trends.length + 1,
1101
+ category: categorizeKeyword(keyword),
1102
+ source: "zum_realtime"
1103
+ });
1104
+ }
1105
+ });
1106
+ }
1107
+ if (trends.length > 0)
1108
+ return trends;
1109
+ // Zum 뉴스 섹션 시도
1110
+ return await scrapeZumNewsTrends();
1111
+ }
1112
+ catch (error) {
1113
+ return await scrapeZumNewsTrends();
1114
+ }
1115
+ }
1116
+ async function scrapeZumNewsTrends() {
1117
+ try {
1118
+ const response = await axios.get('https://news.zum.com/', {
1119
+ headers: {
1120
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
1121
+ 'Accept-Language': 'ko-KR,ko;q=0.9',
1122
+ },
1123
+ timeout: 8000,
1124
+ });
1125
+ const $ = cheerio.load(response.data);
1126
+ const trends = [];
1127
+ // 뉴스 헤드라인에서 키워드 추출
1128
+ $('h2, h3, .headline, .title, [class*="news_title"]').each((i, el) => {
1129
+ if (trends.length >= 10)
1130
+ return false;
1131
+ const text = $(el).text().trim();
1132
+ if (text && text.length > 5 && text.length < 50) {
1133
+ const keyword = extractNewsKeyword(text);
1134
+ if (keyword && !trends.find(t => t.keyword === keyword)) {
1135
+ trends.push({
1136
+ keyword,
1137
+ platform: "zum",
1138
+ rank: trends.length + 1,
1139
+ category: categorizeKeyword(keyword),
1140
+ source: "zum_news"
1141
+ });
1142
+ }
1143
+ }
1144
+ });
1145
+ if (trends.length > 0)
1146
+ return trends;
1147
+ return getZumFallbackTrends();
1148
+ }
1149
+ catch {
1150
+ return getZumFallbackTrends();
1151
+ }
1152
+ }
1153
+ function extractNewsKeyword(headline) {
1154
+ // 헤드라인에서 핵심 키워드 추출
1155
+ const words = headline.split(/[\s,\.…]+/).filter(w => w.length >= 2 && w.length <= 10);
1156
+ return words.slice(0, 3).join(' ') || headline.substring(0, 20);
1157
+ }
1158
+ function getZumFallbackTrends() {
1159
+ const hour = new Date().getHours();
1160
+ const baseKeywords = [
1161
+ { keyword: "오늘의 뉴스", category: "news" },
1162
+ { keyword: "실시간 이슈", category: "general" },
1163
+ { keyword: "연예 소식", category: "entertainment" },
1164
+ { keyword: "스포츠 결과", category: "sports" },
1165
+ { keyword: "경제 동향", category: "finance" },
606
1166
  ];
1167
+ const timeBased = hour >= 7 && hour <= 9
1168
+ ? [{ keyword: "아침 뉴스", category: "news" }]
1169
+ : hour >= 12 && hour <= 14
1170
+ ? [{ keyword: "점심 이슈", category: "general" }]
1171
+ : [{ keyword: "저녁 뉴스", category: "news" }];
1172
+ return [...baseKeywords, ...timeBased].map((item, i) => ({
1173
+ ...item,
1174
+ platform: "zum",
1175
+ rank: i + 1,
1176
+ source: "fallback"
1177
+ }));
607
1178
  }
608
1179
  // 고급 트렌드 인사이트 생성
609
1180
  function generateAdvancedTrendInsights(trends) {
@@ -902,80 +1473,245 @@ function calculateReadabilityScore(text) {
902
1473
  const optimalLength = length >= 20 && length <= 50 ? 15 : length >= 50 && length <= 70 ? 10 : 5;
903
1474
  return Math.min(100, 60 + hasNumbers + hasEmoji + optimalLength);
904
1475
  }
905
- // 고급 SEO 키워드 분석
1476
+ // 고급 SEO 키워드 분석 - 실시간 데이터 기반
906
1477
  async function analyzeAdvancedSEOKeywords(keyword, searchEngine, includeQuestions, includeLongtail, competitorAnalysis) {
907
- // 관련 키워드 생성
908
- const relatedKeywords = [
909
- { keyword: `${keyword} 방법`, volume: "매우 높음", competition: "중간", cpc: "₩850", trend: "상승" },
910
- { keyword: `${keyword} 추천`, volume: "높음", competition: "높음", cpc: "₩1,200", trend: "유지" },
911
- { keyword: `${keyword} 비교`, volume: "중간", competition: "낮음", cpc: "₩650", trend: "상승" },
912
- { keyword: `${keyword} 후기`, volume: "높음", competition: "중간", cpc: "₩780", trend: "상승" },
913
- { keyword: `${keyword} 가격`, volume: "매우 높음", competition: "매우 높음", cpc: "₩1,500", trend: "유지" },
914
- { keyword: `${keyword} 종류`, volume: "중간", competition: "낮음", cpc: "₩450", trend: "상승" },
915
- { keyword: `${keyword} 장단점`, volume: "중간", competition: "낮음", cpc: "₩520", trend: "상승" },
916
- { keyword: `best ${keyword}`, volume: "중간", competition: "중간", cpc: "₩680", trend: "유지" },
1478
+ // 병렬로 실시간 데이터 수집
1479
+ const [naverAutocomplete, googleAutocomplete, naverRelated, naverResultCount, googleResultCount] = await Promise.all([
1480
+ searchEngine !== 'google' ? getNaverAutocomplete(keyword) : Promise.resolve([]),
1481
+ searchEngine !== 'naver' ? getGoogleAutocomplete(keyword) : Promise.resolve([]),
1482
+ getNaverRelatedKeywords(keyword),
1483
+ searchEngine !== 'google' ? getNaverSearchResultCount(keyword) : Promise.resolve(0),
1484
+ searchEngine !== 'naver' ? getGoogleSearchResultCount(keyword) : Promise.resolve(0),
1485
+ ]);
1486
+ // 주요 검색 엔진 결과 선택
1487
+ const primaryResultCount = searchEngine === 'naver' ? naverResultCount :
1488
+ searchEngine === 'google' ? googleResultCount :
1489
+ Math.max(naverResultCount, googleResultCount);
1490
+ // 경쟁도 및 검색량 추정
1491
+ const competition = estimateCompetition(primaryResultCount);
1492
+ const autocompleteKeywords = [...new Set([...naverAutocomplete, ...googleAutocomplete])];
1493
+ const keywordRank = autocompleteKeywords.findIndex(k => k.includes(keyword)) + 1 || 10;
1494
+ const searchVolume = estimateSearchVolume(keywordRank, primaryResultCount);
1495
+ // SEO 난이도 계산
1496
+ const seoDifficulty = calculateSEODifficulty(competition.score, primaryResultCount);
1497
+ const opportunityScore = calculateOpportunityScore(searchVolume, competition.level);
1498
+ // 실시간 관련 키워드 생성
1499
+ const relatedKeywords = [];
1500
+ // 자동완성에서 추출
1501
+ for (let i = 0; i < Math.min(autocompleteKeywords.length, 5); i++) {
1502
+ const kw = autocompleteKeywords[i];
1503
+ if (kw && kw !== keyword) {
1504
+ relatedKeywords.push({
1505
+ keyword: kw,
1506
+ volume: estimateSearchVolume(i + 1, primaryResultCount * 0.7),
1507
+ competition: i < 3 ? "높음" : "중간",
1508
+ trend: "상승",
1509
+ source: "autocomplete"
1510
+ });
1511
+ }
1512
+ }
1513
+ // 연관검색어에서 추출
1514
+ for (const related of naverRelated.slice(0, 5)) {
1515
+ if (!relatedKeywords.find(r => r.keyword === related.keyword)) {
1516
+ relatedKeywords.push({
1517
+ keyword: related.keyword,
1518
+ volume: "중간",
1519
+ competition: "중간",
1520
+ trend: "유지",
1521
+ source: "naver_related"
1522
+ });
1523
+ }
1524
+ }
1525
+ // 템플릿 기반 추가 키워드
1526
+ const templateKeywords = [
1527
+ { suffix: " 방법", volume: "높음", competition: "중간" },
1528
+ { suffix: " 추천", volume: "매우 높음", competition: "높음" },
1529
+ { suffix: " 후기", volume: "높음", competition: "중간" },
1530
+ { suffix: " 비교", volume: "중간", competition: "낮음" },
1531
+ { suffix: " 가격", volume: "매우 높음", competition: "매우 높음" },
917
1532
  ];
918
- const questionKeywords = includeQuestions ? [
919
- { keyword: `${keyword}이란 무엇인가요?`, type: "정의", intent: "정보탐색" },
920
- { keyword: `${keyword} 어떻게 시작하나요?`, type: "방법", intent: "정보탐색" },
921
- { keyword: `${keyword} 왜 필요한가요?`, type: "이유", intent: "정보탐색" },
922
- { keyword: `${keyword} 얼마인가요?`, type: "가격", intent: "구매의도" },
923
- { keyword: `${keyword} 어디서 구매하나요?`, type: "구매처", intent: "구매의도" },
924
- { keyword: `${keyword} vs 대안 뭐가 좋나요?`, type: "비교", intent: "비교검토" },
925
- ] : [];
926
- const longtailKeywords = includeLongtail ? [
927
- { keyword: `초보자를 위한 ${keyword} 완벽 가이드`, difficulty: 35, opportunity: "높음" },
928
- { keyword: `${keyword} 실수 피하는 7가지 방법`, difficulty: 28, opportunity: "매우 높음" },
929
- { keyword: `2025년 ${keyword} 트렌드 전망`, difficulty: 42, opportunity: "높음" },
930
- { keyword: `${keyword} 비용 절약하는 팁`, difficulty: 31, opportunity: "높음" },
931
- { keyword: `${keyword} 전문가가 추천하는`, difficulty: 38, opportunity: "중간" },
932
- ] : [];
1533
+ for (const tmpl of templateKeywords) {
1534
+ const kw = keyword + tmpl.suffix;
1535
+ if (!relatedKeywords.find(r => r.keyword === kw)) {
1536
+ relatedKeywords.push({
1537
+ keyword: kw,
1538
+ volume: tmpl.volume,
1539
+ competition: tmpl.competition,
1540
+ trend: "유지",
1541
+ source: "template"
1542
+ });
1543
+ }
1544
+ }
1545
+ // 질문형 키워드 (자동완성 기반)
1546
+ const questionKeywords = [];
1547
+ if (includeQuestions) {
1548
+ const questionPrefixes = ["", "어떻게 ", "왜 ", "언제 ", "어디서 "];
1549
+ const questionSuffixes = ["란", "이란", " 뭐", " 무엇", " 어떻게", " 왜", " 방법"];
1550
+ // 자동완성에서 질문형 추출
1551
+ for (const ac of autocompleteKeywords) {
1552
+ if (questionSuffixes.some(s => ac.includes(s)) || ac.includes("?")) {
1553
+ questionKeywords.push({
1554
+ keyword: ac,
1555
+ type: detectQuestionType(ac),
1556
+ intent: detectSearchIntent(ac),
1557
+ source: "autocomplete"
1558
+ });
1559
+ }
1560
+ }
1561
+ // 기본 질문 템플릿
1562
+ if (questionKeywords.length < 5) {
1563
+ const defaultQuestions = [
1564
+ { keyword: `${keyword}이란?`, type: "정의", intent: "정보탐색" },
1565
+ { keyword: `${keyword} 어떻게 하나요?`, type: "방법", intent: "정보탐색" },
1566
+ { keyword: `${keyword} 왜 필요한가요?`, type: "이유", intent: "정보탐색" },
1567
+ { keyword: `${keyword} 얼마인가요?`, type: "가격", intent: "구매의도" },
1568
+ ];
1569
+ for (const q of defaultQuestions) {
1570
+ if (!questionKeywords.find(qk => qk.keyword === q.keyword)) {
1571
+ questionKeywords.push({ ...q, source: "template" });
1572
+ }
1573
+ }
1574
+ }
1575
+ }
1576
+ // 롱테일 키워드
1577
+ const longtailKeywords = [];
1578
+ if (includeLongtail) {
1579
+ // 자동완성에서 긴 키워드 추출
1580
+ for (const ac of autocompleteKeywords) {
1581
+ if (ac.length > keyword.length + 5 && !relatedKeywords.find(r => r.keyword === ac)) {
1582
+ longtailKeywords.push({
1583
+ keyword: ac,
1584
+ difficulty: Math.round(seoDifficulty * 0.6 + Math.random() * 20),
1585
+ opportunity: "높음",
1586
+ source: "autocomplete"
1587
+ });
1588
+ }
1589
+ }
1590
+ // 템플릿 기반 롱테일
1591
+ const longtailTemplates = [
1592
+ { pattern: `초보자를 위한 ${keyword} 완벽 가이드`, difficulty: 35 },
1593
+ { pattern: `${keyword} 실수 피하는 방법`, difficulty: 28 },
1594
+ { pattern: `2025년 ${keyword} 트렌드`, difficulty: 42 },
1595
+ { pattern: `${keyword} 비용 절약 팁`, difficulty: 31 },
1596
+ { pattern: `${keyword} 전문가 추천`, difficulty: 38 },
1597
+ ];
1598
+ for (const tmpl of longtailTemplates) {
1599
+ if (!longtailKeywords.find(l => l.keyword === tmpl.pattern)) {
1600
+ longtailKeywords.push({
1601
+ keyword: tmpl.pattern,
1602
+ difficulty: Math.round(tmpl.difficulty + (seoDifficulty - 50) * 0.3),
1603
+ opportunity: tmpl.difficulty < 35 ? "매우 높음" : "높음",
1604
+ source: "template"
1605
+ });
1606
+ }
1607
+ }
1608
+ }
1609
+ // 검색엔진별 전략
933
1610
  const searchEngineStrategy = {
934
1611
  naver: {
1612
+ result_count: naverResultCount.toLocaleString(),
1613
+ competition: estimateCompetition(naverResultCount).level,
935
1614
  tips: [
936
1615
  "네이버 블로그/포스트에 발행하세요",
937
1616
  "키워드를 제목에 정확히 포함하세요",
938
1617
  "이미지 ALT 태그에 키워드 추가",
939
1618
  "체류시간을 늘리는 콘텐츠 작성",
1619
+ `경쟁 블로그 ${Math.min(naverResultCount, 1000000).toLocaleString()}개 이상 - 차별화 필수`,
940
1620
  ],
941
1621
  content_types: ["블로그", "포스트", "지식iN"],
942
1622
  },
943
1623
  google: {
1624
+ result_count: googleResultCount.toLocaleString(),
1625
+ competition: estimateCompetition(googleResultCount).level,
944
1626
  tips: [
945
1627
  "H1, H2 태그에 키워드 배치",
946
1628
  "메타 디스크립션 최적화",
947
1629
  "모바일 친화적 디자인 필수",
948
1630
  "페이지 로딩 속도 개선",
1631
+ "백링크 확보 전략 수립",
949
1632
  ],
950
1633
  content_types: ["웹사이트", "유튜브", "뉴스"],
951
1634
  },
952
1635
  };
1636
+ // 추천 액션 생성
1637
+ const recommendedAction = seoDifficulty > 70
1638
+ ? "경쟁이 치열합니다. 롱테일 키워드로 진입 후 메인 키워드 공략을 권장합니다."
1639
+ : seoDifficulty > 50
1640
+ ? "중간 경쟁입니다. 고품질 콘텐츠와 꾸준한 발행이 중요합니다."
1641
+ : "경쟁이 낮습니다. 빠른 진입으로 선점 효과를 노리세요.";
953
1642
  return {
954
1643
  main_keyword: keyword,
1644
+ data_source: {
1645
+ naver_autocomplete: naverAutocomplete.length,
1646
+ google_autocomplete: googleAutocomplete.length,
1647
+ naver_related: naverRelated.length,
1648
+ naver_results: naverResultCount.toLocaleString(),
1649
+ google_results: googleResultCount.toLocaleString(),
1650
+ },
955
1651
  overall_analysis: {
956
- search_volume: "높음",
957
- competition_level: "중간",
958
- seo_difficulty: 58,
959
- content_opportunity_score: 78,
960
- recommended_action: "롱테일 키워드로 진입 후 메인 키워드 공략",
1652
+ search_volume: searchVolume,
1653
+ search_volume_indicator: keywordRank <= 3 ? "🔥 매우 높음" : keywordRank <= 6 ? "📈 높음" : "📊 보통",
1654
+ competition_level: competition.level,
1655
+ competition_score: competition.score,
1656
+ seo_difficulty: seoDifficulty,
1657
+ seo_difficulty_grade: seoDifficulty > 70 ? "어려움" : seoDifficulty > 50 ? "보통" : "쉬움",
1658
+ content_opportunity_score: opportunityScore,
1659
+ recommended_action: recommendedAction,
961
1660
  },
962
- related_keywords: relatedKeywords,
963
- question_keywords: questionKeywords,
964
- longtail_keywords: longtailKeywords,
1661
+ related_keywords: relatedKeywords.slice(0, 15),
1662
+ question_keywords: questionKeywords.slice(0, 8),
1663
+ longtail_keywords: longtailKeywords.slice(0, 8),
965
1664
  search_engine_strategy: searchEngine === "both" ? searchEngineStrategy : searchEngineStrategy[searchEngine],
966
1665
  content_recommendations: {
967
- ideal_length: "3000-5000자",
968
- must_include: ["정의", "방법", "예시", "FAQ"],
969
- format: "종합 가이드 형식",
1666
+ ideal_length: seoDifficulty > 60 ? "4000-6000 (경쟁 대응)" : "2500-4000자",
1667
+ must_include: ["정의", "방법", "예시", "FAQ", "비교"],
1668
+ format: seoDifficulty > 60 ? "종합 가이드 형식 (심층 분석)" : "핵심 정리 형식",
970
1669
  media: ["이미지 5-10개", "인포그래픽 1개", "영상 임베드"],
1670
+ posting_frequency: seoDifficulty > 70 ? "주 3회 이상" : "주 1-2회",
971
1671
  },
972
1672
  competitor_insights: competitorAnalysis ? {
973
- top_ranking_content_avg_length: "3,500자",
974
- common_headings: ["정의", "방법", "주의사항", "FAQ"],
975
- gap_opportunities: ["최신 트렌드 반영 부족", "실제 사례 부족"],
1673
+ estimated_competitors: primaryResultCount > 100000 ? "10만+" : primaryResultCount > 10000 ? "1만+" : "1천+",
1674
+ top_ranking_strategy: [
1675
+ "제목에 키워드 정확히 포함",
1676
+ "3000자 이상의 상세 콘텐츠",
1677
+ "이미지/영상 풍부하게 활용",
1678
+ "정기적인 업데이트",
1679
+ ],
1680
+ gap_opportunities: [
1681
+ "최신 2025년 트렌드 반영",
1682
+ "실제 사례/후기 포함",
1683
+ "비교 분석 콘텐츠",
1684
+ "FAQ 섹션 추가",
1685
+ ],
976
1686
  } : null,
977
1687
  };
978
1688
  }
1689
+ // 질문 유형 감지
1690
+ function detectQuestionType(text) {
1691
+ if (/이란|무엇|뭐야|뜻/.test(text))
1692
+ return "정의";
1693
+ if (/어떻게|방법|하는법/.test(text))
1694
+ return "방법";
1695
+ if (/왜|이유/.test(text))
1696
+ return "이유";
1697
+ if (/얼마|가격|비용/.test(text))
1698
+ return "가격";
1699
+ if (/어디|장소|위치/.test(text))
1700
+ return "장소";
1701
+ if (/언제|시간|기간/.test(text))
1702
+ return "시간";
1703
+ return "일반";
1704
+ }
1705
+ // 검색 의도 감지
1706
+ function detectSearchIntent(text) {
1707
+ if (/구매|가격|얼마|싼|저렴|할인/.test(text))
1708
+ return "구매의도";
1709
+ if (/vs|비교|차이|어떤게/.test(text))
1710
+ return "비교검토";
1711
+ if (/후기|리뷰|평가|사용/.test(text))
1712
+ return "사용경험";
1713
+ return "정보탐색";
1714
+ }
979
1715
  // 고급 콘텐츠 캘린더 생성
980
1716
  function createAdvancedContentCalendar(topics, durationWeeks, postsPerWeek, platforms, includeEvents, contentMix) {
981
1717
  const calendar = [];
@@ -1374,57 +2110,249 @@ function predictAdvancedViralScore(title, description, platform, hashtags, conte
1374
2110
  ].filter(Boolean).slice(0, 3),
1375
2111
  };
1376
2112
  }
1377
- // 뉴스 분석
2113
+ // 뉴스 분석 - 실제 스크래핑
1378
2114
  async function analyzeKoreanNews(category, timeRange, extractKeywords) {
1379
- // 카테고리별 뉴스 키워드 시뮬레이션
1380
- const newsData = {
2115
+ const news = [];
2116
+ const allKeywords = [];
2117
+ // 카테고리별 네이버 뉴스 섹션 URL
2118
+ const categoryUrls = {
2119
+ general: 'https://news.naver.com/',
2120
+ politics: 'https://news.naver.com/section/100',
2121
+ economy: 'https://news.naver.com/section/101',
2122
+ society: 'https://news.naver.com/section/102',
2123
+ culture: 'https://news.naver.com/section/103',
2124
+ tech: 'https://news.naver.com/section/105',
2125
+ sports: 'https://sports.news.naver.com/',
2126
+ entertainment: 'https://entertain.naver.com/home',
2127
+ };
2128
+ const url = categoryUrls[category] || categoryUrls.general;
2129
+ try {
2130
+ // 네이버 뉴스 스크래핑
2131
+ const response = await axios.get(url, {
2132
+ headers: {
2133
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
2134
+ 'Accept-Language': 'ko-KR,ko;q=0.9',
2135
+ },
2136
+ timeout: 10000,
2137
+ });
2138
+ const $ = cheerio.load(response.data);
2139
+ // 헤드라인 추출 - 다양한 셀렉터 시도
2140
+ const headlineSelectors = [
2141
+ '.cjs_t', // 네이버 뉴스 메인
2142
+ '.sa_text_title',
2143
+ 'a.news_tit',
2144
+ '.cluster_text_headline',
2145
+ '.cluster_head_topic',
2146
+ 'h2.tit',
2147
+ '.link_news',
2148
+ '[class*="headline"] a',
2149
+ '[class*="title"] a',
2150
+ ];
2151
+ for (const selector of headlineSelectors) {
2152
+ if (news.length >= 15)
2153
+ break;
2154
+ $(selector).each((i, el) => {
2155
+ if (news.length >= 15)
2156
+ return false;
2157
+ const headline = $(el).text().trim();
2158
+ const href = $(el).attr('href') || '';
2159
+ if (headline && headline.length > 10 && headline.length < 100) {
2160
+ // 중복 체크
2161
+ if (!news.find(n => n.headline === headline)) {
2162
+ const sentiment = analyzeSentiment(headline);
2163
+ news.push({
2164
+ headline,
2165
+ source: detectNewsSource(href, category),
2166
+ sentiment,
2167
+ url: href.startsWith('http') ? href : `https://news.naver.com${href}`,
2168
+ });
2169
+ // 키워드 추출
2170
+ if (extractKeywords) {
2171
+ const words = extractKeywordsFromText(headline);
2172
+ allKeywords.push(...words);
2173
+ }
2174
+ }
2175
+ }
2176
+ });
2177
+ }
2178
+ }
2179
+ catch (error) {
2180
+ // 네이버 실패 시 다음 뉴스 시도
2181
+ try {
2182
+ const daumNews = await scrapeDaumNews(category);
2183
+ news.push(...daumNews);
2184
+ if (extractKeywords) {
2185
+ daumNews.forEach(n => {
2186
+ allKeywords.push(...extractKeywordsFromText(n.headline));
2187
+ });
2188
+ }
2189
+ }
2190
+ catch {
2191
+ // 모두 실패 시 Fallback
2192
+ }
2193
+ }
2194
+ // Fallback: 뉴스가 없으면 기본 데이터
2195
+ if (news.length === 0) {
2196
+ news.push(...getNewsFallback(category));
2197
+ }
2198
+ // 키워드 빈도 계산
2199
+ const keywordFrequency = extractKeywords ? calculateKeywordFrequency(allKeywords) : [];
2200
+ // 감성 분석 요약
2201
+ const sentiments = news.map(n => n.sentiment);
2202
+ const positiveCount = sentiments.filter(s => s === 'positive').length;
2203
+ const negativeCount = sentiments.filter(s => s === 'negative').length;
2204
+ const neutralCount = sentiments.filter(s => s === 'neutral').length;
2205
+ const total = sentiments.length || 1;
2206
+ return {
2207
+ category,
2208
+ time_range: timeRange,
2209
+ analyzed_at: new Date().toISOString(),
2210
+ source: news.length > 0 && news[0].url?.includes('naver') ? 'naver_news' : 'daum_news',
2211
+ top_news: news.slice(0, 10),
2212
+ extracted_keywords: keywordFrequency.slice(0, 10),
2213
+ sentiment_summary: {
2214
+ positive: `${Math.round((positiveCount / total) * 100)}%`,
2215
+ neutral: `${Math.round((neutralCount / total) * 100)}%`,
2216
+ negative: `${Math.round((negativeCount / total) * 100)}%`,
2217
+ },
2218
+ content_opportunities: generateNewsContentOpportunities(keywordFrequency, category),
2219
+ trending_topics: news.slice(0, 5).map(n => n.headline),
2220
+ };
2221
+ }
2222
+ // 다음 뉴스 스크래핑
2223
+ async function scrapeDaumNews(category) {
2224
+ const categoryUrls = {
2225
+ general: 'https://news.daum.net/',
2226
+ politics: 'https://news.daum.net/politics',
2227
+ economy: 'https://news.daum.net/economic',
2228
+ society: 'https://news.daum.net/society',
2229
+ culture: 'https://news.daum.net/culture',
2230
+ tech: 'https://news.daum.net/digital',
2231
+ sports: 'https://sports.daum.net/',
2232
+ entertainment: 'https://entertain.daum.net/',
2233
+ };
2234
+ const url = categoryUrls[category] || categoryUrls.general;
2235
+ const response = await axios.get(url, {
2236
+ headers: {
2237
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
2238
+ 'Accept-Language': 'ko-KR,ko;q=0.9',
2239
+ },
2240
+ timeout: 8000,
2241
+ });
2242
+ const $ = cheerio.load(response.data);
2243
+ const news = [];
2244
+ $('[class*="link_txt"], .tit_g, .news_view, .txt_info').each((i, el) => {
2245
+ if (news.length >= 10)
2246
+ return false;
2247
+ const headline = $(el).text().trim();
2248
+ if (headline && headline.length > 10 && headline.length < 100) {
2249
+ if (!news.find(n => n.headline === headline)) {
2250
+ news.push({
2251
+ headline,
2252
+ source: '다음뉴스',
2253
+ sentiment: analyzeSentiment(headline),
2254
+ });
2255
+ }
2256
+ }
2257
+ });
2258
+ return news;
2259
+ }
2260
+ // 감성 분석 (간단 버전)
2261
+ function analyzeSentiment(text) {
2262
+ const positiveWords = /성공|상승|호조|기대|돌파|신기록|수상|인기|사랑|행복|좋은|최고|혁신|성장/;
2263
+ const negativeWords = /하락|위기|우려|실패|폭락|충격|논란|피해|사망|사고|비난|급락|위험|문제/;
2264
+ if (positiveWords.test(text))
2265
+ return 'positive';
2266
+ if (negativeWords.test(text))
2267
+ return 'negative';
2268
+ return 'neutral';
2269
+ }
2270
+ // 뉴스 소스 감지
2271
+ function detectNewsSource(url, category) {
2272
+ if (url.includes('sports'))
2273
+ return '스포츠';
2274
+ if (url.includes('entertain'))
2275
+ return '연예';
2276
+ const sources = {
2277
+ politics: '정치',
2278
+ economy: '경제',
2279
+ society: '사회',
2280
+ culture: '문화',
2281
+ tech: 'IT/과학',
2282
+ sports: '스포츠',
2283
+ entertainment: '연예',
2284
+ };
2285
+ return sources[category] || '종합';
2286
+ }
2287
+ // 텍스트에서 키워드 추출
2288
+ function extractKeywordsFromText(text) {
2289
+ // 특수문자 제거 후 2-6자 단어만 추출
2290
+ const words = text
2291
+ .replace(/[^\w\sㄱ-ㅎㅏ-ㅣ가-힣]/g, ' ')
2292
+ .split(/\s+/)
2293
+ .filter(w => w.length >= 2 && w.length <= 6)
2294
+ .filter(w => !/^\d+$/.test(w)); // 숫자만 있는 것 제외
2295
+ return words;
2296
+ }
2297
+ // 키워드 빈도 계산
2298
+ function calculateKeywordFrequency(words) {
2299
+ const frequency = {};
2300
+ words.forEach(word => {
2301
+ frequency[word] = (frequency[word] || 0) + 1;
2302
+ });
2303
+ return Object.entries(frequency)
2304
+ .sort((a, b) => b[1] - a[1])
2305
+ .slice(0, 15)
2306
+ .map(([keyword, freq]) => ({
2307
+ keyword,
2308
+ frequency: freq,
2309
+ trend: freq >= 5 ? '상승' : freq >= 3 ? '유지' : '일반',
2310
+ }));
2311
+ }
2312
+ // 뉴스 기반 콘텐츠 기회 생성
2313
+ function generateNewsContentOpportunities(keywords, category) {
2314
+ const opportunities = [];
2315
+ if (keywords.length > 0) {
2316
+ const topKeyword = keywords[0]?.keyword || '';
2317
+ opportunities.push(`"${topKeyword}" 관련 콘텐츠 수요 증가 - 해설/분석 콘텐츠 추천`);
2318
+ }
2319
+ const categoryOpportunities = {
2320
+ tech: ['AI/테크 트렌드 정리 콘텐츠', '신제품 리뷰 콘텐츠'],
2321
+ economy: ['재테크 팁 콘텐츠', '경제 뉴스 쉽게 풀어주기'],
2322
+ entertainment: ['K-콘텐츠 글로벌 화제', '연예 이슈 정리'],
2323
+ sports: ['경기 하이라이트', '선수 인터뷰 분석'],
2324
+ general: ['오늘의 이슈 정리', '트렌드 분석 콘텐츠'],
2325
+ };
2326
+ opportunities.push(...(categoryOpportunities[category] || categoryOpportunities.general));
2327
+ return opportunities.slice(0, 5);
2328
+ }
2329
+ // 뉴스 Fallback 데이터
2330
+ function getNewsFallback(category) {
2331
+ const now = new Date();
2332
+ const dateStr = now.toISOString().split('T')[0];
2333
+ const fallbackData = {
1381
2334
  general: [
1382
- { headline: "AI 기술 발전과 일자리 변화", source: "종합", sentiment: "neutral" },
1383
- { headline: "글로벌 경제 동향 분석", source: "경제", sentiment: "neutral" },
1384
- { headline: "2025년 트렌드 전망", source: "라이프", sentiment: "positive" },
2335
+ { headline: `${dateStr} 오늘의 주요 뉴스`, source: "종합", sentiment: "neutral" },
2336
+ { headline: "AI 기술 발전과 산업 변화", source: "종합", sentiment: "neutral" },
2337
+ { headline: "글로벌 경제 동향", source: "종합", sentiment: "neutral" },
1385
2338
  ],
1386
2339
  tech: [
1387
- { headline: "ChatGPT 신기능 출시", source: "테크", sentiment: "positive" },
1388
- { headline: "애플 신제품 발표", source: "테크", sentiment: "positive" },
1389
- { headline: "사이버 보안 위협 증가", source: "테크", sentiment: "negative" },
2340
+ { headline: "AI 서비스 업데이트 소식", source: "IT/과학", sentiment: "positive" },
2341
+ { headline: "테크 기업 신제품 발표", source: "IT/과학", sentiment: "positive" },
2342
+ { headline: "사이버 보안 동향", source: "IT/과학", sentiment: "neutral" },
1390
2343
  ],
1391
2344
  economy: [
1392
- { headline: "한국은행 금리 결정", source: "경제", sentiment: "neutral" },
1393
- { headline: "부동산 시장 동향", source: "경제", sentiment: "neutral" },
1394
- { headline: "환율 변동성 확대", source: "경제", sentiment: "negative" },
2345
+ { headline: "금융 시장 동향", source: "경제", sentiment: "neutral" },
2346
+ { headline: "부동산 시장 분석", source: "경제", sentiment: "neutral" },
2347
+ { headline: "환율 변동 현황", source: "경제", sentiment: "neutral" },
1395
2348
  ],
1396
2349
  entertainment: [
1397
2350
  { headline: "K-POP 글로벌 인기", source: "연예", sentiment: "positive" },
1398
- { headline: "넷플릭스 신작 화제", source: "연예", sentiment: "positive" },
1399
- { headline: "연예계 이슈", source: "연예", sentiment: "neutral" },
1400
- ],
1401
- };
1402
- const news = newsData[category] || newsData.general;
1403
- const keywords = extractKeywords ? [
1404
- { keyword: "AI", frequency: 45, trend: "상승" },
1405
- { keyword: "경제", frequency: 38, trend: "유지" },
1406
- { keyword: "트렌드", frequency: 32, trend: "상승" },
1407
- { keyword: "투자", frequency: 28, trend: "상승" },
1408
- { keyword: "기술", frequency: 25, trend: "상승" },
1409
- ] : [];
1410
- return {
1411
- category,
1412
- time_range: timeRange,
1413
- analyzed_at: new Date().toISOString(),
1414
- top_news: news,
1415
- extracted_keywords: keywords,
1416
- sentiment_summary: {
1417
- positive: "35%",
1418
- neutral: "50%",
1419
- negative: "15%",
1420
- },
1421
- content_opportunities: [
1422
- "AI 관련 콘텐츠 수요 증가 - 입문자 가이드 추천",
1423
- "경제 불안 심리 반영 - 재테크 팁 콘텐츠",
1424
- "K-콘텐츠 글로벌 화제 - 한류 관련 콘텐츠",
2351
+ { headline: "드라마/예능 화제작", source: "연예", sentiment: "positive" },
2352
+ { headline: "연예계 소식", source: "연예", sentiment: "neutral" },
1425
2353
  ],
1426
- trending_topics: news.map(n => n.headline),
1427
2354
  };
2355
+ return fallbackData[category] || fallbackData.general;
1428
2356
  }
1429
2357
  // 해시태그 전략 생성
1430
2358
  function generateAdvancedHashtagStrategy(topic, platform, count, includeKorean, includeEnglish) {