o2o-site-AEO/postgres-init/migrations/0003_site_contents.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

62 lines
3.6 KiB
SQL

-- 0003 · 섹션 콘텐츠를 sites.theme 에서 꺼내 행으로
--
-- theme 42,150 B 중 디자인은 636 B 뿐이고 나머지가 콘텐츠다. 크기가 아니라 쓰기 단위가
-- 문제였다 — 영상 주소 하나(592 B)를 고쳐도 42 KB 를 다시 쓰고, 둘이 만지면 뒤가 앞을 덮고,
-- 항목마다 출처·검증 상태를 물을 자리가 없다.
-- ★ 이 테이블이 JSON import/export 의 단위다.
CREATE TABLE IF NOT EXISTS site.site_contents (
site_content_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
site_id uuid NOT NULL, -- site.sites.site_id
section_id VARCHAR(50) NOT NULL, -- 'songs' 'itinerary' 'video' 'people' …
data JSONB NOT NULL, -- 그 섹션의 항목 배열(shared 의 XxxItem[] 계약)
source_type SMALLINT NOT NULL DEFAULT 1, -- SourceType: 1=owner 2=api 3=crawl 4=llm
-- ★ 공유 콘텐츠는 값을 복제하지 않고 **id 로 가리킨다**(축제·지역 이야기).
-- 가리키는 동안 data 는 비어 있을 수 있다 — 발행할 때 원본을 펼쳐 payload 에 싣는다.
shared_ref uuid NULL,
status SMALLINT NOT NULL DEFAULT 1, -- FactStatus 와 같은 축: 3=verified 4=corrected 만 노출
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted BOOLEAN NOT NULL DEFAULT FALSE
);
-- 한 사이트의 한 섹션은 한 행이다. 순서·on/off 는 theme 이 갖는다.
CREATE UNIQUE INDEX IF NOT EXISTS uq_site_contents_section
ON site.site_contents (site_id, section_id) WHERE deleted = FALSE;
-- 지역 이야기는 업장이 아니라 지역의 것이다 — 군산 이야기는 군산 숙소가 같이 쓴다.
CREATE TABLE IF NOT EXISTS local.region_stories (
region_story_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
region_code VARCHAR(10) NOT NULL, -- 카카오 행정구역 코드
kind VARCHAR(50) NOT NULL, -- 'songs' 'people' 'chronicle' 'postcard' 'quiz'
data JSONB NOT NULL,
source_type SMALLINT NOT NULL DEFAULT 4, -- 4=llm 1=운영자
status SMALLINT NOT NULL DEFAULT 1, -- FactStatus. 검수 전에는 안 나간다
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_region_stories_kind
ON local.region_stories (region_code, kind) WHERE deleted = FALSE;
-- theme 의 콘텐츠를 옮긴다. data 는 문자열이라 JSONB 로 되돌린다 — 깨진 것은 건너뛴다.
INSERT INTO site.site_contents (site_id, section_id, data, source_type, status, sort_order)
SELECT s.site_id,
sec->>'id',
(sec->>'data')::JSONB,
1, -- 사장님이 넣은 것으로 본다
3, -- 이미 발행에 쓰이던 값이라 verified
ordinality - 1
FROM site.sites s
CROSS JOIN LATERAL jsonb_array_elements(s.theme->'sections') WITH ORDINALITY AS t(sec, ordinality)
WHERE s.deleted = FALSE
AND s.theme ? 'sections'
AND sec->>'data' IS NOT NULL
AND sec->>'data' <> ''
AND jsonb_typeof((sec->>'data')::JSONB) IS NOT NULL
ON CONFLICT DO NOTHING;
-- theme.sections[].data 는 읽는 쪽이 옮겨간 뒤 별도 번호로 뗀다.