# -*- 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}