Today / app.py
ginipick's picture
Update app.py
3d9dfc6 verified
raw
history blame
27.1 kB
# -*- coding: utf-8 -*-
"""
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
# Flask μ•± μ΄ˆκΈ°ν™”
app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False # ν•œκΈ€ JSON 지원
# ============================================
# HTML ν…œν”Œλ¦Ώ (μ™„μ „νŒ)
# ============================================
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>
"""
# ============================================
# AINewsAnalyzer 클래슀
# ============================================
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')
}
# ============================================
# Flask 라우트 μ •μ˜
# ============================================
@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__':
# ν™˜κ²½ λ³€μˆ˜μ—μ„œ 포트 κ°€μ Έμ˜€κΈ° (κΈ°λ³Έκ°’: 8080)
port = int(os.environ.get('PORT', 8080))
# ν™˜κ²½ λ³€μˆ˜μ—μ„œ 디버그 λͺ¨λ“œ μ„€μ • (κΈ°λ³Έκ°’: False)
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)