feat: 뉴스 자동 수집 시스템 — 6개국 신문 RSS/API
- news_sources 테이블 (소스 관리, UI 동적 제어) - news_collector 워커: RSS(feedparser) + NYT API - 중복 체크: hash(title+date+source) + URL normalize - category 표준화, summary HTML 정제, timezone UTC - 30일 이내만 embed, source별 try/catch - News API: 소스 CRUD + 수동 수집 트리거 - APScheduler: 6시간 간격 자동 수집 - 대상: 경향/아사히/NYT/르몽드/신화/슈피겔 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
109
app/api/news.py
Normal file
109
app/api/news.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""뉴스 소스 관리 API"""
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from core.auth import get_current_user
|
||||
from core.database import get_session
|
||||
from models.news_source import NewsSource
|
||||
from models.user import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class NewsSourceResponse(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
country: str | None
|
||||
feed_url: str
|
||||
feed_type: str
|
||||
category: str | None
|
||||
language: str | None
|
||||
enabled: bool
|
||||
last_fetched_at: str | None
|
||||
created_at: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class NewsSourceCreate(BaseModel):
|
||||
name: str
|
||||
country: str | None = None
|
||||
feed_url: str
|
||||
feed_type: str = "rss"
|
||||
category: str | None = None
|
||||
language: str | None = None
|
||||
|
||||
|
||||
class NewsSourceUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
feed_url: str | None = None
|
||||
category: str | None = None
|
||||
enabled: bool | None = None
|
||||
|
||||
|
||||
@router.get("/sources")
|
||||
async def list_sources(
|
||||
user: Annotated[User, Depends(get_current_user)],
|
||||
session: Annotated[AsyncSession, Depends(get_session)],
|
||||
):
|
||||
result = await session.execute(select(NewsSource).order_by(NewsSource.id))
|
||||
return [NewsSourceResponse.model_validate(s) for s in result.scalars().all()]
|
||||
|
||||
|
||||
@router.post("/sources")
|
||||
async def create_source(
|
||||
body: NewsSourceCreate,
|
||||
user: Annotated[User, Depends(get_current_user)],
|
||||
session: Annotated[AsyncSession, Depends(get_session)],
|
||||
):
|
||||
source = NewsSource(**body.model_dump())
|
||||
session.add(source)
|
||||
await session.commit()
|
||||
return NewsSourceResponse.model_validate(source)
|
||||
|
||||
|
||||
@router.patch("/sources/{source_id}")
|
||||
async def update_source(
|
||||
source_id: int,
|
||||
body: NewsSourceUpdate,
|
||||
user: Annotated[User, Depends(get_current_user)],
|
||||
session: Annotated[AsyncSession, Depends(get_session)],
|
||||
):
|
||||
source = await session.get(NewsSource, source_id)
|
||||
if not source:
|
||||
raise HTTPException(status_code=404)
|
||||
for field, value in body.model_dump(exclude_unset=True).items():
|
||||
setattr(source, field, value)
|
||||
await session.commit()
|
||||
return NewsSourceResponse.model_validate(source)
|
||||
|
||||
|
||||
@router.delete("/sources/{source_id}")
|
||||
async def delete_source(
|
||||
source_id: int,
|
||||
user: Annotated[User, Depends(get_current_user)],
|
||||
session: Annotated[AsyncSession, Depends(get_session)],
|
||||
):
|
||||
source = await session.get(NewsSource, source_id)
|
||||
if not source:
|
||||
raise HTTPException(status_code=404)
|
||||
await session.delete(source)
|
||||
await session.commit()
|
||||
return {"message": f"소스 {source_id} 삭제됨"}
|
||||
|
||||
|
||||
@router.post("/collect")
|
||||
async def trigger_collect(
|
||||
user: Annotated[User, Depends(get_current_user)],
|
||||
):
|
||||
"""수동 수집 트리거"""
|
||||
from workers.news_collector import run
|
||||
import asyncio
|
||||
asyncio.create_task(run())
|
||||
return {"message": "뉴스 수집 시작됨"}
|
||||
Reference in New Issue
Block a user