ssulbox/backend/app/routers/content.py

137 lines
5.7 KiB
Python

# -*- coding: utf-8 -*-
"""콘텐츠(피드)·생성·좋아요·업장명 자동완성 라우터."""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from .. import credit_service, jobs
from ..config import GEMINI_API_KEY, SCENARIO_ENGINE, settings
from ..content_sync import sync_output
from ..database import get_session
from ..deps import Pagination, get_current_user, get_current_user_optional, get_pagination
from ..models import Comment, Content, Like, User
from ..schemas import CreateReq, FeedItem, PaginatedResponse
router = APIRouter(prefix="/api", tags=["Content"])
@router.get("/feed", response_model=PaginatedResponse[FeedItem])
async def feed(
sort: str = Query("latest", pattern="^(latest|likes)$"),
owner: str | None = None, # "me" → 내 채널
pg: Pagination = Depends(get_pagination),
user: User | None = Depends(get_current_user_optional),
session: AsyncSession = Depends(get_session),
):
like_ct = (
select(func.count(Like.id)).where(Like.content_id == Content.id).correlate(Content).scalar_subquery()
)
cmt_ct = (
select(func.count(Comment.id))
.where(Comment.content_id == Content.id, Comment.is_deleted == False) # noqa: E712
.correlate(Content).scalar_subquery()
)
where = [Content.is_deleted == False] # noqa: E712
if owner == "me":
if user is None:
return PaginatedResponse.create([], 0, pg.page, pg.page_size)
where.append(Content.owner_uuid == user.user_uuid)
total = (await session.execute(select(func.count(Content.id)).where(*where))).scalar() or 0
order = like_ct.desc() if sort == "likes" else Content.created_at.desc()
rows = (
await session.execute(
select(Content, like_ct.label("likes"), cmt_ct.label("comments"))
.where(*where).order_by(order).offset(pg.offset).limit(pg.page_size)
)
).all()
ids = [r[0].id for r in rows]
liked = set()
if user and ids:
liked = set(
(await session.execute(
select(Like.content_id).where(Like.user_uuid == user.user_uuid, Like.content_id.in_(ids))
)).scalars().all()
)
items = [
FeedItem(
id=c.id, sc=c.scenario, title=c.title, video=c.video, views=c.views,
likes=likes, comments=comments, liked=(c.id in liked),
mine=(user is not None and c.owner_uuid == user.user_uuid), createdAt=c.created_at,
)
for (c, likes, comments) in rows
]
return PaginatedResponse.create(items, total, pg.page, pg.page_size)
@router.post("/create")
async def create(body: CreateReq, user: User = Depends(get_current_user)):
if body.scenario not in SCENARIO_ENGINE:
raise HTTPException(400, f"알 수 없는 시나리오: {body.scenario}")
if not body.input.strip():
raise HTTPException(400, "링크 또는 업장명이 필요합니다.")
if not GEMINI_API_KEY:
raise HTTPException(400, "GEMINI_API_KEY 가 설정되지 않았습니다.")
if user.credits < settings.CREDITS_PER_VIDEO:
raise HTTPException(402, "크레딧이 부족합니다. 충전 후 이용해주세요.")
job = jobs.create_job(
body.scenario, body.input.strip(),
max(4, min(20, body.scenes)), max(20, min(90, body.seconds)),
owner_uuid=user.user_uuid,
)
return {"id": job["id"], "status": job["status"]}
@router.get("/jobs/{jid}")
async def job_status(jid: str, session: AsyncSession = Depends(get_session)):
job = jobs.get_job(jid)
if not job:
raise HTTPException(404, "잡을 찾을 수 없습니다.")
# 완료 최초 관측 시 1회: 새 output → content 삽입 + 크레딧 차감
if job["status"] == "done" and not job["finalized"]:
job["finalized"] = True
try:
await sync_output(session, owner_uuid=job["owner_uuid"])
if job["owner_uuid"]:
await credit_service.deduct(session, job["owner_uuid"], settings.CREDITS_PER_VIDEO, reason="영상 생성")
await session.commit()
except Exception:
await session.rollback()
return {
"id": job["id"], "status": job["status"], "step": job["step"],
"stepName": jobs.STEP_NAMES[job["step"] - 1] if job["step"] else "대기 중",
"output": job["output"], "error": job["error"], "log": job["log"][-15:],
}
@router.post("/content/{cid}/like")
async def toggle_like(cid: str, user: User = Depends(get_current_user), session: AsyncSession = Depends(get_session)):
c = await session.get(Content, cid)
if c is None or c.is_deleted:
raise HTTPException(404, "콘텐츠를 찾을 수 없습니다.")
existing = (
await session.execute(select(Like).where(Like.content_id == cid, Like.user_uuid == user.user_uuid))
).scalar_one_or_none()
if existing:
await session.delete(existing)
liked = False
else:
session.add(Like(content_id=cid, user_uuid=user.user_uuid))
liked = True
await session.commit()
count = (await session.execute(select(func.count(Like.id)).where(Like.content_id == cid))).scalar() or 0
return {"liked": liked, "likes": count}
@router.get("/search/place", tags=["Search"])
async def search_place(query: str = Query(..., min_length=2)):
"""업장명 → 네이버 지도 후보 목록(동명 업장 주소로 구분). 키 불필요(Playwright).
각 후보는 place_url 을 가지므로, 프론트에서 하나를 고르면 그 URL 로 정확히 크롤링한다.
"""
from .. import place_service
items = await place_service.search_places(query, limit=8)
return {"query": query, "count": len(items), "items": items}