ginipick commited on
Commit
84dfec0
·
verified ·
1 Parent(s): 606e64a

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -971
app.py DELETED
@@ -1,971 +0,0 @@
1
- import gradio as gr
2
- import requests
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
10
- from openai import OpenAI
11
- from bs4 import BeautifulSoup
12
- import re
13
- import pathlib
14
- import sqlite3
15
- import pytz
16
-
17
-
18
- #########################################################
19
- # 한국 기업 리스트
20
- #########################################################
21
- KOREAN_COMPANIES = [
22
- "NVIDIA",
23
- "ALPHABET",
24
- "APPLE",
25
- "TESLA",
26
- "AMAZON",
27
- "MICROSOFT",
28
- "META",
29
- "INTEL",
30
- "SAMSUNG",
31
- "HYNIX",
32
- "BITCOIN",
33
- "crypto",
34
- "stock",
35
- "Economics",
36
- "Finance",
37
- "investing"
38
- ]
39
-
40
-
41
- #########################################################
42
- # 공통 함수
43
- #########################################################
44
-
45
- def convert_to_seoul_time(timestamp_str):
46
- """
47
- 주어진 'YYYY-MM-DD HH:MM:SS' 형태의 UTC 시각을 서울 시간(KST)으로 변환하여
48
- 'YYYY-MM-DD HH:MM:SS KST' 형태의 문자열로 반환.
49
- """
50
- try:
51
- dt = datetime.strptime(timestamp_str, '%Y-%m-%d %H:%M:%S')
52
- seoul_tz = pytz.timezone('Asia/Seoul')
53
- seoul_time = seoul_tz.localize(dt)
54
- return seoul_time.strftime('%Y-%m-%d %H:%M:%S KST')
55
- except Exception as e:
56
- print(f"시간 변환 오류: {str(e)}")
57
- return timestamp_str
58
-
59
-
60
- def analyze_sentiment_batch(articles, client):
61
- """
62
- OpenAI API를 통해 뉴스 기사들(articles)의 제목/내용을 종합하여
63
- 감성 분석(긍정/부정/중립 등)을 수행하고, 요약된 결과를 문자열로 반환.
64
- """
65
- try:
66
- # 모든 기사(제목, snippet)을 합쳐 하나의 텍스트로 만든다.
67
- combined_text = "\n\n".join([
68
- f"제목: {article.get('title', '')}\n내용: {article.get('snippet', '')}"
69
- for article in articles
70
- ])
71
-
72
- # 감성 분석을 요청하는 프롬프트
73
- prompt = f"""다음 뉴스 모음에 대해 전반적인 감성 분석을 수행하세요:
74
-
75
- 뉴스 내용:
76
- {combined_text}
77
-
78
- 다음 형식으로 분석해주세요:
79
- 1. 전반적 감성: [긍정/부정/중립]
80
- 2. 주요 긍정적 요소:
81
- - [항목1]
82
- - [항목2]
83
- 3. 주요 부정적 요소:
84
- - [항목1]
85
- - [항목2]
86
- 4. 종합 평가: [상세 설명]
87
- """
88
-
89
- response = client.chat.completions.create(
90
- model="CohereForAI/c4ai-command-r-plus-08-2024",
91
- messages=[{"role": "user", "content": prompt}],
92
- temperature=0.3,
93
- max_tokens=1000
94
- )
95
-
96
- return response.choices[0].message.content
97
-
98
- except Exception as e:
99
- return f"감성 분석 실패: {str(e)}"
100
-
101
-
102
- #########################################################
103
- # DB 초기화 및 입출력 함수
104
- #########################################################
105
-
106
- def init_db():
107
- """
108
- SQLite DB 파일(search_results.db)이 없다면 생성하고,
109
- 'searches' 테이블이 없다면 생성한다.
110
- """
111
- db_path = pathlib.Path("search_results.db")
112
- conn = sqlite3.connect(db_path)
113
- c = conn.cursor()
114
- c.execute('''CREATE TABLE IF NOT EXISTS searches
115
- (id INTEGER PRIMARY KEY AUTOINCREMENT,
116
- keyword TEXT,
117
- country TEXT,
118
- results TEXT,
119
- timestamp DATETIME DEFAULT CURRENT_TIMESTAMP)''')
120
- conn.commit()
121
- conn.close()
122
-
123
-
124
- def save_to_db(keyword, country, results):
125
- """
126
- 특정 (keyword, country)에 대한 검색 결과(results: JSON 형태)를 DB에 저장.
127
- """
128
- conn = sqlite3.connect("search_results.db")
129
- c = conn.cursor()
130
- seoul_tz = pytz.timezone('Asia/Seoul')
131
- now = datetime.now(seoul_tz)
132
- timestamp = now.strftime('%Y-%m-%d %H:%M:%S')
133
-
134
- c.execute("""INSERT INTO searches
135
- (keyword, country, results, timestamp)
136
- VALUES (?, ?, ?, ?)""",
137
- (keyword, country, json.dumps(results), timestamp))
138
- conn.commit()
139
- conn.close()
140
-
141
-
142
- def load_from_db(keyword, country):
143
- """
144
- DB에서 (keyword, country)에 해당하는 가장 최근 검색 결과를 불러온다.
145
- 결과가 있으면 (JSON 디코딩된 값, 'YYYY-MM-DD HH:MM:SS KST' 형태의 저장 시각) 반환.
146
- 없으면 (None, None) 반환.
147
- """
148
- conn = sqlite3.connect("search_results.db")
149
- c = conn.cursor()
150
- c.execute("""SELECT results, timestamp
151
- FROM searches
152
- WHERE keyword=? AND country=?
153
- ORDER BY timestamp DESC
154
- LIMIT 1""",
155
- (keyword, country))
156
- result = c.fetchone()
157
- conn.close()
158
- if result:
159
- return json.loads(result[0]), convert_to_seoul_time(result[1])
160
- return None, None
161
-
162
-
163
- def load_by_id(search_id):
164
- """
165
- DB의 PRIMARY KEY(id)로 특정 검색 기록을 불러온다.
166
- - keyword, country, results, timestamp
167
- - results를 JSON 디코딩한 뒤 'data'로서 반환.
168
- """
169
- conn = sqlite3.connect("search_results.db")
170
- c = conn.cursor()
171
- c.execute("SELECT keyword, country, results, timestamp FROM searches WHERE id=?",
172
- (search_id,))
173
- row = c.fetchone()
174
- conn.close()
175
-
176
- if row:
177
- keyword, country, results_json, ts = row
178
- data = json.loads(results_json)
179
- return {
180
- "keyword": keyword,
181
- "country": country,
182
- "data": data,
183
- "timestamp": convert_to_seoul_time(ts)
184
- }
185
- return None
186
-
187
-
188
- #########################################################
189
- # 결과 표시
190
- #########################################################
191
-
192
- def display_results(articles):
193
- """
194
- 기사 목록(articles)을 Markdown 형식으로 예쁘게 정리.
195
- """
196
- output = ""
197
- for idx, article in enumerate(articles, 1):
198
- output += f"### {idx}. {article['title']}\n"
199
- output += f"출처: {article['channel']}\n"
200
- output += f"시간: {article['time']}\n"
201
- output += f"링크: {article['link']}\n"
202
- output += f"요약: {article['snippet']}\n\n"
203
- return output
204
-
205
-
206
- #########################################################
207
- # SerpHouse API: 번역 / 요청 / 응답 가공
208
- #########################################################
209
-
210
- API_KEY = os.getenv("SERPHOUSE_API_KEY") # SERPHOUSE_API_KEY 환경변수
211
-
212
- def is_english(text):
213
- """
214
- text가 모두 ASCII 범위 내에 있으면 True, 아니면 False.
215
- """
216
- return all(ord(char) < 128 for char in text.replace(' ', '').replace('-', '').replace('_', ''))
217
-
218
-
219
- @lru_cache(maxsize=100)
220
- def translate_query(query, country):
221
- """
222
- 검색어(query)를 해당 country 언어로 번역.
223
- (단, is_english(query)가 True이거나, country가 특정 조건이면 그대로 리턴)
224
- """
225
- try:
226
- if is_english(query):
227
- return query
228
-
229
- if country in COUNTRY_LANGUAGES:
230
- target_lang = COUNTRY_LANGUAGES[country]
231
-
232
- url = "https://translate.googleapis.com/translate_a/single"
233
- params = {
234
- "client": "gtx",
235
- "sl": "auto",
236
- "tl": target_lang,
237
- "dt": "t",
238
- "q": query
239
- }
240
-
241
- session = requests.Session()
242
- retries = Retry(total=3, backoff_factor=0.5)
243
- session.mount('https://', HTTPAdapter(max_retries=retries))
244
-
245
- response = session.get(url, params=params, timeout=(5, 10))
246
- translated_text = response.json()[0][0][0]
247
- return translated_text
248
-
249
- return query
250
-
251
- except Exception as e:
252
- print(f"번역 오류: {str(e)}")
253
- return query
254
-
255
-
256
- def serphouse_search(query, country, page=1, num_result=10):
257
- """
258
- SerpHouse API에 실시간 'news' 검색 요청을 보내고,
259
- 결과를 특정 형식(오류 메시지 or 기사 목록)으로 반환한다.
260
- """
261
- url = "https://api.serphouse.com/serp/live"
262
-
263
- now = datetime.utcnow()
264
- yesterday = now - timedelta(days=1)
265
- date_range = f"{yesterday.strftime('%Y-%m-%d')},{now.strftime('%Y-%m-%d')}"
266
-
267
- # 검색어 번역
268
- translated_query = translate_query(query, country)
269
-
270
- payload = {
271
- "data": {
272
- "q": translated_query,
273
- "domain": "google.com",
274
- "loc": COUNTRY_LOCATIONS.get(country, "United States"),
275
- "lang": COUNTRY_LANGUAGES.get(country, "en"),
276
- "device": "desktop",
277
- "serp_type": "news",
278
- "page": str(page),
279
- "num": "100",
280
- "date_range": date_range,
281
- "sort_by": "date"
282
- }
283
- }
284
-
285
- headers = {
286
- "accept": "application/json",
287
- "content-type": "application/json",
288
- "authorization": f"Bearer {API_KEY}"
289
- }
290
-
291
- try:
292
- session = requests.Session()
293
-
294
- retries = Retry(
295
- total=5,
296
- backoff_factor=1,
297
- status_forcelist=[500, 502, 503, 504, 429],
298
- allowed_methods=["POST"]
299
- )
300
-
301
- adapter = HTTPAdapter(max_retries=retries)
302
- session.mount('http://', adapter)
303
- session.mount('https://', adapter)
304
-
305
- # API 호출
306
- response = session.post(
307
- url,
308
- json=payload,
309
- headers=headers,
310
- timeout=(30, 30)
311
- )
312
-
313
- response.raise_for_status()
314
- response_data = response.json()
315
-
316
- # 응답 데이터에서 기사 부분만 추출
317
- return format_results_from_raw({
318
- "results": response_data,
319
- "translated_query": translated_query
320
- })
321
-
322
- except requests.exceptions.Timeout:
323
- return ("검색 시간이 초과되었습니다. 잠시 후 다시 시도해주세요.", [])
324
- except requests.exceptions.RequestException as e:
325
- return (f"검색 중 오류가 발생했습니다: {str(e)}", [])
326
- except Exception as e:
327
- return (f"예기치 않은 오류가 발생했습니다: {str(e)}", [])
328
-
329
-
330
- def format_results_from_raw(response_data):
331
- """
332
- SerpHouse API raw 데이터(response_data)에서
333
- - error 가 있으면 ("Error: ...", [])
334
- - 정상인 경우 ("", [기사1, 기사2, ...]) 형태로 반환
335
- - 또한 한국 도메인/키워드를 가진 기사 제외 (필터링)
336
- """
337
- if "error" in response_data:
338
- return ("Error: " + response_data["error"], [])
339
-
340
- try:
341
- results = response_data["results"]
342
- translated_query = response_data["translated_query"]
343
-
344
- news_results = results.get('results', {}).get('results', {}).get('news', [])
345
- if not news_results:
346
- return ("검색 결과가 없습니다.", [])
347
-
348
- # 한국 도메인 / 한국 키워드 기사 제외
349
- korean_domains = [
350
- '.kr', 'korea', 'korean', 'yonhap', 'hankyung', 'chosun',
351
- 'donga', 'joins', 'hani', 'koreatimes', 'koreaherald'
352
- ]
353
- korean_keywords = [
354
- 'korea', 'korean', 'seoul', 'busan', 'incheon', 'daegu',
355
- 'gwangju', 'daejeon', 'ulsan', 'sejong'
356
- ]
357
-
358
- filtered_articles = []
359
- for idx, result in enumerate(news_results, 1):
360
- url = result.get("url", result.get("link", "")).lower()
361
- title = result.get("title", "").lower()
362
- channel = result.get("channel", result.get("source", "")).lower()
363
-
364
- # 한국 관련 기사 필터
365
- is_korean_content = (
366
- any(domain in url or domain in channel for domain in korean_domains) or
367
- any(keyword in title for keyword in korean_keywords)
368
- )
369
-
370
- if not is_korean_content:
371
- filtered_articles.append({
372
- "index": idx,
373
- "title": result.get("title", "제목 없음"),
374
- "link": url,
375
- "snippet": result.get("snippet", "내용 없음"),
376
- "channel": result.get("channel", result.get("source", "알 수 없음")),
377
- "time": result.get("time", result.get("date", "알 수 없는 시간")),
378
- "image_url": result.get("img", result.get("thumbnail", "")),
379
- "translated_query": translated_query
380
- })
381
-
382
- return ("", filtered_articles)
383
-
384
- except Exception as e:
385
- return (f"결과 처리 중 오류 발생: {str(e)}", [])
386
-
387
-
388
- #########################################################
389
- # 국가 설정
390
- #########################################################
391
-
392
- COUNTRY_LANGUAGES = {
393
- "United States": "en",
394
- "KOREA": "ko",
395
- "United Kingdom": "en",
396
- "Taiwan": "zh-TW",
397
- "Canada": "en",
398
- "Australia": "en",
399
- "Germany": "de",
400
- "France": "fr",
401
- "Japan": "ja",
402
- "China": "zh",
403
- "India": "hi",
404
- "Brazil": "pt",
405
- "Mexico": "es",
406
- "Russia": "ru",
407
- "Italy": "it",
408
- "Spain": "es",
409
- "Netherlands": "nl",
410
- "Singapore": "en",
411
- "Hong Kong": "zh-HK",
412
- "Indonesia": "id",
413
- "Malaysia": "ms",
414
- "Philippines": "tl",
415
- "Thailand": "th",
416
- "Vietnam": "vi",
417
- "Belgium": "nl",
418
- "Denmark": "da",
419
- "Finland": "fi",
420
- "Ireland": "en",
421
- "Norway": "no",
422
- "Poland": "pl",
423
- "Sweden": "sv",
424
- "Switzerland": "de",
425
- "Austria": "de",
426
- "Czech Republic": "cs",
427
- "Greece": "el",
428
- "Hungary": "hu",
429
- "Portugal": "pt",
430
- "Romania": "ro",
431
- "Turkey": "tr",
432
- "Israel": "he",
433
- "Saudi Arabia": "ar",
434
- "United Arab Emirates": "ar",
435
- "South Africa": "en",
436
- "Argentina": "es",
437
- "Chile": "es",
438
- "Colombia": "es",
439
- "Peru": "es",
440
- "Venezuela": "es",
441
- "New Zealand": "en",
442
- "Bangladesh": "bn",
443
- "Pakistan": "ur",
444
- "Egypt": "ar",
445
- "Morocco": "ar",
446
- "Nigeria": "en",
447
- "Kenya": "sw",
448
- "Ukraine": "uk",
449
- "Croatia": "hr",
450
- "Slovakia": "sk",
451
- "Bulgaria": "bg",
452
- "Serbia": "sr",
453
- "Estonia": "et",
454
- "Latvia": "lv",
455
- "Lithuania": "lt",
456
- "Slovenia": "sl",
457
- "Luxembourg": "Luxembourg",
458
- "Malta": "Malta",
459
- "Cyprus": "Cyprus",
460
- "Iceland": "is"
461
- }
462
-
463
- COUNTRY_LOCATIONS = {
464
- "United States": "United States",
465
- "KOREA": "kr",
466
- "United Kingdom": "United Kingdom",
467
- "Taiwan": "Taiwan",
468
- "Canada": "Canada",
469
- "Australia": "Australia",
470
- "Germany": "Germany",
471
- "France": "France",
472
- "Japan": "Japan",
473
- "China": "China",
474
- "India": "India",
475
- "Brazil": "Brazil",
476
- "Mexico": "Mexico",
477
- "Russia": "Russia",
478
- "Italy": "Italy",
479
- "Spain": "Spain",
480
- "Netherlands": "Netherlands",
481
- "Singapore": "Singapore",
482
- "Hong Kong": "Hong Kong",
483
- "Indonesia": "Indonesia",
484
- "Malaysia": "Malaysia",
485
- "Philippines": "Philippines",
486
- "Thailand": "Thailand",
487
- "Vietnam": "Vietnam",
488
- "Belgium": "Belgium",
489
- "Denmark": "Denmark",
490
- "Finland": "Finland",
491
- "Ireland": "Ireland",
492
- "Norway": "Norway",
493
- "Poland": "Poland",
494
- "Sweden": "Sweden",
495
- "Switzerland": "Switzerland",
496
- "Austria": "Austria",
497
- "Czech Republic": "Czech Republic",
498
- "Greece": "Greece",
499
- "Hungary": "Hungary",
500
- "Portugal": "Portugal",
501
- "Romania": "Romania",
502
- "Turkey": "Turkey",
503
- "Israel": "Israel",
504
- "Saudi Arabia": "Saudi Arabia",
505
- "United Arab Emirates": "United Arab Emirates",
506
- "South Africa": "South Africa",
507
- "Argentina": "Argentina",
508
- "Chile": "Chile",
509
- "Colombia": "Colombia",
510
- "Peru": "Peru",
511
- "Venezuela": "Venezuela",
512
- "New Zealand": "New Zealand",
513
- "Bangladesh": "Bangladesh",
514
- "Pakistan": "Pakistan",
515
- "Egypt": "Egypt",
516
- "Morocco": "Morocco",
517
- "Nigeria": "Nigeria",
518
- "Kenya": "Kenya",
519
- "Ukraine": "Ukraine",
520
- "Croatia": "Croatia",
521
- "Slovakia": "Slovakia",
522
- "Bulgaria": "Bulgaria",
523
- "Serbia": "Serbia",
524
- "Estonia": "et",
525
- "Latvia": "lv",
526
- "Lithuania": "lt",
527
- "Slovenia": "sl",
528
- "Luxembourg": "Luxembourg",
529
- "Malta": "Malta",
530
- "Cyprus": "Cyprus",
531
- "Iceland": "Iceland"
532
- }
533
-
534
-
535
- #########################################################
536
- # 검색/출력 함수 (기업 검색, 로드)
537
- #########################################################
538
-
539
- def search_company(company):
540
- """
541
- KOREAN_COMPANIES에 있는 company를 미국(United States) 기준으로 검색 + 감성 분석
542
- DB에 저장 후 기사 목록 + 분석 결과를 markdown으로 반환.
543
- """
544
- error_message, articles = serphouse_search(company, "United States")
545
- if not error_message and articles:
546
- analysis = analyze_sentiment_batch(articles, client)
547
- store_dict = {
548
- "articles": articles,
549
- "analysis": analysis
550
- }
551
- # DB 저장
552
- save_to_db(company, "United States", store_dict)
553
-
554
- # 결과 출력
555
- output = display_results(articles)
556
- output += f"\n\n### 분석 보고\n{analysis}\n"
557
- return output
558
- else:
559
- if error_message:
560
- return error_message
561
- return f"{company}에 대한 검색 결과가 없습니다."
562
-
563
-
564
- def load_company(company):
565
- """
566
- DB에 저장된 (company, United States) 검색 결과를 불러와 기사+분석 을 반환.
567
- """
568
- data, timestamp = load_from_db(company, "United States")
569
- if data:
570
- articles = data.get("articles", [])
571
- analysis = data.get("analysis", "")
572
-
573
- output = f"### {company} 검색 결과\n저장 시간: {timestamp}\n\n"
574
- output += display_results(articles)
575
- output += f"\n\n### 분석 보고\n{analysis}\n"
576
- return output
577
- return f"{company}에 대한 저장된 결과가 없습니다."
578
-
579
-
580
- #########################################################
581
- # 전체 통계
582
- #########################################################
583
-
584
- def show_stats():
585
- """
586
- (기존 "한국 기업 뉴스 분석 리포트") -> "EarnBOT 분석 리포트" 로 명칭 변경
587
- 각 기업의 최신 DB 기록(기사+분석) 수를 표시하고,
588
- 감성 분석 결과를 함께 출력.
589
- """
590
- conn = sqlite3.connect("search_results.db")
591
- c = conn.cursor()
592
-
593
- output = "## EarnBOT 분석 리포트\n\n"
594
-
595
- data_list = []
596
- for company in KOREAN_COMPANIES:
597
- c.execute("""
598
- SELECT results, timestamp
599
- FROM searches
600
- WHERE keyword = ?
601
- ORDER BY timestamp DESC
602
- LIMIT 1
603
- """, (company,))
604
-
605
- row = c.fetchone()
606
- if row:
607
- results_json, tstamp = row
608
- data_list.append((company, tstamp, results_json))
609
-
610
- conn.close()
611
-
612
- def analyze_data(item):
613
- comp, tstamp, results_json = item
614
- data = json.loads(results_json)
615
- articles = data.get("articles", [])
616
- analysis = data.get("analysis", "")
617
- count_articles = len(articles)
618
- return (comp, tstamp, count_articles, analysis)
619
-
620
- results_list = []
621
- with ThreadPoolExecutor(max_workers=5) as executor:
622
- futures = [executor.submit(analyze_data, dl) for dl in data_list]
623
- for future in as_completed(futures):
624
- results_list.append(future.result())
625
-
626
- for comp, tstamp, count, analysis in results_list:
627
- seoul_time = convert_to_seoul_time(tstamp)
628
- output += f"### {comp}\n"
629
- output += f"- 마지막 업데이트: {seoul_time}\n"
630
- output += f"- 저장된 기사 수: {count}건\n\n"
631
- if analysis:
632
- output += "#### 뉴스 감성 분석\n"
633
- output += f"{analysis}\n\n"
634
- output += "---\n\n"
635
-
636
- return output
637
-
638
-
639
- #########################################################
640
- # 전체 검색/출력 + 종합 보고
641
- #########################################################
642
-
643
- def search_all_companies():
644
- """
645
- KOREAN_COMPANIES 리스트에 대해 병렬 검색 + 분석
646
- """
647
- overall_result = "# [전체 검색 결과]\n\n"
648
-
649
- def do_search(comp):
650
- return comp, search_company(comp)
651
-
652
- with ThreadPoolExecutor(max_workers=5) as executor:
653
- futures = [executor.submit(do_search, c) for c in KOREAN_COMPANIES]
654
- for future in as_completed(futures):
655
- comp, res_text = future.result()
656
- overall_result += f"## {comp}\n"
657
- overall_result += res_text + "\n\n"
658
-
659
- return overall_result
660
-
661
-
662
- def load_all_companies():
663
- """
664
- KOREAN_COMPANIES 리스트에 대해 DB에 저장된 값(기사+분석) 일괄 출력
665
- """
666
- overall_result = "# [전체 출력 결과]\n\n"
667
- for comp in KOREAN_COMPANIES:
668
- overall_result += f"## {comp}\n"
669
- overall_result += load_company(comp)
670
- overall_result += "\n"
671
- return overall_result
672
-
673
-
674
- def full_summary_report():
675
- """
676
- 1) search_all_companies => 기사+분석 => DB 저장
677
- 2) load_all_companies => DB 로드
678
- 3) show_stats => 종합 감성 분석
679
- """
680
- search_result_text = search_all_companies()
681
- load_result_text = load_all_companies()
682
- stats_text = show_stats()
683
-
684
- combined_report = (
685
- "# 전체 분석 보고 요약\n\n"
686
- "아래 순서로 실행되었습니다:\n"
687
- "1. 모든 종목 검색(병렬) + 분석 => 2. 모든 종목 DB 결과 출력 => 3. 전체 감성 분석 통계\n\n"
688
- f"{search_result_text}\n\n"
689
- f"{load_result_text}\n\n"
690
- "## [전체 감성 분석 통계]\n\n"
691
- f"{stats_text}"
692
- )
693
- return combined_report
694
-
695
-
696
- #########################################################
697
- # (추가) 사용자 임의 검색 + 분석
698
- #########################################################
699
-
700
- def search_custom(query, country):
701
- """
702
- 1) query & country에 대해 검색 + 분석 => DB저장
703
- 2) DB에서 다시 로딩 => 기사 + 분석 결과 표시
704
- """
705
- error_message, articles = serphouse_search(query, country)
706
- if error_message:
707
- return f"오류 발생: {error_message}"
708
- if not articles:
709
- return "검색 결과가 없습니다."
710
-
711
- analysis = analyze_sentiment_batch(articles, client)
712
- save_data = {
713
- "articles": articles,
714
- "analysis": analysis
715
- }
716
- save_to_db(query, country, save_data)
717
-
718
- loaded_data, timestamp = load_from_db(query, country)
719
- if not loaded_data:
720
- return "DB에서 로드 실패"
721
-
722
- arts = loaded_data.get("articles", [])
723
- analy = loaded_data.get("analysis", "")
724
-
725
- out = f"## [사용자 임의 검색 결과]\n\n"
726
- out += f"**키워드**: {query}\n\n"
727
- out += f"**국가**: {country}\n\n"
728
- out += f"**저장 시간**: {timestamp}\n\n"
729
-
730
- out += display_results(arts)
731
- out += f"### 뉴스 감성 분석\n{analy}\n"
732
-
733
- return out
734
-
735
-
736
- #########################################################
737
- # (추가) 히스토리 함수
738
- #########################################################
739
-
740
- def get_custom_search_history():
741
- """
742
- KOREAN_COMPANIES 목록에 없는 keyword로 검색된 기록만 (id, label) 리스트로 반환.
743
- label 예: "12 | 2025-01-22 10:23:00 KST | Apple (United States)"
744
- """
745
- company_set = set(k.lower() for k in KOREAN_COMPANIES)
746
-
747
- conn = sqlite3.connect("search_results.db")
748
- c = conn.cursor()
749
- c.execute("""SELECT id, keyword, country, timestamp
750
- FROM searches
751
- ORDER BY timestamp DESC""")
752
- rows = c.fetchall()
753
- conn.close()
754
-
755
- history_list = []
756
- for sid, kw, cty, ts in rows:
757
- if kw.lower() not in company_set:
758
- display_time = convert_to_seoul_time(ts)
759
- label = f"{sid} | {display_time} | {kw} ({cty})"
760
- history_list.append((str(sid), label))
761
- return history_list
762
-
763
-
764
- def view_history_record(record_id):
765
- """
766
- 주어진 record_id 로부터 load_by_id() 로 로드한 기사+분석 결과를 Markdown 표시
767
- """
768
- if not record_id:
769
- return "기록이 없습니다."
770
-
771
- data = load_by_id(int(record_id))
772
- if not data:
773
- return "해당 ID의 기록이 없습니다."
774
-
775
- keyword = data["keyword"]
776
- country = data["country"]
777
- timestamp = data["timestamp"]
778
- stored = data["data"] # {"articles": [...], "analysis": ...}
779
-
780
- articles = stored.get("articles", [])
781
- analysis = stored.get("analysis", "")
782
-
783
- out = f"### [히스토리 검색 결과]\n\n"
784
- out += f"- ID: {record_id}\n"
785
- out += f"- 키워드: {keyword}\n"
786
- out += f"- 국가: {country}\n"
787
- out += f"- 저장 시간: {timestamp}\n\n"
788
- out += display_results(articles)
789
- out += f"\n\n### 분석 보고\n{analysis}\n"
790
- return out
791
-
792
-
793
- #########################################################
794
- # Gradio 인터페이스
795
- #########################################################
796
-
797
- ACCESS_TOKEN = os.getenv("HF_TOKEN")
798
- if not ACCESS_TOKEN:
799
- raise ValueError("HF_TOKEN environment variable is not set")
800
-
801
- client = OpenAI(
802
- base_url="https://api-inference.huggingface.co/v1/",
803
- api_key=ACCESS_TOKEN,
804
- )
805
-
806
- css = """
807
- /* 전역 스타일 */
808
- footer {visibility: hidden;}
809
-
810
- /* 기타 CSS 등... (이하 동일) */
811
- """
812
-
813
- with gr.Blocks(theme="Yntec/HaleyCH_Theme_Orange", css=css, title="NewsAI 서비스") as iface:
814
- init_db()
815
-
816
- # 원하는 탭들 구성
817
- with gr.Tabs():
818
- with gr.Tab("지정 자동 검색/분석"):
819
- gr.Markdown("## EarnBot: 글로벌 빅테크 기업 및 투자 전망 AI 자동 분석")
820
- gr.Markdown("- '전체 분석 보고 요약' 클릭 시 전체 자동 보고 생성.\n"
821
- "- 아래 개별 종목의 '검색(DB 자동 저장)'과 '출력(DB 자동 호출)'도 가능.\n"
822
- "- 하단 '수동 검색 히스토리'에서 이전에 수동 입력한 검색어 기록을 확인 가능.")
823
-
824
- with gr.Row():
825
- full_report_btn = gr.Button("전체 분석 보고 요약", variant="primary")
826
- full_report_display = gr.Markdown()
827
- full_report_btn.click(fn=full_summary_report, outputs=full_report_display)
828
-
829
- # 개별 종목 검색/출력
830
- with gr.Column():
831
- for i in range(0, len(KOREAN_COMPANIES), 2):
832
- with gr.Row():
833
- # 왼쪽 열
834
- with gr.Column():
835
- company = KOREAN_COMPANIES[i]
836
- with gr.Group():
837
- gr.Markdown(f"### {company}")
838
- with gr.Row():
839
- search_btn = gr.Button("검색", variant="primary")
840
- load_btn = gr.Button("출력", variant="secondary")
841
- result_display = gr.Markdown()
842
-
843
- search_btn.click(
844
- fn=lambda c=company: search_company(c),
845
- inputs=[],
846
- outputs=result_display
847
- )
848
- load_btn.click(
849
- fn=lambda c=company: load_company(c),
850
- inputs=[],
851
- outputs=result_display
852
- )
853
-
854
- # 오른쪽 열
855
- if i + 1 < len(KOREAN_COMPANIES):
856
- with gr.Column():
857
- company = KOREAN_COMPANIES[i + 1]
858
- with gr.Group():
859
- gr.Markdown(f"### {company}")
860
- with gr.Row():
861
- search_btn = gr.Button("검색", variant="primary")
862
- load_btn = gr.Button("출력", variant="secondary")
863
- result_display = gr.Markdown()
864
-
865
- search_btn.click(
866
- fn=lambda c=company: search_company(c),
867
- inputs=[],
868
- outputs=result_display
869
- )
870
- load_btn.click(
871
- fn=lambda c=company: load_company(c),
872
- inputs=[],
873
- outputs=result_display
874
- )
875
-
876
- gr.Markdown("---")
877
- gr.Markdown("### 수동 검색 히스토리")
878
-
879
- with gr.Row():
880
- refresh_hist_btn = gr.Button("히스토리 갱신", variant="secondary")
881
-
882
- history_dropdown = gr.Dropdown(label="검색 기록 목록", choices=[], value=None)
883
- hist_view_btn = gr.Button("보기", variant="primary")
884
- hist_result_display = gr.Markdown()
885
-
886
- def update_history_dropdown():
887
- history_list = get_custom_search_history()
888
- choice_list = []
889
- for (id_val, label) in history_list:
890
- choice_list.append(label)
891
- return gr.update(choices=choice_list, value=None)
892
-
893
- refresh_hist_btn.click(
894
- fn=update_history_dropdown,
895
- inputs=[],
896
- outputs=history_dropdown
897
- )
898
-
899
- def show_history_record(selected_label):
900
- if not selected_label:
901
- return "히스토리가 선택되지 않았습니다."
902
- splitted = selected_label.split("|")
903
- if len(splitted) < 2:
904
- return "형식 오류"
905
- record_id = splitted[0].strip()
906
- return view_history_record(record_id)
907
-
908
- hist_view_btn.click(
909
- fn=show_history_record,
910
- inputs=[history_dropdown],
911
- outputs=hist_result_display
912
- )
913
-
914
- # 두 번째 탭: "수동 검색/분석"
915
- with gr.Tab("수동 검색/분석"):
916
- gr.Markdown("## 사용자 임의 키워드 + 국가 검색/분석")
917
- gr.Markdown("검색 결과가 DB에 저장되며, 아래 '수동 검색 히스토리'에서도 확인 가능합니다.")
918
-
919
- with gr.Row():
920
- with gr.Column():
921
- user_input = gr.Textbox(
922
- label="검색어 입력",
923
- placeholder="예) Apple, Samsung 등 자유롭게"
924
- )
925
- with gr.Column():
926
- country_selection = gr.Dropdown(
927
- choices=list(COUNTRY_LOCATIONS.keys()),
928
- value="United States",
929
- label="국가 선택"
930
- )
931
- with gr.Column():
932
- custom_search_btn = gr.Button("실행", variant="primary")
933
-
934
- custom_search_output = gr.Markdown()
935
-
936
- custom_search_btn.click(
937
- fn=search_custom,
938
- inputs=[user_input, country_selection],
939
- outputs=custom_search_output
940
- )
941
-
942
- gr.Markdown("---")
943
- gr.Markdown("### 수동 검색 히스토리 (두 번째 탭)")
944
-
945
- with gr.Row():
946
- refresh_hist_btn2 = gr.Button("히스토리 갱신", variant="secondary")
947
-
948
- history_dropdown2 = gr.Dropdown(label="검색 기록 목록", choices=[], value=None)
949
- hist_view_btn2 = gr.Button("보기", variant="primary")
950
- hist_result_display2 = gr.Markdown()
951
-
952
- refresh_hist_btn2.click(
953
- fn=update_history_dropdown,
954
- inputs=[],
955
- outputs=history_dropdown2
956
- )
957
-
958
- hist_view_btn2.click(
959
- fn=show_history_record,
960
- inputs=[history_dropdown2],
961
- outputs=hist_result_display2
962
- )
963
-
964
-
965
- iface.launch(
966
- server_name="0.0.0.0",
967
- server_port=7860,
968
- share=True,
969
- ssl_verify=False,
970
- show_error=True
971
- )