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

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

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

95 lines
4.6 KiB
Python

"""군산 공통 맛집 한일옥 등록. 기본은 조회, --apply로 현재 설정 DB에 반영한다.
external_id가 없는 지역 공통 항목은 snapshot이 모든 군산 업장에 포함한다.
네이버 ID는 body에 보관하여 자동 수집의 (source, external_id) 갱신과 분리한다.
"""
import argparse
import asyncio
import json
import sys
import uuid
from datetime import datetime, timezone
from pathlib import Path
from types import SimpleNamespace
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from sqlalchemy import select
from sqlalchemy.dialects.postgresql import insert
from sqlalchemy.engine import URL
from sqlalchemy.ext.asyncio import create_async_engine
from common.database.db_session_manager import DB_SESSION_MNG
from common.database.model.models import area_contents, places
from common.enums import LocalContentStatus, LocalContentType, LocalSource
from config.server_configs import main_db_config as cfg
from services.site_payload import _local
from services.snapshot import _local_contents
REGION = '52군산시'
NAVER_ID = '11861452'
CONTENT_ID = uuid.uuid5(uuid.NAMESPACE_URL, 'web4ai:52군산시:restaurant:11861452')
async def main(apply: bool):
engine = create_async_engine(URL.create(
'postgresql+asyncpg', username=cfg.write_id, password=cfg.write_pw,
host=cfg.write_host, port=cfg.write_port, database=cfg.name,
))
try:
async with engine.begin() as conn:
rows = (await conn.execute(select(
area_contents.local_content_id, area_contents.title, area_contents.body,
).where(
area_contents.region_code == REGION,
area_contents.kind == 'restaurant', area_contents.external_id.is_(None),
area_contents.deleted.is_(False),
))).mappings().all()
if rows and any(r['body'].get('naverPlaceId') != NAVER_ID for r in rows):
raise RuntimeError('다른 군산 공통 맛집이 이미 있어 덮어쓰지 않습니다.')
content_id = rows[0]['local_content_id'] if rows else CONTENT_ID
now = datetime.now(timezone.utc)
values = dict(
local_content_id=content_id, region_code=REGION,
content_type=LocalContentType.RESTAURANT.value, kind='restaurant',
# 사용자 확인을 거친 수기 웹 등록. 크롤링한 값으로 표시하지 않는다.
source=LocalSource.OFFICIAL_WEB.value, external_id=None, title='한일옥',
body={**(rows[0]['body'] if rows else {}),
'name': '한일옥', 'searchQuery': '군산 한일옥',
'naverPlaceId': NAVER_ID,
'sourceUrl': f'https://m.place.naver.com/restaurant/{NAVER_ID}/home',
'registration': 'owner_confirmed_region_default'},
status=LocalContentStatus.PUBLISHED.value, published_at=now,
collected_at=now, display_start_at=None, display_end_at=None,
expires_at=None, deleted=False, updated_at=now,
)
if apply:
await conn.execute(insert(area_contents).values(**values).on_conflict_do_update(
index_elements=[area_contents.local_content_id],
set_={k: v for k, v in values.items() if k != 'local_content_id'},
))
targets = (await conn.execute(select(places.place_id, places.name).where(
places.deleted.is_(False), places.region_code == REGION,
))).mappings().all()
print(json.dumps({'applied': apply, 'database': cfg.name, 'region': REGION,
'contentId': str(content_id), 'naverPlaceId': NAVER_ID,
'existingPlaces': [dict(r) for r in targets]},
default=str, ensure_ascii=False))
if apply:
# place_id 없는 새 군산 업장도 지역 공통 경로만으로 받는지 확인한다.
snapshot = await _local_contents(SimpleNamespace(region_code=REGION))
local, _ = _local(snapshot, None, None)
matches = [r for r in local['restaurants'] if r['name'] == '한일옥']
assert len(matches) == 1, '지역 공통 payload에 한일옥이 정확히 한 번 있어야 합니다.'
print(json.dumps({'verifiedRegionalPayload': matches}, ensure_ascii=False))
finally:
await engine.dispose()
await DB_SESSION_MNG.dispose_all()
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--apply', action='store_true')
asyncio.run(main(parser.parse_args().apply))