-- W4 유저 배지/평판. 멱등. db/apply-local-ddl.sh 로 실행 DB 비파괴 적용. -- 신규 도메인(스토리지 0). 추가만, 파괴 없음. search_path=dev. -- 1) badges (배지 카탈로그 — 정의 가능·확장 가능) CREATE SEQUENCE IF NOT EXISTS "badges_id_seq"; CREATE TABLE IF NOT EXISTS "badges" ( "id" bigint DEFAULT nextval('badges_id_seq'::regclass) NOT NULL, "badge_key" character varying(50) NOT NULL, "display_name" character varying(100) NOT NULL, "description" character varying(500), "badge_type" character varying(30) NOT NULL, "criteria_json" text, "is_active" boolean DEFAULT true NOT NULL, "created_at" timestamp with time zone DEFAULT now() NOT NULL, PRIMARY KEY ("id") ); ALTER SEQUENCE "badges_id_seq" OWNED BY "badges"."id"; CREATE UNIQUE INDEX IF NOT EXISTS "ux_badges_key" ON "badges" ("badge_key"); DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'badges_type_check') THEN ALTER TABLE "badges" ADD CONSTRAINT "badges_type_check" CHECK ("badge_type" IN ('REVIEWER', 'TECHNICIAN')); END IF; END $$; -- 2) user_badges (유저↔배지 보유. 자동/수동 부여 공통 기록) CREATE SEQUENCE IF NOT EXISTS "user_badges_id_seq"; CREATE TABLE IF NOT EXISTS "user_badges" ( "id" bigint DEFAULT nextval('user_badges_id_seq'::regclass) NOT NULL, "user_id" bigint NOT NULL, "badge_key" character varying(50) NOT NULL, "awarded_by" bigint, "awarded_at" timestamp with time zone DEFAULT now() NOT NULL, "revoked_at" timestamp with time zone, "revoke_reason" character varying(500), PRIMARY KEY ("id") ); ALTER SEQUENCE "user_badges_id_seq" OWNED BY "user_badges"."id"; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_badges_user_id_fkey') THEN ALTER TABLE "user_badges" ADD CONSTRAINT "user_badges_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id"); END IF; END $$; CREATE UNIQUE INDEX IF NOT EXISTS "ux_user_badges_active" ON "user_badges" ("user_id", "badge_key") WHERE "revoked_at" IS NULL; CREATE INDEX IF NOT EXISTS "idx_user_badges_user_active" ON "user_badges" ("user_id") WHERE "revoked_at" IS NULL; -- 3) reputation_events (평판 신호 감사로그 — append-only, 임계 판정 단일 소스) CREATE SEQUENCE IF NOT EXISTS "reputation_events_id_seq"; CREATE TABLE IF NOT EXISTS "reputation_events" ( "id" bigint DEFAULT nextval('reputation_events_id_seq'::regclass) NOT NULL, "user_id" bigint NOT NULL, "event_type" character varying(40) NOT NULL, "source_ref" character varying(100), "weight" integer DEFAULT 1 NOT NULL, "created_at" timestamp with time zone DEFAULT now() NOT NULL, PRIMARY KEY ("id") ); ALTER SEQUENCE "reputation_events_id_seq" OWNED BY "reputation_events"."id"; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'reputation_events_type_check') THEN ALTER TABLE "reputation_events" ADD CONSTRAINT "reputation_events_type_check" CHECK ("event_type" IN ('REVIEW_WRITTEN', 'GAME_UPLOADED')); END IF; END $$; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'reputation_events_user_id_fkey') THEN ALTER TABLE "reputation_events" ADD CONSTRAINT "reputation_events_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id"); END IF; END $$; CREATE UNIQUE INDEX IF NOT EXISTS "ux_reputation_events_source" ON "reputation_events" ("user_id", "event_type", "source_ref") WHERE "source_ref" IS NOT NULL; CREATE INDEX IF NOT EXISTS "idx_reputation_events_user_type" ON "reputation_events" ("user_id", "event_type");