수집한 객실·이용 정보가 발행 화면에 연결되지 않던 경로를 보완하고, 숙소 소개와 지역 맛집 표시를 개선한다. - NOL 브라우저 수집 어댑터와 수집·반영 스크립트 추가 - 크롤링 fact 즉시 노출 및 직접 입력·정정값 보호 - 이용안내 항목별 구조화와 기존 표 연결, 원문 UI 비표시 - 군산 한일옥 고정 등록과 지역 맛집 탐색·보강 경로 추가 - 숙소 소개 요약, 히어로 문구, 지역 콘텐츠·목업 표시 개선 검증: 작업 트리 기준 site 타입·린트·빌드 및 안내 렌더링 테스트 통과, PC·모바일 화면 확인. 스테이징 diff 공백 검사 통과. 사용자 요청에 따라 현재 스테이징된 55개 파일만 포함하며 미스테이징 문서·테스트 등은 제외.
95 lines
4.6 KiB
Python
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))
|