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.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()), )