|
|
|
|
|
""" |
|
|
AI 뉴스 & 허깅페이스 트렌딩 분석 웹 앱 (Flask 버전) - 완전판 |
|
|
파일명: app.py |
|
|
|
|
|
실행 방법: |
|
|
1. pip install Flask requests beautifulsoup4 lxml gunicorn |
|
|
2. python app.py |
|
|
3. 브라우저에서 http://localhost:8080 접속 |
|
|
|
|
|
프로덕션 실행: |
|
|
gunicorn -w 4 -b 0.0.0.0:8080 app:app |
|
|
""" |
|
|
|
|
|
from flask import Flask, render_template_string, jsonify, request |
|
|
import requests |
|
|
from bs4 import BeautifulSoup |
|
|
import json |
|
|
from datetime import datetime |
|
|
from typing import List, Dict, Optional |
|
|
import os |
|
|
import sys |
|
|
|
|
|
|
|
|
app = Flask(__name__) |
|
|
app.config['JSON_AS_ASCII'] = False |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
HTML_TEMPLATE = """ |
|
|
<!DOCTYPE html> |
|
|
<html lang="ko"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>AI 뉴스 & 허깅페이스 트렌딩 분석 시스템</title> |
|
|
<style> |
|
|
* { |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
box-sizing: border-box; |
|
|
} |
|
|
|
|
|
body { |
|
|
font-family: 'Segoe UI', 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif; |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
padding: 20px; |
|
|
color: #333; |
|
|
min-height: 100vh; |
|
|
} |
|
|
|
|
|
.container { |
|
|
max-width: 1400px; |
|
|
margin: 0 auto; |
|
|
background: white; |
|
|
border-radius: 20px; |
|
|
padding: 40px; |
|
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3); |
|
|
} |
|
|
|
|
|
h1 { |
|
|
text-align: center; |
|
|
color: #667eea; |
|
|
margin-bottom: 10px; |
|
|
font-size: 2.8em; |
|
|
font-weight: 800; |
|
|
} |
|
|
|
|
|
.subtitle { |
|
|
text-align: center; |
|
|
color: #666; |
|
|
margin-bottom: 40px; |
|
|
font-size: 1.2em; |
|
|
} |
|
|
|
|
|
.stats { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); |
|
|
gap: 25px; |
|
|
margin-bottom: 50px; |
|
|
} |
|
|
|
|
|
.stat-card { |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
color: white; |
|
|
padding: 30px; |
|
|
border-radius: 15px; |
|
|
text-align: center; |
|
|
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4); |
|
|
transform: translateY(0); |
|
|
transition: transform 0.3s, box-shadow 0.3s; |
|
|
} |
|
|
|
|
|
.stat-card:hover { |
|
|
transform: translateY(-5px); |
|
|
box-shadow: 0 12px 30px rgba(102, 126, 234, 0.6); |
|
|
} |
|
|
|
|
|
.stat-number { |
|
|
font-size: 3.5em; |
|
|
font-weight: bold; |
|
|
margin-bottom: 10px; |
|
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.2); |
|
|
} |
|
|
|
|
|
.stat-label { |
|
|
font-size: 1.2em; |
|
|
opacity: 0.95; |
|
|
font-weight: 500; |
|
|
} |
|
|
|
|
|
.category-section { |
|
|
margin-bottom: 50px; |
|
|
} |
|
|
|
|
|
.category-title { |
|
|
background: linear-gradient(90deg, #667eea, #764ba2); |
|
|
color: white; |
|
|
padding: 18px 25px; |
|
|
border-radius: 12px; |
|
|
font-size: 1.6em; |
|
|
font-weight: 700; |
|
|
margin-bottom: 25px; |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3); |
|
|
} |
|
|
|
|
|
.news-item { |
|
|
background: #f8f9fa; |
|
|
padding: 25px; |
|
|
border-radius: 12px; |
|
|
margin-bottom: 20px; |
|
|
border-left: 6px solid #667eea; |
|
|
transition: all 0.3s; |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
.news-item:hover { |
|
|
transform: translateX(8px); |
|
|
box-shadow: 0 8px 20px rgba(0,0,0,0.12); |
|
|
background: #f0f4ff; |
|
|
} |
|
|
|
|
|
.news-title { |
|
|
font-size: 1.3em; |
|
|
font-weight: 700; |
|
|
color: #2c3e50; |
|
|
margin-bottom: 12px; |
|
|
line-height: 1.5; |
|
|
} |
|
|
|
|
|
.news-meta { |
|
|
color: #7f8c8d; |
|
|
font-size: 0.95em; |
|
|
margin-bottom: 15px; |
|
|
display: flex; |
|
|
gap: 20px; |
|
|
flex-wrap: wrap; |
|
|
} |
|
|
|
|
|
.news-link { |
|
|
display: inline-block; |
|
|
background: #667eea; |
|
|
color: white; |
|
|
padding: 10px 20px; |
|
|
border-radius: 8px; |
|
|
text-decoration: none; |
|
|
font-size: 0.95em; |
|
|
font-weight: 600; |
|
|
transition: all 0.3s; |
|
|
} |
|
|
|
|
|
.news-link:hover { |
|
|
background: #764ba2; |
|
|
transform: scale(1.05); |
|
|
box-shadow: 0 4px 10px rgba(102, 126, 234, 0.4); |
|
|
} |
|
|
|
|
|
.hf-section { |
|
|
background: linear-gradient(135deg, #f0f4ff 0%, #e8f0fe 100%); |
|
|
padding: 40px; |
|
|
border-radius: 20px; |
|
|
margin-top: 50px; |
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.08); |
|
|
} |
|
|
|
|
|
.hf-title { |
|
|
font-size: 2.2em; |
|
|
color: #667eea; |
|
|
margin-bottom: 30px; |
|
|
text-align: center; |
|
|
font-weight: 800; |
|
|
} |
|
|
|
|
|
.model-grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); |
|
|
gap: 25px; |
|
|
margin-top: 30px; |
|
|
} |
|
|
|
|
|
.model-card { |
|
|
background: white; |
|
|
padding: 25px; |
|
|
border-radius: 12px; |
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.1); |
|
|
transition: all 0.3s; |
|
|
border-top: 4px solid #667eea; |
|
|
} |
|
|
|
|
|
.model-card:hover { |
|
|
transform: translateY(-5px); |
|
|
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3); |
|
|
} |
|
|
|
|
|
.model-name { |
|
|
font-weight: 700; |
|
|
color: #667eea; |
|
|
margin-bottom: 15px; |
|
|
font-size: 1.15em; |
|
|
word-break: break-word; |
|
|
} |
|
|
|
|
|
.model-stats { |
|
|
font-size: 0.95em; |
|
|
color: #555; |
|
|
margin-bottom: 15px; |
|
|
line-height: 1.8; |
|
|
} |
|
|
|
|
|
.model-task { |
|
|
background: #e8f0fe; |
|
|
color: #667eea; |
|
|
padding: 6px 12px; |
|
|
border-radius: 20px; |
|
|
font-size: 0.85em; |
|
|
display: inline-block; |
|
|
margin-bottom: 15px; |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.button-group { |
|
|
text-align: center; |
|
|
margin: 40px 0; |
|
|
} |
|
|
|
|
|
.refresh-btn { |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
color: white; |
|
|
border: none; |
|
|
padding: 18px 50px; |
|
|
font-size: 1.2em; |
|
|
font-weight: 700; |
|
|
border-radius: 50px; |
|
|
cursor: pointer; |
|
|
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4); |
|
|
transition: all 0.3s; |
|
|
margin: 0 10px; |
|
|
} |
|
|
|
|
|
.refresh-btn:hover { |
|
|
transform: scale(1.08); |
|
|
box-shadow: 0 12px 30px rgba(102, 126, 234, 0.6); |
|
|
} |
|
|
|
|
|
.api-btn { |
|
|
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); |
|
|
color: white; |
|
|
border: none; |
|
|
padding: 18px 50px; |
|
|
font-size: 1.2em; |
|
|
font-weight: 700; |
|
|
border-radius: 50px; |
|
|
cursor: pointer; |
|
|
box-shadow: 0 8px 20px rgba(17, 153, 142, 0.4); |
|
|
transition: all 0.3s; |
|
|
margin: 0 10px; |
|
|
} |
|
|
|
|
|
.api-btn:hover { |
|
|
transform: scale(1.08); |
|
|
box-shadow: 0 12px 30px rgba(17, 153, 142, 0.6); |
|
|
} |
|
|
|
|
|
.loading { |
|
|
text-align: center; |
|
|
padding: 60px; |
|
|
font-size: 1.8em; |
|
|
color: #667eea; |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.timestamp { |
|
|
text-align: center; |
|
|
color: #999; |
|
|
margin-top: 40px; |
|
|
font-size: 1em; |
|
|
padding: 20px; |
|
|
background: #f8f9fa; |
|
|
border-radius: 10px; |
|
|
} |
|
|
|
|
|
.footer { |
|
|
text-align: center; |
|
|
margin-top: 50px; |
|
|
padding-top: 30px; |
|
|
border-top: 2px solid #e0e0e0; |
|
|
color: #666; |
|
|
} |
|
|
|
|
|
.badge { |
|
|
display: inline-block; |
|
|
background: #ff6b6b; |
|
|
color: white; |
|
|
padding: 4px 10px; |
|
|
border-radius: 12px; |
|
|
font-size: 0.75em; |
|
|
font-weight: 600; |
|
|
margin-left: 8px; |
|
|
} |
|
|
|
|
|
@media (max-width: 768px) { |
|
|
.container { |
|
|
padding: 20px; |
|
|
} |
|
|
|
|
|
h1 { |
|
|
font-size: 2em; |
|
|
} |
|
|
|
|
|
.stats { |
|
|
grid-template-columns: repeat(2, 1fr); |
|
|
} |
|
|
|
|
|
.model-grid { |
|
|
grid-template-columns: 1fr; |
|
|
} |
|
|
|
|
|
.button-group { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 15px; |
|
|
} |
|
|
|
|
|
.refresh-btn, .api-btn { |
|
|
margin: 0; |
|
|
width: 100%; |
|
|
} |
|
|
} |
|
|
|
|
|
/* 애니메이션 */ |
|
|
@keyframes fadeIn { |
|
|
from { |
|
|
opacity: 0; |
|
|
transform: translateY(20px); |
|
|
} |
|
|
to { |
|
|
opacity: 1; |
|
|
transform: translateY(0); |
|
|
} |
|
|
} |
|
|
|
|
|
.news-item { |
|
|
animation: fadeIn 0.5s ease-out; |
|
|
} |
|
|
|
|
|
.model-card { |
|
|
animation: fadeIn 0.5s ease-out; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="container"> |
|
|
<h1>🤖 AI 뉴스 & 허깅페이스 트렌딩</h1> |
|
|
<p class="subtitle">실시간 AI 산업 동향 분석 시스템 📊</p> |
|
|
|
|
|
<!-- 통계 카드 --> |
|
|
<div class="stats"> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-number">{{ stats.total_news }}</div> |
|
|
<div class="stat-label">📰 총 뉴스</div> |
|
|
</div> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-number">{{ stats.categories }}</div> |
|
|
<div class="stat-label">📁 카테고리</div> |
|
|
</div> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-number">{{ stats.hf_models }}</div> |
|
|
<div class="stat-label">🤗 HF 모델</div> |
|
|
</div> |
|
|
<div class="stat-card"> |
|
|
<div class="stat-number">{{ stats.hf_spaces }}</div> |
|
|
<div class="stat-label">🚀 HF 스페이스</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<!-- 카테고리별 뉴스 --> |
|
|
{% for category, articles in news_by_category.items() %} |
|
|
<div class="category-section"> |
|
|
<div class="category-title"> |
|
|
<span>📌 {{ category }}</span> |
|
|
<span class="badge">{{ articles|length }}건</span> |
|
|
</div> |
|
|
{% for article in articles %} |
|
|
<div class="news-item"> |
|
|
<div class="news-title">{{ loop.index }}. {{ article.title }}</div> |
|
|
<div class="news-meta"> |
|
|
<span>📅 {{ article.date }}</span> |
|
|
<span>📰 {{ article.source }}</span> |
|
|
</div> |
|
|
<a href="{{ article.url }}" target="_blank" class="news-link"> |
|
|
🔗 기사 전문 보기 |
|
|
</a> |
|
|
</div> |
|
|
{% endfor %} |
|
|
</div> |
|
|
{% endfor %} |
|
|
|
|
|
<!-- 허깅페이스 트렌딩 모델 --> |
|
|
<div class="hf-section"> |
|
|
<div class="hf-title">🤗 허깅페이스 트렌딩 모델 TOP 10</div> |
|
|
|
|
|
{% if hf_models|length > 0 %} |
|
|
<div class="model-grid"> |
|
|
{% for model in hf_models[:10] %} |
|
|
<div class="model-card"> |
|
|
<div class="model-name"> |
|
|
{{ loop.index }}. {{ model.name }} |
|
|
</div> |
|
|
<div class="model-task"> |
|
|
🏷️ {{ model.task }} |
|
|
</div> |
|
|
<div class="model-stats"> |
|
|
📊 다운로드: <strong>{{ "{:,}".format(model.downloads) }}</strong><br> |
|
|
❤️ 좋아요: <strong>{{ "{:,}".format(model.likes) }}</strong> |
|
|
</div> |
|
|
<a href="{{ model.url }}" target="_blank" class="news-link"> |
|
|
🔗 모델 페이지 방문 |
|
|
</a> |
|
|
</div> |
|
|
{% endfor %} |
|
|
</div> |
|
|
{% else %} |
|
|
<div class="loading"> |
|
|
⚠️ 모델 데이터를 불러오지 못했습니다. |
|
|
</div> |
|
|
{% endif %} |
|
|
</div> |
|
|
|
|
|
<!-- 버튼 그룹 --> |
|
|
<div class="button-group"> |
|
|
<button class="refresh-btn" onclick="location.reload()"> |
|
|
🔄 새로고침 |
|
|
</button> |
|
|
<button class="api-btn" onclick="window.open('/api/data', '_blank')"> |
|
|
📊 JSON API 보기 |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
<!-- 타임스탬프 --> |
|
|
<div class="timestamp"> |
|
|
⏰ 마지막 업데이트: {{ timestamp }} |
|
|
</div> |
|
|
|
|
|
<!-- 푸터 --> |
|
|
<div class="footer"> |
|
|
<p>🤖 AI 뉴스 분석 시스템 v1.0</p> |
|
|
<p style="margin-top: 10px; font-size: 0.9em;"> |
|
|
데이터 출처: AI Times, Hugging Face |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
// 자동 새로고침 (5분마다) - 선택사항 |
|
|
// setTimeout(() => location.reload(), 5 * 60 * 1000); |
|
|
|
|
|
console.log('✅ AI 뉴스 분석 시스템 로드 완료'); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AINewsAnalyzer: |
|
|
"""AI 뉴스 및 허깅페이스 트렌딩 분석기""" |
|
|
|
|
|
def __init__(self, fireworks_api_key: Optional[str] = None, brave_api_key: Optional[str] = None): |
|
|
""" |
|
|
Args: |
|
|
fireworks_api_key: Fireworks AI API 키 (선택) |
|
|
brave_api_key: Brave Search API 키 (선택) |
|
|
""" |
|
|
self.fireworks_api_key = fireworks_api_key or os.getenv('FIREWORKS_API_KEY') |
|
|
self.brave_api_key = brave_api_key or os.getenv('BRAVE_API_KEY') |
|
|
|
|
|
|
|
|
self.categories = { |
|
|
"산업동향": ["산업", "기업", "투자", "인수", "파트너십", "시장", "MS", "구글", "아마존", "소프트뱅크"], |
|
|
"기술혁신": ["기술", "모델", "알고리즘", "개발", "연구", "논문", "삼성", "SAIT"], |
|
|
"제품출시": ["출시", "공개", "발표", "서비스", "제품", "챗GPT", "소라", "팬서"], |
|
|
"정책규제": ["규제", "정책", "법", "정부", "제재", "EU", "투자"], |
|
|
"보안이슈": ["보안", "취약점", "해킹", "위험", "프라이버시"], |
|
|
} |
|
|
|
|
|
self.huggingface_data = { |
|
|
"models": [], |
|
|
"spaces": [] |
|
|
} |
|
|
|
|
|
self.news_data = [] |
|
|
|
|
|
def fetch_huggingface_trending(self) -> Dict: |
|
|
"""허깅페이스 트렌딩 모델 수집""" |
|
|
print("🤗 허깅페이스 트렌딩 정보 수집 중...") |
|
|
|
|
|
try: |
|
|
models_url = "https://huggingface.co/api/models" |
|
|
params = { |
|
|
'sort': 'trending', |
|
|
'limit': 30 |
|
|
} |
|
|
|
|
|
response = requests.get(models_url, params=params, timeout=15) |
|
|
|
|
|
if response.status_code == 200: |
|
|
models = response.json() |
|
|
|
|
|
for model in models[:30]: |
|
|
self.huggingface_data['models'].append({ |
|
|
'name': model.get('id', 'Unknown'), |
|
|
'downloads': model.get('downloads', 0), |
|
|
'likes': model.get('likes', 0), |
|
|
'task': model.get('pipeline_tag', 'N/A'), |
|
|
'url': f"https://huggingface.co/{model.get('id', '')}" |
|
|
}) |
|
|
|
|
|
print(f"✅ {len(self.huggingface_data['models'])}개 트렌딩 모델 수집 완료") |
|
|
else: |
|
|
print(f"⚠️ 모델 API 오류: {response.status_code}") |
|
|
|
|
|
except Exception as e: |
|
|
print(f"❌ 모델 수집 오류: {e}") |
|
|
|
|
|
|
|
|
sample_spaces = [ |
|
|
{"name": "Wan2.2-5B", "title": "고품질 비디오 생성", "url": "https://huggingface.co/spaces/"}, |
|
|
{"name": "FLUX-Image", "title": "텍스트→이미지 생성", "url": "https://huggingface.co/spaces/"}, |
|
|
{"name": "DeepSeek-App", "title": "AI 앱 생성기", "url": "https://huggingface.co/spaces/"}, |
|
|
] |
|
|
|
|
|
self.huggingface_data['spaces'] = sample_spaces |
|
|
|
|
|
return self.huggingface_data |
|
|
|
|
|
def create_sample_news(self) -> List[Dict]: |
|
|
"""오늘의 AI 뉴스 샘플 데이터 (2025-10-10 기준)""" |
|
|
sample_news = [ |
|
|
{ |
|
|
'title': 'MS "챗GPT 수요 폭증으로 데이터센터 부족...2026년까지 지속"', |
|
|
'url': 'https://www.aitimes.com/news/articleView.html?idxno=203055', |
|
|
'date': '10-10 15:10', |
|
|
'source': 'AI Times', |
|
|
'category': '산업동향' |
|
|
}, |
|
|
{ |
|
|
'title': '미국, UAE에 GPU 판매 일부 승인...엔비디아 시총 5조달러 눈앞', |
|
|
'url': 'https://www.aitimes.com/news/articleView.html?idxno=203053', |
|
|
'date': '10-10 14:46', |
|
|
'source': 'AI Times', |
|
|
'category': '산업동향' |
|
|
}, |
|
|
{ |
|
|
'title': '오픈AI, 저렴한 챗GPT 고 요금제 아시아 16개국으로 확대', |
|
|
'url': 'https://www.aitimes.com/news/articleView.html?idxno=203054', |
|
|
'date': '10-10 14:15', |
|
|
'source': 'AI Times', |
|
|
'category': '제품출시' |
|
|
}, |
|
|
{ |
|
|
'title': '인텔, 18A 공정으로 자체 제작한 노트북용 칩 팬서 레이크 공개', |
|
|
'url': 'https://www.aitimes.com/news/articleView.html?idxno=203057', |
|
|
'date': '10-10 14:03', |
|
|
'source': 'AI Times', |
|
|
'category': '제품출시' |
|
|
}, |
|
|
{ |
|
|
'title': '소라, 챗GPT보다 빨리 100만 다운로드 돌파', |
|
|
'url': 'https://www.aitimes.com/news/articleView.html?idxno=203045', |
|
|
'date': '10-10 12:55', |
|
|
'source': 'AI Times', |
|
|
'category': '제품출시' |
|
|
}, |
|
|
{ |
|
|
'title': '구글·아마존, 기업용 AI 서비스 나란히 출시', |
|
|
'url': 'https://www.aitimes.com/news/articleView.html?idxno=203047', |
|
|
'date': '10-10 12:41', |
|
|
'source': 'AI Times', |
|
|
'category': '제품출시' |
|
|
}, |
|
|
{ |
|
|
'title': '삼성 SAIT, 거대 모델 능가하는 초소형 추론 모델 TRM 공개', |
|
|
'url': 'https://www.aitimes.com/news/articleView.html?idxno=203035', |
|
|
'date': '10-09 21:22', |
|
|
'source': 'AI Times', |
|
|
'category': '기술혁신' |
|
|
}, |
|
|
{ |
|
|
'title': '구글, GUI 에이전트 제미나이 2.5 컴퓨터 유즈 공개', |
|
|
'url': 'https://www.aitimes.com/news/articleView.html?idxno=203039', |
|
|
'date': '10-09 20:57', |
|
|
'source': 'AI Times', |
|
|
'category': '기술혁신' |
|
|
}, |
|
|
{ |
|
|
'title': 'EU, 핵심 산업 AX 위한 1.6조 규모 투자 계획 발표', |
|
|
'url': 'https://www.aitimes.com/news/articleView.html?idxno=203041', |
|
|
'date': '10-09 18:51', |
|
|
'source': 'AI Times', |
|
|
'category': '정책규제' |
|
|
}, |
|
|
{ |
|
|
'title': '소프트뱅크, ABB 로봇 사업부 7.6조원에 인수', |
|
|
'url': 'https://www.aitimes.com/news/articleView.html?idxno=203034', |
|
|
'date': '10-09 18:07', |
|
|
'source': 'AI Times', |
|
|
'category': '산업동향' |
|
|
} |
|
|
] |
|
|
|
|
|
self.news_data = sample_news |
|
|
return sample_news |
|
|
|
|
|
def categorize_news(self, news_list: List[Dict]) -> List[Dict]: |
|
|
"""뉴스 카테고리 자동 분류""" |
|
|
for news in news_list: |
|
|
if 'category' not in news or news['category'] == '기타': |
|
|
title = news['title'].lower() |
|
|
news['category'] = "기타" |
|
|
|
|
|
for category, keywords in self.categories.items(): |
|
|
if any(keyword.lower() in title for keyword in keywords): |
|
|
news['category'] = category |
|
|
break |
|
|
|
|
|
return news_list |
|
|
|
|
|
def get_data(self) -> Dict: |
|
|
"""모든 데이터 수집 및 반환""" |
|
|
|
|
|
news = self.create_sample_news() |
|
|
news = self.categorize_news(news) |
|
|
|
|
|
|
|
|
hf_data = self.fetch_huggingface_trending() |
|
|
|
|
|
|
|
|
news_by_category = {} |
|
|
for article in news: |
|
|
category = article['category'] |
|
|
if category not in news_by_category: |
|
|
news_by_category[category] = [] |
|
|
news_by_category[category].append(article) |
|
|
|
|
|
|
|
|
stats = { |
|
|
'total_news': len(news), |
|
|
'categories': len(news_by_category), |
|
|
'hf_models': len(hf_data['models']), |
|
|
'hf_spaces': len(hf_data['spaces']) |
|
|
} |
|
|
|
|
|
return { |
|
|
'news_by_category': news_by_category, |
|
|
'hf_models': hf_data['models'], |
|
|
'hf_spaces': hf_data['spaces'], |
|
|
'stats': stats, |
|
|
'timestamp': datetime.now().strftime('%Y년 %m월 %d일 %H:%M:%S') |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/') |
|
|
def index(): |
|
|
"""메인 페이지""" |
|
|
try: |
|
|
analyzer = AINewsAnalyzer() |
|
|
data = analyzer.get_data() |
|
|
return render_template_string(HTML_TEMPLATE, **data) |
|
|
except Exception as e: |
|
|
return f""" |
|
|
<html> |
|
|
<body style="font-family: Arial; padding: 50px; text-align: center;"> |
|
|
<h1 style="color: #e74c3c;">⚠️ 오류 발생</h1> |
|
|
<p>데이터를 불러오는 중 오류가 발생했습니다.</p> |
|
|
<p style="color: #7f8c8d;">{str(e)}</p> |
|
|
<button onclick="location.reload()" style="padding: 10px 20px; font-size: 16px; margin-top: 20px; cursor: pointer;"> |
|
|
🔄 새로고침 |
|
|
</button> |
|
|
</body> |
|
|
</html> |
|
|
""", 500 |
|
|
|
|
|
|
|
|
@app.route('/api/data') |
|
|
def api_data(): |
|
|
"""JSON API 엔드포인트""" |
|
|
try: |
|
|
analyzer = AINewsAnalyzer() |
|
|
data = analyzer.get_data() |
|
|
return jsonify({ |
|
|
'success': True, |
|
|
'data': data, |
|
|
'timestamp': datetime.now().isoformat() |
|
|
}) |
|
|
except Exception as e: |
|
|
return jsonify({ |
|
|
'success': False, |
|
|
'error': str(e), |
|
|
'timestamp': datetime.now().isoformat() |
|
|
}), 500 |
|
|
|
|
|
|
|
|
@app.route('/health') |
|
|
def health(): |
|
|
"""헬스 체크 엔드포인트""" |
|
|
return jsonify({ |
|
|
"status": "healthy", |
|
|
"service": "AI News Analyzer", |
|
|
"version": "1.0.0", |
|
|
"timestamp": datetime.now().isoformat() |
|
|
}) |
|
|
|
|
|
|
|
|
@app.route('/api/news') |
|
|
def api_news(): |
|
|
"""뉴스만 반환하는 API""" |
|
|
try: |
|
|
analyzer = AINewsAnalyzer() |
|
|
news = analyzer.create_sample_news() |
|
|
return jsonify({ |
|
|
'success': True, |
|
|
'count': len(news), |
|
|
'news': news |
|
|
}) |
|
|
except Exception as e: |
|
|
return jsonify({ |
|
|
'success': False, |
|
|
'error': str(e) |
|
|
}), 500 |
|
|
|
|
|
|
|
|
@app.route('/api/hf-models') |
|
|
def api_hf_models(): |
|
|
"""허깅페이스 모델만 반환하는 API""" |
|
|
try: |
|
|
analyzer = AINewsAnalyzer() |
|
|
hf_data = analyzer.fetch_huggingface_trending() |
|
|
return jsonify({ |
|
|
'success': True, |
|
|
'count': len(hf_data['models']), |
|
|
'models': hf_data['models'] |
|
|
}) |
|
|
except Exception as e: |
|
|
return jsonify({ |
|
|
'success': False, |
|
|
'error': str(e) |
|
|
}), 500 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__': |
|
|
|
|
|
port = int(os.environ.get('PORT', 8080)) |
|
|
|
|
|
|
|
|
debug = os.environ.get('DEBUG', 'False').lower() == 'true' |
|
|
|
|
|
print(f""" |
|
|
╔════════════════════════════════════════════════════════════╗ |
|
|
║ ║ |
|
|
║ 🤖 AI 뉴스 & 허깅페이스 트렌딩 웹 앱 시작! ║ |
|
|
║ ║ |
|
|
╚════════════════════════════════════════════════════════════╝ |
|
|
|
|
|
🚀 Flask 서버 시작 중... |
|
|
📍 메인 페이지: http://localhost:{port} |
|
|
📊 JSON API: http://localhost:{port}/api/data |
|
|
📰 뉴스 API: http://localhost:{port}/api/news |
|
|
🤗 모델 API: http://localhost:{port}/api/hf-models |
|
|
💚 Health Check: http://localhost:{port}/health |
|
|
|
|
|
{'🐛 디버그 모드: 활성화' if debug else '⚡ 프로덕션 모드: 최적화됨'} |
|
|
|
|
|
브라우저에서 위 URL을 열어주세요! |
|
|
종료하려면 Ctrl+C를 누르세요. |
|
|
""") |
|
|
|
|
|
try: |
|
|
app.run( |
|
|
host='0.0.0.0', |
|
|
port=port, |
|
|
debug=debug, |
|
|
threaded=True |
|
|
) |
|
|
except KeyboardInterrupt: |
|
|
print("\n\n👋 서버를 종료합니다. 안녕히 가세요!") |
|
|
sys.exit(0) |
|
|
except Exception as e: |
|
|
print(f"\n❌ 서버 시작 실패: {e}") |
|
|
sys.exit(1) |