ginipick commited on
Commit
372c3a2
·
verified ·
1 Parent(s): c6beee2

Update app-backup2.py

Browse files
Files changed (1) hide show
  1. app-backup2.py +251 -91
app-backup2.py CHANGED
@@ -3,7 +3,7 @@ import requests
3
  import json
4
  import os
5
  from datetime import datetime, timedelta
6
- from concurrent.futures import ThreadPoolExecutor
7
  from functools import lru_cache
8
  from requests.adapters import HTTPAdapter
9
  from requests.packages.urllib3.util.retry import Retry
@@ -16,49 +16,45 @@ import pytz
16
 
17
  # 한국 기업 리스트
18
  KOREAN_COMPANIES = [
 
 
 
 
 
 
 
 
19
  "SAMSUNG",
20
  "HYNIX",
21
- "HYUNDAI",
22
- "KIA",
23
- "LG",
24
- "HANWHA",
25
- "SKT",
26
- "Lotte",
27
- "KOGAS",
28
- "KEPCO",
29
- "SK",
30
- "POSCO",
31
- "DOOSAN",
32
- "WOORI",
33
- "KAKAO",
34
- "Celltrion"
35
  ]
36
 
37
  def convert_to_seoul_time(timestamp_str):
38
  try:
39
- # 입력된 시간을 naive datetime 객체로 변환
40
  dt = datetime.strptime(timestamp_str, '%Y-%m-%d %H:%M:%S')
41
-
42
- # 서울 시간대 설정
43
  seoul_tz = pytz.timezone('Asia/Seoul')
44
-
45
- # 현재 시간을 서울 시간으로 인식하도록 수정
46
  seoul_time = seoul_tz.localize(dt)
47
-
48
  return seoul_time.strftime('%Y-%m-%d %H:%M:%S KST')
49
  except Exception as e:
50
  print(f"시간 변환 오류: {str(e)}")
51
  return timestamp_str
52
 
53
-
54
  def analyze_sentiment_batch(articles, client):
 
 
 
55
  try:
56
  # 모든 기사의 제목과 내용을 하나의 텍스트로 결합
57
  combined_text = "\n\n".join([
58
  f"제목: {article.get('title', '')}\n내용: {article.get('snippet', '')}"
59
  for article in articles
60
  ])
61
-
62
  prompt = f"""다음 뉴스 모음에 대해 전반적인 감성 분석을 수행하세요:
63
 
64
  뉴스 내용:
@@ -74,7 +70,7 @@ def analyze_sentiment_batch(articles, client):
74
  - [항목2]
75
  4. 종합 평가: [상세 설명]
76
  """
77
-
78
  response = client.chat.completions.create(
79
  model="CohereForAI/c4ai-command-r-plus-08-2024",
80
  messages=[{"role": "user", "content": prompt}],
@@ -101,28 +97,27 @@ def init_db():
101
  conn.commit()
102
  conn.close()
103
 
104
-
105
  def save_to_db(keyword, country, results):
 
 
 
106
  conn = sqlite3.connect("search_results.db")
107
  c = conn.cursor()
108
-
109
- # 현재 시간을 서울 시간으로 가져오기
110
  seoul_tz = pytz.timezone('Asia/Seoul')
111
  now = datetime.now(seoul_tz)
112
-
113
- # 시간대 정보를 제거하고 저장
114
  timestamp = now.strftime('%Y-%m-%d %H:%M:%S')
115
 
116
  c.execute("""INSERT INTO searches
117
  (keyword, country, results, timestamp)
118
  VALUES (?, ?, ?, ?)""",
119
  (keyword, country, json.dumps(results), timestamp))
120
-
121
  conn.commit()
122
  conn.close()
123
 
124
-
125
  def load_from_db(keyword, country):
 
 
 
126
  conn = sqlite3.connect("search_results.db")
127
  c = conn.cursor()
128
  c.execute("SELECT results, timestamp FROM searches WHERE keyword=? AND country=? ORDER BY timestamp DESC LIMIT 1",
@@ -133,8 +128,10 @@ def load_from_db(keyword, country):
133
  return json.loads(result[0]), convert_to_seoul_time(result[1])
134
  return None, None
135
 
136
-
137
  def display_results(articles):
 
 
 
138
  output = ""
139
  for idx, article in enumerate(articles, 1):
140
  output += f"### {idx}. {article['title']}\n"
@@ -144,28 +141,40 @@ def display_results(articles):
144
  output += f"요약: {article['snippet']}\n\n"
145
  return output
146
 
147
-
148
  def search_company(company):
 
 
 
149
  error_message, articles = serphouse_search(company, "United States")
150
  if not error_message and articles:
151
  save_to_db(company, "United States", articles)
152
  return display_results(articles)
153
  return f"{company}에 대한 검색 결과가 없습니다."
154
 
155
-
156
  def load_company(company):
 
 
 
157
  results, timestamp = load_from_db(company, "United States")
158
  if results:
159
  return f"### {company} 검색 결과\n저장 시간: {timestamp}\n\n" + display_results(results)
160
  return f"{company}에 대한 저장된 결과가 없습니다."
161
 
162
-
163
  def show_stats():
 
 
 
 
 
 
 
164
  conn = sqlite3.connect("search_results.db")
165
  c = conn.cursor()
166
 
167
  output = "## 한국 기업 뉴스 분석 리포트\n\n"
168
 
 
 
169
  for company in KOREAN_COMPANIES:
170
  c.execute("""
171
  SELECT results, timestamp
@@ -175,28 +184,142 @@ def show_stats():
175
  LIMIT 1
176
  """, (company,))
177
 
178
- result = c.fetchone()
179
- if result:
180
- results_json, timestamp = result
181
  articles = json.loads(results_json)
182
  seoul_time = convert_to_seoul_time(timestamp)
183
-
184
- output += f"### {company}\n"
185
- output += f"- 마지막 업데이트: {seoul_time}\n"
186
- output += f"- 저장된 기사 수: {len(articles)}건\n\n"
187
-
188
- if articles:
189
- # 전체 기사에 대한 감성 분석
190
- sentiment_analysis = analyze_sentiment_batch(articles, client)
191
- output += "#### 뉴스 감성 분석\n"
192
- output += f"{sentiment_analysis}\n\n"
193
-
194
- output += "---\n\n"
195
 
196
  conn.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  return output
198
 
199
 
 
200
  ACCESS_TOKEN = os.getenv("HF_TOKEN")
201
  if not ACCESS_TOKEN:
202
  raise ValueError("HF_TOKEN environment variable is not set")
@@ -209,7 +332,7 @@ client = OpenAI(
209
  API_KEY = os.getenv("SERPHOUSE_API_KEY")
210
 
211
 
212
- # 국가별 언어 코드 매핑 (첫 번째 탭에서는 'United States'만 주로 사용)
213
  COUNTRY_LANGUAGES = {
214
  "United States": "en",
215
  "KOREA": "ko",
@@ -275,10 +398,10 @@ COUNTRY_LANGUAGES = {
275
  "Latvia": "lv",
276
  "Lithuania": "lt",
277
  "Slovenia": "sl",
278
- "Luxembourg": "fr",
279
- "Malta": "mt",
280
- "Cyprus": "el",
281
- "Iceland": "is"
282
  }
283
 
284
  COUNTRY_LOCATIONS = {
@@ -342,10 +465,10 @@ COUNTRY_LOCATIONS = {
342
  "Slovakia": "Slovakia",
343
  "Bulgaria": "Bulgaria",
344
  "Serbia": "Serbia",
345
- "Estonia": "Estonia",
346
- "Latvia": "Latvia",
347
- "Lithuania": "Lithuania",
348
- "Slovenia": "Slovenia",
349
  "Luxembourg": "Luxembourg",
350
  "Malta": "Malta",
351
  "Cyprus": "Cyprus",
@@ -355,14 +478,16 @@ COUNTRY_LOCATIONS = {
355
 
356
  @lru_cache(maxsize=100)
357
  def translate_query(query, country):
 
 
 
358
  try:
359
  if is_english(query):
360
  return query
361
-
362
  if country in COUNTRY_LANGUAGES:
363
  if country == "South Korea":
364
  return query
365
-
366
  target_lang = COUNTRY_LANGUAGES[country]
367
 
368
  url = "https://translate.googleapis.com/translate_a/single"
@@ -388,12 +513,14 @@ def translate_query(query, country):
388
  print(f"번역 오류: {str(e)}")
389
  return query
390
 
391
-
392
  def is_english(text):
393
  return all(ord(char) < 128 for char in text.replace(' ', '').replace('-', '').replace('_', ''))
394
 
395
-
396
  def search_serphouse(query, country, page=1, num_result=10):
 
 
 
 
397
  url = "https://api.serphouse.com/serp/live"
398
 
399
  now = datetime.utcnow()
@@ -410,7 +537,7 @@ def search_serphouse(query, country, page=1, num_result=10):
410
  "lang": COUNTRY_LANGUAGES.get(country, "en"),
411
  "device": "desktop",
412
  "serp_type": "news",
413
- "page": "1",
414
  "num": "100",
415
  "date_range": date_range,
416
  "sort_by": "date"
@@ -463,8 +590,11 @@ def search_serphouse(query, country, page=1, num_result=10):
463
  "translated_query": query
464
  }
465
 
466
-
467
  def format_results_from_raw(response_data):
 
 
 
 
468
  if "error" in response_data:
469
  return "Error: " + response_data["error"], []
470
 
@@ -472,11 +602,12 @@ def format_results_from_raw(response_data):
472
  results = response_data["results"]
473
  translated_query = response_data["translated_query"]
474
 
 
475
  news_results = results.get('results', {}).get('results', {}).get('news', [])
476
  if not news_results:
477
  return "검색 결과가 없습니다.", []
478
-
479
- # 한국 도메인 및 한국 관련 키워드 필터링
480
  korean_domains = [
481
  '.kr', 'korea', 'korean', 'yonhap', 'hankyung', 'chosun',
482
  'donga', 'joins', 'hani', 'koreatimes', 'koreaherald'
@@ -492,12 +623,12 @@ def format_results_from_raw(response_data):
492
  title = result.get("title", "").lower()
493
  channel = result.get("channel", result.get("source", "")).lower()
494
 
495
- # 한국 관련 컨텐츠 필터링
496
  is_korean_content = (
497
  any(domain in url or domain in channel for domain in korean_domains) or
498
- any(keyword in title.lower() for keyword in korean_keywords)
499
  )
500
 
 
501
  if not is_korean_content:
502
  filtered_articles.append({
503
  "index": idx,
@@ -514,12 +645,15 @@ def format_results_from_raw(response_data):
514
  except Exception as e:
515
  return f"결과 처리 중 오류 발생: {str(e)}", []
516
 
517
-
518
  def serphouse_search(query, country):
 
 
 
519
  response_data = search_serphouse(query, country)
520
  return format_results_from_raw(response_data)
521
 
522
 
 
523
  css = """
524
  /* 전역 스타일 */
525
  footer {visibility: hidden;}
@@ -598,7 +732,7 @@ footer {visibility: hidden;}
598
  z-index: 1000;
599
  }
600
 
601
- /* 프로그레스바 */
602
  .progress-bar {
603
  height: 100%;
604
  background: linear-gradient(90deg, #2196F3, #00BCD4);
@@ -695,16 +829,54 @@ footer {visibility: hidden;}
695
  }
696
  """
697
 
 
698
 
699
  with gr.Blocks(theme="Yntec/HaleyCH_Theme_Orange", css=css, title="NewsAI 서비스") as iface:
700
  init_db()
701
 
702
  with gr.Tabs():
703
- # 첫 번째 탭 (DB 검색)만 유지
704
- with gr.Tab("DB 검색"):
705
- gr.Markdown("## 한국 주요 기업 미국 뉴스 DB")
706
- gr.Markdown(" 기업의 미국 뉴스를 검색하여 DB 저장하고 불러올 있습니다.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
707
 
 
 
 
 
 
 
708
  with gr.Column():
709
  for i in range(0, len(KOREAN_COMPANIES), 2):
710
  with gr.Row():
@@ -714,8 +886,8 @@ with gr.Blocks(theme="Yntec/HaleyCH_Theme_Orange", css=css, title="NewsAI 서비
714
  with gr.Group():
715
  gr.Markdown(f"### {company}")
716
  with gr.Row():
717
- search_btn = gr.Button(f"검색", variant="primary")
718
- load_btn = gr.Button(f"출력", variant="secondary")
719
  result_display = gr.Markdown()
720
 
721
  search_btn.click(
@@ -734,8 +906,8 @@ with gr.Blocks(theme="Yntec/HaleyCH_Theme_Orange", css=css, title="NewsAI 서비
734
  with gr.Group():
735
  gr.Markdown(f"### {company}")
736
  with gr.Row():
737
- search_btn = gr.Button(f"검색", variant="primary")
738
- load_btn = gr.Button(f"출력", variant="secondary")
739
  result_display = gr.Markdown()
740
 
741
  search_btn.click(
@@ -746,23 +918,11 @@ with gr.Blocks(theme="Yntec/HaleyCH_Theme_Orange", css=css, title="NewsAI 서비
746
  fn=lambda c=company: load_company(c),
747
  outputs=result_display
748
  )
749
-
750
- # 전체 검색 통계
751
- with gr.Row():
752
- stats_btn = gr.Button("전체 검색 통계 보기", variant="secondary")
753
- stats_display = gr.Markdown()
754
-
755
- stats_btn.click(
756
- fn=show_stats,
757
- outputs=stats_display
758
- )
759
-
760
 
761
  iface.launch(
762
  server_name="0.0.0.0",
763
  server_port=7860,
764
  share=True,
765
-
766
  ssl_verify=False,
767
  show_error=True
768
  )
 
3
  import json
4
  import os
5
  from datetime import datetime, timedelta
6
+ from concurrent.futures import ThreadPoolExecutor, as_completed
7
  from functools import lru_cache
8
  from requests.adapters import HTTPAdapter
9
  from requests.packages.urllib3.util.retry import Retry
 
16
 
17
  # 한국 기업 리스트
18
  KOREAN_COMPANIES = [
19
+ "NVIDIA",
20
+ "ALPHABET",
21
+ "APPLE",
22
+ "TESLA",
23
+ "AMAZON",
24
+ "MICROSOFT",
25
+ "META",
26
+ "INTEL",
27
  "SAMSUNG",
28
  "HYNIX",
29
+ "BITCOIN",
30
+ "crypto",
31
+ "stock",
32
+ "Economics",
33
+ "Finance",
34
+ "investing"
 
 
 
 
 
 
 
 
35
  ]
36
 
37
  def convert_to_seoul_time(timestamp_str):
38
  try:
 
39
  dt = datetime.strptime(timestamp_str, '%Y-%m-%d %H:%M:%S')
 
 
40
  seoul_tz = pytz.timezone('Asia/Seoul')
 
 
41
  seoul_time = seoul_tz.localize(dt)
 
42
  return seoul_time.strftime('%Y-%m-%d %H:%M:%S KST')
43
  except Exception as e:
44
  print(f"시간 변환 오류: {str(e)}")
45
  return timestamp_str
46
 
 
47
  def analyze_sentiment_batch(articles, client):
48
+ """
49
+ OpenAI API를 통해 뉴스 기사들의 종합 감성 분석을 수행
50
+ """
51
  try:
52
  # 모든 기사의 제목과 내용을 하나의 텍스트로 결합
53
  combined_text = "\n\n".join([
54
  f"제목: {article.get('title', '')}\n내용: {article.get('snippet', '')}"
55
  for article in articles
56
  ])
57
+
58
  prompt = f"""다음 뉴스 모음에 대해 전반적인 감성 분석을 수행하세요:
59
 
60
  뉴스 내용:
 
70
  - [항목2]
71
  4. 종합 평가: [상세 설명]
72
  """
73
+
74
  response = client.chat.completions.create(
75
  model="CohereForAI/c4ai-command-r-plus-08-2024",
76
  messages=[{"role": "user", "content": prompt}],
 
97
  conn.commit()
98
  conn.close()
99
 
 
100
  def save_to_db(keyword, country, results):
101
+ """
102
+ 특정 (keyword, country) 조합에 대한 검색 결과를 DB에 저장
103
+ """
104
  conn = sqlite3.connect("search_results.db")
105
  c = conn.cursor()
 
 
106
  seoul_tz = pytz.timezone('Asia/Seoul')
107
  now = datetime.now(seoul_tz)
 
 
108
  timestamp = now.strftime('%Y-%m-%d %H:%M:%S')
109
 
110
  c.execute("""INSERT INTO searches
111
  (keyword, country, results, timestamp)
112
  VALUES (?, ?, ?, ?)""",
113
  (keyword, country, json.dumps(results), timestamp))
 
114
  conn.commit()
115
  conn.close()
116
 
 
117
  def load_from_db(keyword, country):
118
+ """
119
+ 특정 (keyword, country) 조합에 대한 가장 최근 검색 결과를 DB에서 불러오기
120
+ """
121
  conn = sqlite3.connect("search_results.db")
122
  c = conn.cursor()
123
  c.execute("SELECT results, timestamp FROM searches WHERE keyword=? AND country=? ORDER BY timestamp DESC LIMIT 1",
 
128
  return json.loads(result[0]), convert_to_seoul_time(result[1])
129
  return None, None
130
 
 
131
  def display_results(articles):
132
+ """
133
+ 뉴스 기사 목록을 Markdown 문자열로 변환하여 반환
134
+ """
135
  output = ""
136
  for idx, article in enumerate(articles, 1):
137
  output += f"### {idx}. {article['title']}\n"
 
141
  output += f"요약: {article['snippet']}\n\n"
142
  return output
143
 
 
144
  def search_company(company):
145
+ """
146
+ 단일 기업(또는 키워드)에 대해 미국 뉴스 검색, DB 저장 후 결과 Markdown 반환
147
+ """
148
  error_message, articles = serphouse_search(company, "United States")
149
  if not error_message and articles:
150
  save_to_db(company, "United States", articles)
151
  return display_results(articles)
152
  return f"{company}에 대한 검색 결과가 없습니다."
153
 
 
154
  def load_company(company):
155
+ """
156
+ DB에서 단일 기업(또는 키워드)의 미국 뉴스 검색 결과를 불러와 Markdown 반환
157
+ """
158
  results, timestamp = load_from_db(company, "United States")
159
  if results:
160
  return f"### {company} 검색 결과\n저장 시간: {timestamp}\n\n" + display_results(results)
161
  return f"{company}에 대한 저장된 결과가 없습니다."
162
 
 
163
  def show_stats():
164
+ """
165
+ KOREAN_COMPANIES 목록 내 모든 기업에 대해:
166
+ - 가장 최근 DB 저장 일자
167
+ - 기사 수
168
+ - 감성 분석 결과
169
+ 를 순차(또는 병렬)로 조회하여 보고서 형태로 반환
170
+ """
171
  conn = sqlite3.connect("search_results.db")
172
  c = conn.cursor()
173
 
174
  output = "## 한국 기업 뉴스 분석 리포트\n\n"
175
 
176
+ # 모든 기업에 대해 DB에서 읽어올 (company, timestamp, articles) 목록 수집
177
+ data_list = []
178
  for company in KOREAN_COMPANIES:
179
  c.execute("""
180
  SELECT results, timestamp
 
184
  LIMIT 1
185
  """, (company,))
186
 
187
+ row = c.fetchone()
188
+ if row:
189
+ results_json, timestamp = row
190
  articles = json.loads(results_json)
191
  seoul_time = convert_to_seoul_time(timestamp)
192
+ data_list.append((company, seoul_time, articles))
 
 
 
 
 
 
 
 
 
 
 
193
 
194
  conn.close()
195
+
196
+ # (옵션) 각 기업 감성 분석을 병렬 처리
197
+ def analyze_data(item):
198
+ comp, tstamp, arts = item
199
+ sentiment = ""
200
+ if arts:
201
+ sentiment = analyze_sentiment_batch(arts, client)
202
+ return (comp, tstamp, len(arts), sentiment)
203
+
204
+ # ThreadPoolExecutor로 병렬 감성 분석
205
+ results_list = []
206
+ with ThreadPoolExecutor(max_workers=5) as executor:
207
+ futures = [executor.submit(analyze_data, dl) for dl in data_list]
208
+ for future in as_completed(futures):
209
+ results_list.append(future.result())
210
+
211
+ # 결과 정렬(원하는 순서대로) - 여기서는 기업명 기준 or 그냥 순서 없음
212
+ for comp, tstamp, count, sentiment in results_list:
213
+ output += f"### {comp}\n"
214
+ output += f"- 마지막 업데이트: {tstamp}\n"
215
+ output += f"- 저장된 기사 수: {count}건\n\n"
216
+ if sentiment:
217
+ output += "#### 뉴스 감성 분석\n"
218
+ output += f"{sentiment}\n\n"
219
+ output += "---\n\n"
220
+
221
+ return output
222
+
223
+
224
+ ### (1) 전체 검색: 멀티스레드 적용
225
+ def search_all_companies():
226
+ """
227
+ KOREAN_COMPANIES 리스트 내 모든 기업에 대해,
228
+ 검색을 병렬(쓰레드)로 수행 후 결과를 합쳐 Markdown 형태로 반환
229
+ """
230
+ overall_result = "# [전체 검색 결과]\n\n"
231
+
232
+ def do_search(comp):
233
+ return comp, search_company(comp)
234
+
235
+ with ThreadPoolExecutor(max_workers=5) as executor:
236
+ futures = [executor.submit(do_search, c) for c in KOREAN_COMPANIES]
237
+ for future in as_completed(futures):
238
+ comp, res_text = future.result()
239
+ overall_result += f"## {comp}\n"
240
+ overall_result += res_text + "\n\n"
241
+
242
+ return overall_result
243
+
244
+ def load_all_companies():
245
+ """
246
+ KOREAN_COMPANIES 리스트 내 모든 기업에 대해,
247
+ DB에서 불러온 결과를 순차(또는 병렬)로 합쳐서 Markdown 형태로 반환
248
+ """
249
+ overall_result = "# [전체 출력 결과]\n\n"
250
+
251
+ for comp in KOREAN_COMPANIES:
252
+ overall_result += f"## {comp}\n"
253
+ overall_result += load_company(comp)
254
+ overall_result += "\n"
255
+ return overall_result
256
+
257
+ def full_summary_report():
258
+ """
259
+ (1) 모든 기업 검색 -> (2) DB에서 모든 기업 불러오기 -> (3) 감성 분석 통계
260
+ 순서대로 실행하여, 전체 리포트를 합쳐 반환
261
+ """
262
+ # 1) 전체 검색(병렬)
263
+ search_result_text = search_all_companies()
264
+
265
+ # 2) 전체 출력(순차)
266
+ load_result_text = load_all_companies()
267
+
268
+ # 3) 전체 통계(감성 분석)
269
+ stats_text = show_stats()
270
+
271
+ combined_report = (
272
+ "# 전체 분석 보고 요약\n\n"
273
+ "아래 순서로 실행되었습니다:\n"
274
+ "1. 모든 종목 검색(병렬) → 2. 모든 종목 DB 결과 출력 → 3. 전체 감성 분석 통계\n\n"
275
+ f"{search_result_text}\n\n"
276
+ f"{load_result_text}\n\n"
277
+ "## [전체 감성 분석 통계]\n\n"
278
+ f"{stats_text}"
279
+ )
280
+ return combined_report
281
+
282
+
283
+ ### (2) 사용자 임의 검색 + 국가 선택 기능
284
+ def search_custom(query, country):
285
+ """
286
+ 사용자가 입력한 (query, country)를 대상으로
287
+ - 검색 (API 요청)
288
+ - DB 저장
289
+ - DB 로드 후 감성 분석
290
+ - 최종 결과를 Markdown 형태로 반환
291
+ """
292
+ # 1) 검색
293
+ error_message, articles = serphouse_search(query, country)
294
+ if error_message:
295
+ return f"오류 발생: {error_message}"
296
+ if not articles:
297
+ return "검색 결과가 없습니다."
298
+
299
+ # 2) DB 저장
300
+ save_to_db(query, country, articles)
301
+
302
+ # 3) DB에서 다시 불러오기
303
+ results, timestamp = load_from_db(query, country)
304
+ if not results:
305
+ return f"DB 로드 실패: 저장된 결과가 없습니다."
306
+
307
+ # 4) 감성 분석
308
+ sentiment_analysis = analyze_sentiment_batch(results, client)
309
+
310
+ # 5) 최종 리포트(기사 목록 + 감성 분석)
311
+ output = f"## [사용자 임의 검색 결과]\n\n"
312
+ output += f"**키워드**: {query}\n\n"
313
+ output += f"**국가**: {country}\n\n"
314
+ output += f"**저장 시간**: {timestamp}\n\n"
315
+ output += display_results(results)
316
+
317
+ output += "### 뉴스 감성 분석\n"
318
+ output += f"{sentiment_analysis}\n"
319
  return output
320
 
321
 
322
+ ### (필수) API 인증
323
  ACCESS_TOKEN = os.getenv("HF_TOKEN")
324
  if not ACCESS_TOKEN:
325
  raise ValueError("HF_TOKEN environment variable is not set")
 
332
  API_KEY = os.getenv("SERPHOUSE_API_KEY")
333
 
334
 
335
+ ### 국가별 설정
336
  COUNTRY_LANGUAGES = {
337
  "United States": "en",
338
  "KOREA": "ko",
 
398
  "Latvia": "lv",
399
  "Lithuania": "lt",
400
  "Slovenia": "sl",
401
+ "Luxembourg": "Luxembourg",
402
+ "Malta": "Malta",
403
+ "Cyprus": "Cyprus",
404
+ "Iceland": "Iceland"
405
  }
406
 
407
  COUNTRY_LOCATIONS = {
 
465
  "Slovakia": "Slovakia",
466
  "Bulgaria": "Bulgaria",
467
  "Serbia": "Serbia",
468
+ "Estonia": "et",
469
+ "Latvia": "lv",
470
+ "Lithuania": "lt",
471
+ "Slovenia": "sl",
472
  "Luxembourg": "Luxembourg",
473
  "Malta": "Malta",
474
  "Cyprus": "Cyprus",
 
478
 
479
  @lru_cache(maxsize=100)
480
  def translate_query(query, country):
481
+ """
482
+ Google Translation API(비공식) 사용하여 검색어를 해당 국가 언어로 번역
483
+ """
484
  try:
485
  if is_english(query):
486
  return query
487
+
488
  if country in COUNTRY_LANGUAGES:
489
  if country == "South Korea":
490
  return query
 
491
  target_lang = COUNTRY_LANGUAGES[country]
492
 
493
  url = "https://translate.googleapis.com/translate_a/single"
 
513
  print(f"번역 오류: {str(e)}")
514
  return query
515
 
 
516
  def is_english(text):
517
  return all(ord(char) < 128 for char in text.replace(' ', '').replace('-', '').replace('_', ''))
518
 
 
519
  def search_serphouse(query, country, page=1, num_result=10):
520
+ """
521
+ SerpHouse API에 실시간 검색 요청을 보내어,
522
+ '뉴스' 탭 (sort_by=date)에서 해당 query에 대한 기사 목록을 가져온다.
523
+ """
524
  url = "https://api.serphouse.com/serp/live"
525
 
526
  now = datetime.utcnow()
 
537
  "lang": COUNTRY_LANGUAGES.get(country, "en"),
538
  "device": "desktop",
539
  "serp_type": "news",
540
+ "page": str(page),
541
  "num": "100",
542
  "date_range": date_range,
543
  "sort_by": "date"
 
590
  "translated_query": query
591
  }
592
 
 
593
  def format_results_from_raw(response_data):
594
+ """
595
+ SerpHouse API의 응답 데이터를 가공하여,
596
+ (에러메시지, 기사리스트) 형태로 반환.
597
+ """
598
  if "error" in response_data:
599
  return "Error: " + response_data["error"], []
600
 
 
602
  results = response_data["results"]
603
  translated_query = response_data["translated_query"]
604
 
605
+ # 실제 뉴스 결과
606
  news_results = results.get('results', {}).get('results', {}).get('news', [])
607
  if not news_results:
608
  return "검색 결과가 없습니다.", []
609
+
610
+ # 한국 도메인 및 한국 관련 키워드 포함 기사 제외
611
  korean_domains = [
612
  '.kr', 'korea', 'korean', 'yonhap', 'hankyung', 'chosun',
613
  'donga', 'joins', 'hani', 'koreatimes', 'koreaherald'
 
623
  title = result.get("title", "").lower()
624
  channel = result.get("channel", result.get("source", "")).lower()
625
 
 
626
  is_korean_content = (
627
  any(domain in url or domain in channel for domain in korean_domains) or
628
+ any(keyword in title for keyword in korean_keywords)
629
  )
630
 
631
+ # 한국어 뉴스(또는 한국 도메인) 제외
632
  if not is_korean_content:
633
  filtered_articles.append({
634
  "index": idx,
 
645
  except Exception as e:
646
  return f"결과 처리 중 오류 발생: {str(e)}", []
647
 
 
648
  def serphouse_search(query, country):
649
+ """
650
+ 검색 및 결과 포매팅까지 일괄 처리
651
+ """
652
  response_data = search_serphouse(query, country)
653
  return format_results_from_raw(response_data)
654
 
655
 
656
+ # CSS (UI 커스터마이징)
657
  css = """
658
  /* 전역 스타일 */
659
  footer {visibility: hidden;}
 
732
  z-index: 1000;
733
  }
734
 
735
+ /* 프로그레스bar */
736
  .progress-bar {
737
  height: 100%;
738
  background: linear-gradient(90deg, #2196F3, #00BCD4);
 
829
  }
830
  """
831
 
832
+ import gradio as gr
833
 
834
  with gr.Blocks(theme="Yntec/HaleyCH_Theme_Orange", css=css, title="NewsAI 서비스") as iface:
835
  init_db()
836
 
837
  with gr.Tabs():
838
+ # 첫 번째 탭
839
+ with gr.Tab("Earnbot"):
840
+ gr.Markdown("## EarnBot: 글로벌 빅테크 기업 투자 전망 AI 자동 분석")
841
+ gr.Markdown(" * '전체 분석 보고 요약' 클릭 시 전체 자동 보고 생성.\n * 아래 개별 종목의 '검색(DB 자동 저장)'과 '출력(DB 자동 호출)'도 가능.\n * 추가로, 원하는 임의 키워드 및 국가로 검색/분석할 수도 있습니다.")
842
+
843
+ # (2) 사용자 임의 검색 섹션
844
+ with gr.Group():
845
+ gr.Markdown("### 사용자 임의 검색")
846
+ with gr.Row():
847
+ with gr.Column():
848
+ user_input = gr.Textbox(
849
+ label="검색어 입력",
850
+ placeholder="예) Apple, Samsung 등 자유롭게"
851
+ )
852
+ with gr.Column():
853
+ country_selection = gr.Dropdown(
854
+ choices=list(COUNTRY_LOCATIONS.keys()),
855
+ value="United States",
856
+ label="국가 선택"
857
+ )
858
+ with gr.Column():
859
+ custom_search_btn = gr.Button("실행", variant="primary")
860
+
861
+ custom_search_output = gr.Markdown()
862
+
863
+ custom_search_btn.click(
864
+ fn=search_custom,
865
+ inputs=[user_input, country_selection],
866
+ outputs=custom_search_output
867
+ )
868
+
869
+ # 전체 분석 보고 요약 버튼
870
+ with gr.Row():
871
+ full_report_btn = gr.Button("전체 분석 보고 요약", variant="primary")
872
+ full_report_display = gr.Markdown()
873
 
874
+ full_report_btn.click(
875
+ fn=full_summary_report,
876
+ outputs=full_report_display
877
+ )
878
+
879
+ # 기존 개별 기업 검색/출력 영역
880
  with gr.Column():
881
  for i in range(0, len(KOREAN_COMPANIES), 2):
882
  with gr.Row():
 
886
  with gr.Group():
887
  gr.Markdown(f"### {company}")
888
  with gr.Row():
889
+ search_btn = gr.Button("검색", variant="primary")
890
+ load_btn = gr.Button("출력", variant="secondary")
891
  result_display = gr.Markdown()
892
 
893
  search_btn.click(
 
906
  with gr.Group():
907
  gr.Markdown(f"### {company}")
908
  with gr.Row():
909
+ search_btn = gr.Button("검색", variant="primary")
910
+ load_btn = gr.Button("출력", variant="secondary")
911
  result_display = gr.Markdown()
912
 
913
  search_btn.click(
 
918
  fn=lambda c=company: load_company(c),
919
  outputs=result_display
920
  )
 
 
 
 
 
 
 
 
 
 
 
921
 
922
  iface.launch(
923
  server_name="0.0.0.0",
924
  server_port=7860,
925
  share=True,
 
926
  ssl_verify=False,
927
  show_error=True
928
  )