135 lines
6.6 KiB
Python
135 lines
6.6 KiB
Python
import logging
|
|
import uuid6
|
|
from fastapi import APIRouter, BackgroundTasks, Depends, File, Form, HTTPException, UploadFile, status
|
|
from common.deps import verify_api_key
|
|
from common.db import fetchone, insert_instagram_row, insert_facebook_row, insert_naver_blog_row, insert_youtube_row, insert_gangnam_unni_row, insert_analysis_run
|
|
from models.analysis import AnalysisCreate, AnalysisStartResponse, AnalysisStatusResponse
|
|
from models.file import FileListItem, FileType, FileUploadResponse
|
|
from models.status import AnalysisStatus
|
|
from services.pipeline import run_pipeline
|
|
from services.file import get_analysis_files_response, handle_analysis_file_upload, soft_delete_analysis_file
|
|
from mock_urls import MOCK_CLINICS
|
|
from common.utils import _normalize_homepage, _with_scheme
|
|
|
|
router = APIRouter(prefix="/api/analysis", tags=["analysis"], dependencies=[Depends(verify_api_key)])
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# 추후 DB에 클리닉별로 매핑할 채널들 — 지금은 mock_urls에서 homepage 매칭으로 보충.
|
|
# 메인 채널(IG/FB/YT/네이버블로그/강남언니) + 부가 채널(틱톡/영문 IG·FB/카카오/네이버카페) 모두 포함.
|
|
# 클라가 일부만 보내거나 빈 값이면 mock에서 동일 hospital을 찾아 채워줌.
|
|
def _channels_from_mockurls(homepage_url: str) -> dict:
|
|
target = _normalize_homepage(homepage_url)
|
|
if not target:
|
|
return {}
|
|
for c in MOCK_CLINICS:
|
|
urls = c["urls"]
|
|
if _normalize_homepage(urls.get("homepage", "")) == target:
|
|
return {
|
|
# main
|
|
"instagram": _with_scheme(urls.get("instagram")),
|
|
"facebook": _with_scheme(urls.get("facebook")),
|
|
"naver_blog": _with_scheme(urls.get("naverBlog")),
|
|
"youtube": _with_scheme(urls.get("youtube")),
|
|
"gangnam_unni": _with_scheme(urls.get("gangnamUnni")),
|
|
# extra
|
|
"tiktok": _with_scheme(urls.get("tiktok")),
|
|
"instagram_en": _with_scheme(urls.get("instagramEn")),
|
|
"facebook_en": _with_scheme(urls.get("facebookEn")),
|
|
"kakao_talk": _with_scheme(urls.get("kakaoTalk")),
|
|
"naver_cafe": _with_scheme(urls.get("naverCafe")),
|
|
}
|
|
return {}
|
|
|
|
|
|
@router.post("", status_code=status.HTTP_202_ACCEPTED, response_model=AnalysisStartResponse)
|
|
async def start_analysis(body: AnalysisCreate, background_tasks: BackgroundTasks):
|
|
logger.info("POST /api/analysis clinic_id=%s", body.clinic_id)
|
|
analysis_run_id = str(uuid6.uuid7())
|
|
hospital_id = body.clinic_id
|
|
|
|
# 사실 hospital과 owner_user_id 비교 후 검증이 필요한 거지만 일단 PoC 니까. 나중에 바꿉니다.
|
|
hospital = await fetchone(
|
|
"SELECT owner_user_id, url FROM hospital_baseinfo WHERE hospital_id = %s",
|
|
(hospital_id,),
|
|
)
|
|
if not hospital:
|
|
raise HTTPException(status_code=409, detail="Clinic not found")
|
|
|
|
# 클라가 안 보낸 채널은 mock_urls에서 homepage 매칭으로 보충 (main + extra 동일 규칙)
|
|
mock = _channels_from_mockurls(hospital["url"])
|
|
|
|
# 사용자가 'gangnamunni.com/...' 같이 scheme/www 없이 줘도 _with_scheme이 https://www. 보강.
|
|
ig_url = _with_scheme(body.channels.instagram) or mock.get("instagram")
|
|
fb_url = _with_scheme(body.channels.facebook) or mock.get("facebook")
|
|
nb_url = _with_scheme(body.channels.naver_blog) or mock.get("naver_blog")
|
|
yt_url = _with_scheme(body.channels.youtube) or mock.get("youtube")
|
|
gu_url = _with_scheme(body.channels.gangnam_unni) or mock.get("gangnam_unni")
|
|
|
|
ig_id = await insert_instagram_row(hospital_id, ig_url) if ig_url else None
|
|
fb_id = await insert_facebook_row(hospital_id, fb_url) if fb_url else None
|
|
nb_id = await insert_naver_blog_row(hospital_id, nb_url) if nb_url else None
|
|
yt_id = await insert_youtube_row(hospital_id, yt_url) if yt_url else None
|
|
gu_id = await insert_gangnam_unni_row(hospital_id, gu_url) if gu_url else None
|
|
|
|
analysis_run_id = await insert_analysis_run(
|
|
analysis_run_id, hospital_id, hospital["owner_user_id"],
|
|
ig_id, fb_id, nb_id, yt_id, gu_id,
|
|
)
|
|
|
|
extra_channels = {
|
|
"tiktok": body.channels.tiktok or mock.get("tiktok"),
|
|
"instagram_en": body.channels.instagram_en or mock.get("instagram_en"),
|
|
"facebook_en": body.channels.facebook_en or mock.get("facebook_en"),
|
|
"kakao_talk": body.channels.kakao_talk or mock.get("kakao_talk"),
|
|
"naver_cafe": body.channels.naver_cafe or mock.get("naver_cafe"),
|
|
}
|
|
logger.info("[analysis] main+extra channels resolved (mock_matched=%s)", bool(mock))
|
|
background_tasks.add_task(run_pipeline, analysis_run_id, extra_channels)
|
|
|
|
return AnalysisStartResponse(
|
|
analysis_run_id=analysis_run_id,
|
|
clinic_id=hospital_id,
|
|
status=AnalysisStatus.DISCOVERING,
|
|
estimated_seconds=90,
|
|
poll_url=f"/api/analysis/{analysis_run_id}/status",
|
|
)
|
|
|
|
|
|
@router.post("/{run_id}/files", status_code=status.HTTP_201_CREATED, response_model=FileUploadResponse)
|
|
async def upload_analysis_run_file(
|
|
run_id: str,
|
|
file: UploadFile = File(..., description="업로드할 파일"),
|
|
file_type: FileType = Form(default=FileType.FILE, description="파일 타입 (image/video/audio/document/file)"),
|
|
) -> FileUploadResponse:
|
|
logger.info("POST /api/analysis/%s/files name=%s file_type=%s", run_id, file.filename, file_type.value)
|
|
return await handle_analysis_file_upload(run_id, file, file_type)
|
|
|
|
|
|
@router.get("/{run_id}/files", response_model=list[FileListItem])
|
|
async def get_analysis_run_files(run_id: str) -> list[FileListItem]:
|
|
logger.info("GET /api/analysis/%s/files", run_id)
|
|
return await get_analysis_files_response(run_id)
|
|
|
|
|
|
@router.delete("/{run_id}/files/{file_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_analysis_run_file(run_id: str, file_id: int) -> None:
|
|
logger.info("DELETE /api/analysis/%s/files/%s", run_id, file_id)
|
|
await soft_delete_analysis_file(analysis_run_id=run_id, file_id=file_id)
|
|
return None
|
|
|
|
|
|
@router.get("/{run_id}/status", response_model=AnalysisStatusResponse)
|
|
async def get_analysis_status(run_id: str):
|
|
logger.info("GET /api/analysis/%s/status", run_id)
|
|
row = await fetchone("SELECT status FROM analysis_runs WHERE analysis_run_id = %s", (run_id,))
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="Run not found")
|
|
return AnalysisStatusResponse(
|
|
analysis_run_id=run_id,
|
|
status=AnalysisStatus(row["status"]),
|
|
progress=50.0,
|
|
current_step="",
|
|
channel_errors={},
|
|
completed_at=None,
|
|
)
|