o2o-site-AEO/postgres-init/migrations/0008_personalization_to_site_sections.sql
Mina Choi 328d9e18ee [feat] solution: 지역 이야기 생성 · 발행본 섹션 손질 · 마이그레이션 주석 축약
- 지역 이야기(가요·인물·연표·엽서·퀴즈) 생성 경로: story_service · grounding/story ·
  section_prompts. 지금까지 만들 자리가 없어 시안에만 손으로 넣은 3만 자였다
- 발행본 섹션: ItinerarySection · Carousel 레일 자동재생(use-rail-autoplay) ·
  Festival · LocalGuide · Weather · Gallery · Header/Footer
- 목업 payload 를 payloads-mockup/ 으로 분리 — 발행 대상과 섞이지 않게
- DB 새 구조 후속: site_payload · local_content_crud 조인 정리 · 테스트
- 마이그레이션 주석 축약: 9개 파일 합계 주석 비율 48% → 25%.
  실측과 밟은 함정만 남기고 논증은 커밋 메시지로 옮겼다

검증: site·frontend 빌드 통과

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 14:36:00 +09:00

66 lines
3.5 KiB
SQL

-- 0008 · 공용과 개인화를 이름으로 가른다
--
-- area_* 공용. 지역 단위, 여러 사이트가 나눠 쓴다 → 렌더러 모양 그대로 담는다.
-- site_sections 개인화 전부(거리·숨김·순서·사장님 편집).
--
-- 지금은 body 가 TourAPI 원문 이름이고(빌드마다 바꿔 왔다), 거리·숨김이 공용 쪽에 있다.
-- 1. 타입명을 모든 행에 채운다
-- 인덱스를 먼저 뗀다 — 옛 (region_code, kind) 는 같은 지역 관광지 여러 건이 전부
-- kind='attraction' 이 되는 순간 부딪친다. 장소류 유일성은 (source, external_id) 가 잡는다.
DROP INDEX IF EXISTS public.uq_local_contents_kind;
UPDATE public.area_contents SET kind = CASE content_type
WHEN 1 THEN 'weather' WHEN 2 THEN 'festival' WHEN 3 THEN 'attraction'
WHEN 4 THEN 'restaurant' WHEN 5 THEN 'course' END,
updated_at = now()
WHERE deleted = FALSE AND kind IS NULL AND content_type BETWEEN 1 AND 5;
CREATE UNIQUE INDEX IF NOT EXISTS uq_local_contents_kind
ON public.area_contents (region_code, kind)
WHERE deleted = FALSE AND kind IS NOT NULL AND external_id IS NULL;
-- 2. body 를 렌더러 모양으로
-- 좌표는 뺀다 — latitude/longitude 컬럼이 그 자리다(0004).
-- lclsSystm2 는 남긴다 — 주변 맛집에서 같은 업태를 뺄 때 쓴다.
UPDATE public.area_contents SET body = (
jsonb_strip_nulls(jsonb_build_object(
'name', COALESCE(NULLIF(body->>'title', ''), title),
'searchQuery', COALESCE(NULLIF(body->>'title', ''), title),
'location', NULLIF(body->>'addr1', ''),
'imageUrl', NULLIF(body->>'firstimage', ''),
'lclsSystm2', NULLIF(body->>'lclsSystm2', '')
))
-- 축제는 날짜 원값을 남긴다 — 노출 기간 필터가 읽는다.
|| CASE WHEN content_type = 2 THEN jsonb_strip_nulls(jsonb_build_object(
'eventstartdate', NULLIF(body->>'eventstartdate', ''),
'eventenddate', NULLIF(body->>'eventenddate', ''),
'homepage', NULLIF(body->>'homepage', ''),
'overview', NULLIF(body->>'overview', '')
)) ELSE '{}'::jsonb END
), updated_at = now()
WHERE deleted = FALSE AND content_type BETWEEN 2 AND 5 AND body ? 'contentid';
-- 3. 거리·숨김을 사이트 쪽으로
-- 사이트가 없는 업장은 다음 수집이 다시 쓴다 — 여기서 sites 행을 만들지 않는다.
-- items 배열이 아니라 ref → 값 맵이다. 순서는 읽는 쪽이 정한다.
INSERT INTO public.site_sections (site_id, section_id, data, source_type, status, sort_order)
SELECT s.site_id,
'local',
jsonb_build_object('kind', 'local', 'places', COALESCE(jsonb_object_agg(
r.local_content_id::text,
jsonb_build_object('kind', a.kind, 'distanceMeters', r.distance_m, 'hidden', r.hidden)
), '{}'::jsonb)),
2, -- SourceType.API — 사람이 쓴 게 아니라 수집이 만든 개인화 값이다
1,
0
FROM public.place_area_refs r
JOIN public.area_contents a ON a.local_content_id = r.local_content_id AND a.deleted = FALSE
JOIN public.sites s ON s.place_id = r.place_id AND s.deleted = FALSE
WHERE r.deleted = FALSE
GROUP BY s.site_id
ON CONFLICT DO NOTHING;
-- place_area_refs 는 아직 지우지 않는다. 읽는 코드가 옮겨 간 것을 확인한 뒤 별도 번호로 뗀다 —
-- 같은 마이그레이션에서 옮기고 지우면, 이관이 틀렸을 때 되돌릴 원본이 없다.