ginipick commited on
Commit
bca6d69
ยท
verified ยท
1 Parent(s): c4a9537

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +373 -291
app.py CHANGED
@@ -34,6 +34,11 @@ KOREAN_COMPANIES = [
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')
@@ -49,7 +54,6 @@ def analyze_sentiment_batch(articles, client):
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
@@ -83,7 +87,10 @@ def analyze_sentiment_batch(articles, client):
83
  return f"๊ฐ์„ฑ ๋ถ„์„ ์‹คํŒจ: {str(e)}"
84
 
85
 
86
- # DB ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜
 
 
 
87
  def init_db():
88
  db_path = pathlib.Path("search_results.db")
89
  conn = sqlite3.connect(db_path)
@@ -98,9 +105,6 @@ def init_db():
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')
@@ -115,9 +119,6 @@ def save_to_db(keyword, country, results):
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,10 +129,31 @@ def load_from_db(keyword, country):
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"
@@ -142,44 +164,29 @@ def display_results(articles):
142
  return output
143
 
144
 
145
- ########################################
146
- # 1) ๊ฒ€์ƒ‰ ์‹œ => ๊ธฐ์‚ฌ + ๋ถ„์„ ๋™์‹œ ์ถœ๋ ฅ, DB ์ €์žฅ
147
- ########################################
148
  def search_company(company):
149
- """
150
- ๋‹จ์ผ ๊ธฐ์—…(๋˜๋Š” ํ‚ค์›Œ๋“œ)์— ๋Œ€ํ•ด ๋ฏธ๊ตญ ๋‰ด์Šค ๊ฒ€์ƒ‰ ํ›„,
151
- 1) ๊ธฐ์‚ฌ ๋ชฉ๋ก + 2) ๊ฐ์„ฑ ๋ถ„์„ ๋ณด๊ณ ๋ฅผ ํ•จ๊ป˜ ์ถœ๋ ฅ
152
- => { "articles": [...], "analysis": ... } ํ˜•ํƒœ๋กœ DB์— ์ €์žฅ
153
- """
154
  error_message, articles = serphouse_search(company, "United States")
155
  if not error_message and articles:
156
- # ๊ฐ์„ฑ ๋ถ„์„
157
  analysis = analyze_sentiment_batch(articles, client)
158
-
159
- # DB ์ €์žฅ์šฉ ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ
160
  store_dict = {
161
  "articles": articles,
162
  "analysis": analysis
163
  }
164
  save_to_db(company, "United States", store_dict)
165
-
166
- # ํ™”๋ฉด ์ถœ๋ ฅ์šฉ
167
  output = display_results(articles)
168
  output += f"\n\n### ๋ถ„์„ ๋ณด๊ณ \n{analysis}\n"
169
  return output
170
  return f"{company}์— ๋Œ€ํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
171
 
172
- ########################################
173
- # 2) ์ถœ๋ ฅ ์‹œ => DB์— ์ €์žฅ๋œ ๊ธฐ์‚ฌ + ๋ถ„์„ ํ•จ๊ป˜ ์ถœ๋ ฅ
174
- ########################################
175
  def load_company(company):
176
- """
177
- DB์—์„œ ๋‹จ์ผ ๊ธฐ์—…(๋˜๋Š” ํ‚ค์›Œ๋“œ)์˜ ๋ฏธ๊ตญ ๋‰ด์Šค ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๋ถˆ๋Ÿฌ์™€
178
- ๊ธฐ์‚ฌ ๋ชฉ๋ก + ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ํ•จ๊ป˜ ์ถœ๋ ฅ
179
- """
180
  data, timestamp = load_from_db(company, "United States")
181
  if data:
182
- # data๋Š” { "articles": [...], "analysis": "..."} ํ˜•ํƒœ
183
  articles = data.get("articles", [])
184
  analysis = data.get("analysis", "")
185
 
@@ -190,25 +197,18 @@ def load_company(company):
190
  return f"{company}์— ๋Œ€ํ•œ ์ €์žฅ๋œ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
191
 
192
 
193
- ########################################
194
- # 3) ๊ธฐ์กด show_stats()์—์„œ ๋ฆฌํฌํŠธ ์ œ๋ชฉ ๋ณ€๊ฒฝ
195
- ########################################
196
  def show_stats():
197
  """
198
- KOREAN_COMPANIES ๋ชฉ๋ก ๋‚ด ๋ชจ๋“  ๊ธฐ์—…์— ๋Œ€ํ•ด:
199
- - ๊ฐ€์žฅ ์ตœ๊ทผ DB ์ €์žฅ ์ผ์ž
200
- - ๊ธฐ์‚ฌ ์ˆ˜
201
- - ๊ฐ์„ฑ ๋ถ„์„ ๊ฒฐ๊ณผ
202
- ๋ฅผ ๋ณ‘๋ ฌ์ฒ˜๋ฆฌ๋กœ ์กฐํšŒํ•˜์—ฌ ๋ณด๊ณ ์„œ ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜
203
-
204
- (๋ฌธ๊ตฌ ๋ณ€๊ฒฝ) "ํ•œ๊ตญ ๊ธฐ์—… ๋‰ด์Šค ๋ถ„์„ ๋ฆฌํฌํŠธ" -> "EarnBOT ๋ถ„์„ ๋ฆฌํฌํŠธ"
205
  """
206
  conn = sqlite3.connect("search_results.db")
207
  c = conn.cursor()
208
 
209
- output = "## EarnBOT ๋ถ„์„ ๋ฆฌํฌํŠธ\n\n" # ์—ฌ๊ธฐ์„œ ๋ฌธ๊ตฌ ๋ณ€๊ฒฝ
210
 
211
- # ๋ชจ๋“  ๊ธฐ์—…์— ๋Œ€ํ•ด DB์—์„œ ์ฝ์–ด์˜ฌ (company, timestamp, articles) ๋ชฉ๋ก ์ˆ˜์ง‘
212
  data_list = []
213
  for company in KOREAN_COMPANIES:
214
  c.execute("""
@@ -221,23 +221,17 @@ def show_stats():
221
 
222
  row = c.fetchone()
223
  if row:
224
- results_json, timestamp = row
225
- data_list.append((company, timestamp, results_json))
226
 
227
  conn.close()
228
 
229
- # ๊ฐ์„ฑ ๋ถ„์„ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜
230
  def analyze_data(item):
231
  comp, tstamp, results_json = item
232
  data = json.loads(results_json)
233
  articles = data.get("articles", [])
234
  analysis = data.get("analysis", "")
235
-
236
  count_articles = len(articles)
237
- # ์—ฌ๊ธฐ์„œ๋Š” ์ด๋ฏธ DB์— "analysis"๊ฐ€ ๋“ค์–ด ์žˆ์œผ๋ฏ€๋กœ,
238
- # ๊ตณ์ด ์žฌ๋ถ„์„ํ•  ํ•„์š”๊ฐ€ ์—†์œผ๋ฉด ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ
239
- # (ํ•„์š” ์‹œ ์žฌ๋ถ„์„ ๊ฐ€๋Šฅ)
240
-
241
  return (comp, tstamp, count_articles, analysis)
242
 
243
  results_list = []
@@ -246,7 +240,6 @@ def show_stats():
246
  for future in as_completed(futures):
247
  results_list.append(future.result())
248
 
249
- # ๊ฒฐ๊ณผ ์ถœ๋ ฅ
250
  for comp, tstamp, count, analysis in results_list:
251
  seoul_time = convert_to_seoul_time(tstamp)
252
  output += f"### {comp}\n"
@@ -260,11 +253,10 @@ def show_stats():
260
  return output
261
 
262
 
 
 
 
263
  def search_all_companies():
264
- """
265
- KOREAN_COMPANIES ๋ฆฌ์ŠคํŠธ ๋‚ด ๋ชจ๋“  ๊ธฐ์—… ๊ฒ€์ƒ‰ (๋ฉ€ํ‹ฐ์Šค๋ ˆ๋”ฉ) =>
266
- => ๋ถ„์„ + DB ์ €์žฅ => ๊ฒฐ๊ณผ Markdown ๋ฐ˜ํ™˜
267
- """
268
  overall_result = "# [์ „์ฒด ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ]\n\n"
269
 
270
  def do_search(comp):
@@ -280,12 +272,7 @@ def search_all_companies():
280
  return overall_result
281
 
282
  def load_all_companies():
283
- """
284
- KOREAN_COMPANIES ๋ฆฌ์ŠคํŠธ ๋‚ด ๋ชจ๋“  ๊ธฐ์—… DB ๋ถˆ๋Ÿฌ์˜ค๊ธฐ =>
285
- ๊ธฐ์‚ฌ ๋ชฉ๋ก + ๋ถ„์„ ๋ณด๊ณ  => ๊ฒฐ๊ณผ Markdown
286
- """
287
  overall_result = "# [์ „์ฒด ์ถœ๋ ฅ ๊ฒฐ๊ณผ]\n\n"
288
-
289
  for comp in KOREAN_COMPANIES:
290
  overall_result += f"## {comp}\n"
291
  overall_result += load_company(comp)
@@ -293,17 +280,8 @@ def load_all_companies():
293
  return overall_result
294
 
295
  def full_summary_report():
296
- """
297
- (1) ๋ชจ๋“  ๊ธฐ์—… ๊ฒ€์ƒ‰(๋ณ‘๋ ฌ) -> (2) DB์—์„œ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ -> (3) ๊ฐ์„ฑ ๋ถ„์„ ํ†ต๊ณ„
298
- ์ˆœ์„œ๋Œ€๋กœ ์‹คํ–‰ํ•˜์—ฌ, ์ „์ฒด ๋ฆฌํฌํŠธ๋ฅผ ํ•ฉ์ณ ๋ฐ˜ํ™˜
299
- """
300
- # 1) ์ „์ฒด ๊ฒ€์ƒ‰(๋ณ‘๋ ฌ) => ๊ธฐ์‚ฌ + ๋ถ„์„ DB ์ €์žฅ
301
  search_result_text = search_all_companies()
302
-
303
- # 2) ์ „์ฒด ์ถœ๋ ฅ => DB์— ์ €์žฅ๋œ ๊ธฐ์‚ฌ + ๋ถ„์„ ๊ฒฐ๊ณผ
304
  load_result_text = load_all_companies()
305
-
306
- # 3) ์ „์ฒด ํ†ต๊ณ„(๊ฐ์„ฑ ๋ถ„์„) - ๋ฆฌํฌํŠธ ์ œ๋ชฉ ๋ณ€๊ฒฝ๋จ(EarnBOT ๋ถ„์„ ๋ฆฌํฌํŠธ)
307
  stats_text = show_stats()
308
 
309
  combined_report = (
@@ -318,9 +296,9 @@ def full_summary_report():
318
  return combined_report
319
 
320
 
321
- ########################################
322
- # ์‚ฌ์šฉ์ž ์ž„์˜ ๊ฒ€์ƒ‰ (์ถ”๊ฐ€ ๊ธฐ๋Šฅ)
323
- ########################################
324
  def search_custom(query, country):
325
  """
326
  ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ (query, country)์— ๋Œ€ํ•ด
@@ -333,202 +311,103 @@ def search_custom(query, country):
333
  if not articles:
334
  return "๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
335
 
336
- # 1) ๋ถ„์„
337
  analysis = analyze_sentiment_batch(articles, client)
338
-
339
- # 2) DB ์ €์žฅ
340
  save_data = {
341
  "articles": articles,
342
  "analysis": analysis
343
  }
344
  save_to_db(query, country, save_data)
345
 
346
- # 3) DB ์žฌ๋กœ๋“œ
347
  loaded_data, timestamp = load_from_db(query, country)
348
  if not loaded_data:
349
  return "DB์—์„œ ๋กœ๋“œ ์‹คํŒจ"
350
 
351
- # 4) ๊ฒฐ๊ณผ ํ‘œ์‹œ
 
 
352
  out = f"## [์‚ฌ์šฉ์ž ์ž„์˜ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ]\n\n"
353
  out += f"**ํ‚ค์›Œ๋“œ**: {query}\n\n"
354
  out += f"**๊ตญ๊ฐ€**: {country}\n\n"
355
  out += f"**์ €์žฅ ์‹œ๊ฐ„**: {timestamp}\n\n"
356
 
357
- arts = loaded_data.get("articles", [])
358
- analy = loaded_data.get("analysis", "")
359
-
360
  out += display_results(arts)
361
  out += f"### ๋‰ด์Šค ๊ฐ์„ฑ ๋ถ„์„\n{analy}\n"
362
 
363
  return out
364
 
365
 
366
- ########################################
367
- # API ์ธ์ฆ
368
- ########################################
369
- ACCESS_TOKEN = os.getenv("HF_TOKEN")
370
- if not ACCESS_TOKEN:
371
- raise ValueError("HF_TOKEN environment variable is not set")
372
-
373
- client = OpenAI(
374
- base_url="https://api-inference.huggingface.co/v1/",
375
- api_key=ACCESS_TOKEN,
376
- )
377
-
378
- API_KEY = os.getenv("SERPHOUSE_API_KEY")
379
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
 
381
- ########################################
382
- # ๊ตญ๊ฐ€๋ณ„ ์„ค์ •
383
- ########################################
384
- COUNTRY_LANGUAGES = {
385
- "United States": "en",
386
- "KOREA": "ko",
387
- "United Kingdom": "en",
388
- "Taiwan": "zh-TW",
389
- "Canada": "en",
390
- "Australia": "en",
391
- "Germany": "de",
392
- "France": "fr",
393
- "Japan": "ja",
394
- "China": "zh",
395
- "India": "hi",
396
- "Brazil": "pt",
397
- "Mexico": "es",
398
- "Russia": "ru",
399
- "Italy": "it",
400
- "Spain": "es",
401
- "Netherlands": "nl",
402
- "Singapore": "en",
403
- "Hong Kong": "zh-HK",
404
- "Indonesia": "id",
405
- "Malaysia": "ms",
406
- "Philippines": "tl",
407
- "Thailand": "th",
408
- "Vietnam": "vi",
409
- "Belgium": "nl",
410
- "Denmark": "da",
411
- "Finland": "fi",
412
- "Ireland": "en",
413
- "Norway": "no",
414
- "Poland": "pl",
415
- "Sweden": "sv",
416
- "Switzerland": "de",
417
- "Austria": "de",
418
- "Czech Republic": "cs",
419
- "Greece": "el",
420
- "Hungary": "hu",
421
- "Portugal": "pt",
422
- "Romania": "ro",
423
- "Turkey": "tr",
424
- "Israel": "he",
425
- "Saudi Arabia": "ar",
426
- "United Arab Emirates": "ar",
427
- "South Africa": "en",
428
- "Argentina": "es",
429
- "Chile": "es",
430
- "Colombia": "es",
431
- "Peru": "es",
432
- "Venezuela": "es",
433
- "New Zealand": "en",
434
- "Bangladesh": "bn",
435
- "Pakistan": "ur",
436
- "Egypt": "ar",
437
- "Morocco": "ar",
438
- "Nigeria": "en",
439
- "Kenya": "sw",
440
- "Ukraine": "uk",
441
- "Croatia": "hr",
442
- "Slovakia": "sk",
443
- "Bulgaria": "bg",
444
- "Serbia": "sr",
445
- "Estonia": "et",
446
- "Latvia": "lv",
447
- "Lithuania": "lt",
448
- "Slovenia": "sl",
449
- "Luxembourg": "Luxembourg",
450
- "Malta": "Malta",
451
- "Cyprus": "Cyprus",
452
- "Iceland": "Iceland"
453
- }
454
 
455
- COUNTRY_LOCATIONS = {
456
- "United States": "United States",
457
- "KOREA": "kr",
458
- "United Kingdom": "United Kingdom",
459
- "Taiwan": "Taiwan",
460
- "Canada": "Canada",
461
- "Australia": "Australia",
462
- "Germany": "Germany",
463
- "France": "France",
464
- "Japan": "Japan",
465
- "China": "China",
466
- "India": "India",
467
- "Brazil": "Brazil",
468
- "Mexico": "Mexico",
469
- "Russia": "Russia",
470
- "Italy": "Italy",
471
- "Spain": "Spain",
472
- "Netherlands": "Netherlands",
473
- "Singapore": "Singapore",
474
- "Hong Kong": "Hong Kong",
475
- "Indonesia": "Indonesia",
476
- "Malaysia": "Malaysia",
477
- "Philippines": "Philippines",
478
- "Thailand": "Thailand",
479
- "Vietnam": "Vietnam",
480
- "Belgium": "Belgium",
481
- "Denmark": "Denmark",
482
- "Finland": "Finland",
483
- "Ireland": "Ireland",
484
- "Norway": "Norway",
485
- "Poland": "Poland",
486
- "Sweden": "Sweden",
487
- "Switzerland": "Switzerland",
488
- "Austria": "Austria",
489
- "Czech Republic": "Czech Republic",
490
- "Greece": "Greece",
491
- "Hungary": "Hungary",
492
- "Portugal": "Portugal",
493
- "Romania": "Romania",
494
- "Turkey": "Turkey",
495
- "Israel": "Israel",
496
- "Saudi Arabia": "Saudi Arabia",
497
- "United Arab Emirates": "United Arab Emirates",
498
- "South Africa": "South Africa",
499
- "Argentina": "Argentina",
500
- "Chile": "Chile",
501
- "Colombia": "Colombia",
502
- "Peru": "Peru",
503
- "Venezuela": "Venezuela",
504
- "New Zealand": "New Zealand",
505
- "Bangladesh": "Bangladesh",
506
- "Pakistan": "Pakistan",
507
- "Egypt": "Egypt",
508
- "Morocco": "Morocco",
509
- "Nigeria": "Nigeria",
510
- "Kenya": "Kenya",
511
- "Ukraine": "Ukraine",
512
- "Croatia": "Croatia",
513
- "Slovakia": "Slovakia",
514
- "Bulgaria": "Bulgaria",
515
- "Serbia": "Serbia",
516
- "Estonia": "et",
517
- "Latvia": "lv",
518
- "Lithuania": "lt",
519
- "Slovenia": "sl",
520
- "Luxembourg": "Luxembourg",
521
- "Malta": "Malta",
522
- "Cyprus": "Cyprus",
523
- "Iceland": "Iceland"
524
- }
525
 
 
 
 
 
 
526
 
527
  @lru_cache(maxsize=100)
528
  def translate_query(query, country):
529
- """
530
- Google Translation API(๋น„๊ณต์‹) ์‚ฌ์šฉํ•˜์—ฌ ๊ฒ€์ƒ‰์–ด๋ฅผ ํ•ด๋‹น ๊ตญ๊ฐ€ ์–ธ์–ด๋กœ ๋ฒˆ์—ญ
531
- """
532
  try:
533
  if is_english(query):
534
  return query
@@ -561,14 +440,7 @@ def translate_query(query, country):
561
  print(f"๋ฒˆ์—ญ ์˜ค๋ฅ˜: {str(e)}")
562
  return query
563
 
564
- def is_english(text):
565
- return all(ord(char) < 128 for char in text.replace(' ', '').replace('-', '').replace('_', ''))
566
-
567
  def search_serphouse(query, country, page=1, num_result=10):
568
- """
569
- SerpHouse API์— ์‹ค์‹œ๊ฐ„ ๊ฒ€์ƒ‰ ์š”์ฒญ์„ ๋ณด๋‚ด์–ด,
570
- '๋‰ด์Šค' ํƒญ (sort_by=date)์—์„œ ํ•ด๋‹น query์— ๋Œ€ํ•œ ๊ธฐ์‚ฌ ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜จ๋‹ค.
571
- """
572
  url = "https://api.serphouse.com/serp/live"
573
 
574
  now = datetime.utcnow()
@@ -639,10 +511,6 @@ def search_serphouse(query, country, page=1, num_result=10):
639
  }
640
 
641
  def format_results_from_raw(response_data):
642
- """
643
- SerpHouse API์˜ ์‘๋‹ต ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€๊ณตํ•˜์—ฌ,
644
- (์—๋Ÿฌ๋ฉ”์‹œ์ง€, ๊ธฐ์‚ฌ๋ฆฌ์ŠคํŠธ) ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜.
645
- """
646
  if "error" in response_data:
647
  return "Error: " + response_data["error"], []
648
 
@@ -650,12 +518,10 @@ def format_results_from_raw(response_data):
650
  results = response_data["results"]
651
  translated_query = response_data["translated_query"]
652
 
653
- # ์‹ค์ œ ๋‰ด์Šค ๊ฒฐ๊ณผ
654
  news_results = results.get('results', {}).get('results', {}).get('news', [])
655
  if not news_results:
656
  return "๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.", []
657
 
658
- # ํ•œ๊ตญ ๋„๋ฉ”์ธ ๋ฐ ํ•œ๊ตญ ๊ด€๋ จ ํ‚ค์›Œ๋“œ ํฌํ•จ ๊ธฐ์‚ฌ ์ œ์™ธ
659
  korean_domains = [
660
  '.kr', 'korea', 'korean', 'yonhap', 'hankyung', 'chosun',
661
  'donga', 'joins', 'hani', 'koreatimes', 'koreaherald'
@@ -676,7 +542,6 @@ def format_results_from_raw(response_data):
676
  any(keyword in title for keyword in korean_keywords)
677
  )
678
 
679
- # ํ•œ๊ตญ์–ด ๋‰ด์Šค(๋˜๋Š” ํ•œ๊ตญ ๋„๋ฉ”์ธ) ์ œ์™ธ
680
  if not is_korean_content:
681
  filtered_articles.append({
682
  "index": idx,
@@ -693,15 +558,163 @@ def format_results_from_raw(response_data):
693
  except Exception as e:
694
  return f"๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}", []
695
 
696
- def serphouse_search(query, country):
697
- """
698
- ๊ฒ€์ƒ‰ ๋ฐ ๊ฒฐ๊ณผ ํฌ๋งคํŒ…๊นŒ์ง€ ์ผ๊ด„ ์ฒ˜๋ฆฌ
699
- """
700
- response_data = search_serphouse(query, country)
701
- return format_results_from_raw(response_data)
702
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
703
 
704
- # CSS (UI ์ปค์Šคํ„ฐ๋งˆ์ด์ง•)
705
  css = """
706
  /* ์ „์—ญ ์Šคํƒ€์ผ */
707
  footer {visibility: hidden;}
@@ -886,35 +899,9 @@ with gr.Blocks(theme="Yntec/HaleyCH_Theme_Orange", css=css, title="NewsAI ์„œ๋น„
886
  # ์ฒซ ๋ฒˆ์งธ ํƒญ
887
  with gr.Tab("Earnbot"):
888
  gr.Markdown("## EarnBot: ๊ธ€๋กœ๋ฒŒ ๋น…ํ…Œํฌ ๊ธฐ์—… ๋ฐ ํˆฌ์ž ์ „๋ง AI ์ž๋™ ๋ถ„์„")
889
- gr.Markdown(" * '์ „์ฒด ๋ถ„์„ ๋ณด๊ณ  ์š”์•ฝ' ํด๋ฆญ ์‹œ ์ „์ฒด ์ž๋™ ๋ณด๊ณ  ์ƒ์„ฑ.\n * ์•„๋ž˜ ๊ฐœ๋ณ„ ์ข…๋ชฉ์˜ '๊ฒ€์ƒ‰(DB ์ž๋™ ์ €์žฅ)'๊ณผ '์ถœ๋ ฅ(DB ์ž๋™ ํ˜ธ์ถœ)'๋„ ๊ฐ€๋Šฅ.\n * ์ถ”๊ฐ€๋กœ, ์›ํ•˜๋Š” ์ž„์˜ ํ‚ค์›Œ๋“œ ๋ฐ ๊ตญ๊ฐ€๋กœ ๊ฒ€์ƒ‰/๋ถ„์„ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.")
890
 
891
- # (์‚ฌ์šฉ์ž ์ž„์˜ ๊ฒ€์ƒ‰ ์„น์…˜)
892
- with gr.Group():
893
- gr.Markdown("### ์‚ฌ์šฉ์ž ์ž„์˜ ๊ฒ€์ƒ‰")
894
- with gr.Row():
895
- with gr.Column():
896
- user_input = gr.Textbox(
897
- label="๊ฒ€์ƒ‰์–ด ์ž…๋ ฅ",
898
- placeholder="์˜ˆ) Apple, Samsung ๋“ฑ ์ž์œ ๋กญ๊ฒŒ"
899
- )
900
- with gr.Column():
901
- country_selection = gr.Dropdown(
902
- choices=list(COUNTRY_LOCATIONS.keys()),
903
- value="United States",
904
- label="๊ตญ๊ฐ€ ์„ ํƒ"
905
- )
906
- with gr.Column():
907
- custom_search_btn = gr.Button("์‹คํ–‰", variant="primary")
908
-
909
- custom_search_output = gr.Markdown()
910
-
911
- custom_search_btn.click(
912
- fn=search_custom,
913
- inputs=[user_input, country_selection],
914
- outputs=custom_search_output
915
- )
916
-
917
- # ์ „์ฒด ๋ถ„์„ ๋ณด๊ณ  ์š”์•ฝ ๋ฒ„ํŠผ
918
  with gr.Row():
919
  full_report_btn = gr.Button("์ „์ฒด ๋ถ„์„ ๋ณด๊ณ  ์š”์•ฝ", variant="primary")
920
  full_report_display = gr.Markdown()
@@ -924,7 +911,7 @@ with gr.Blocks(theme="Yntec/HaleyCH_Theme_Orange", css=css, title="NewsAI ์„œ๋น„
924
  outputs=full_report_display
925
  )
926
 
927
- # ์ง€์ •๋œ ๋ฆฌ์ŠคํŠธ (KOREAN_COMPANIES) ๊ฐœ๋ณ„ ๊ธฐ์—… ๊ฒ€์ƒ‰/์ถœ๋ ฅ
928
  with gr.Column():
929
  for i in range(0, len(KOREAN_COMPANIES), 2):
930
  with gr.Row():
@@ -940,10 +927,12 @@ with gr.Blocks(theme="Yntec/HaleyCH_Theme_Orange", css=css, title="NewsAI ์„œ๋น„
940
 
941
  search_btn.click(
942
  fn=lambda c=company: search_company(c),
 
943
  outputs=result_display
944
  )
945
  load_btn.click(
946
  fn=lambda c=company: load_company(c),
 
947
  outputs=result_display
948
  )
949
 
@@ -960,12 +949,105 @@ with gr.Blocks(theme="Yntec/HaleyCH_Theme_Orange", css=css, title="NewsAI ์„œ๋น„
960
 
961
  search_btn.click(
962
  fn=lambda c=company: search_company(c),
 
963
  outputs=result_display
964
  )
965
  load_btn.click(
966
  fn=lambda c=company: load_company(c),
 
967
  outputs=result_display
968
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
969
 
970
  iface.launch(
971
  server_name="0.0.0.0",
 
34
  "investing"
35
  ]
36
 
37
+
38
+ #########################################################
39
+ # ๊ณตํ†ต ํ•จ์ˆ˜
40
+ #########################################################
41
+
42
  def convert_to_seoul_time(timestamp_str):
43
  try:
44
  dt = datetime.strptime(timestamp_str, '%Y-%m-%d %H:%M:%S')
 
54
  OpenAI API๋ฅผ ํ†ตํ•ด ๋‰ด์Šค ๊ธฐ์‚ฌ๋“ค์˜ ์ข…ํ•ฉ ๊ฐ์„ฑ ๋ถ„์„์„ ์ˆ˜ํ–‰
55
  """
56
  try:
 
57
  combined_text = "\n\n".join([
58
  f"์ œ๋ชฉ: {article.get('title', '')}\n๋‚ด์šฉ: {article.get('snippet', '')}"
59
  for article in articles
 
87
  return f"๊ฐ์„ฑ ๋ถ„์„ ์‹คํŒจ: {str(e)}"
88
 
89
 
90
+ #########################################################
91
+ # DB ๊ด€๋ จ
92
+ #########################################################
93
+
94
  def init_db():
95
  db_path = pathlib.Path("search_results.db")
96
  conn = sqlite3.connect(db_path)
 
105
  conn.close()
106
 
107
  def save_to_db(keyword, country, results):
 
 
 
108
  conn = sqlite3.connect("search_results.db")
109
  c = conn.cursor()
110
  seoul_tz = pytz.timezone('Asia/Seoul')
 
119
  conn.close()
120
 
121
  def load_from_db(keyword, country):
 
 
 
122
  conn = sqlite3.connect("search_results.db")
123
  c = conn.cursor()
124
  c.execute("SELECT results, timestamp FROM searches WHERE keyword=? AND country=? ORDER BY timestamp DESC LIMIT 1",
 
129
  return json.loads(result[0]), convert_to_seoul_time(result[1])
130
  return None, None
131
 
132
+ #########################################################
133
+ # "id"๋กœ ์ง์ ‘ ๋กœ๋”ฉํ•˜๊ธฐ (ํžˆ์Šคํ† ๋ฆฌ์—์„œ ์‚ฌ์šฉ)
134
+ #########################################################
135
+ def load_by_id(search_id):
136
  """
137
+ DB์˜ PRIMARY KEY(id)๋กœ ํŠน์ • ๊ฒ€์ƒ‰ ๊ธฐ๋ก์„ ๋กœ๋”ฉ
138
  """
139
+ conn = sqlite3.connect("search_results.db")
140
+ c = conn.cursor()
141
+ c.execute("SELECT keyword, country, results, timestamp FROM searches WHERE id=?", (search_id,))
142
+ row = c.fetchone()
143
+ conn.close()
144
+
145
+ if row:
146
+ keyword, country, results_json, ts = row
147
+ data = json.loads(results_json)
148
+ return {
149
+ "keyword": keyword,
150
+ "country": country,
151
+ "data": data,
152
+ "timestamp": convert_to_seoul_time(ts)
153
+ }
154
+ return None
155
+
156
+ def display_results(articles):
157
  output = ""
158
  for idx, article in enumerate(articles, 1):
159
  output += f"### {idx}. {article['title']}\n"
 
164
  return output
165
 
166
 
167
+ #########################################################
168
+ # (1) ๊ฒ€์ƒ‰ ์‹œ => ๊ธฐ์‚ฌ + ๋ถ„์„ ๋™์‹œ ์ถœ๋ ฅ, DB ์ €์žฅ
169
+ #########################################################
170
  def search_company(company):
 
 
 
 
 
171
  error_message, articles = serphouse_search(company, "United States")
172
  if not error_message and articles:
 
173
  analysis = analyze_sentiment_batch(articles, client)
 
 
174
  store_dict = {
175
  "articles": articles,
176
  "analysis": analysis
177
  }
178
  save_to_db(company, "United States", store_dict)
 
 
179
  output = display_results(articles)
180
  output += f"\n\n### ๋ถ„์„ ๋ณด๊ณ \n{analysis}\n"
181
  return output
182
  return f"{company}์— ๋Œ€ํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
183
 
184
+ #########################################################
185
+ # (2) ์ถœ๋ ฅ ์‹œ => DB์— ์ €์žฅ๋œ ๊ธฐ์‚ฌ + ๋ถ„์„ ํ•จ๊ป˜ ์ถœ๋ ฅ
186
+ #########################################################
187
  def load_company(company):
 
 
 
 
188
  data, timestamp = load_from_db(company, "United States")
189
  if data:
 
190
  articles = data.get("articles", [])
191
  analysis = data.get("analysis", "")
192
 
 
197
  return f"{company}์— ๋Œ€ํ•œ ์ €์žฅ๋œ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
198
 
199
 
200
+ #########################################################
201
+ # (3) EarnBOT ๋ถ„์„ ๋ฆฌํฌํŠธ
202
+ #########################################################
203
  def show_stats():
204
  """
205
+ ๊ธฐ์กด "ํ•œ๊ตญ ๊ธฐ์—… ๋‰ด์Šค ๋ถ„์„ ๋ฆฌํฌํŠธ" -> "EarnBOT ๋ถ„์„ ๋ฆฌํฌํŠธ"
 
 
 
 
 
 
206
  """
207
  conn = sqlite3.connect("search_results.db")
208
  c = conn.cursor()
209
 
210
+ output = "## EarnBOT ๋ถ„์„ ๋ฆฌํฌํŠธ\n\n"
211
 
 
212
  data_list = []
213
  for company in KOREAN_COMPANIES:
214
  c.execute("""
 
221
 
222
  row = c.fetchone()
223
  if row:
224
+ results_json, tstamp = row
225
+ data_list.append((company, tstamp, results_json))
226
 
227
  conn.close()
228
 
 
229
  def analyze_data(item):
230
  comp, tstamp, results_json = item
231
  data = json.loads(results_json)
232
  articles = data.get("articles", [])
233
  analysis = data.get("analysis", "")
 
234
  count_articles = len(articles)
 
 
 
 
235
  return (comp, tstamp, count_articles, analysis)
236
 
237
  results_list = []
 
240
  for future in as_completed(futures):
241
  results_list.append(future.result())
242
 
 
243
  for comp, tstamp, count, analysis in results_list:
244
  seoul_time = convert_to_seoul_time(tstamp)
245
  output += f"### {comp}\n"
 
253
  return output
254
 
255
 
256
+ #########################################################
257
+ # ์ „์ฒด ๊ฒ€์ƒ‰ (๋ณ‘๋ ฌ) / ์ „์ฒด ์ถœ๋ ฅ / ์ „์ฒด ๋ฆฌํฌํŠธ
258
+ #########################################################
259
  def search_all_companies():
 
 
 
 
260
  overall_result = "# [์ „์ฒด ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ]\n\n"
261
 
262
  def do_search(comp):
 
272
  return overall_result
273
 
274
  def load_all_companies():
 
 
 
 
275
  overall_result = "# [์ „์ฒด ์ถœ๋ ฅ ๊ฒฐ๊ณผ]\n\n"
 
276
  for comp in KOREAN_COMPANIES:
277
  overall_result += f"## {comp}\n"
278
  overall_result += load_company(comp)
 
280
  return overall_result
281
 
282
  def full_summary_report():
 
 
 
 
 
283
  search_result_text = search_all_companies()
 
 
284
  load_result_text = load_all_companies()
 
 
285
  stats_text = show_stats()
286
 
287
  combined_report = (
 
296
  return combined_report
297
 
298
 
299
+ #########################################################
300
+ # (์ถ”๊ฐ€) ์‚ฌ์šฉ์ž ์ž„์˜ ๊ฒ€์ƒ‰ ํ•จ์ˆ˜ (๋‘ ๋ฒˆ์งธ ํƒญ)
301
+ #########################################################
302
  def search_custom(query, country):
303
  """
304
  ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ (query, country)์— ๋Œ€ํ•ด
 
311
  if not articles:
312
  return "๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
313
 
 
314
  analysis = analyze_sentiment_batch(articles, client)
 
 
315
  save_data = {
316
  "articles": articles,
317
  "analysis": analysis
318
  }
319
  save_to_db(query, country, save_data)
320
 
 
321
  loaded_data, timestamp = load_from_db(query, country)
322
  if not loaded_data:
323
  return "DB์—์„œ ๋กœ๋“œ ์‹คํŒจ"
324
 
325
+ arts = loaded_data.get("articles", [])
326
+ analy = loaded_data.get("analysis", "")
327
+
328
  out = f"## [์‚ฌ์šฉ์ž ์ž„์˜ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ]\n\n"
329
  out += f"**ํ‚ค์›Œ๋“œ**: {query}\n\n"
330
  out += f"**๊ตญ๊ฐ€**: {country}\n\n"
331
  out += f"**์ €์žฅ ์‹œ๊ฐ„**: {timestamp}\n\n"
332
 
 
 
 
333
  out += display_results(arts)
334
  out += f"### ๋‰ด์Šค ๊ฐ์„ฑ ๋ถ„์„\n{analy}\n"
335
 
336
  return out
337
 
338
 
339
+ #########################################################
340
+ # (์ถ”๊ฐ€) ํžˆ์Šคํ† ๋ฆฌ ํ•จ์ˆ˜
341
+ #########################################################
342
+ def get_custom_search_history():
343
+ """
344
+ KOREAN_COMPANIES์— ์—†๋Š” keyword๋กœ ๊ฒ€์ƒ‰๋œ ๊ธฐ๋ก๋งŒ
345
+ (id, timestamp, keyword, country) ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜
346
+ ์ตœ์‹ ์ˆœ ์ •๋ ฌ
347
+ """
348
+ # set์œผ๋กœ ๋งŒ๋“ค์–ด์„œ ๋น ๋ฅด๊ฒŒ ๊ฒ€์ƒ‰
349
+ company_set = set(k.lower() for k in KOREAN_COMPANIES)
350
+
351
+ conn = sqlite3.connect("search_results.db")
352
+ c = conn.cursor()
353
+ c.execute("""SELECT id, keyword, country, timestamp
354
+ FROM searches
355
+ ORDER BY timestamp DESC""")
356
+ rows = c.fetchall()
357
+ conn.close()
358
+
359
+ history_list = []
360
+ for (sid, kw, cty, ts) in rows:
361
+ # KOREAN_COMPANIES ์— ์—†๋Š” ๊ฒฝ์šฐ -> ์‚ฌ์šฉ์ž ์ž„์˜ ๊ฒ€์ƒ‰
362
+ if kw.lower() not in company_set:
363
+ # "id, ์‹œ๊ฐ„, ํ‚ค์›Œ๋“œ(๊ตญ๊ฐ€)" ๋กœ ํ‘œ์‹œํ•  ํ…์ŠคํŠธ ๊ตฌ์„ฑ
364
+ # ์˜ˆ: "12 | 2025-01-22 10:23:00 KST | Apple (United States)"
365
+ # ์‹ค์ œ dropdown value ๋Š” sid (id) ๋งŒ ์ €์žฅ
366
+ display_time = convert_to_seoul_time(ts)
367
+ label = f"{sid} | {display_time} | {kw} ({cty})"
368
+ history_list.append((str(sid), label))
369
+
370
+ return history_list
371
 
372
+ def view_history_record(record_id):
373
+ """
374
+ Dropdown ์—์„œ ์„ ํƒ๋œ record_id (๋ฌธ์ž์—ด)๋กœ๋ถ€ํ„ฐ
375
+ ํ•ด๋‹น ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ(๊ธฐ์‚ฌ+๋ถ„์„)์„ Markdown ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜
376
+ """
377
+ if not record_id:
378
+ return "๊ธฐ๋ก์ด ์—†์Šต๋‹ˆ๋‹ค."
379
+
380
+ data = load_by_id(int(record_id))
381
+ if not data:
382
+ return "ํ•ด๋‹น ID์˜ ๊ธฐ๋ก์ด ์—†์Šต๋‹ˆ๋‹ค."
383
+
384
+ keyword = data["keyword"]
385
+ country = data["country"]
386
+ timestamp = data["timestamp"]
387
+ stored = data["data"] # { articles, analysis }
388
+
389
+ articles = stored.get("articles", [])
390
+ analysis = stored.get("analysis", "")
391
+
392
+ out = f"### [ํžˆ์Šคํ† ๋ฆฌ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ]\n\n"
393
+ out += f"- ID: {record_id}\n"
394
+ out += f"- ํ‚ค์›Œ๋“œ: {keyword}\n"
395
+ out += f"- ๊ตญ๊ฐ€: {country}\n"
396
+ out += f"- ์ €์žฅ ์‹œ๊ฐ„: {timestamp}\n\n"
397
+ out += display_results(articles)
398
+ out += f"\n\n### ๋ถ„์„ ๋ณด๊ณ \n{analysis}\n"
399
+
400
+ return out
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
402
 
403
+ #########################################################
404
+ # SerpHouse API
405
+ #########################################################
406
+ def is_english(text):
407
+ return all(ord(char) < 128 for char in text.replace(' ', '').replace('-', '').replace('_', ''))
408
 
409
  @lru_cache(maxsize=100)
410
  def translate_query(query, country):
 
 
 
411
  try:
412
  if is_english(query):
413
  return query
 
440
  print(f"๋ฒˆ์—ญ ์˜ค๋ฅ˜: {str(e)}")
441
  return query
442
 
 
 
 
443
  def search_serphouse(query, country, page=1, num_result=10):
 
 
 
 
444
  url = "https://api.serphouse.com/serp/live"
445
 
446
  now = datetime.utcnow()
 
511
  }
512
 
513
  def format_results_from_raw(response_data):
 
 
 
 
514
  if "error" in response_data:
515
  return "Error: " + response_data["error"], []
516
 
 
518
  results = response_data["results"]
519
  translated_query = response_data["translated_query"]
520
 
 
521
  news_results = results.get('results', {}).get('results', {}).get('news', [])
522
  if not news_results:
523
  return "๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.", []
524
 
 
525
  korean_domains = [
526
  '.kr', 'korea', 'korean', 'yonhap', 'hankyung', 'chosun',
527
  'donga', 'joins', 'hani', 'koreatimes', 'koreaherald'
 
542
  any(keyword in title for keyword in korean_keywords)
543
  )
544
 
 
545
  if not is_korean_content:
546
  filtered_articles.append({
547
  "index": idx,
 
558
  except Exception as e:
559
  return f"๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}", []
560
 
 
 
 
 
 
 
561
 
562
+ ###################################
563
+ # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ฐ CSS
564
+ ###################################
565
+ ACCESS_TOKEN = os.getenv("HF_TOKEN")
566
+ if not ACCESS_TOKEN:
567
+ raise ValueError("HF_TOKEN environment variable is not set")
568
+
569
+ client = OpenAI(
570
+ base_url="https://api-inference.huggingface.co/v1/",
571
+ api_key=ACCESS_TOKEN,
572
+ )
573
+
574
+ API_KEY = os.getenv("SERPHOUSE_API_KEY")
575
+
576
+ COUNTRY_LANGUAGES = {
577
+ "United States": "en",
578
+ "KOREA": "ko",
579
+ "United Kingdom": "en",
580
+ "Taiwan": "zh-TW",
581
+ "Canada": "en",
582
+ "Australia": "en",
583
+ "Germany": "de",
584
+ "France": "fr",
585
+ "Japan": "ja",
586
+ "China": "zh",
587
+ "India": "hi",
588
+ "Brazil": "pt",
589
+ "Mexico": "es",
590
+ "Russia": "ru",
591
+ "Italy": "it",
592
+ "Spain": "es",
593
+ "Netherlands": "nl",
594
+ "Singapore": "en",
595
+ "Hong Kong": "zh-HK",
596
+ "Indonesia": "id",
597
+ "Malaysia": "ms",
598
+ "Philippines": "tl",
599
+ "Thailand": "th",
600
+ "Vietnam": "vi",
601
+ "Belgium": "nl",
602
+ "Denmark": "da",
603
+ "Finland": "fi",
604
+ "Ireland": "en",
605
+ "Norway": "no",
606
+ "Poland": "pl",
607
+ "Sweden": "sv",
608
+ "Switzerland": "de",
609
+ "Austria": "de",
610
+ "Czech Republic": "cs",
611
+ "Greece": "el",
612
+ "Hungary": "hu",
613
+ "Portugal": "pt",
614
+ "Romania": "ro",
615
+ "Turkey": "tr",
616
+ "Israel": "he",
617
+ "Saudi Arabia": "ar",
618
+ "United Arab Emirates": "ar",
619
+ "South Africa": "en",
620
+ "Argentina": "es",
621
+ "Chile": "es",
622
+ "Colombia": "es",
623
+ "Peru": "es",
624
+ "Venezuela": "es",
625
+ "New Zealand": "en",
626
+ "Bangladesh": "bn",
627
+ "Pakistan": "ur",
628
+ "Egypt": "ar",
629
+ "Morocco": "ar",
630
+ "Nigeria": "en",
631
+ "Kenya": "sw",
632
+ "Ukraine": "uk",
633
+ "Croatia": "hr",
634
+ "Slovakia": "sk",
635
+ "Bulgaria": "bg",
636
+ "Serbia": "sr",
637
+ "Estonia": "et",
638
+ "Latvia": "lv",
639
+ "Lithuania": "lt",
640
+ "Slovenia": "sl",
641
+ "Luxembourg": "Luxembourg",
642
+ "Malta": "Malta",
643
+ "Cyprus": "Cyprus",
644
+ "Iceland": "Iceland"
645
+ }
646
+
647
+ COUNTRY_LOCATIONS = {
648
+ "United States": "United States",
649
+ "KOREA": "kr",
650
+ "United Kingdom": "United Kingdom",
651
+ "Taiwan": "Taiwan",
652
+ "Canada": "Canada",
653
+ "Australia": "Australia",
654
+ "Germany": "Germany",
655
+ "France": "France",
656
+ "Japan": "Japan",
657
+ "China": "China",
658
+ "India": "India",
659
+ "Brazil": "Brazil",
660
+ "Mexico": "Mexico",
661
+ "Russia": "Russia",
662
+ "Italy": "Italy",
663
+ "Spain": "Spain",
664
+ "Netherlands": "Netherlands",
665
+ "Singapore": "Singapore",
666
+ "Hong Kong": "Hong Kong",
667
+ "Indonesia": "Indonesia",
668
+ "Malaysia": "Malaysia",
669
+ "Philippines": "Philippines",
670
+ "Thailand": "Thailand",
671
+ "Vietnam": "Vietnam",
672
+ "Belgium": "Belgium",
673
+ "Denmark": "Denmark",
674
+ "Finland": "Finland",
675
+ "Ireland": "Ireland",
676
+ "Norway": "Norway",
677
+ "Poland": "Poland",
678
+ "Sweden": "Sweden",
679
+ "Switzerland": "Switzerland",
680
+ "Austria": "Austria",
681
+ "Czech Republic": "Czech Republic",
682
+ "Greece": "Greece",
683
+ "Hungary": "Hungary",
684
+ "Portugal": "Portugal",
685
+ "Romania": "Romania",
686
+ "Turkey": "Turkey",
687
+ "Israel": "Israel",
688
+ "Saudi Arabia": "Saudi Arabia",
689
+ "United Arab Emirates": "United Arab Emirates",
690
+ "South Africa": "South Africa",
691
+ "Argentina": "Argentina",
692
+ "Chile": "Chile",
693
+ "Colombia": "Colombia",
694
+ "Peru": "Peru",
695
+ "Venezuela": "Venezuela",
696
+ "New Zealand": "New Zealand",
697
+ "Bangladesh": "Bangladesh",
698
+ "Pakistan": "Pakistan",
699
+ "Egypt": "Egypt",
700
+ "Morocco": "Morocco",
701
+ "Nigeria": "Nigeria",
702
+ "Kenya": "Kenya",
703
+ "Ukraine": "Ukraine",
704
+ "Croatia": "Croatia",
705
+ "Slovakia": "Slovakia",
706
+ "Bulgaria": "Bulgaria",
707
+ "Serbia": "Serbia",
708
+ "Estonia": "et",
709
+ "Latvia": "lv",
710
+ "Lithuania": "lt",
711
+ "Slovenia": "sl",
712
+ "Luxembourg": "Luxembourg",
713
+ "Malta": "Malta",
714
+ "Cyprus": "Cyprus",
715
+ "Iceland": "Iceland"
716
+ }
717
 
 
718
  css = """
719
  /* ์ „์—ญ ์Šคํƒ€์ผ */
720
  footer {visibility: hidden;}
 
899
  # ์ฒซ ๋ฒˆ์งธ ํƒญ
900
  with gr.Tab("Earnbot"):
901
  gr.Markdown("## EarnBot: ๊ธ€๋กœ๋ฒŒ ๋น…ํ…Œํฌ ๊ธฐ์—… ๋ฐ ํˆฌ์ž ์ „๋ง AI ์ž๋™ ๋ถ„์„")
902
+ gr.Markdown(" - '์ „์ฒด ๋ถ„์„ ๋ณด๊ณ  ์š”์•ฝ' ํด๋ฆญ ์‹œ ์ „์ฒด ์ž๋™ ๋ณด๊ณ  ์ƒ์„ฑ.\n - ์•„๋ž˜ ๊ฐœ๋ณ„ ์ข…๋ชฉ์˜ '๊ฒ€์ƒ‰(DB ์ž๋™ ์ €์žฅ)'๊ณผ '์ถœ๋ ฅ(DB ์ž๋™ ํ˜ธ์ถœ)'๋„ ๊ฐ€๋Šฅ.\n - ํ•˜๋‹จ '์ˆ˜๋™ ๊ฒ€์ƒ‰ ํžˆ์Šคํ† ๋ฆฌ'์—์„œ ์ด์ „์— ์ˆ˜๋™ ์ž…๋ ฅํ•œ ๊ฒ€์ƒ‰์–ด ๊ธฐ๋ก ํ™•์ธ ๊ฐ€๋Šฅ.")
903
 
904
+ # ์ „์ฒด ๋ถ„์„ ๋ณด๊ณ  ์š”์•ฝ
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
905
  with gr.Row():
906
  full_report_btn = gr.Button("์ „์ฒด ๋ถ„์„ ๋ณด๊ณ  ์š”์•ฝ", variant="primary")
907
  full_report_display = gr.Markdown()
 
911
  outputs=full_report_display
912
  )
913
 
914
+ # ์ง€์ •๋œ (KOREAN_COMPANIES) ๊ธฐ์—… ๊ฒ€์ƒ‰/์ถœ๋ ฅ
915
  with gr.Column():
916
  for i in range(0, len(KOREAN_COMPANIES), 2):
917
  with gr.Row():
 
927
 
928
  search_btn.click(
929
  fn=lambda c=company: search_company(c),
930
+ inputs=[],
931
  outputs=result_display
932
  )
933
  load_btn.click(
934
  fn=lambda c=company: load_company(c),
935
+ inputs=[],
936
  outputs=result_display
937
  )
938
 
 
949
 
950
  search_btn.click(
951
  fn=lambda c=company: search_company(c),
952
+ inputs=[],
953
  outputs=result_display
954
  )
955
  load_btn.click(
956
  fn=lambda c=company: load_company(c),
957
+ inputs=[],
958
  outputs=result_display
959
  )
960
+
961
+ # (์ถ”๊ฐ€) ์ˆ˜๋™ ๊ฒ€์ƒ‰ ํžˆ์Šคํ† ๋ฆฌ
962
+ gr.Markdown("---")
963
+ gr.Markdown("### ์ˆ˜๋™ ๊ฒ€์ƒ‰ ํžˆ์Šคํ† ๋ฆฌ")
964
+ with gr.Row():
965
+ refresh_hist_btn = gr.Button("ํžˆ์Šคํ† ๋ฆฌ ๊ฐฑ์‹ ", variant="secondary")
966
+
967
+ history_dropdown = gr.Dropdown(
968
+ label="๊ฒ€์ƒ‰ ๊ธฐ๋ก ๋ชฉ๋ก",
969
+ choices=[], # ์ด ๋ถ€๋ถ„์€ click ์ด๋ฒคํŠธ์—์„œ ๋™์ ์œผ๋กœ ์—…๋ฐ์ดํŠธ
970
+ value=None
971
+ )
972
+ hist_view_btn = gr.Button("๋ณด๊ธฐ", variant="primary")
973
+ hist_result_display = gr.Markdown()
974
+
975
+ # 1) ํžˆ์Šคํ† ๋ฆฌ ๊ฐฑ์‹  ํ•จ์ˆ˜
976
+ def update_history_dropdown():
977
+ history_list = get_custom_search_history() # [(id, label), ...]
978
+ labels = [lbl for (id_val, lbl) in history_list]
979
+ values = [id_val for (id_val, lbl) in history_list]
980
+
981
+ # gr.Dropdown ์— choices=[..., ...] ํ˜•ํƒœ๋กœ ์ „๋‹ฌํ•ด์•ผ ํ•˜๋ฏ€๋กœ
982
+ # zip ํ•˜์—ฌ (value, label) ์Œ์„ ๋งŒ๋“ฆ
983
+ # Gradio 3.x์—์„œ๋Š” choices๋ฅผ list๋กœ ์ฃผ๊ณ , label/value ๋ถ„๋ฆฌ ๋ถˆ๊ฐ€.
984
+ # -> workaround: "value|label" ์‹ or just store label in "choices" and store value separate
985
+ # ์—ฌ๊ธฐ์„œ๋Š” choices ์— label๋งŒ ๋„ฃ๊ณ , id๋Š” label ํŒŒ์‹ฑ ๋“ฑ ํ•ด์•ผํ•จ.
986
+ # ์ข€ ๋” ํŽธํ•˜๊ฒŒ: return a dictionary for "choices"
987
+
988
+ # ๋ฐฉ๋ฒ•1) label๋งŒ ์ „๋‹ฌ -> user picks label -> then parse.
989
+ # ๊ฐ„๋‹จํžˆ "value=label"์ด ๋˜๋„๋ก:
990
+ choice_list = []
991
+ for (id_val, label) in history_list:
992
+ choice_list.append(label)
993
+ return gr.update(choices=choice_list, value=None)
994
+
995
+ refresh_hist_btn.click(
996
+ fn=update_history_dropdown,
997
+ inputs=[],
998
+ outputs=history_dropdown
999
+ )
1000
+
1001
+ # 2) ํžˆ์Šคํ† ๋ฆฌ ๋ณด๊ธฐ ํ•จ์ˆ˜
1002
+ def show_history_record(selected_label):
1003
+ # selected_label ์ด "id | time | keyword (country)" ํ˜•์‹
1004
+ if not selected_label:
1005
+ return "ํžˆ์Šคํ† ๋ฆฌ๊ฐ€ ์„ ํƒ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."
1006
+
1007
+ # id๋Š” ๊ฐ€์žฅ ์•ž๋ถ€๋ถ„์˜ ์ˆซ์ž
1008
+ # ์˜ˆ: "12 | 2025-01-22 10:23:00 KST | Apple (United States)"
1009
+ # -> id=12
1010
+ splitted = selected_label.split("|")
1011
+ if len(splitted) < 2:
1012
+ return "ํ˜•์‹ ์˜ค๋ฅ˜"
1013
+ record_id = splitted[0].strip() # "12"
1014
+ return view_history_record(record_id)
1015
+
1016
+ hist_view_btn.click(
1017
+ fn=show_history_record,
1018
+ inputs=[history_dropdown],
1019
+ outputs=hist_result_display
1020
+ )
1021
+
1022
+
1023
+ # ๋‘ ๋ฒˆ์งธ ํƒญ: "์ง€์ • ์ž๋™ ๊ฒ€์ƒ‰/๋ถ„์„"
1024
+ with gr.Tab("์ง€์ • ์ž๋™ ๊ฒ€์ƒ‰/๋ถ„์„"):
1025
+ gr.Markdown("## ์‚ฌ์šฉ์ž ์ž„์˜ ํ‚ค์›Œ๋“œ + ๊ตญ๊ฐ€ ๊ฒ€์ƒ‰/๋ถ„์„")
1026
+ gr.Markdown("๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ DB์— ์ €์žฅ๋˜๋ฉฐ, ์ฒซ ๋ฒˆ์งธ ํƒญ์˜ '์ˆ˜๋™ ๊ฒ€์ƒ‰ ํžˆ์Šคํ† ๋ฆฌ'์—์„œ ํ™•์ธ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.")
1027
+
1028
+ with gr.Row():
1029
+ with gr.Column():
1030
+ user_input = gr.Textbox(
1031
+ label="๊ฒ€์ƒ‰์–ด ์ž…๋ ฅ",
1032
+ placeholder="์˜ˆ) Apple, Samsung ๋“ฑ ์ž์œ ๋กญ๊ฒŒ"
1033
+ )
1034
+ with gr.Column():
1035
+ country_selection = gr.Dropdown(
1036
+ choices=list(COUNTRY_LOCATIONS.keys()),
1037
+ value="United States",
1038
+ label="๊ตญ๊ฐ€ ์„ ํƒ"
1039
+ )
1040
+ with gr.Column():
1041
+ custom_search_btn = gr.Button("์‹คํ–‰", variant="primary")
1042
+
1043
+ custom_search_output = gr.Markdown()
1044
+
1045
+ custom_search_btn.click(
1046
+ fn=search_custom,
1047
+ inputs=[user_input, country_selection],
1048
+ outputs=custom_search_output
1049
+ )
1050
+
1051
 
1052
  iface.launch(
1053
  server_name="0.0.0.0",