"""뉴스 소스 관리 API""" from datetime import datetime from typing import Annotated from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from sqlalchemy import String, 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: datetime | None = None created_at: datetime | None = None 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: if '/' in source: # 신문사/분야 형태 → file_path에서 폴더명 매칭 # source = "경향신문/문화" → file_path LIKE 'news/경향신문 문화/%' folder = source.replace('/', ' ') query = query.where(Document.file_path.like(f"news/{folder}/%")) else: # 신문사만 → ai_sub_group 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": "뉴스 수집 시작됨"}