"""뉴스 소스 관리 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.get("/articles") async def list_articles( user: Annotated[User, Depends(get_current_user)], session: Annotated[AsyncSession, Depends(get_session)], source: str | None = None, unread_only: bool = False, page: int = 1, page_size: int = 30, ): """뉴스 기사 목록""" from sqlalchemy import func from models.document import Document query = select(Document).where( Document.source_channel == "news", Document.deleted_at == None, ) if source: query = query.where(Document.ai_sub_group == source) if unread_only: query = query.where(Document.is_read == False) count_q = select(func.count()).select_from(query.subquery()) total = (await session.execute(count_q)).scalar() query = query.order_by(Document.is_read.asc(), Document.created_at.desc()) query = query.offset((page - 1) * page_size).limit(page_size) result = await session.execute(query) items = result.scalars().all() from api.documents import DocumentResponse return { "items": [DocumentResponse.model_validate(doc) for doc in items], "total": total, "page": page, } @router.post("/mark-all-read") async def mark_all_read( user: Annotated[User, Depends(get_current_user)], session: Annotated[AsyncSession, Depends(get_session)], ): """전체 읽음 처리""" from sqlalchemy import update from models.document import Document result = await session.execute( update(Document) .where(Document.source_channel == "news", Document.is_read == False) .values(is_read=True) ) await session.commit() return {"marked": result.rowcount} @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": "뉴스 수집 시작됨"}