ginipick commited on
Commit
2cf935e
ยท
verified ยท
1 Parent(s): b0e155b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +335 -244
app.py CHANGED
@@ -26,15 +26,22 @@ KOREAN_COMPANIES = [
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')
@@ -44,19 +51,22 @@ def convert_to_seoul_time(timestamp_str):
44
  print(f"์‹œ๊ฐ„ ๋ณ€ํ™˜ ์˜ค๋ฅ˜: {str(e)}")
45
  return timestamp_str
46
 
 
 
 
47
  def analyze_sentiment_batch(articles, client):
48
  """
49
- OpenAI API๋ฅผ ํ†ตํ•ด ๋‰ด์Šค ๊ธฐ์‚ฌ๋“ค์˜ ์ข…ํ•ฉ ๊ฐ์„ฑ ๋ถ„์„์„ ์ˆ˜ํ–‰
50
- (๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ํ•œ๊ตญ์–ด๋กœ ์ž‘์„ฑํ•˜๋„๋ก ์œ ๋„)
51
  """
52
  try:
53
- # ๋ชจ๋“  ๊ธฐ์‚ฌ์˜ ์ œ๋ชฉ๊ณผ ๋‚ด์šฉ์„ ํ•˜๋‚˜์˜ ํ…์ŠคํŠธ๋กœ ๊ฒฐํ•ฉ
54
  combined_text = "\n\n".join([
55
  f"์ œ๋ชฉ: {article.get('title', '')}\n๋‚ด์šฉ: {article.get('snippet', '')}"
56
  for article in articles
57
  ])
58
 
59
- # ํ•œ๊ตญ์–ด ์ž‘์„ฑ์„ ์œ ๋„ํ•˜๋Š” ๋ฌธ๊ตฌ ์ถ”๊ฐ€
60
  prompt = f"""๋‹ค์Œ ๋‰ด์Šค ๋ชจ์Œ์— ๋Œ€ํ•ด ์ „๋ฐ˜์ ์ธ ๊ฐ์„ฑ ๋ถ„์„์„ ์ˆ˜ํ–‰ํ•˜์„ธ์š”. (ํ•œ๊ตญ์–ด๋กœ ์ž‘์„ฑํ•˜์„ธ์š”)
61
 
62
  ๋‰ด์Šค ๋‚ด์šฉ:
@@ -84,9 +94,14 @@ def analyze_sentiment_batch(articles, client):
84
  except Exception as e:
85
  return f"๊ฐ์„ฑ ๋ถ„์„ ์‹คํŒจ: {str(e)}"
86
 
87
-
88
- # DB ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜
 
89
  def init_db():
 
 
 
 
90
  db_path = pathlib.Path("search_results.db")
91
  conn = sqlite3.connect(db_path)
92
  c = conn.cursor()
@@ -101,16 +116,17 @@ def init_db():
101
 
102
  def save_to_db(keyword, country, results):
103
  """
104
- ํŠน์ • (keyword, country) ์กฐํ•ฉ์— ๋Œ€ํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ DB์— ์ €์žฅ
105
  """
106
  conn = sqlite3.connect("search_results.db")
107
  c = conn.cursor()
 
108
  seoul_tz = pytz.timezone('Asia/Seoul')
109
  now = datetime.now(seoul_tz)
110
  timestamp = now.strftime('%Y-%m-%d %H:%M:%S')
111
 
112
- c.execute("""INSERT INTO searches
113
- (keyword, country, results, timestamp)
114
  VALUES (?, ?, ?, ?)""",
115
  (keyword, country, json.dumps(results), timestamp))
116
  conn.commit()
@@ -118,21 +134,207 @@ def save_to_db(keyword, country, results):
118
 
119
  def load_from_db(keyword, country):
120
  """
121
- ํŠน์ • (keyword, country) ์กฐํ•ฉ์— ๋Œ€ํ•œ ๊ฐ€์žฅ ์ตœ๊ทผ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ DB์—์„œ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
 
 
122
  """
123
  conn = sqlite3.connect("search_results.db")
124
  c = conn.cursor()
125
- c.execute("SELECT results, timestamp FROM searches WHERE keyword=? AND country=? ORDER BY timestamp DESC LIMIT 1",
 
 
 
 
126
  (keyword, country))
127
- result = c.fetchone()
128
  conn.close()
129
- if result:
130
- return json.loads(result[0]), convert_to_seoul_time(result[1])
131
  return None, None
132
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  def display_results(articles):
134
  """
135
- ๋‰ด์Šค ๊ธฐ์‚ฌ ๋ชฉ๋ก์„ Markdown ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋ฐ˜ํ™˜
136
  """
137
  output = ""
138
  for idx, article in enumerate(articles, 1):
@@ -143,88 +345,81 @@ def display_results(articles):
143
  output += f"์š”์•ฝ: {article['snippet']}\n\n"
144
  return output
145
 
 
 
 
146
 
147
- ########################################
148
- # 1) ๊ฒ€์ƒ‰ ์‹œ => ๊ธฐ์‚ฌ + ๋ถ„์„ ๋™์‹œ ์ถœ๋ ฅ, DB ์ €์žฅ
149
- ########################################
150
  def search_company(company):
151
  """
152
- ๋‹จ์ผ ๊ธฐ์—…(๋˜๋Š” ํ‚ค์›Œ๋“œ)์— ๋Œ€ํ•ด ๋ฏธ๊ตญ ๋‰ด์Šค ๊ฒ€์ƒ‰ ํ›„,
153
- ๊ธฐ์‚ฌ ๋ชฉ๋ก + ๊ฐ์„ฑ ๋ถ„์„ ๋ณด๊ณ ๋ฅผ ํ•จ๊ป˜ ์ถœ๋ ฅ
154
- => { "articles": [...], "analysis": ... } ํ˜•ํƒœ๋กœ DB์— ์ €์žฅ
155
  """
156
  error_message, articles = serphouse_search(company, "United States")
157
  if not error_message and articles:
158
  analysis = analyze_sentiment_batch(articles, client)
159
- store_dict = {
160
  "articles": articles,
161
  "analysis": analysis
162
  }
163
- save_to_db(company, "United States", store_dict)
164
-
165
- output = display_results(articles)
166
- output += f"\n\n### ๋ถ„์„ ๋ณด๊ณ \n{analysis}\n"
167
- return output
168
- return f"{company}์— ๋Œ€ํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
169
-
170
- ########################################
171
- # 2) ์ถœ๋ ฅ ์‹œ => DB์— ์ €์žฅ๋œ ๊ธฐ์‚ฌ + ๋ถ„์„ ํ•จ๊ป˜ ์ถœ๋ ฅ
172
- ########################################
173
  def load_company(company):
174
  """
175
- DB์—์„œ (keyword=company, country=United States)์— ํ•ด๋‹นํ•˜๋Š”
176
- ๊ธฐ์‚ฌ ๋ชฉ๋ก + ๋ถ„์„ ๋ณด๊ณ ๋ฅผ ํ•จ๊ป˜ ์ถœ๋ ฅ
177
  """
178
- data, timestamp = load_from_db(company, "United States")
179
- if data:
180
- articles = data.get("articles", [])
181
- analysis = data.get("analysis", "")
182
-
183
- output = f"### {company} ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ\n์ €์žฅ ์‹œ๊ฐ„: {timestamp}\n\n"
184
- output += display_results(articles)
185
- output += f"\n\n### ๋ถ„์„ ๋ณด๊ณ \n{analysis}\n"
186
- return output
187
  return f"{company}์— ๋Œ€ํ•œ ์ €์žฅ๋œ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
188
 
189
-
190
- ########################################
191
- # 3) ๋ฆฌํฌํŠธ: "EarnBOT ๋ถ„์„ ๋ฆฌํฌํŠธ"
192
- ########################################
193
  def show_stats():
194
  """
195
- KOREAN_COMPANIES ๋‚ด ๋ชจ๋“  ๊ธฐ์—…์˜
196
- - ์ตœ์‹  DB ์ €์žฅ ์‹œ๊ฐ
197
- - ๊ธฐ์‚ฌ ์ˆ˜
198
- - ๊ฐ์„ฑ ๋ถ„์„ ๊ฒฐ๊ณผ
199
- ๋“ฑ์„ ๋ณ‘๋ ฌ๋กœ ์กฐํšŒ, ๋ณด๊ณ ์„œ ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜
200
  """
201
  conn = sqlite3.connect("search_results.db")
202
  c = conn.cursor()
203
-
204
  output = "## EarnBOT ๋ถ„์„ ๋ฆฌํฌํŠธ\n\n"
205
-
 
206
  data_list = []
207
- for company in KOREAN_COMPANIES:
208
  c.execute("""
209
- SELECT results, timestamp
210
- FROM searches
211
- WHERE keyword = ?
212
- ORDER BY timestamp DESC
213
  LIMIT 1
214
- """, (company,))
215
-
216
  row = c.fetchone()
217
  if row:
218
- results_json, tstamp = row
219
- data_list.append((company, tstamp, results_json))
220
-
221
  conn.close()
222
-
223
  def analyze_data(item):
224
- comp, tstamp, results_json = item
225
- data = json.loads(results_json)
226
- articles = data.get("articles", [])
227
- analysis = data.get("analysis", "")
228
  count_articles = len(articles)
229
  return (comp, tstamp, count_articles, analysis)
230
 
@@ -233,79 +428,80 @@ def show_stats():
233
  futures = [executor.submit(analyze_data, dl) for dl in data_list]
234
  for future in as_completed(futures):
235
  results_list.append(future.result())
236
-
237
- for comp, tstamp, count, analysis in results_list:
238
- seoul_time = convert_to_seoul_time(tstamp)
239
  output += f"### {comp}\n"
240
- output += f"- ๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ: {seoul_time}\n"
241
- output += f"- ์ €์žฅ๋œ ๊ธฐ์‚ฌ ์ˆ˜: {count}๊ฑด\n\n"
242
  if analysis:
243
  output += "#### ๋‰ด์Šค ๊ฐ์„ฑ ๋ถ„์„\n"
244
  output += f"{analysis}\n\n"
245
  output += "---\n\n"
246
-
247
  return output
248
 
 
 
 
249
  def search_all_companies():
250
  """
251
- ๋ชจ๋“  ๊ธฐ์—… ๊ฒ€์ƒ‰ + ๋ถ„์„ -> DB ์ €์žฅ
252
  """
253
- overall_result = "# [์ „์ฒด ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ]\n\n"
254
-
255
  def do_search(comp):
256
  return comp, search_company(comp)
257
-
258
  with ThreadPoolExecutor(max_workers=5) as executor:
259
  futures = [executor.submit(do_search, c) for c in KOREAN_COMPANIES]
260
  for future in as_completed(futures):
261
- comp, res_text = future.result()
262
- overall_result += f"## {comp}\n"
263
- overall_result += res_text + "\n\n"
264
-
265
- return overall_result
266
 
267
  def load_all_companies():
268
  """
269
- ๋ชจ๋“  ๊ธฐ์—…์— ๋Œ€ํ•œ DB ์ €์žฅ๋œ ๊ธฐ์‚ฌ + ๋ถ„์„ ์ถœ๋ ฅ
270
  """
271
- overall_result = "# [์ „์ฒด ์ถœ๋ ฅ ๊ฒฐ๊ณผ]\n\n"
272
-
273
  for comp in KOREAN_COMPANIES:
274
- overall_result += f"## {comp}\n"
275
- overall_result += load_company(comp)
276
- overall_result += "\n"
277
- return overall_result
278
 
279
  def full_summary_report():
280
  """
281
- 1) ๋ชจ๋“  ๊ธฐ์—… ๋ณ‘๋ ฌ ๊ฒ€์ƒ‰+๋ถ„์„ => DB ์ €์žฅ
282
- 2) DB์—์„œ ๋กœ๋“œ => ๊ธฐ์‚ฌ + ๋ถ„์„ ์ถœ๋ ฅ
283
- 3) EarnBOT ๋ถ„์„ ๋ฆฌํฌํŠธ
284
  """
285
- search_result_text = search_all_companies()
286
- load_result_text = load_all_companies()
287
  stats_text = show_stats()
288
-
289
- combined_report = (
290
  "# ์ „์ฒด ๋ถ„์„ ๋ณด๊ณ  ์š”์•ฝ\n\n"
291
  "์•„๋ž˜ ์ˆœ์„œ๋กœ ์‹คํ–‰๋˜์—ˆ์Šต๋‹ˆ๋‹ค:\n"
292
  "1. ๋ชจ๋“  ์ข…๋ชฉ ๊ฒ€์ƒ‰(๋ณ‘๋ ฌ) + ๋ถ„์„ => 2. ๋ชจ๋“  ์ข…๋ชฉ DB ๊ฒฐ๊ณผ ์ถœ๋ ฅ => 3. ์ „์ฒด ๊ฐ์„ฑ ๋ถ„์„ ํ†ต๊ณ„\n\n"
293
- f"{search_result_text}\n\n"
294
- f"{load_result_text}\n\n"
295
  "## [์ „์ฒด ๊ฐ์„ฑ ๋ถ„์„ ํ†ต๊ณ„]\n\n"
296
  f"{stats_text}"
297
  )
298
- return combined_report
299
 
300
-
301
- ########################################
302
  # ์‚ฌ์šฉ์ž ์ž„์˜ ๊ฒ€์ƒ‰
303
- ########################################
304
  def search_custom(query, country):
305
  """
306
- (query, country)์— ๋Œ€ํ•ด
307
- 1) ๊ฒ€์ƒ‰ + ๋ถ„์„ => DB ์ €์žฅ
308
- 2) DB ๋กœ๋“œ => ๊ธฐ์‚ฌ+๋ถ„์„ ์ถœ๋ ฅ
309
  """
310
  error_message, articles = serphouse_search(query, country)
311
  if error_message:
@@ -314,29 +510,30 @@ def search_custom(query, country):
314
  return "๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
315
 
316
  analysis = analyze_sentiment_batch(articles, client)
317
- save_data = {
318
  "articles": articles,
319
  "analysis": analysis
320
  }
321
- save_to_db(query, country, save_data)
322
-
323
- loaded_data, timestamp = load_from_db(query, country)
324
- if not loaded_data:
325
  return "DB์—์„œ ๋กœ๋“œ ์‹คํŒจ"
326
 
327
- arts = loaded_data.get("articles", [])
328
- analy = loaded_data.get("analysis", "")
329
-
330
  out = f"## [์‚ฌ์šฉ์ž ์ž„์˜ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ]\n\n"
331
  out += f"**ํ‚ค์›Œ๋“œ**: {query}\n\n"
332
  out += f"**๊ตญ๊ฐ€**: {country}\n\n"
333
- out += f"**์ €์žฅ ์‹œ๊ฐ„**: {timestamp}\n\n"
334
  out += display_results(arts)
335
  out += f"### ๋‰ด์Šค ๊ฐ์„ฑ ๋ถ„์„\n{analy}\n"
336
-
337
  return out
338
 
339
-
 
 
340
  ACCESS_TOKEN = os.getenv("HF_TOKEN")
341
  if not ACCESS_TOKEN:
342
  raise ValueError("HF_TOKEN environment variable is not set")
@@ -346,14 +543,14 @@ client = OpenAI(
346
  api_key=ACCESS_TOKEN,
347
  )
348
 
349
- API_KEY = os.getenv("SERPHOUSE_API_KEY")
350
-
351
-
352
  COUNTRY_LANGUAGES = {
353
  "United States": "en",
354
  "KOREA": "ko",
355
  "United Kingdom": "en",
356
- "Taiwan": "zh-TW",
357
  "Canada": "en",
358
  "Australia": "en",
359
  "Germany": "de",
@@ -495,7 +692,7 @@ css = """
495
  /* ์ „์—ญ ์Šคํƒ€์ผ */
496
  footer {visibility: hidden;}
497
 
498
- /* ๋ ˆ์ด์•„์›ƒ ์ปจํ…Œ์ด๋„ˆ */
499
  #status_area {
500
  background: rgba(255, 255, 255, 0.9);
501
  padding: 15px;
@@ -509,7 +706,6 @@ footer {visibility: hidden;}
509
  margin-top: 10px;
510
  }
511
 
512
- /* ํƒญ ์Šคํƒ€์ผ */
513
  .tabs {
514
  border-bottom: 2px solid #ddd !important;
515
  margin-bottom: 20px !important;
@@ -537,7 +733,6 @@ footer {visibility: hidden;}
537
  padding: 10px 0;
538
  }
539
 
540
- /* ๊ธฐ๋ณธ ์ปจํ…Œ์ด๋„ˆ */
541
  .group {
542
  border: 1px solid #eee;
543
  padding: 15px;
@@ -552,121 +747,13 @@ footer {visibility: hidden;}
552
  border: none !important;
553
  }
554
 
555
- /* ์ž…๋ ฅ ํ•„๋“œ */
556
- .textbox {
557
- border: 1px solid #ddd !important;
558
- border-radius: 4px !important;
559
- }
560
-
561
- /* ํ”„๋กœ๊ทธ๋ ˆ์Šค๋ฐ” ์ปจํ…Œ์ด๋„ˆ */
562
- .progress-container {
563
- position: fixed;
564
- top: 0;
565
- left: 0;
566
- width: 100%;
567
- height: 6px;
568
- background: #e0e0e0;
569
- z-index: 1000;
570
- }
571
-
572
- /* ํ”„๋กœ๊ทธ๋ ˆ์Šคbar */
573
- .progress-bar {
574
- height: 100%;
575
- background: linear-gradient(90deg, #2196F3, #00BCD4);
576
- box-shadow: 0 0 10px rgba(33, 150, 243, 0.5);
577
- transition: width 0.3s ease;
578
- animation: progress-glow 1.5s ease-in-out infinite;
579
- }
580
-
581
- /* ํ”„๋กœ๊ทธ๋ ˆ์Šค ํ…์ŠคํŠธ */
582
- .progress-text {
583
- position: fixed;
584
- top: 8px;
585
- left: 50%;
586
- transform: translateX(-50%);
587
- background: #333;
588
- color: white;
589
- padding: 4px 12px;
590
- border-radius: 15px;
591
- font-size: 14px;
592
- z-index: 1001;
593
- box-shadow: 0 2px 5px rgba(0,0,0,0.2);
594
- }
595
-
596
- /* ํ”„๋กœ๊ทธ๋ ˆ์Šค๋ฐ” ์• ๋‹ˆ๋ฉ”์ด์…˜ */
597
- @keyframes progress-glow {
598
- 0% {
599
- box-shadow: 0 0 5px rgba(33, 150, 243, 0.5);
600
- }
601
- 50% {
602
- box-shadow: 0 0 20px rgba(33, 150, 243, 0.8);
603
- }
604
- 100% {
605
- box-shadow: 0 0 5px rgba(33, 150, 243, 0.5);
606
- }
607
- }
608
-
609
- /* ๋ฐ˜์‘ํ˜• ๋””์ž์ธ */
610
- @media (max-width: 768px) {
611
- .group {
612
- padding: 10px;
613
- margin-bottom: 15px;
614
- }
615
-
616
- .progress-text {
617
- font-size: 12px;
618
- padding: 3px 10px;
619
- }
620
- }
621
-
622
- /* ๋กœ๋”ฉ ์ƒํƒœ ํ‘œ์‹œ ๊ฐœ์„  */
623
- .loading {
624
- opacity: 0.7;
625
- pointer-events: none;
626
- transition: opacity 0.3s ease;
627
- }
628
-
629
- /* ๊ฒฐ๊ณผ ์ปจํ…Œ์ด๋„ˆ ์• ๋‹ˆ๋ฉ”์ด์…˜ */
630
- .group {
631
- transition: all 0.3s ease;
632
- opacity: 0;
633
- transform: translateY(20px);
634
- }
635
-
636
- .group.visible {
637
- opacity: 1;
638
- transform: translateY(0);
639
- }
640
-
641
- /* Examples ์Šคํƒ€์ผ๋ง */
642
- .examples-table {
643
- margin-top: 10px !important;
644
- margin-bottom: 20px !important;
645
- }
646
-
647
- .examples-table button {
648
- background-color: #f0f0f0 !important;
649
- border: 1px solid #ddd !important;
650
- border-radius: 4px !important;
651
- padding: 5px 10px !important;
652
- margin: 2px !important;
653
- transition: all 0.3s ease !important;
654
- }
655
-
656
- .examples-table button:hover {
657
- background-color: #e0e0e0 !important;
658
- transform: translateY(-1px) !important;
659
- box-shadow: 0 2px 5px rgba(0,0,0,0.1) !important;
660
- }
661
-
662
- .examples-table .label {
663
- font-weight: bold !important;
664
- color: #444 !important;
665
- margin-bottom: 5px !important;
666
- }
667
  """
668
 
 
 
669
  with gr.Blocks(theme="Yntec/HaleyCH_Theme_Orange", css=css, title="NewsAI ์„œ๋น„์Šค") as iface:
 
670
  init_db()
671
 
672
  with gr.Tabs():
@@ -675,7 +762,7 @@ with gr.Blocks(theme="Yntec/HaleyCH_Theme_Orange", css=css, title="NewsAI ์„œ๋น„
675
  gr.Markdown("## EarnBot: ๊ธ€๋กœ๋ฒŒ ๋น…ํ…Œํฌ ๊ธฐ์—… ๋ฐ ํˆฌ์ž ์ „๋ง AI ์ž๋™ ๋ถ„์„")
676
  gr.Markdown(" * '์ „์ฒด ๋ถ„์„ ๋ณด๊ณ  ์š”์•ฝ' ํด๋ฆญ ์‹œ ์ „์ฒด ์ž๋™ ๋ณด๊ณ  ์ƒ์„ฑ.\n * ์•„๋ž˜ ๊ฐœ๋ณ„ ์ข…๋ชฉ์˜ '๊ฒ€์ƒ‰(DB ์ž๋™ ์ €์žฅ)'๊ณผ '์ถœ๋ ฅ(DB ์ž๋™ ํ˜ธ์ถœ)'๋„ ๊ฐ€๋Šฅ.\n * ์ถ”๊ฐ€๋กœ, ์›ํ•˜๋Š” ์ž„์˜ ํ‚ค์›Œ๋“œ ๋ฐ ๊ตญ๊ฐ€๋กœ ๊ฒ€์ƒ‰/๋ถ„์„ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.")
677
 
678
- # (์‚ฌ์šฉ์ž ์ž„์˜ ๊ฒ€์ƒ‰ ์„น์…˜)
679
  with gr.Group():
680
  gr.Markdown("### ์‚ฌ์šฉ์ž ์ž„์˜ ๊ฒ€์ƒ‰")
681
  with gr.Row():
@@ -695,27 +782,29 @@ with gr.Blocks(theme="Yntec/HaleyCH_Theme_Orange", css=css, title="NewsAI ์„œ๋น„
695
 
696
  custom_search_output = gr.Markdown()
697
 
 
698
  custom_search_btn.click(
699
  fn=search_custom,
700
  inputs=[user_input, country_selection],
701
  outputs=custom_search_output
702
  )
703
 
704
- # ์ „์ฒด ๋ถ„์„ ๋ณด๊ณ  ์š”์•ฝ ๋ฒ„ํŠผ
705
  with gr.Row():
706
  full_report_btn = gr.Button("์ „์ฒด ๋ถ„์„ ๋ณด๊ณ  ์š”์•ฝ", variant="primary")
707
  full_report_display = gr.Markdown()
708
 
 
709
  full_report_btn.click(
710
  fn=full_summary_report,
711
  outputs=full_report_display
712
  )
713
 
714
- # ์ง€์ •๋œ ๋ฆฌ์ŠคํŠธ (KOREAN_COMPANIES) ๊ฐœ๋ณ„ ๊ธฐ์—… ๊ฒ€์ƒ‰/์ถœ๋ ฅ
715
  with gr.Column():
716
  for i in range(0, len(KOREAN_COMPANIES), 2):
717
  with gr.Row():
718
- # ์™ผ์ชฝ ์—ด
719
  with gr.Column():
720
  company = KOREAN_COMPANIES[i]
721
  with gr.Group():
@@ -725,16 +814,18 @@ with gr.Blocks(theme="Yntec/HaleyCH_Theme_Orange", css=css, title="NewsAI ์„œ๋น„
725
  load_btn = gr.Button("์ถœ๋ ฅ", variant="secondary")
726
  result_display = gr.Markdown()
727
 
 
728
  search_btn.click(
729
  fn=lambda c=company: search_company(c),
730
  outputs=result_display
731
  )
 
732
  load_btn.click(
733
  fn=lambda c=company: load_company(c),
734
  outputs=result_display
735
  )
736
 
737
- # ์˜ค๋ฅธ์ชฝ ์—ด
738
  if i + 1 < len(KOREAN_COMPANIES):
739
  with gr.Column():
740
  company = KOREAN_COMPANIES[i + 1]
 
26
  "INTEL",
27
  "SAMSUNG",
28
  "HYNIX",
29
+ "BITCOIN",
30
  "crypto",
31
  "stock",
32
  "Economics",
33
  "Finance",
34
+ "investing"
35
  ]
36
 
37
+ ######################################################################
38
+ # ๊ณตํ†ต ํ•จ์ˆ˜: ์‹œ๊ฐ„ ๋ณ€ํ™˜
39
+ ######################################################################
40
  def convert_to_seoul_time(timestamp_str):
41
+ """
42
+ ์ฃผ์–ด์ง„ 'YYYY-MM-DD HH:MM:SS' ํ˜•ํƒœ์˜ ์‹œ๊ฐ(UTC ๊ธฐ์ค€ ๋“ฑ)์„
43
+ 'YYYY-MM-DD HH:MM:SS KST' ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋ฐ˜ํ™˜.
44
+ """
45
  try:
46
  dt = datetime.strptime(timestamp_str, '%Y-%m-%d %H:%M:%S')
47
  seoul_tz = pytz.timezone('Asia/Seoul')
 
51
  print(f"์‹œ๊ฐ„ ๋ณ€ํ™˜ ์˜ค๋ฅ˜: {str(e)}")
52
  return timestamp_str
53
 
54
+ ######################################################################
55
+ # ๊ณตํ†ต ํ•จ์ˆ˜: ๊ฐ์„ฑ ๋ถ„์„
56
+ ######################################################################
57
  def analyze_sentiment_batch(articles, client):
58
  """
59
+ OpenAI API๋ฅผ ํ†ตํ•ด, ๋‰ด์Šค ๊ธฐ์‚ฌ๋“ค์˜ ์ œ๋ชฉ+๋‚ด์šฉ์„ ์ข…ํ•ฉํ•˜์—ฌ ๊ฐ์„ฑ ๋ถ„์„์„ ์ˆ˜ํ–‰.
60
+ - ๊ฒฐ๊ณผ๋ฅผ ํ•œ๊ตญ์–ด๋กœ ์ž‘์„ฑํ•˜๋„๋ก ํ”„๋กฌํ”„ํŠธ ๋‚ด์— ๋ช…์‹œ.
61
  """
62
  try:
63
+ # ๊ธฐ์‚ฌ๋“ค์˜ title/snippet ๊ฒฐํ•ฉ
64
  combined_text = "\n\n".join([
65
  f"์ œ๋ชฉ: {article.get('title', '')}\n๋‚ด์šฉ: {article.get('snippet', '')}"
66
  for article in articles
67
  ])
68
 
69
+ # ํ•œ๊ตญ์–ด๋กœ ์ž‘์„ฑํ•  ๊ฒƒ์„ ์œ ๋„ํ•˜๋Š” ๋ฌธ๊ตฌ
70
  prompt = f"""๋‹ค์Œ ๋‰ด์Šค ๋ชจ์Œ์— ๋Œ€ํ•ด ์ „๋ฐ˜์ ์ธ ๊ฐ์„ฑ ๋ถ„์„์„ ์ˆ˜ํ–‰ํ•˜์„ธ์š”. (ํ•œ๊ตญ์–ด๋กœ ์ž‘์„ฑํ•˜์„ธ์š”)
71
 
72
  ๋‰ด์Šค ๋‚ด์šฉ:
 
94
  except Exception as e:
95
  return f"๊ฐ์„ฑ ๋ถ„์„ ์‹คํŒจ: {str(e)}"
96
 
97
+ ######################################################################
98
+ # DB ์ดˆ๊ธฐํ™” ๋ฐ ์ž…์ถœ๋ ฅ
99
+ ######################################################################
100
  def init_db():
101
+ """
102
+ SQLite DB ํŒŒ์ผ(search_results.db)์ด ์—†์œผ๋ฉด ์ƒ์„ฑ,
103
+ 'searches' ํ…Œ์ด๋ธ”์ด ์—†์œผ๋ฉด ์ƒ์„ฑ
104
+ """
105
  db_path = pathlib.Path("search_results.db")
106
  conn = sqlite3.connect(db_path)
107
  c = conn.cursor()
 
116
 
117
  def save_to_db(keyword, country, results):
118
  """
119
+ (keyword, country)์— ๋Œ€ํ•œ ๊ฒฐ๊ณผ(JSON)๋ฅผ DB์— insert.
120
  """
121
  conn = sqlite3.connect("search_results.db")
122
  c = conn.cursor()
123
+
124
  seoul_tz = pytz.timezone('Asia/Seoul')
125
  now = datetime.now(seoul_tz)
126
  timestamp = now.strftime('%Y-%m-%d %H:%M:%S')
127
 
128
+ c.execute("""INSERT INTO searches
129
+ (keyword, country, results, timestamp)
130
  VALUES (?, ?, ?, ?)""",
131
  (keyword, country, json.dumps(results), timestamp))
132
  conn.commit()
 
134
 
135
  def load_from_db(keyword, country):
136
  """
137
+ DB์—์„œ (keyword, country)์— ํ•ด๋‹นํ•˜๋Š” ๊ฐ€์žฅ ์ตœ๊ทผ ๊ธฐ๋ก์„ ๋กœ๋“œ
138
+ - ์„ฑ๊ณต์‹œ (json.loads(...)๋œ results, KST ์‹œ๊ฐ„)
139
+ - ์‹คํŒจ์‹œ (None, None)
140
  """
141
  conn = sqlite3.connect("search_results.db")
142
  c = conn.cursor()
143
+ c.execute("""SELECT results, timestamp
144
+ FROM searches
145
+ WHERE keyword=? AND country=?
146
+ ORDER BY timestamp DESC
147
+ LIMIT 1""",
148
  (keyword, country))
149
+ row = c.fetchone()
150
  conn.close()
151
+ if row:
152
+ return json.loads(row[0]), convert_to_seoul_time(row[1])
153
  return None, None
154
 
155
+ ######################################################################
156
+ # SerpHouse API (๊ฒ€์ƒ‰ ํ•จ์ˆ˜๋“ค)
157
+ ######################################################################
158
+ API_KEY = os.getenv("SERPHOUSE_API_KEY")
159
+
160
+ def is_english(text):
161
+ """
162
+ ํ…์ŠคํŠธ๊ฐ€ ์ „๋ถ€ ASCII ๋ฒ”์œ„๋ฉด True, ์•„๋‹ˆ๋ฉด False
163
+ """
164
+ return all(ord(char) < 128 for char in text.replace(' ', '').replace('-', '').replace('_', ''))
165
+
166
+ @lru_cache(maxsize=100)
167
+ def translate_query(query, country):
168
+ """
169
+ query๋ฅผ ํ•ด๋‹น country ์–ธ์–ด๋กœ ๋ฒˆ์—ญ
170
+ """
171
+ try:
172
+ # ์ด๋ฏธ ์˜์–ด๋ฉด ๊ทธ๋ƒฅ ๋ฐ˜ํ™˜
173
+ if is_english(query):
174
+ return query
175
+
176
+ if country in COUNTRY_LANGUAGES:
177
+ target_lang = COUNTRY_LANGUAGES[country]
178
+
179
+ url = "https://translate.googleapis.com/translate_a/single"
180
+ params = {
181
+ "client": "gtx",
182
+ "sl": "auto",
183
+ "tl": target_lang,
184
+ "dt": "t",
185
+ "q": query
186
+ }
187
+
188
+ session = requests.Session()
189
+ retries = Retry(total=3, backoff_factor=0.5)
190
+ session.mount('https://', HTTPAdapter(max_retries=retries))
191
+
192
+ resp = session.get(url, params=params, timeout=(5, 10))
193
+ translated_text = resp.json()[0][0][0]
194
+ return translated_text
195
+
196
+ return query
197
+ except Exception as e:
198
+ print(f"๋ฒˆ์—ญ ์˜ค๋ฅ˜: {str(e)}")
199
+ return query
200
+
201
+ def search_serphouse(query, country, page=1, num_result=10):
202
+ """
203
+ SerpHouse API ์‹ค์‹œ๊ฐ„ ๊ฒ€์ƒ‰ -> 'news' (sort_by=date)
204
+ """
205
+ url = "https://api.serphouse.com/serp/live"
206
+
207
+ now = datetime.utcnow()
208
+ yesterday = now - timedelta(days=1)
209
+ date_range = f"{yesterday.strftime('%Y-%m-%d')},{now.strftime('%Y-%m-%d')}"
210
+
211
+ translated_query = translate_query(query, country)
212
+
213
+ payload = {
214
+ "data": {
215
+ "q": translated_query,
216
+ "domain": "google.com",
217
+ "loc": COUNTRY_LOCATIONS.get(country, "United States"),
218
+ "lang": COUNTRY_LANGUAGES.get(country, "en"),
219
+ "device": "desktop",
220
+ "serp_type": "news",
221
+ "page": str(page),
222
+ "num": "100",
223
+ "date_range": date_range,
224
+ "sort_by": "date"
225
+ }
226
+ }
227
+
228
+ headers = {
229
+ "accept": "application/json",
230
+ "content-type": "application/json",
231
+ "authorization": f"Bearer {API_KEY}"
232
+ }
233
+
234
+ try:
235
+ session = requests.Session()
236
+ retries = Retry(
237
+ total=5,
238
+ backoff_factor=1,
239
+ status_forcelist=[429, 500, 502, 503, 504],
240
+ allowed_methods=["POST"]
241
+ )
242
+ adapter = HTTPAdapter(max_retries=retries)
243
+ session.mount('http://', adapter)
244
+ session.mount('https://', adapter)
245
+
246
+ resp = session.post(url, json=payload, headers=headers, timeout=(30, 30))
247
+ resp.raise_for_status()
248
+
249
+ # ์‘๋‹ต JSON
250
+ return {
251
+ "results": resp.json(),
252
+ "translated_query": translated_query
253
+ }
254
+ except requests.exceptions.Timeout:
255
+ return {
256
+ "error": "๊ฒ€์ƒ‰ ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.",
257
+ "translated_query": query
258
+ }
259
+ except requests.exceptions.RequestException as e:
260
+ return {
261
+ "error": f"๊ฒ€์ƒ‰ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}",
262
+ "translated_query": query
263
+ }
264
+ except Exception as e:
265
+ return {
266
+ "error": f"์˜ˆ๊ธฐ์น˜ ์•Š์€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}",
267
+ "translated_query": query
268
+ }
269
+
270
+ def format_results_from_raw(response_data):
271
+ """
272
+ SerpHouse API ์‘๋‹ต์„ (error_message, articles_list) ํ˜•ํƒœ๋กœ ๊ฐ€๊ณต
273
+ - ํ•œ๊ตญ ๋„๋ฉ”์ธ(kr, korea, etc) ์ œ์™ธ
274
+ - empty์‹œ "๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
275
+ """
276
+ if "error" in response_data:
277
+ return "Error: " + response_data["error"], []
278
+
279
+ try:
280
+ results = response_data["results"]
281
+ translated_query = response_data["translated_query"]
282
+
283
+ # ์‹ค์ œ ๋‰ด์Šค ํƒญ ๊ฒฐ๊ณผ
284
+ news_results = results.get('results', {}).get('results', {}).get('news', [])
285
+ if not news_results:
286
+ return "๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.", []
287
+
288
+ # ํ•œ๊ตญ์–ด ์ œ์™ธ
289
+ korean_domains = [
290
+ '.kr', 'korea', 'korean', 'yonhap', 'hankyung', 'chosun',
291
+ 'donga', 'joins', 'hani', 'koreatimes', 'koreaherald'
292
+ ]
293
+ korean_keywords = [
294
+ 'korea', 'korean', 'seoul', 'busan', 'incheon', 'daegu',
295
+ 'gwangju', 'daejeon', 'ulsan', 'sejong'
296
+ ]
297
+
298
+ filtered_articles = []
299
+ for idx, result in enumerate(news_results, 1):
300
+ url = result.get("url", result.get("link", "")).lower()
301
+ title = result.get("title", "").lower()
302
+ channel = result.get("channel", result.get("source", "")).lower()
303
+
304
+ is_korean_content = (
305
+ any(domain in url or domain in channel for domain in korean_domains)
306
+ or any(keyword in title for keyword in korean_keywords)
307
+ )
308
+ if not is_korean_content:
309
+ filtered_articles.append({
310
+ "index": idx,
311
+ "title": result.get("title", "์ œ๋ชฉ ์—†์Œ"),
312
+ "link": url,
313
+ "snippet": result.get("snippet", "๋‚ด์šฉ ์—†์Œ"),
314
+ "channel": result.get("channel", result.get("source", "์•Œ ์ˆ˜ ์—†์Œ")),
315
+ "time": result.get("time", result.get("date", "์•Œ ๏ฟฝ๏ฟฝ๏ฟฝ ์—†๋Š” ์‹œ๊ฐ„")),
316
+ "image_url": result.get("img", result.get("thumbnail", "")),
317
+ "translated_query": translated_query
318
+ })
319
+
320
+ return "", filtered_articles
321
+ except Exception as e:
322
+ return f"๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}", []
323
+
324
+ def serphouse_search(query, country):
325
+ """
326
+ ์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ (search_serphouse -> format_results_from_raw)
327
+ ๋ฐ˜ํ™˜: (error_message, articles_list)
328
+ """
329
+ response_data = search_serphouse(query, country)
330
+ return format_results_from_raw(response_data)
331
+
332
+ ######################################################################
333
+ # ๋‰ด์Šค ๊ธฐ์‚ฌ ๋ชฉ๋ก -> Markdown
334
+ ######################################################################
335
  def display_results(articles):
336
  """
337
+ ๊ธฐ์‚ฌ ๋ชฉ๋ก์„ Markdown ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜
338
  """
339
  output = ""
340
  for idx, article in enumerate(articles, 1):
 
345
  output += f"์š”์•ฝ: {article['snippet']}\n\n"
346
  return output
347
 
348
+ ######################################################################
349
+ # ํ•œ๊ตญ ๊ธฐ์—… ๋ชฉ๋ก (์ด๋ฏธ ์„ ์–ธ๋จ)
350
+ ######################################################################
351
 
352
+ ######################################################################
353
+ # ๊ฒ€์ƒ‰/์ถœ๋ ฅ ํ•จ์ˆ˜
354
+ ######################################################################
355
  def search_company(company):
356
  """
357
+ ๋ฏธ๊ตญ(United States) ๋‰ด์Šค ๊ฒ€์ƒ‰ -> ๊ฐ์„ฑ๋ถ„์„(ํ•œ๊ตญ์–ด) -> DB์ €์žฅ -> Markdown ๋ฐ˜ํ™˜
 
 
358
  """
359
  error_message, articles = serphouse_search(company, "United States")
360
  if not error_message and articles:
361
  analysis = analyze_sentiment_batch(articles, client)
362
+ data_to_store = {
363
  "articles": articles,
364
  "analysis": analysis
365
  }
366
+ save_to_db(company, "United States", data_to_store)
367
+
368
+ out = display_results(articles)
369
+ out += f"\n\n### ๋ถ„์„ ๋ณด๊ณ \n{analysis}\n"
370
+ return out
371
+ else:
372
+ if error_message:
373
+ return error_message
374
+ return f"{company}์— ๋Œ€ํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
375
+
376
  def load_company(company):
377
  """
378
+ DB์—์„œ (company, United States) ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๋ถˆ๋Ÿฌ์™€ ๊ธฐ์‚ฌ+๋ถ„์„ ์ถœ๋ ฅ
 
379
  """
380
+ loaded, ts = load_from_db(company, "United States")
381
+ if loaded:
382
+ articles = loaded.get("articles", [])
383
+ analysis = loaded.get("analysis", "")
384
+ out = f"### {company} ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ\n์ €์žฅ ์‹œ๊ฐ„: {ts}\n\n"
385
+ out += display_results(articles)
386
+ out += f"\n\n### ๋ถ„์„ ๋ณด๊ณ \n{analysis}\n"
387
+ return out
 
388
  return f"{company}์— ๋Œ€ํ•œ ์ €์žฅ๋œ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
389
 
390
+ ######################################################################
391
+ # ํ†ต๊ณ„ (EarnBOT ๋ถ„์„ ๋ฆฌํฌํŠธ)
392
+ ######################################################################
 
393
  def show_stats():
394
  """
395
+ KOREAN_COMPANIES ๋‚ด ๋ชจ๋“  ๊ธฐ์—…์˜ ๊ฐ€์žฅ ์ตœ๊ทผ DB ๊ฒฐ๊ณผ -> ๊ธฐ์‚ฌ์ˆ˜, ๋ถ„์„, timestamp
 
 
 
 
396
  """
397
  conn = sqlite3.connect("search_results.db")
398
  c = conn.cursor()
399
+
400
  output = "## EarnBOT ๋ถ„์„ ๋ฆฌํฌํŠธ\n\n"
401
+
402
+ # DB์—์„œ ๊ฐ ๊ธฐ์—…์˜ ์ตœ์‹  ์ €์žฅ ๊ธฐ๋ก
403
  data_list = []
404
+ for comp in KOREAN_COMPANIES:
405
  c.execute("""
406
+ SELECT results, timestamp
407
+ FROM searches
408
+ WHERE keyword=?
409
+ ORDER BY timestamp DESC
410
  LIMIT 1
411
+ """, (comp,))
 
412
  row = c.fetchone()
413
  if row:
414
+ results_json, ts = row
415
+ data_list.append((comp, ts, results_json))
 
416
  conn.close()
417
+
418
  def analyze_data(item):
419
+ comp, tstamp, json_str = item
420
+ data_obj = json.loads(json_str)
421
+ articles = data_obj.get("articles", [])
422
+ analysis = data_obj.get("analysis", "")
423
  count_articles = len(articles)
424
  return (comp, tstamp, count_articles, analysis)
425
 
 
428
  futures = [executor.submit(analyze_data, dl) for dl in data_list]
429
  for future in as_completed(futures):
430
  results_list.append(future.result())
431
+
432
+ for comp, tstamp, count_articles, analysis in results_list:
433
+ kst_time = convert_to_seoul_time(tstamp)
434
  output += f"### {comp}\n"
435
+ output += f"- ๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ: {kst_time}\n"
436
+ output += f"- ์ €์žฅ๋œ ๊ธฐ์‚ฌ ์ˆ˜: {count_articles}๊ฑด\n\n"
437
  if analysis:
438
  output += "#### ๋‰ด์Šค ๊ฐ์„ฑ ๋ถ„์„\n"
439
  output += f"{analysis}\n\n"
440
  output += "---\n\n"
441
+
442
  return output
443
 
444
+ ######################################################################
445
+ # ์ „์ฒด ๊ฒ€์ƒ‰+์ถœ๋ ฅ+๋ถ„์„ ์ข…ํ•ฉ
446
+ ######################################################################
447
  def search_all_companies():
448
  """
449
+ ๋ชจ๋“  ๊ธฐ์—… ๋ณ‘๋ ฌ ๊ฒ€์ƒ‰+๋ถ„์„ -> DB ์ €์žฅ -> Markdown ์ถœ๋ ฅ
450
  """
451
+ overall = "# [์ „์ฒด ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ]\n\n"
452
+
453
  def do_search(comp):
454
  return comp, search_company(comp)
455
+
456
  with ThreadPoolExecutor(max_workers=5) as executor:
457
  futures = [executor.submit(do_search, c) for c in KOREAN_COMPANIES]
458
  for future in as_completed(futures):
459
+ comp, res = future.result()
460
+ overall += f"## {comp}\n"
461
+ overall += res + "\n\n"
462
+
463
+ return overall
464
 
465
  def load_all_companies():
466
  """
467
+ ๋ชจ๋“  ๊ธฐ์—… DB ๋กœ๋“œ -> ๊ธฐ์‚ฌ+๋ถ„์„
468
  """
469
+ overall = "# [์ „์ฒด ์ถœ๋ ฅ ๊ฒฐ๊ณผ]\n\n"
 
470
  for comp in KOREAN_COMPANIES:
471
+ overall += f"## {comp}\n"
472
+ overall += load_company(comp)
473
+ overall += "\n"
474
+ return overall
475
 
476
  def full_summary_report():
477
  """
478
+ 1) ์ „์ฒด ๊ฒ€์ƒ‰+๋ถ„์„ => DB
479
+ 2) ์ „์ฒด DB ๋กœ๋“œ
480
+ 3) ๊ฐ์„ฑ ๋ถ„์„ ํ†ต๊ณ„
481
  """
482
+ search_text = search_all_companies()
483
+ load_text = load_all_companies()
484
  stats_text = show_stats()
485
+
486
+ combined = (
487
  "# ์ „์ฒด ๋ถ„์„ ๋ณด๊ณ  ์š”์•ฝ\n\n"
488
  "์•„๋ž˜ ์ˆœ์„œ๋กœ ์‹คํ–‰๋˜์—ˆ์Šต๋‹ˆ๋‹ค:\n"
489
  "1. ๋ชจ๋“  ์ข…๋ชฉ ๊ฒ€์ƒ‰(๋ณ‘๋ ฌ) + ๋ถ„์„ => 2. ๋ชจ๋“  ์ข…๋ชฉ DB ๊ฒฐ๊ณผ ์ถœ๋ ฅ => 3. ์ „์ฒด ๊ฐ์„ฑ ๋ถ„์„ ํ†ต๊ณ„\n\n"
490
+ f"{search_text}\n\n"
491
+ f"{load_text}\n\n"
492
  "## [์ „์ฒด ๊ฐ์„ฑ ๋ถ„์„ ํ†ต๊ณ„]\n\n"
493
  f"{stats_text}"
494
  )
495
+ return combined
496
 
497
+ ######################################################################
 
498
  # ์‚ฌ์šฉ์ž ์ž„์˜ ๊ฒ€์ƒ‰
499
+ ######################################################################
500
  def search_custom(query, country):
501
  """
502
+ 1) query & country -> ๊ฒ€์ƒ‰+๋ถ„์„
503
+ 2) DB ์ €์žฅ
504
+ 3) DB ์žฌ๋กœ๋“œ -> ๊ธฐ์‚ฌ+๋ถ„์„ ์ถœ๋ ฅ
505
  """
506
  error_message, articles = serphouse_search(query, country)
507
  if error_message:
 
510
  return "๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
511
 
512
  analysis = analyze_sentiment_batch(articles, client)
513
+ store = {
514
  "articles": articles,
515
  "analysis": analysis
516
  }
517
+ save_to_db(query, country, store)
518
+
519
+ loaded, ts = load_from_db(query, country)
520
+ if not loaded:
521
  return "DB์—์„œ ๋กœ๋“œ ์‹คํŒจ"
522
 
523
+ arts = loaded.get("articles", [])
524
+ analy = loaded.get("analysis", "")
525
+
526
  out = f"## [์‚ฌ์šฉ์ž ์ž„์˜ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ]\n\n"
527
  out += f"**ํ‚ค์›Œ๋“œ**: {query}\n\n"
528
  out += f"**๊ตญ๊ฐ€**: {country}\n\n"
529
+ out += f"**์ €์žฅ ์‹œ๊ฐ„**: {ts}\n\n"
530
  out += display_results(arts)
531
  out += f"### ๋‰ด์Šค ๊ฐ์„ฑ ๋ถ„์„\n{analy}\n"
 
532
  return out
533
 
534
+ ######################################################################
535
+ # Hugging Face openai Client
536
+ ######################################################################
537
  ACCESS_TOKEN = os.getenv("HF_TOKEN")
538
  if not ACCESS_TOKEN:
539
  raise ValueError("HF_TOKEN environment variable is not set")
 
543
  api_key=ACCESS_TOKEN,
544
  )
545
 
546
+ ######################################################################
547
+ # ๊ตญ๊ฐ€ ์„ค์ •
548
+ ######################################################################
549
  COUNTRY_LANGUAGES = {
550
  "United States": "en",
551
  "KOREA": "ko",
552
  "United Kingdom": "en",
553
+ "Taiwan": "zh-TW",
554
  "Canada": "en",
555
  "Australia": "en",
556
  "Germany": "de",
 
692
  /* ์ „์—ญ ์Šคํƒ€์ผ */
693
  footer {visibility: hidden;}
694
 
695
+ /* ๋ ˆ์ด์•„์›ƒ ์Šคํƒ€์ผ, ํƒญ ์Šคํƒ€์ผ, ๋“ฑ๋“ฑ */
696
  #status_area {
697
  background: rgba(255, 255, 255, 0.9);
698
  padding: 15px;
 
706
  margin-top: 10px;
707
  }
708
 
 
709
  .tabs {
710
  border-bottom: 2px solid #ddd !important;
711
  margin-bottom: 20px !important;
 
733
  padding: 10px 0;
734
  }
735
 
 
736
  .group {
737
  border: 1px solid #eee;
738
  padding: 15px;
 
747
  border: none !important;
748
  }
749
 
750
+ /* ๊ธฐํƒ€ ... */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
751
  """
752
 
753
+ import gradio as gr
754
+
755
  with gr.Blocks(theme="Yntec/HaleyCH_Theme_Orange", css=css, title="NewsAI ์„œ๋น„์Šค") as iface:
756
+ # DB ์ดˆ๊ธฐํ™”
757
  init_db()
758
 
759
  with gr.Tabs():
 
762
  gr.Markdown("## EarnBot: ๊ธ€๋กœ๋ฒŒ ๋น…ํ…Œํฌ ๊ธฐ์—… ๋ฐ ํˆฌ์ž ์ „๋ง AI ์ž๋™ ๋ถ„์„")
763
  gr.Markdown(" * '์ „์ฒด ๋ถ„์„ ๋ณด๊ณ  ์š”์•ฝ' ํด๋ฆญ ์‹œ ์ „์ฒด ์ž๋™ ๋ณด๊ณ  ์ƒ์„ฑ.\n * ์•„๋ž˜ ๊ฐœ๋ณ„ ์ข…๋ชฉ์˜ '๊ฒ€์ƒ‰(DB ์ž๋™ ์ €์žฅ)'๊ณผ '์ถœ๋ ฅ(DB ์ž๋™ ํ˜ธ์ถœ)'๋„ ๊ฐ€๋Šฅ.\n * ์ถ”๊ฐ€๋กœ, ์›ํ•˜๋Š” ์ž„์˜ ํ‚ค์›Œ๋“œ ๋ฐ ๊ตญ๊ฐ€๋กœ ๊ฒ€์ƒ‰/๋ถ„์„ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.")
764
 
765
+ # ์‚ฌ์šฉ์ž ์ž„์˜ ๊ฒ€์ƒ‰ ์„น์…˜
766
  with gr.Group():
767
  gr.Markdown("### ์‚ฌ์šฉ์ž ์ž„์˜ ๊ฒ€์ƒ‰")
768
  with gr.Row():
 
782
 
783
  custom_search_output = gr.Markdown()
784
 
785
+ # ์ž„์˜ ๊ฒ€์ƒ‰ ๋ฒ„ํŠผ ํด๋ฆญ
786
  custom_search_btn.click(
787
  fn=search_custom,
788
  inputs=[user_input, country_selection],
789
  outputs=custom_search_output
790
  )
791
 
792
+ # ์ „์ฒด ๋ถ„์„ ๋ณด๊ณ  ๋ฒ„ํŠผ
793
  with gr.Row():
794
  full_report_btn = gr.Button("์ „์ฒด ๋ถ„์„ ๋ณด๊ณ  ์š”์•ฝ", variant="primary")
795
  full_report_display = gr.Markdown()
796
 
797
+ # ์ „์ฒด ๋ณด๊ณ  -> full_summary_report
798
  full_report_btn.click(
799
  fn=full_summary_report,
800
  outputs=full_report_display
801
  )
802
 
803
+ # ์ง€์ •๋œ ๊ธฐ์—… ๋ชฉ๋ก: ๊ฒ€์ƒ‰ / ์ถœ๋ ฅ
804
  with gr.Column():
805
  for i in range(0, len(KOREAN_COMPANIES), 2):
806
  with gr.Row():
807
+ # ์™ผ์ชฝ
808
  with gr.Column():
809
  company = KOREAN_COMPANIES[i]
810
  with gr.Group():
 
814
  load_btn = gr.Button("์ถœ๋ ฅ", variant="secondary")
815
  result_display = gr.Markdown()
816
 
817
+ # ๊ฒ€์ƒ‰
818
  search_btn.click(
819
  fn=lambda c=company: search_company(c),
820
  outputs=result_display
821
  )
822
+ # ์ถœ๋ ฅ
823
  load_btn.click(
824
  fn=lambda c=company: load_company(c),
825
  outputs=result_display
826
  )
827
 
828
+ # ์˜ค๋ฅธ์ชฝ
829
  if i + 1 < len(KOREAN_COMPANIES):
830
  with gr.Column():
831
  company = KOREAN_COMPANIES[i + 1]