Files
hyungi_document_server/app/api/news.py
Hyungi Ahn 7ca3abf17c feat: 뉴스 전용 페이지 + 분류 격리 + 읽음 상태
- /news 전용 페이지: 신문사 필터, 읽지않음 필터, 시간순 리스트, 미리보기
- 뉴스 분류 격리: ai_domain='News', classify 제거, embed만 등록
- is_read: 클릭 시 자동 읽음, 전체 읽음 API
- documents 목록에서 뉴스 제외 (source_channel != 'news')
- nav에 뉴스 링크 추가
- GET /api/news/articles, POST /api/news/mark-all-read

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:16:00 +09:00

166 lines
4.6 KiB
Python

"""뉴스 소스 관리 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": "뉴스 수집 시작됨"}