수집한 객실·이용 정보가 발행 화면에 연결되지 않던 경로를 보완하고, 숙소 소개와 지역 맛집 표시를 개선한다. - NOL 브라우저 수집 어댑터와 수집·반영 스크립트 추가 - 크롤링 fact 즉시 노출 및 직접 입력·정정값 보호 - 이용안내 항목별 구조화와 기존 표 연결, 원문 UI 비표시 - 군산 한일옥 고정 등록과 지역 맛집 탐색·보강 경로 추가 - 숙소 소개 요약, 히어로 문구, 지역 콘텐츠·목업 표시 개선 검증: 작업 트리 기준 site 타입·린트·빌드 및 안내 렌더링 테스트 통과, PC·모바일 화면 확인. 스테이징 diff 공백 검사 통과. 사용자 요청에 따라 현재 스테이징된 55개 파일만 포함하며 미스테이징 문서·테스트 등은 제외.
135 lines
6.3 KiB
Python
135 lines
6.3 KiB
Python
from sqlalchemy import and_, select, update
|
|
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
|
|
|
from common.database.db_session_manager import DB_SESSION_MNG
|
|
from common.database.model.models import area_contents, place_area_refs
|
|
from common.utils.gtime import GTime
|
|
|
|
|
|
class PlaceContentCRUD:
|
|
"""업장 주변의 지역 콘텐츠.
|
|
|
|
★ 실체와 관계가 갈려 있다 (2026-09-09).
|
|
예전에는 한 테이블이 값을 통째로 들고 있었고 키가 place_id 라, 업장마다 TourAPI
|
|
응답이 복제됐다 — 실측 조이모텔 한 곳에 144행이고 같은 축제가 업장 수만큼 늘었다.
|
|
지금은 실체가 `area_contents` 에 한 행(전국 공용, external_id 로 유일)이고
|
|
`place_area_refs` 에는 그 업장에서만 다른 것 — 거리와 숨김 — 만 남는다.
|
|
그래서 읽을 때 조인이 하나 는다. 그 값으로 복제를 없앴다.
|
|
"""
|
|
|
|
async def list_by_place(self, db, place_id, *, include_hidden: bool = True):
|
|
"""이 업장 주변의 콘텐츠. 실체(area_contents)와 거리(place_area_refs)를 함께 준다."""
|
|
conds = [
|
|
place_area_refs.place_id == place_id,
|
|
place_area_refs.deleted == False, # noqa: E712
|
|
area_contents.deleted == False, # noqa: E712
|
|
]
|
|
if not include_hidden:
|
|
conds.append(place_area_refs.hidden == False) # noqa: E712
|
|
return await DB_SESSION_MNG.execute(
|
|
db,
|
|
select(
|
|
area_contents.local_content_id,
|
|
area_contents.content_type,
|
|
area_contents.source,
|
|
area_contents.external_id,
|
|
area_contents.title,
|
|
area_contents.body,
|
|
area_contents.latitude,
|
|
area_contents.longitude,
|
|
area_contents.display_end_at,
|
|
# ★ 이름을 옛 컬럼과 맞춘다 — 읽는 쪽(snapshot)이 행을 그대로 쓰던 모양이다.
|
|
place_area_refs.distance_m.label("distance_m"),
|
|
place_area_refs.hidden.label("hidden"),
|
|
)
|
|
.join(area_contents, area_contents.local_content_id == place_area_refs.local_content_id)
|
|
.where(and_(*conds))
|
|
.order_by(
|
|
area_contents.content_type.asc(),
|
|
place_area_refs.distance_m.asc(),
|
|
),
|
|
)
|
|
|
|
async def upsert_content(self, db, values: dict):
|
|
"""공용 콘텐츠 한 건. (source, external_id) 가 같으면 갱신한다 — 지역과 무관하게 한 벌이다.
|
|
|
|
★ RETURNING 을 쓰지 않는다. 세션 매니저의 execute 는 SELECT 만 받고
|
|
("DO NOT USE NON-SELECT QUERY IN DBJOB"), 쓰기는 add 로 간다. id 는 뒤이어 조회한다.
|
|
"""
|
|
stmt = pg_insert(area_contents).values(**values)
|
|
stmt = stmt.on_conflict_do_update(
|
|
index_elements=[area_contents.source, area_contents.external_id],
|
|
index_where=and_(
|
|
area_contents.deleted == False, # noqa: E712
|
|
area_contents.external_id.isnot(None),
|
|
),
|
|
set_={
|
|
"title": stmt.excluded.title,
|
|
"body": stmt.excluded.body,
|
|
"latitude": stmt.excluded.latitude,
|
|
"longitude": stmt.excluded.longitude,
|
|
"region_code": stmt.excluded.region_code,
|
|
"display_end_at": stmt.excluded.display_end_at,
|
|
"collected_at": stmt.excluded.collected_at,
|
|
"updated_at": GTime.UTC(),
|
|
},
|
|
)
|
|
return await DB_SESSION_MNG.add(db, stmt)
|
|
|
|
async def find_content_id(self, db, source: int, external_id: str):
|
|
"""방금 upsert 한 공용 콘텐츠의 id."""
|
|
return await DB_SESSION_MNG.execute(
|
|
db,
|
|
select(area_contents.local_content_id).where(
|
|
area_contents.source == source,
|
|
area_contents.external_id == external_id,
|
|
area_contents.deleted == False, # noqa: E712
|
|
).limit(1),
|
|
)
|
|
|
|
async def upsert_ref(self, db, place_id, local_content_id, distance_m):
|
|
"""업장 ↔ 콘텐츠 관계. ★ hidden 은 건드리지 않는다 — 운영자가 숨긴 것을 재수집이 되살리면 안 된다."""
|
|
stmt = pg_insert(place_area_refs).values(
|
|
place_id=place_id, local_content_id=local_content_id, distance_m=distance_m, deleted=False,
|
|
)
|
|
stmt = stmt.on_conflict_do_update(
|
|
index_elements=[place_area_refs.place_id, place_area_refs.local_content_id],
|
|
set_={"distance_m": stmt.excluded.distance_m, "deleted": False, "updated_at": GTime.UTC()},
|
|
)
|
|
return await DB_SESSION_MNG.add(db, stmt)
|
|
|
|
async def soft_delete_missing(self, db, place_id, keep_ids: set):
|
|
"""이번 응답에 없는 **관계**를 끊는다. 실체(area_contents)는 지우지 않는다 —
|
|
다른 업장이 같은 장소를 가리키고 있을 수 있다."""
|
|
err, rows = await DB_SESSION_MNG.execute(
|
|
db,
|
|
select(place_area_refs.local_content_id).where(
|
|
place_area_refs.place_id == place_id,
|
|
place_area_refs.deleted == False, # noqa: E712
|
|
),
|
|
)
|
|
# 단일 컬럼 SELECT 라 행이 스칼라로 온다.
|
|
# ★ 단일 컬럼 SELECT 는 세션 매니저가 scalars() 로 편다 — 행이 곧 값이다.
|
|
gone = [r for r in (rows or []) if r not in keep_ids]
|
|
if not gone:
|
|
return err, 0
|
|
return await DB_SESSION_MNG.add_with_rowcount(
|
|
db,
|
|
update(place_area_refs)
|
|
.where(place_area_refs.place_id == place_id, place_area_refs.local_content_id.in_(gone))
|
|
.values(deleted=True, updated_at=GTime.UTC()),
|
|
)
|
|
|
|
async def set_hidden(self, db, place_id, local_content_id, hidden: bool):
|
|
"""이 업장에서만 숨긴다. 실체는 그대로라 다른 업장에는 계속 보인다."""
|
|
return await DB_SESSION_MNG.add_with_rowcount(
|
|
db,
|
|
update(place_area_refs)
|
|
.where(
|
|
place_area_refs.place_id == place_id,
|
|
place_area_refs.local_content_id == local_content_id,
|
|
place_area_refs.deleted == False, # noqa: E712
|
|
)
|
|
.values(hidden=hidden, updated_at=GTime.UTC()),
|
|
)
|