feat(video): 영상 응답에 official_site_url 필드 추가
GET /video/{id}와 GET /video/all 응답에 업체 공식 페이지 URL을 추가한다.
Project.marketing_intelligence → MarketingIntel.place_id("nv{place_id}")를
경유해 네이버 플레이스 URL로 복원하며, 크롤링 없이 직접 입력된 업체는
null로 내려간다.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVSeUT2pEmeHc6hG6gVXUS
This commit is contained in:
parent
9d40313fbb
commit
61bdc27cd3
@ -74,6 +74,51 @@ logger = get_logger("video")
|
||||
router = APIRouter(prefix="/video", tags=["Video"])
|
||||
|
||||
|
||||
def _place_id_to_site_url(place_id: str | None) -> str | None:
|
||||
"""MarketingIntel.place_id("nv{네이버 place ID}")를 네이버 플레이스 URL로 변환한다.
|
||||
|
||||
크롤링 없이 직접 입력된 업체는 place_id가 없으므로 None을 반환한다.
|
||||
"""
|
||||
if place_id and place_id.startswith("nv") and place_id[2:].isdigit():
|
||||
return f"https://map.naver.com/p/entry/place/{place_id[2:]}"
|
||||
return None
|
||||
|
||||
|
||||
async def _get_official_site_urls(
|
||||
session: AsyncSession, projects: list[Project]
|
||||
) -> dict[int, str | None]:
|
||||
"""프로젝트 목록에 대해 {project_id: 공식 페이지 URL(or None)}을 일괄 조회한다.
|
||||
|
||||
Project.marketing_intelligence(문자열로 저장된 MarketingIntel.id)를 경유해
|
||||
place_id를 찾고, 이를 네이버 플레이스 URL로 변환한다.
|
||||
"""
|
||||
m_id_by_project: dict[int, int] = {}
|
||||
for p in projects:
|
||||
try:
|
||||
if p.marketing_intelligence is not None:
|
||||
m_id_by_project[p.id] = int(p.marketing_intelligence)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
url_by_project: dict[int, str | None] = {p.id: None for p in projects}
|
||||
if not m_id_by_project:
|
||||
return url_by_project
|
||||
|
||||
rows = (
|
||||
await session.execute(
|
||||
select(MarketingIntel.id, MarketingIntel.place_id).where(
|
||||
MarketingIntel.id.in_(set(m_id_by_project.values()))
|
||||
)
|
||||
)
|
||||
).all()
|
||||
place_id_by_m_id = {m_id: place_id for m_id, place_id in rows}
|
||||
|
||||
for project_id, m_id in m_id_by_project.items():
|
||||
url_by_project[project_id] = _place_id_to_site_url(
|
||||
place_id_by_m_id.get(m_id)
|
||||
)
|
||||
return url_by_project
|
||||
|
||||
|
||||
@router.get(
|
||||
"/generate/{task_id}",
|
||||
@ -989,6 +1034,10 @@ async def get_all_videos(
|
||||
|
||||
liked_map = {vid: bool(liked) for vid, liked in raw_liked.items()}
|
||||
|
||||
official_site_url_map = await _get_official_site_urls(
|
||||
session, [p for _, p, _ in rows]
|
||||
)
|
||||
|
||||
items = [
|
||||
VideoThumbnailItem(
|
||||
video_id=v.id,
|
||||
@ -999,6 +1048,7 @@ async def get_all_videos(
|
||||
like_count=like_count_map.get(v.id) or 0,
|
||||
is_liked_by_me=liked_map.get(v.id, False),
|
||||
comment_count=comment_count or 0,
|
||||
official_site_url=official_site_url_map.get(p.id),
|
||||
)
|
||||
for v, p, comment_count in rows
|
||||
]
|
||||
@ -1214,6 +1264,8 @@ async def get_video_detail(
|
||||
liked = False
|
||||
is_liked_by_me = liked
|
||||
|
||||
official_site_url_map = await _get_official_site_urls(session, [project])
|
||||
|
||||
logger.info(f"[get_video_detail] SUCCESS - video_id: {video_id}")
|
||||
return VideoDetailResponse(
|
||||
video_id=video.id,
|
||||
@ -1226,6 +1278,7 @@ async def get_video_detail(
|
||||
created_at=video.created_at,
|
||||
like_count=like_count,
|
||||
is_liked_by_me=is_liked_by_me,
|
||||
official_site_url=official_site_url_map.get(project.id),
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
|
||||
@ -186,6 +186,10 @@ class VideoThumbnailItem(BaseModel):
|
||||
like_count: int = Field(..., description="좋아요 수")
|
||||
is_liked_by_me: bool = Field(..., description="현재 로그인 사용자가 좋아요를 눌렀는지 (비로그인은 항상 false)")
|
||||
comment_count: int = Field(..., description="댓글 수 (대댓글 포함)")
|
||||
official_site_url: Optional[str] = Field(
|
||||
None,
|
||||
description="업체 공식 페이지 URL (크롤링된 네이버 플레이스 기준, 없으면 null)",
|
||||
)
|
||||
|
||||
|
||||
class VideoDetailResponse(BaseModel):
|
||||
@ -205,6 +209,10 @@ class VideoDetailResponse(BaseModel):
|
||||
created_at: datetime = Field(..., description="생성 일시")
|
||||
like_count: int = Field(..., description="좋아요 수")
|
||||
is_liked_by_me: bool = Field(..., description="현재 로그인 사용자가 좋아요를 눌렀는지 (비로그인은 항상 false)")
|
||||
official_site_url: Optional[str] = Field(
|
||||
None,
|
||||
description="업체 공식 페이지 URL (크롤링된 네이버 플레이스 기준, 없으면 null)",
|
||||
)
|
||||
|
||||
|
||||
class LikeToggleResponse(BaseModel):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user