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"])
|
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(
|
@router.get(
|
||||||
"/generate/{task_id}",
|
"/generate/{task_id}",
|
||||||
@ -989,6 +1034,10 @@ async def get_all_videos(
|
|||||||
|
|
||||||
liked_map = {vid: bool(liked) for vid, liked in raw_liked.items()}
|
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 = [
|
items = [
|
||||||
VideoThumbnailItem(
|
VideoThumbnailItem(
|
||||||
video_id=v.id,
|
video_id=v.id,
|
||||||
@ -999,6 +1048,7 @@ async def get_all_videos(
|
|||||||
like_count=like_count_map.get(v.id) or 0,
|
like_count=like_count_map.get(v.id) or 0,
|
||||||
is_liked_by_me=liked_map.get(v.id, False),
|
is_liked_by_me=liked_map.get(v.id, False),
|
||||||
comment_count=comment_count or 0,
|
comment_count=comment_count or 0,
|
||||||
|
official_site_url=official_site_url_map.get(p.id),
|
||||||
)
|
)
|
||||||
for v, p, comment_count in rows
|
for v, p, comment_count in rows
|
||||||
]
|
]
|
||||||
@ -1214,6 +1264,8 @@ async def get_video_detail(
|
|||||||
liked = False
|
liked = False
|
||||||
is_liked_by_me = liked
|
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}")
|
logger.info(f"[get_video_detail] SUCCESS - video_id: {video_id}")
|
||||||
return VideoDetailResponse(
|
return VideoDetailResponse(
|
||||||
video_id=video.id,
|
video_id=video.id,
|
||||||
@ -1226,6 +1278,7 @@ async def get_video_detail(
|
|||||||
created_at=video.created_at,
|
created_at=video.created_at,
|
||||||
like_count=like_count,
|
like_count=like_count,
|
||||||
is_liked_by_me=is_liked_by_me,
|
is_liked_by_me=is_liked_by_me,
|
||||||
|
official_site_url=official_site_url_map.get(project.id),
|
||||||
)
|
)
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
|
|||||||
@ -186,6 +186,10 @@ class VideoThumbnailItem(BaseModel):
|
|||||||
like_count: int = Field(..., description="좋아요 수")
|
like_count: int = Field(..., description="좋아요 수")
|
||||||
is_liked_by_me: bool = Field(..., description="현재 로그인 사용자가 좋아요를 눌렀는지 (비로그인은 항상 false)")
|
is_liked_by_me: bool = Field(..., description="현재 로그인 사용자가 좋아요를 눌렀는지 (비로그인은 항상 false)")
|
||||||
comment_count: int = Field(..., description="댓글 수 (대댓글 포함)")
|
comment_count: int = Field(..., description="댓글 수 (대댓글 포함)")
|
||||||
|
official_site_url: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
description="업체 공식 페이지 URL (크롤링된 네이버 플레이스 기준, 없으면 null)",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class VideoDetailResponse(BaseModel):
|
class VideoDetailResponse(BaseModel):
|
||||||
@ -205,6 +209,10 @@ class VideoDetailResponse(BaseModel):
|
|||||||
created_at: datetime = Field(..., description="생성 일시")
|
created_at: datetime = Field(..., description="생성 일시")
|
||||||
like_count: int = Field(..., description="좋아요 수")
|
like_count: int = Field(..., description="좋아요 수")
|
||||||
is_liked_by_me: bool = Field(..., description="현재 로그인 사용자가 좋아요를 눌렀는지 (비로그인은 항상 false)")
|
is_liked_by_me: bool = Field(..., description="현재 로그인 사용자가 좋아요를 눌렀는지 (비로그인은 항상 false)")
|
||||||
|
official_site_url: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
description="업체 공식 페이지 URL (크롤링된 네이버 플레이스 기준, 없으면 null)",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class LikeToggleResponse(BaseModel):
|
class LikeToggleResponse(BaseModel):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user