o2o-site-AEO/solution/backend/scripts/verify_yanolja_test_place.py
김성경 a093921b62 feat(backend,site): NOL 수집 및 숙박 안내 구조화와 지역 콘텐츠 연동
수집한 객실·이용 정보가 발행 화면에 연결되지 않던 경로를 보완하고, 숙소 소개와 지역 맛집 표시를 개선한다.

- NOL 브라우저 수집 어댑터와 수집·반영 스크립트 추가
- 크롤링 fact 즉시 노출 및 직접 입력·정정값 보호
- 이용안내 항목별 구조화와 기존 표 연결, 원문 UI 비표시
- 군산 한일옥 고정 등록과 지역 맛집 탐색·보강 경로 추가
- 숙소 소개 요약, 히어로 문구, 지역 콘텐츠·목업 표시 개선

검증: 작업 트리 기준 site 타입·린트·빌드 및 안내 렌더링 테스트 통과, PC·모바일 화면 확인. 스테이징 diff 공백 검사 통과. 사용자 요청에 따라 현재 스테이징된 55개 파일만 포함하며 미스테이징 문서·테스트 등은 제외.
2026-09-14 19:40:04 +09:00

143 lines
5.7 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
테스트 place에 야놀자 크롤링으로 넣은 객실 fact/사진을 **수동으로 검수·승인**해서,
실제 미리보기 렌더러(site_payload.to_site_payload → UnitsSection)에 뜨는지 확인하는
1회성 스크립트.
★ 이것도 운영 파이프라인이 아니다 — 사람이 눈으로 확인하려고 이번 테스트 place 하나에만
쓰는 수동 검수 도구다. 실제 서비스에서 크롤링 값을 이렇게 자동 승인하면 안 된다
(fact는 사람이 [맞아요]를 눌러야, 사진은 Vision/사람이 봐야 승인 상태가 된다).
사용법 (solution/backend 에서, 가상환경 안):
PGSSLMODE=disable DB_PASSWORD=... python scripts/verify_yanolja_test_place.py --place-id <UUID>
"""
from __future__ import annotations
import argparse
import asyncio
import json
import os
import sys
import uuid
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.environ.setdefault("APP_ENV", "local")
from sqlalchemy import select # noqa: E402
from common.database.db_session_manager import DB_SESSION_MNG # noqa: E402
from common.database.model.models import places, place_facts, place_photos # noqa: E402
from common.enums import DBWRType, ErrorType, FactStatus, MediaStatus # noqa: E402
from common.models.gmodel import UserInfo # noqa: E402
from common.utils.gtime import GTime # noqa: E402
from crud.fact_crud import FactCRUD # noqa: E402
from crud.media_crud import MediaCRUD # noqa: E402
from crud.place_crud import PlaceCRUD # noqa: E402
from crud.site_crud import SiteCRUD # noqa: E402
from crud.job_crud import JobQueue # noqa: E402
from router.v1.fact.protocol import Req_TransitionFact # noqa: E402
from services.fact_service import FactService # noqa: E402
from services.site_service import SiteService # noqa: E402
# 검수 전 상태(우리가 이번에 넣은 값들)만 건드린다.
_PENDING_FACT_STATUSES = (FactStatus.UNVERIFIED.value, FactStatus.PENDING_OWNER.value)
async def _load_place(place_id: str):
stmt = select(places).where(places.place_id == uuid.UUID(place_id), places.deleted == False) # noqa: E712
err, row = await DB_SESSION_MNG.execute_lambda(
places.DBType(), DBWRType.DB_READ.value, lambda s: DB_SESSION_MNG.execute(s, stmt)
)
if err != ErrorType.SUCCESS or not row:
return None
return row[0]
async def _unit_scope_facts(place_id: str):
stmt = select(place_facts).where(
place_facts.place_id == uuid.UUID(place_id),
place_facts.deleted == False, # noqa: E712
place_facts.unit_id.is_not(None),
place_facts.status.in_(_PENDING_FACT_STATUSES),
)
err, rows = await DB_SESSION_MNG.execute_lambda(
place_facts.DBType(), DBWRType.DB_READ.value, lambda s: DB_SESSION_MNG.execute(s, stmt)
)
return rows if err == ErrorType.SUCCESS else []
async def _unit_scope_pending_photos(place_id: str):
stmt = select(place_photos).where(
place_photos.place_id == uuid.UUID(place_id),
place_photos.deleted == False, # noqa: E712
place_photos.unit_id.is_not(None),
place_photos.status == MediaStatus.PENDING_REVIEW.value,
)
err, rows = await DB_SESSION_MNG.execute_lambda(
place_photos.DBType(), DBWRType.DB_READ.value, lambda s: DB_SESSION_MNG.execute(s, stmt)
)
return rows if err == ErrorType.SUCCESS else []
async def run(place_id: str) -> None:
place = await _load_place(place_id)
if place is None:
raise SystemExit(f"place를 찾을 수 없습니다: {place_id}")
actor = UserInfo(user_id=str(place.owner_user_id), id="yanolja_test_verify", role=1)
fact_service = FactService(FactCRUD(), PlaceCRUD())
media_crud = MediaCRUD()
facts = await _unit_scope_facts(place_id)
print(f"[검수 대상 fact] {len(facts)}")
verified = 0
for f in facts:
res = await fact_service.transition(
actor, place_id, str(f.fact_id), Req_TransitionFact(status=FactStatus.VERIFIED)
)
if res.result.success:
verified += 1
else:
print(f" ! {f.key}({f.fact_id}) 전이 실패: {res.result.desc}")
print(f" -> VERIFIED 처리 {verified}/{len(facts)}")
photos = await _unit_scope_pending_photos(place_id)
print(f"[승인 대상 사진] {len(photos)}")
approved = 0
now = GTime.UTC()
for p in photos:
alt_text = p.label or "객실 사진"
run_err, _rowcount = await DB_SESSION_MNG.execute_lambda_claim(
place_photos.DBType(),
lambda s, m=p.media_id, a=alt_text: media_crud.apply_vision(
s, m, p.label, a, None, MediaStatus.APPROVED.value, now
),
)
if run_err == ErrorType.SUCCESS:
approved += 1
print(f" -> APPROVED 처리 {approved}/{len(photos)}건 (alt_text 채움)")
site_service = SiteService(SiteCRUD(), PlaceCRUD(), JobQueue())
payload = await site_service.preview_payload(actor, place_id)
units = (payload or {}).get("units", [])
print(f"\n[/preview 재확인] units {len(units)}")
for u in units:
print(f" - {u.get('name')}: fact {len(u.get('facts', []))}개, media {len(u.get('mediaIds', []))}")
if not units:
print(" (여전히 비어 있습니다 — payload 구조나 상태값을 다시 확인하세요)")
print(json.dumps(payload, ensure_ascii=False, default=str)[:2000])
await DB_SESSION_MNG.dispose_all()
def main() -> None:
parser = argparse.ArgumentParser(description="테스트 place의 야놀자 객실 데이터를 검수·승인해서 미리보기에 반영")
parser.add_argument("--place-id", required=True)
args = parser.parse_args()
asyncio.run(run(args.place_id))
if __name__ == "__main__":
main()