-- W3-3 포스팅 보드. 멱등. db/apply-local-ddl.sh 로 실행 DB 비파괴 적용. -- posts / post_categories / unity_feed_sources / unity_feed_items. 추가만, 파괴 없음. -- 1) post_categories (운영자 CRUD 카테고리 — D1/G2) CREATE SEQUENCE IF NOT EXISTS "post_categories_id_seq"; CREATE TABLE IF NOT EXISTS "post_categories" ( "id" bigint DEFAULT nextval('post_categories_id_seq'::regclass) NOT NULL, "name" character varying(80) NOT NULL, "slug" character varying(80) NOT NULL, "sort_order" integer DEFAULT 0 NOT NULL, "is_active" boolean DEFAULT true NOT NULL, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, PRIMARY KEY ("id") ); ALTER SEQUENCE "post_categories_id_seq" OWNED BY "post_categories"."id"; CREATE UNIQUE INDEX IF NOT EXISTS "ux_post_categories_slug" ON "post_categories" ("slug"); -- 2) posts (D1) — body_markdown 원본 + body_sanitized_html 캐시(D4), og_* 캐시(D5) CREATE SEQUENCE IF NOT EXISTS "posts_id_seq"; CREATE TABLE IF NOT EXISTS "posts" ( "id" bigint DEFAULT nextval('posts_id_seq'::regclass) NOT NULL, "category_id" bigint NOT NULL, "author_user_id" bigint NOT NULL, "title" character varying(200) NOT NULL, "body_markdown" text NOT NULL, "body_sanitized_html" text NOT NULL, "link_url" character varying(2048), "og_title" character varying(300), "og_description" character varying(600), "og_image_url" character varying(2048), "og_site_name" character varying(200), "og_fetched_at" timestamp with time zone, "status" character varying(20) DEFAULT 'PUBLISHED' NOT NULL, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, "deleted_at" timestamp with time zone, "is_delete" boolean DEFAULT false NOT NULL, PRIMARY KEY ("id") ); ALTER SEQUENCE "posts_id_seq" OWNED BY "posts"."id"; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'posts_category_id_fkey') THEN ALTER TABLE "posts" ADD CONSTRAINT "posts_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "post_categories" ("id"); END IF; IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'posts_author_user_id_fkey') THEN ALTER TABLE "posts" ADD CONSTRAINT "posts_author_user_id_fkey" FOREIGN KEY ("author_user_id") REFERENCES "users" ("id"); END IF; IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'posts_status_check') THEN ALTER TABLE "posts" ADD CONSTRAINT "posts_status_check" CHECK ("status" IN ('DRAFT', 'PUBLISHED')); END IF; END $$; CREATE INDEX IF NOT EXISTS "idx_posts_published_keyset" ON "posts" ("status", "is_delete", "created_at" DESC, "id" DESC); CREATE INDEX IF NOT EXISTS "idx_posts_category_keyset" ON "posts" ("category_id", "created_at" DESC, "id" DESC) WHERE "is_delete" = false AND "status" = 'PUBLISHED'; -- 3) unity_feed_sources (외부 피드 감시 소스 — D6/G5) CREATE SEQUENCE IF NOT EXISTS "unity_feed_sources_id_seq"; CREATE TABLE IF NOT EXISTS "unity_feed_sources" ( "id" bigint DEFAULT nextval('unity_feed_sources_id_seq'::regclass) NOT NULL, "name" character varying(120) NOT NULL, "feed_url" character varying(2048) NOT NULL, "is_active" boolean DEFAULT true NOT NULL, "last_polled_at" timestamp with time zone, "last_seen_guid" character varying(512), "last_error" character varying(500), "created_at" timestamp with time zone DEFAULT now() NOT NULL, PRIMARY KEY ("id") ); ALTER SEQUENCE "unity_feed_sources_id_seq" OWNED BY "unity_feed_sources"."id"; CREATE UNIQUE INDEX IF NOT EXISTS "ux_unity_feed_sources_url" ON "unity_feed_sources" ("feed_url"); -- 4) unity_feed_items (감지된 새 글 — 운영자 알림 표면 + dedupe 영속화) CREATE SEQUENCE IF NOT EXISTS "unity_feed_items_id_seq"; CREATE TABLE IF NOT EXISTS "unity_feed_items" ( "id" bigint DEFAULT nextval('unity_feed_items_id_seq'::regclass) NOT NULL, "source_id" bigint NOT NULL, "guid" character varying(512) NOT NULL, "title" character varying(500) NOT NULL, "link_url" character varying(2048) NOT NULL, "published_at" timestamp with time zone, "is_acknowledged" boolean DEFAULT false NOT NULL, "detected_at" timestamp with time zone DEFAULT now() NOT NULL, PRIMARY KEY ("id") ); ALTER SEQUENCE "unity_feed_items_id_seq" OWNED BY "unity_feed_items"."id"; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'unity_feed_items_source_id_fkey') THEN ALTER TABLE "unity_feed_items" ADD CONSTRAINT "unity_feed_items_source_id_fkey" FOREIGN KEY ("source_id") REFERENCES "unity_feed_sources" ("id"); END IF; END $$; CREATE UNIQUE INDEX IF NOT EXISTS "ux_unity_feed_items_source_guid" ON "unity_feed_items" ("source_id", "guid"); CREATE INDEX IF NOT EXISTS "idx_unity_feed_items_unack" ON "unity_feed_items" ("is_acknowledged", "detected_at" DESC) WHERE "is_acknowledged" = false;