-- =============================================================================
-- bibimbap local-dev schema bootstrap
-- =============================================================================
-- 출처/권위 수준 (반드시 읽을 것):
--   * recruit_posts          : 권위 DDL. docs/recruit-posts-ddl.sql 와 동일 스펙.
--   * user_auth_identities   : security-hardening 의 active-unique index 포함(docs/security-hardening-ddl.sql).
--   * users / games /
--     game_comments / game_likes : **비권위 복원본**. 운영 DB 의 pg_dump 가 아니라
--       MyBatis 매퍼(@Insert/@Select)의 컬럼 목록 + data POJO 의 Java 타입에서 역추출.
--       컬럼 "이름/유무" 는 매퍼와 일치(확인됨)하나, **타입·길이·기본값·제약은 추론값**이다.
--       실제 운영 스키마와 대조(pg_dump) 전까지 권위 자료로 신뢰하지 말 것.
--
-- 타입 매핑 근거: Long→bigint, Integer→integer, Boolean→boolean,
--                 String→varchar(길이추정)/text, OffsetDateTime→timestamptz.
--
-- 적용 범위: 이 스크립트는 dev 스키마를 채운다(기본 프로필). live 스키마는 빈 상태로
--   생성만 한다. live 도 채우려면 아래 'SET search_path TO live;' 로 바꿔 동일 블록을 재실행.
--   PostgreSQL 컨테이너의 docker-entrypoint-initdb.d 에서 최초 1회 자동 실행된다.
-- =============================================================================

CREATE SCHEMA IF NOT EXISTS dev;
CREATE SCHEMA IF NOT EXISTS live;

SET search_path TO dev;

-- ---------------------------------------------------------------------------
-- users  (비권위 복원본)
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS "users_id_seq";
CREATE TABLE IF NOT EXISTS "users" (
    "id"              bigint DEFAULT nextval('users_id_seq'::regclass) NOT NULL,
    "display_name"    character varying(200),
    "canonical_email" character varying(320),
    "avatar_url"      character varying(500),
    "role"            character varying(30) DEFAULT 'USER' NOT NULL,
    "status"          character varying(30),
    "last_login_at"   timestamp with time zone,
    "created_at"      timestamp with time zone DEFAULT now() NOT NULL,
    "updated_at"      timestamp with time zone DEFAULT now() NOT NULL,
    "is_delete"       boolean DEFAULT false NOT NULL,
    PRIMARY KEY ("id")
);
ALTER SEQUENCE "users_id_seq" OWNED BY "users"."id";

-- W1 RBAC: users.role 값집합 확장(CHECK) + permissions_epoch (권위: docs/rbac-ddl.sql)
DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'users_role_check') THEN
        ALTER TABLE "users"
            ADD CONSTRAINT "users_role_check"
            CHECK ("role" IN ('ADMIN', 'SUBADMIN', 'USER'));
    END IF;
END
$$;
ALTER TABLE "users"
    ADD COLUMN IF NOT EXISTS "permissions_epoch" bigint DEFAULT 0 NOT NULL;

-- ---------------------------------------------------------------------------
-- user_auth_identities  (비권위 복원본 + security-hardening active-unique index)
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS "user_auth_identities_id_seq";
CREATE TABLE IF NOT EXISTS "user_auth_identities" (
    "id"               bigint DEFAULT nextval('user_auth_identities_id_seq'::regclass) NOT NULL,
    "user_id"          bigint NOT NULL REFERENCES "users" ("id"),
    "provider"         character varying(30) NOT NULL,
    "provider_user_id" character varying(200) NOT NULL,
    "email"            character varying(320),
    "password_hash"    character varying(500),
    "display_name"     character varying(200),
    "avatar_url"       character varying(500),
    "last_login_at"    timestamp with time zone,
    "created_at"       timestamp with time zone DEFAULT now() NOT NULL,
    "updated_at"       timestamp with time zone DEFAULT now() NOT NULL,
    "is_delete"        boolean DEFAULT false NOT NULL,
    PRIMARY KEY ("id")
);
ALTER SEQUENCE "user_auth_identities_id_seq" OWNED BY "user_auth_identities"."id";

CREATE UNIQUE INDEX IF NOT EXISTS "ux_user_auth_identities_provider_user_id_active"
    ON "user_auth_identities" ("provider", "provider_user_id")
    WHERE "is_delete" IS NOT TRUE;

-- ---------------------------------------------------------------------------
-- games  (비권위 복원본)
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS "games_id_seq";
CREATE TABLE IF NOT EXISTS "games" (
    "id"            bigint DEFAULT nextval('games_id_seq'::regclass) NOT NULL,
    "user_id"       bigint NOT NULL REFERENCES "users" ("id"),
    "name"          character varying(200) NOT NULL,
    "creator_note"  text,
    "git_url"       character varying(500),
    "webgl_path"    character varying(500),
    "thumbnail_url" character varying(500),
    "like_count"    integer DEFAULT 0 NOT NULL,
    "is_visible"    boolean DEFAULT true NOT NULL,
    "sort_order"    integer DEFAULT 0 NOT NULL,
    "created_at"    timestamp with time zone DEFAULT now() NOT NULL,
    "updated_at"    timestamp with time zone DEFAULT now() NOT NULL,
    "is_delete"     boolean DEFAULT false NOT NULL,
    PRIMARY KEY ("id")
);
ALTER SEQUENCE "games_id_seq" OWNED BY "games"."id";

CREATE INDEX IF NOT EXISTS "idx_games_visible_order"
    ON "games" ("is_visible", "is_delete", "sort_order", "created_at" DESC, "id" DESC);

CREATE INDEX IF NOT EXISTS "idx_games_visible_keyset"
    ON "games" ("is_visible", "is_delete", "sort_order" ASC, "created_at" DESC, "id" DESC);

-- ---------------------------------------------------------------------------
-- game_comments  (비권위 복원본 — 매퍼 존재하나 컨트롤러 미연결)
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS "game_comments_id_seq";
CREATE TABLE IF NOT EXISTS "game_comments" (
    "id"         bigint DEFAULT nextval('game_comments_id_seq'::regclass) NOT NULL,
    "game_id"    bigint NOT NULL REFERENCES "games" ("id"),
    "nickname"   character varying(100),
    "content"    text,
    "created_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 "game_comments_id_seq" OWNED BY "game_comments"."id";

-- game_comments user_id 컬럼 (W3-2: 작성자 귀속, nullable 비파괴)
ALTER TABLE "game_comments" ADD COLUMN IF NOT EXISTS "user_id" bigint REFERENCES "users" ("id");

-- ---------------------------------------------------------------------------
-- game_reviews  (권위 DDL — docs/game-reviews-ddl.sql 와 동일. W3-2 신규)
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS "game_reviews_id_seq";
CREATE TABLE IF NOT EXISTS "game_reviews" (
    "id"         bigint DEFAULT nextval('game_reviews_id_seq'::regclass) NOT NULL,
    "game_id"    bigint NOT NULL REFERENCES "games" ("id"),
    "user_id"    bigint NOT NULL REFERENCES "users" ("id"),
    "rating"     smallint NOT NULL,
    "body"       text,
    "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"),
    CONSTRAINT "game_reviews_rating_check" CHECK ("rating" BETWEEN 1 AND 5)
);
ALTER SEQUENCE "game_reviews_id_seq" OWNED BY "game_reviews"."id";
CREATE UNIQUE INDEX IF NOT EXISTS "ux_game_reviews_game_user_active"
    ON "game_reviews" ("game_id", "user_id") WHERE "is_delete" IS NOT TRUE;
CREATE INDEX IF NOT EXISTS "idx_game_reviews_game"
    ON "game_reviews" ("game_id") WHERE "is_delete" = false;
-- C3 재분류: 다축(육각형) game_review_axes + 집계뷰 game_review_stats 는 아래에 W3-2 일반 DDL 로 신설(roadmap.md:63,201,202, W2-3 동결 무관).

-- ---------------------------------------------------------------------------
-- game_review_axes / is_rating_manual / updated_at / game_review_stats (W3-2 고도화, docs/game-reviews-ddl.sql 와 동일)

-- 1) game_comments.updated_at (A3)
ALTER TABLE "game_comments"
    ADD COLUMN IF NOT EXISTS "updated_at" timestamp with time zone DEFAULT now() NOT NULL;
-- 기존 댓글이 '수정됨' 오표시되지 않도록 정렬(멱등: 이미 정렬된 행엔 무영향)
UPDATE "game_comments" SET "updated_at" = "created_at" WHERE "updated_at" > "created_at";
COMMENT ON COLUMN "game_comments"."updated_at" IS '덧글 마지막 수정 시각. updated_at > created_at 이면 수정됨(리뷰 대칭)';

-- 2) game_reviews.is_rating_manual (overall 출처 구분)
ALTER TABLE "game_reviews"
    ADD COLUMN IF NOT EXISTS "is_rating_manual" boolean DEFAULT false NOT NULL;
COMMENT ON COLUMN "game_reviews"."is_rating_manual" IS 'true=유저 직접선택 overall, false=6축 자동평균';

-- 3) game_review_axes (다축 평점, 리뷰당 6행)
CREATE SEQUENCE IF NOT EXISTS "game_review_axes_id_seq";
CREATE TABLE IF NOT EXISTS "game_review_axes" (
    "id"        bigint DEFAULT nextval('game_review_axes_id_seq'::regclass) NOT NULL,
    "review_id" bigint NOT NULL,
    "axis_key"  character varying(20) NOT NULL,
    "score"     smallint NOT NULL,
    PRIMARY KEY ("id")
);
ALTER SEQUENCE "game_review_axes_id_seq" OWNED BY "game_review_axes"."id";

DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'game_review_axes_review_id_fkey') THEN
        ALTER TABLE "game_review_axes"
            ADD CONSTRAINT "game_review_axes_review_id_fkey"
            FOREIGN KEY ("review_id") REFERENCES "game_reviews" ("id");
    END IF;
END
$$;

DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'game_review_axes_score_check') THEN
        ALTER TABLE "game_review_axes"
            ADD CONSTRAINT "game_review_axes_score_check" CHECK ("score" BETWEEN 1 AND 5);
    END IF;
END
$$;

DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'game_review_axes_axis_key_check') THEN
        ALTER TABLE "game_review_axes"
            ADD CONSTRAINT "game_review_axes_axis_key_check"
            CHECK ("axis_key" IN ('immersion','creativity','controls','completeness','sound','visual'));
    END IF;
END
$$;

CREATE UNIQUE INDEX IF NOT EXISTS "ux_game_review_axes_review_axis"
    ON "game_review_axes" ("review_id", "axis_key");
CREATE INDEX IF NOT EXISTS "idx_game_review_axes_review"
    ON "game_review_axes" ("review_id");

COMMENT ON TABLE "game_review_axes" IS '리뷰 다축 평점(6축, 리뷰당 6행). axis_key 6종 각 1~5';
COMMENT ON COLUMN "game_review_axes"."axis_key" IS '몰입성 immersion/창의성 creativity/조작성 controls/완성도 completeness/사운드 sound/비주얼 visual';

-- 4) game_review_stats (읽기전용 집계뷰 — 클라 평균계산 폐기 공급원)
-- fan-out 방지: game_review_axes 직접 JOIN 시 리뷰가 axes 행수만큼 복제되어
-- COUNT(*)/AVG(rating) 왜곡. 축 평균은 서브쿼리에서 game 단위 선집계 후 LEFT JOIN.
CREATE OR REPLACE VIEW "game_review_stats" AS
SELECT
    r."game_id"                          AS "game_id",
    ROUND(AVG(r."rating")::numeric, 1)   AS "avg_rating",
    COUNT(*)                             AS "review_count",
    MAX(ax."avg_immersion")              AS "avg_immersion",
    MAX(ax."avg_creativity")             AS "avg_creativity",
    MAX(ax."avg_controls")               AS "avg_controls",
    MAX(ax."avg_completeness")           AS "avg_completeness",
    MAX(ax."avg_sound")                  AS "avg_sound",
    MAX(ax."avg_visual")                 AS "avg_visual"
FROM "game_reviews" r
LEFT JOIN (
    SELECT
        rv."game_id"                                                       AS "game_id",
        ROUND(AVG(a."score") FILTER (WHERE a."axis_key"='immersion'),1)    AS "avg_immersion",
        ROUND(AVG(a."score") FILTER (WHERE a."axis_key"='creativity'),1)   AS "avg_creativity",
        ROUND(AVG(a."score") FILTER (WHERE a."axis_key"='controls'),1)     AS "avg_controls",
        ROUND(AVG(a."score") FILTER (WHERE a."axis_key"='completeness'),1)  AS "avg_completeness",
        ROUND(AVG(a."score") FILTER (WHERE a."axis_key"='sound'),1)        AS "avg_sound",
        ROUND(AVG(a."score") FILTER (WHERE a."axis_key"='visual'),1)       AS "avg_visual"
    FROM "game_reviews" rv
    JOIN "game_review_axes" a ON a."review_id" = rv."id"
    WHERE rv."is_delete" IS NOT TRUE
    GROUP BY rv."game_id"
) ax ON ax."game_id" = r."game_id"
WHERE r."is_delete" IS NOT TRUE
GROUP BY r."game_id";
COMMENT ON VIEW "game_review_stats" IS 'W3-2 일반 집계뷰(W2-3 동결 무관). 게임별 평균별점·리뷰수·6축평균';

-- ---------------------------------------------------------------------------
-- game_likes  (비권위 복원본 — 매퍼는 hard delete 사용, is_delete 컬럼 없음)
-- uq_game_likes_game_user: (game_id, user_key) UNIQUE 로 사용자별 1회 좋아요 멱등 보장
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS "game_likes_id_seq";
CREATE TABLE IF NOT EXISTS "game_likes" (
    "id"         bigint DEFAULT nextval('game_likes_id_seq'::regclass) NOT NULL,
    "game_id"    bigint NOT NULL REFERENCES "games" ("id"),
    "user_key"   character varying(200) NOT NULL,
    "created_at" timestamp with time zone DEFAULT now() NOT NULL,
    PRIMARY KEY ("id"),
    CONSTRAINT "uq_game_likes_game_user" UNIQUE ("game_id", "user_key")
);
ALTER SEQUENCE "game_likes_id_seq" OWNED BY "game_likes"."id";

-- ---------------------------------------------------------------------------
-- recruit_posts  (권위 DDL — docs/recruit-posts-ddl.sql 와 동일)
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS "recruit_posts_id_seq";
CREATE TABLE IF NOT EXISTS "recruit_posts" (
    "id"                 bigint DEFAULT nextval('recruit_posts_id_seq'::regclass) NOT NULL,
    "user_id"            bigint NOT NULL,
    "project_name"       character varying(200) NOT NULL,
    "genre"              character varying(80),
    "summary"            character varying(200) NOT NULL,
    "role"               character varying(30) NOT NULL,
    "project_status"     character varying(50) NOT NULL,
    "participation_type" character varying(30) NOT NULL,
    "expected_period"    character varying(80),
    "team_members"       character varying(200),
    "contact"            character varying(200) NOT NULL,
    "description"        text,
    "reference_url"      character varying(500),
    "deadline_at"        timestamp with time zone,
    "is_visible"         boolean DEFAULT true NOT NULL,
    "sort_order"         integer DEFAULT 0 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 "recruit_posts_id_seq" OWNED BY "recruit_posts"."id";

ALTER TABLE "recruit_posts"
    ADD CONSTRAINT "recruit_posts_user_id_fkey"
    FOREIGN KEY ("user_id") REFERENCES "users" ("id");
ALTER TABLE "recruit_posts"
    ADD CONSTRAINT "recruit_posts_role_check"
    CHECK ("role" IN ('기획', '아트', '프로그래머'));
ALTER TABLE "recruit_posts"
    ADD CONSTRAINT "recruit_posts_participation_type_check"
    CHECK ("participation_type" IN ('취미', '수익쉐어', '유급', '게임잼'));

CREATE INDEX IF NOT EXISTS "idx_recruit_posts_visible_order"
    ON "recruit_posts" ("is_visible", "is_delete", "sort_order", "created_at" DESC, "id" DESC);
CREATE INDEX IF NOT EXISTS "idx_recruit_posts_role"
    ON "recruit_posts" ("role")
    WHERE "is_delete" = false AND "is_visible" = true;
CREATE INDEX IF NOT EXISTS "idx_recruit_posts_participation_type"
    ON "recruit_posts" ("participation_type")
    WHERE "is_delete" = false AND "is_visible" = true;

-- ---------------------------------------------------------------------------
-- W1 RBAC: permissions / user_permissions / rbac_audit_log
-- (권위 DDL — docs/rbac-ddl.sql 와 동일 스펙)
-- ---------------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS "permissions_id_seq";
CREATE TABLE IF NOT EXISTS "permissions" (
    "id"             bigint DEFAULT nextval('permissions_id_seq'::regclass) NOT NULL,
    "permission_key" character varying(50) NOT NULL,
    "display_name"   character varying(100) NOT NULL,
    "is_active"      boolean DEFAULT true NOT NULL,
    "created_at"     timestamp with time zone DEFAULT now() NOT NULL,
    PRIMARY KEY ("id")
);
ALTER SEQUENCE "permissions_id_seq" OWNED BY "permissions"."id";
CREATE UNIQUE INDEX IF NOT EXISTS "ux_permissions_key"
    ON "permissions" ("permission_key");

CREATE SEQUENCE IF NOT EXISTS "user_permissions_id_seq";
CREATE TABLE IF NOT EXISTS "user_permissions" (
    "id"             bigint DEFAULT nextval('user_permissions_id_seq'::regclass) NOT NULL,
    "user_id"        bigint NOT NULL,
    "permission_key" character varying(50) NOT NULL,
    "granted_by"     bigint,
    "created_at"     timestamp with time zone DEFAULT now() NOT NULL,
    PRIMARY KEY ("id")
);
ALTER SEQUENCE "user_permissions_id_seq" OWNED BY "user_permissions"."id";
DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_permissions_user_id_fkey') THEN
        ALTER TABLE "user_permissions"
            ADD CONSTRAINT "user_permissions_user_id_fkey"
            FOREIGN KEY ("user_id") REFERENCES "users" ("id");
    END IF;
END
$$;
CREATE UNIQUE INDEX IF NOT EXISTS "ux_user_permissions_user_key"
    ON "user_permissions" ("user_id", "permission_key");
CREATE INDEX IF NOT EXISTS "idx_user_permissions_user"
    ON "user_permissions" ("user_id");

CREATE SEQUENCE IF NOT EXISTS "rbac_audit_log_id_seq";
CREATE TABLE IF NOT EXISTS "rbac_audit_log" (
    "id"            bigint DEFAULT nextval('rbac_audit_log_id_seq'::regclass) NOT NULL,
    "actor_id"      bigint NOT NULL,
    "target_id"     bigint NOT NULL,
    "action"        character varying(30) NOT NULL,
    "permission_key" character varying(50),
    "created_at"    timestamp with time zone DEFAULT now() NOT NULL,
    PRIMARY KEY ("id")
);
ALTER SEQUENCE "rbac_audit_log_id_seq" OWNED BY "rbac_audit_log"."id";
DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'rbac_audit_log_action_check') THEN
        ALTER TABLE "rbac_audit_log"
            ADD CONSTRAINT "rbac_audit_log_action_check"
            CHECK ("action" IN ('APPOINT', 'DEMOTE', 'GRANT', 'REVOKE'));
    END IF;
END
$$;

-- ---------------------------------------------------------------------------
-- 게임잼 W2-1: jams / jam_teams / jam_team_members / jam_entries / jam_status_log
-- (권위 DDL — docs/jam-ddl.sql 와 동일)
-- ---------------------------------------------------------------------------
-- W2-1 게임잼 엔티티/라이프사이클. 멱등. db/apply-local-ddl.sh 로 실행 DB 비파괴 적용.
-- games 변경 없음(연결은 jam_entries 가 보유). 추가만, 파괴 없음.

-- ===========================================================================
-- 1) jams (게임잼 회차. 회차 독립 = 다중 인스턴스)
-- ===========================================================================
CREATE SEQUENCE IF NOT EXISTS "jams_id_seq";
CREATE TABLE IF NOT EXISTS "jams" (
    "id"             bigint DEFAULT nextval('jams_id_seq'::regclass) NOT NULL,
    "slug"           character varying(80)  NOT NULL,
    "title"          character varying(200) NOT NULL,
    "description"    text,
    "status"         character varying(20)  DEFAULT 'RECRUIT' NOT NULL,
    "recruit_start_at" timestamp with time zone,
    "dev_start_at"     timestamp with time zone,
    "eval_start_at"    timestamp with time zone,
    "eval_end_at"      timestamp with time zone,
    "discord_url"      character varying(500),
    "prize_info"       text,
    "sponsor_info"     text,
    "is_visible"     boolean DEFAULT true  NOT NULL,
    "created_by"     bigint,
    "created_at"     timestamp with time zone DEFAULT now() NOT NULL,
    "updated_at"     timestamp with time zone DEFAULT now() NOT NULL,
    "is_delete"      boolean DEFAULT false NOT NULL,
    PRIMARY KEY ("id")
);
ALTER SEQUENCE "jams_id_seq" OWNED BY "jams"."id";

DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jams_status_check') THEN
        ALTER TABLE "jams"
            ADD CONSTRAINT "jams_status_check"
            CHECK ("status" IN ('RECRUIT', 'DEV', 'EVAL', 'CLOSED'));
    END IF;
END
$$;

CREATE UNIQUE INDEX IF NOT EXISTS "ux_jams_slug_active"
    ON "jams" ("slug") WHERE "is_delete" IS NOT TRUE;
CREATE INDEX IF NOT EXISTS "idx_jams_visible_keyset"
    ON "jams" ("is_visible", "is_delete", "created_at" DESC, "id" DESC);

-- ===========================================================================
-- 2) jam_teams
-- ===========================================================================
CREATE SEQUENCE IF NOT EXISTS "jam_teams_id_seq";
CREATE TABLE IF NOT EXISTS "jam_teams" (
    "id"          bigint DEFAULT nextval('jam_teams_id_seq'::regclass) NOT NULL,
    "jam_id"      bigint NOT NULL,
    "name"        character varying(120) NOT NULL,
    "owner_user_id" bigint NOT NULL,
    "created_at"  timestamp with time zone DEFAULT now() NOT NULL,
    "is_delete"   boolean DEFAULT false NOT NULL,
    PRIMARY KEY ("id")
);
ALTER SEQUENCE "jam_teams_id_seq" OWNED BY "jam_teams"."id";
DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_teams_jam_id_fkey') THEN
        ALTER TABLE "jam_teams" ADD CONSTRAINT "jam_teams_jam_id_fkey"
            FOREIGN KEY ("jam_id") REFERENCES "jams" ("id");
    END IF;
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_teams_owner_fkey') THEN
        ALTER TABLE "jam_teams" ADD CONSTRAINT "jam_teams_owner_fkey"
            FOREIGN KEY ("owner_user_id") REFERENCES "users" ("id");
    END IF;
END
$$;
CREATE INDEX IF NOT EXISTS "idx_jam_teams_jam" ON "jam_teams" ("jam_id");

-- ===========================================================================
-- 3) jam_team_members
-- ===========================================================================
CREATE SEQUENCE IF NOT EXISTS "jam_team_members_id_seq";
CREATE TABLE IF NOT EXISTS "jam_team_members" (
    "id"           bigint DEFAULT nextval('jam_team_members_id_seq'::regclass) NOT NULL,
    "jam_team_id"  bigint NOT NULL,
    "user_id"      bigint NOT NULL,
    "created_at"   timestamp with time zone DEFAULT now() NOT NULL,
    PRIMARY KEY ("id")
);
ALTER SEQUENCE "jam_team_members_id_seq" OWNED BY "jam_team_members"."id";
DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_team_members_team_fkey') THEN
        ALTER TABLE "jam_team_members" ADD CONSTRAINT "jam_team_members_team_fkey"
            FOREIGN KEY ("jam_team_id") REFERENCES "jam_teams" ("id");
    END IF;
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_team_members_user_fkey') THEN
        ALTER TABLE "jam_team_members" ADD CONSTRAINT "jam_team_members_user_fkey"
            FOREIGN KEY ("user_id") REFERENCES "users" ("id");
    END IF;
END
$$;
CREATE UNIQUE INDEX IF NOT EXISTS "ux_jam_team_members_team_user"
    ON "jam_team_members" ("jam_team_id", "user_id");

-- ===========================================================================
-- 4) jam_entries (출품작 = 잼-게임 연결 조인. 평가 단위 = (jam_id, game_id) 활성 자연키)
-- ===========================================================================
CREATE SEQUENCE IF NOT EXISTS "jam_entries_id_seq";
CREATE TABLE IF NOT EXISTS "jam_entries" (
    "id"             bigint DEFAULT nextval('jam_entries_id_seq'::regclass) NOT NULL,
    "jam_id"         bigint NOT NULL,
    "game_id"        bigint NOT NULL,
    "entrant_type"   character varying(10) NOT NULL,
    "entrant_user_id" bigint,
    "jam_team_id"    bigint,
    "submitted_at"   timestamp with time zone DEFAULT now() NOT NULL,
    "is_delete"      boolean DEFAULT false NOT NULL,
    PRIMARY KEY ("id")
);
ALTER SEQUENCE "jam_entries_id_seq" OWNED BY "jam_entries"."id";
DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_entries_jam_fkey') THEN
        ALTER TABLE "jam_entries" ADD CONSTRAINT "jam_entries_jam_fkey"
            FOREIGN KEY ("jam_id") REFERENCES "jams" ("id");
    END IF;
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_entries_game_fkey') THEN
        ALTER TABLE "jam_entries" ADD CONSTRAINT "jam_entries_game_fkey"
            FOREIGN KEY ("game_id") REFERENCES "games" ("id");
    END IF;
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_entries_team_fkey') THEN
        ALTER TABLE "jam_entries" ADD CONSTRAINT "jam_entries_team_fkey"
            FOREIGN KEY ("jam_team_id") REFERENCES "jam_teams" ("id");
    END IF;
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_entries_user_fkey') THEN
        ALTER TABLE "jam_entries" ADD CONSTRAINT "jam_entries_user_fkey"
            FOREIGN KEY ("entrant_user_id") REFERENCES "users" ("id");
    END IF;
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_entries_entrant_type_check') THEN
        ALTER TABLE "jam_entries" ADD CONSTRAINT "jam_entries_entrant_type_check"
            CHECK ("entrant_type" IN ('USER', 'TEAM'));
    END IF;
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_entries_entrant_xor_check') THEN
        ALTER TABLE "jam_entries" ADD CONSTRAINT "jam_entries_entrant_xor_check"
            CHECK (
                ("entrant_type" = 'USER' AND "entrant_user_id" IS NOT NULL AND "jam_team_id" IS NULL)
                OR
                ("entrant_type" = 'TEAM' AND "jam_team_id" IS NOT NULL AND "entrant_user_id" IS NULL)
            );
    END IF;
END
$$;
CREATE UNIQUE INDEX IF NOT EXISTS "ux_jam_entries_jam_game_active"
    ON "jam_entries" ("jam_id", "game_id") WHERE "is_delete" IS NOT TRUE;
CREATE INDEX IF NOT EXISTS "idx_jam_entries_jam" ON "jam_entries" ("jam_id");
CREATE INDEX IF NOT EXISTS "idx_jam_entries_game" ON "jam_entries" ("game_id");

-- ===========================================================================
-- 5) jam_status_log
-- ===========================================================================
CREATE SEQUENCE IF NOT EXISTS "jam_status_log_id_seq";
CREATE TABLE IF NOT EXISTS "jam_status_log" (
    "id"          bigint DEFAULT nextval('jam_status_log_id_seq'::regclass) NOT NULL,
    "jam_id"      bigint NOT NULL,
    "from_status" character varying(20),
    "to_status"   character varying(20) NOT NULL,
    "actor_id"    bigint,
    "transition_type" character varying(10) DEFAULT 'MANUAL' NOT NULL,
    "created_at"  timestamp with time zone DEFAULT now() NOT NULL,
    PRIMARY KEY ("id")
);
ALTER SEQUENCE "jam_status_log_id_seq" OWNED BY "jam_status_log"."id";
DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_status_log_jam_fkey') THEN
        ALTER TABLE "jam_status_log" ADD CONSTRAINT "jam_status_log_jam_fkey"
            FOREIGN KEY ("jam_id") REFERENCES "jams" ("id");
    END IF;
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_status_log_to_status_check') THEN
        ALTER TABLE "jam_status_log" ADD CONSTRAINT "jam_status_log_to_status_check"
            CHECK ("to_status" IN ('RECRUIT', 'DEV', 'EVAL', 'CLOSED'));
    END IF;
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_status_log_transition_type_check') THEN
        ALTER TABLE "jam_status_log" ADD CONSTRAINT "jam_status_log_transition_type_check"
            CHECK ("transition_type" IN ('MANUAL', 'AUTO'));
    END IF;
END
$$;
CREATE INDEX IF NOT EXISTS "idx_jam_status_log_jam" ON "jam_status_log" ("jam_id", "created_at" DESC);

-- ===========================================================================
-- 심사위원 역할 W2-2 (권위 DDL — docs/jam-judge-ddl.sql 와 동일. 잼 스코프 역할)
-- ===========================================================================
-- ===========================================================================
-- 1) jam_judges (잼별 심사위원. 잼 스코프 역할. 전역 권한과 별도 축)
-- ===========================================================================
CREATE SEQUENCE IF NOT EXISTS "jam_judges_id_seq";
CREATE TABLE IF NOT EXISTS "jam_judges" (
    "id"          bigint DEFAULT nextval('jam_judges_id_seq'::regclass) NOT NULL,
    "jam_id"      bigint NOT NULL,                          -- 잼 회차(FK jams)
    "user_id"     bigint NOT NULL,                          -- 심사위원(FK users; 누구나 가능)
    "assigned_by" bigint,                                   -- 지정 관리자(FK users; 감사 보조, nullable)
    "created_at"  timestamp with time zone DEFAULT now() NOT NULL,
    PRIMARY KEY ("id")
);
ALTER SEQUENCE "jam_judges_id_seq" OWNED BY "jam_judges"."id";
DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_judges_jam_fkey') THEN
        ALTER TABLE "jam_judges" ADD CONSTRAINT "jam_judges_jam_fkey"
            FOREIGN KEY ("jam_id") REFERENCES "jams" ("id");
    END IF;
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_judges_user_fkey') THEN
        ALTER TABLE "jam_judges" ADD CONSTRAINT "jam_judges_user_fkey"
            FOREIGN KEY ("user_id") REFERENCES "users" ("id");
    END IF;
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_judges_assigned_by_fkey') THEN
        ALTER TABLE "jam_judges" ADD CONSTRAINT "jam_judges_assigned_by_fkey"
            FOREIGN KEY ("assigned_by") REFERENCES "users" ("id");
    END IF;
END
$$;
-- 같은 잼에 같은 유저 중복 지정 방지(멱등 지정). 잼 종료 후 잔존(J5) — soft delete 없음(이력=행 존재).
-- 해제는 hard DELETE(역할 회수). 재지정은 다시 INSERT.
CREATE UNIQUE INDEX IF NOT EXISTS "ux_jam_judges_jam_user"
    ON "jam_judges" ("jam_id", "user_id");
CREATE INDEX IF NOT EXISTS "idx_jam_judges_jam"
    ON "jam_judges" ("jam_id");

-- ---------------------------------------------------------------------------
-- 잼 평가 W2-3 (권위 DDL — docs/jam-eval-ddl.sql 와 동일. 심사/투표/시상 동결)
-- ---------------------------------------------------------------------------
-- 평가단위 = (jam_id, game_id) 자연키 = 활성 출품작(jam_entries 1:1). jams/games/users 선존재.
-- game_reviews/game_review_stats(W3-2) 무변경 — 시상 USER_RATING 트랙이 VIEW 를 읽기만.

-- ===========================================================================
-- 1) jam_criteria (잼별 설정형 심사 기준. 고정 6축 강제 대신 정석)
-- ===========================================================================
CREATE SEQUENCE IF NOT EXISTS "jam_criteria_id_seq";
CREATE TABLE IF NOT EXISTS "jam_criteria" (
    "id"           bigint DEFAULT nextval('jam_criteria_id_seq'::regclass) NOT NULL,
    "jam_id"       bigint NOT NULL,
    "criterion_key" character varying(40) NOT NULL,           -- 잼 내 기준 식별(영문 키)
    "display_name" character varying(100) NOT NULL,           -- 표시 라벨
    "sort_order"   integer DEFAULT 0 NOT NULL,                -- 표시 순서
    "weight"       numeric(6,3) DEFAULT 1.0 NOT NULL,         -- 가중 종합 산정 가중치(>0 권장)
    "created_at"   timestamp with time zone DEFAULT now() NOT NULL,
    PRIMARY KEY ("id")
);
ALTER SEQUENCE "jam_criteria_id_seq" OWNED BY "jam_criteria"."id";
DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_criteria_jam_fkey') THEN
        ALTER TABLE "jam_criteria" ADD CONSTRAINT "jam_criteria_jam_fkey"
            FOREIGN KEY ("jam_id") REFERENCES "jams" ("id");
    END IF;
    -- weight 음수 방지(0 은 허용하되 가중종합에서 NULLIF 가드 — 0-division 방어는 VIEW)
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_criteria_weight_check') THEN
        ALTER TABLE "jam_criteria" ADD CONSTRAINT "jam_criteria_weight_check"
            CHECK ("weight" >= 0);
    END IF;
END
$$;
-- 잼 내 criterion_key 중복 방지
CREATE UNIQUE INDEX IF NOT EXISTS "ux_jam_criteria_jam_key"
    ON "jam_criteria" ("jam_id", "criterion_key");
CREATE INDEX IF NOT EXISTS "idx_jam_criteria_jam"
    ON "jam_criteria" ("jam_id", "sort_order");

-- ===========================================================================
-- 2) jam_scores (심사위원 점수. criterion 단위 행. is-judge 검증은 앱계층 W2-2)
-- ===========================================================================
CREATE SEQUENCE IF NOT EXISTS "jam_scores_id_seq";
CREATE TABLE IF NOT EXISTS "jam_scores" (
    "id"            bigint DEFAULT nextval('jam_scores_id_seq'::regclass) NOT NULL,
    "jam_id"        bigint NOT NULL,                          -- 평가단위 자연키 1/2
    "game_id"       bigint NOT NULL,                          -- 평가단위 자연키 2/2(출품작)
    "judge_user_id" bigint NOT NULL,                          -- 심사위원(FK users; is-judge 는 W2-2)
    "criterion_key" character varying(40) NOT NULL,           -- jam_criteria.criterion_key 논리참조
    "score"         smallint NOT NULL,                        -- 1~5
    "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 "jam_scores_id_seq" OWNED BY "jam_scores"."id";
DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_scores_jam_fkey') THEN
        ALTER TABLE "jam_scores" ADD CONSTRAINT "jam_scores_jam_fkey"
            FOREIGN KEY ("jam_id") REFERENCES "jams" ("id");
    END IF;
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_scores_game_fkey') THEN
        ALTER TABLE "jam_scores" ADD CONSTRAINT "jam_scores_game_fkey"
            FOREIGN KEY ("game_id") REFERENCES "games" ("id");
    END IF;
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_scores_judge_fkey') THEN
        ALTER TABLE "jam_scores" ADD CONSTRAINT "jam_scores_judge_fkey"
            FOREIGN KEY ("judge_user_id") REFERENCES "users" ("id");
    END IF;
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_scores_score_check') THEN
        ALTER TABLE "jam_scores" ADD CONSTRAINT "jam_scores_score_check"
            CHECK ("score" BETWEEN 1 AND 5);
    END IF;
END
$$;
-- 한 심사위원이 한 출품작의 한 기준에 1점만(수정=UPDATE, 재입력 멱등)
CREATE UNIQUE INDEX IF NOT EXISTS "ux_jam_scores_jam_game_judge_criterion"
    ON "jam_scores" ("jam_id", "game_id", "judge_user_id", "criterion_key");
-- 집계 join 대상(출품작 단위 선집계)
CREATE INDEX IF NOT EXISTS "idx_jam_scores_jam_game"
    ON "jam_scores" ("jam_id", "game_id");

-- ===========================================================================
-- 3) jam_votes (인기투표. 잼당 1인 1표. game_likes 와 별개. 평가기간 게이트=앱계층)
-- ===========================================================================
CREATE SEQUENCE IF NOT EXISTS "jam_votes_id_seq";
CREATE TABLE IF NOT EXISTS "jam_votes" (
    "id"            bigint DEFAULT nextval('jam_votes_id_seq'::regclass) NOT NULL,
    "jam_id"        bigint NOT NULL,                          -- 평가단위 자연키 1/2
    "game_id"       bigint NOT NULL,                          -- 투표 대상 출품작
    "voter_user_id" bigint NOT NULL,                          -- 투표자(FK users; 로그인 1인1표)
    "created_at"    timestamp with time zone DEFAULT now() NOT NULL,
    PRIMARY KEY ("id")
);
ALTER SEQUENCE "jam_votes_id_seq" OWNED BY "jam_votes"."id";
DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_votes_jam_fkey') THEN
        ALTER TABLE "jam_votes" ADD CONSTRAINT "jam_votes_jam_fkey"
            FOREIGN KEY ("jam_id") REFERENCES "jams" ("id");
    END IF;
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_votes_game_fkey') THEN
        ALTER TABLE "jam_votes" ADD CONSTRAINT "jam_votes_game_fkey"
            FOREIGN KEY ("game_id") REFERENCES "games" ("id");
    END IF;
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_votes_voter_fkey') THEN
        ALTER TABLE "jam_votes" ADD CONSTRAINT "jam_votes_voter_fkey"
            FOREIGN KEY ("voter_user_id") REFERENCES "users" ("id");
    END IF;
END
$$;
-- 잼당 1인 1표(최애 1개). 표 변경 = UPDATE game_id 또는 DELETE→INSERT(앱 정책 W2-5)
CREATE UNIQUE INDEX IF NOT EXISTS "ux_jam_votes_jam_voter"
    ON "jam_votes" ("jam_id", "voter_user_id");
-- 투표 집계(출품작별 count)
CREATE INDEX IF NOT EXISTS "idx_jam_votes_jam_game"
    ON "jam_votes" ("jam_id", "game_id");

-- ===========================================================================
-- 4) jam_awards (시상. 3트랙 + grand. 트랙별 개별 수상 + 가중 grand)
-- ===========================================================================
CREATE SEQUENCE IF NOT EXISTS "jam_awards_id_seq";
CREATE TABLE IF NOT EXISTS "jam_awards" (
    "id"          bigint DEFAULT nextval('jam_awards_id_seq'::regclass) NOT NULL,
    "jam_id"      bigint NOT NULL,                            -- 평가단위 자연키 1/2
    "game_id"     bigint NOT NULL,                            -- 수상 출품작
    "award_track" character varying(20) NOT NULL,             -- JUDGE|USER_RATING|POPULAR|GRAND
    "rank"        integer NOT NULL,                           -- 트랙 내 순위(1=대상)
    "score_value" numeric(10,4),                              -- 산정 점수(트랙별 의미 다름; NULL 허용)
    "computed_at" timestamp with time zone DEFAULT now() NOT NULL,
    PRIMARY KEY ("id")
);
ALTER SEQUENCE "jam_awards_id_seq" OWNED BY "jam_awards"."id";
DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_awards_jam_fkey') THEN
        ALTER TABLE "jam_awards" ADD CONSTRAINT "jam_awards_jam_fkey"
            FOREIGN KEY ("jam_id") REFERENCES "jams" ("id");
    END IF;
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_awards_game_fkey') THEN
        ALTER TABLE "jam_awards" ADD CONSTRAINT "jam_awards_game_fkey"
            FOREIGN KEY ("game_id") REFERENCES "games" ("id");
    END IF;
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_awards_track_check') THEN
        ALTER TABLE "jam_awards" ADD CONSTRAINT "jam_awards_track_check"
            CHECK ("award_track" IN ('JUDGE', 'USER_RATING', 'POPULAR', 'GRAND'));
    END IF;
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'jam_awards_rank_check') THEN
        ALTER TABLE "jam_awards" ADD CONSTRAINT "jam_awards_rank_check"
            CHECK ("rank" >= 1);
    END IF;
END
$$;
-- 잼·트랙·순위 1행(재산정=DELETE→INSERT 또는 UPSERT 멱등). 동률은 같은 rank 허용 위해
-- (jam_id, award_track, game_id) UNIQUE 로 출품작 트랙 중복만 차단(같은 rank 동률 허용).
CREATE UNIQUE INDEX IF NOT EXISTS "ux_jam_awards_jam_track_game"
    ON "jam_awards" ("jam_id", "award_track", "game_id");
CREATE INDEX IF NOT EXISTS "idx_jam_awards_jam_track"
    ON "jam_awards" ("jam_id", "award_track", "rank");

-- ===========================================================================
-- 5) jam_score_stats (심사 집계 VIEW. criterion별 평균 + 가중 종합. fan-out 방지)
--    game_review_stats(schema.sql:217) 선례: 출품작 단위 선집계 후 노출.
--    가중 종합 = SUM(criterion_avg * weight) / NULLIF(SUM(weight),0)  (0-division 가드)
--    criterion 미채점 시 그 criterion 은 가중 종합에서 제외(점수 있는 기준만).
-- ===========================================================================
CREATE OR REPLACE VIEW "jam_score_stats" AS
WITH per_criterion AS (
    SELECT
        s."jam_id"                          AS jam_id,
        s."game_id"                         AS game_id,
        s."criterion_key"                   AS criterion_key,
        AVG(s."score")::numeric             AS criterion_avg,
        COUNT(DISTINCT s."judge_user_id")   AS judge_count
    FROM "jam_scores" s
    GROUP BY s."jam_id", s."game_id", s."criterion_key"
)
SELECT
    pc."jam_id"                                     AS jam_id,
    pc."game_id"                                    AS game_id,
    -- 출품작 단위 종합: 채점된 criterion 의 가중 평균
    ROUND(
        SUM(pc.criterion_avg * COALESCE(c."weight", 1.0))
        / NULLIF(SUM(COALESCE(c."weight", 1.0)), 0)
    , 3)                                            AS weighted_total,
    ROUND(AVG(pc.criterion_avg), 3)                 AS simple_total,    -- 비가중 평균(참고)
    COUNT(pc.criterion_key)                         AS scored_criteria, -- 채점된 기준 수
    MAX(pc.judge_count)                             AS judge_count      -- 최다 기준 심사 인원
FROM per_criterion pc
LEFT JOIN "jam_criteria" c
    ON c."jam_id" = pc."jam_id" AND c."criterion_key" = pc."criterion_key"
GROUP BY pc."jam_id", pc."game_id";
COMMENT ON VIEW "jam_score_stats" IS
    'W2-3 동결 심사 집계뷰. 출품작(jam_id,game_id)별 가중 종합·비가중 평균·채점 기준수·심사 인원. fan-out 방지 선집계(game_review_stats 선례).';

-- ===========================================================================
-- W3-1 태그 + 검색 확장 (docs/tag-ddl.sql 동기 사본 — 의미 동등, 식별자 인용 스타일만 상이)
--   FK 의존: users/games/jams 선존재(파일 앞쪽 정의). 끝에 추가하여 FK 순서 안전.
-- ===========================================================================

-- 1) 통합 태그 마스터
CREATE SEQUENCE IF NOT EXISTS "tags_id_seq";
CREATE TABLE IF NOT EXISTS "tags" (
    "id"         bigint DEFAULT nextval('tags_id_seq'::regclass) NOT NULL,
    "name"       character varying(20) NOT NULL,
    "slug"       character varying(40) NOT NULL,
    "tag_type"   character varying(10) NOT NULL,
    "is_active"  boolean DEFAULT false NOT NULL,
    "created_by" bigint,
    "created_at" timestamp with time zone DEFAULT now() NOT NULL,
    PRIMARY KEY ("id")
);
ALTER SEQUENCE "tags_id_seq" OWNED BY "tags"."id";
DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_tags_name_len') THEN
        ALTER TABLE "tags" ADD CONSTRAINT "chk_tags_name_len"
            CHECK (char_length("name") BETWEEN 2 AND 20);
    END IF;
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_tags_type') THEN
        ALTER TABLE "tags" ADD CONSTRAINT "chk_tags_type"
            CHECK ("tag_type" IN ('GAME', 'JAM', 'COMMON'));
    END IF;
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_tags_created_by') THEN
        ALTER TABLE "tags" ADD CONSTRAINT "fk_tags_created_by"
            FOREIGN KEY ("created_by") REFERENCES "users" ("id");
    END IF;
END
$$;
CREATE UNIQUE INDEX IF NOT EXISTS "uq_tags_slug" ON "tags" ("slug");
CREATE UNIQUE INDEX IF NOT EXISTS "uq_tags_name" ON "tags" ("name");
CREATE INDEX IF NOT EXISTS "idx_tags_type_active" ON "tags" ("tag_type", "is_active");

-- 2) 게임-태그 조인 (N:M)
CREATE SEQUENCE IF NOT EXISTS "game_tags_id_seq";
CREATE TABLE IF NOT EXISTS "game_tags" (
    "id"         bigint DEFAULT nextval('game_tags_id_seq'::regclass) NOT NULL,
    "game_id"    bigint NOT NULL,
    "tag_id"     bigint NOT NULL,
    "created_at" timestamp with time zone DEFAULT now() NOT NULL,
    PRIMARY KEY ("id")
);
ALTER SEQUENCE "game_tags_id_seq" OWNED BY "game_tags"."id";
DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_game_tags_game') THEN
        ALTER TABLE "game_tags" ADD CONSTRAINT "fk_game_tags_game"
            FOREIGN KEY ("game_id") REFERENCES "games" ("id");
    END IF;
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_game_tags_tag') THEN
        ALTER TABLE "game_tags" ADD CONSTRAINT "fk_game_tags_tag"
            FOREIGN KEY ("tag_id") REFERENCES "tags" ("id");
    END IF;
END
$$;
CREATE UNIQUE INDEX IF NOT EXISTS "uq_game_tags" ON "game_tags" ("game_id", "tag_id");
CREATE INDEX IF NOT EXISTS "idx_game_tags_tag" ON "game_tags" ("tag_id");

-- 3) 잼-태그 조인 (N:M, jams 는 W2-1 소관)
CREATE SEQUENCE IF NOT EXISTS "jam_tags_id_seq";
CREATE TABLE IF NOT EXISTS "jam_tags" (
    "id"         bigint DEFAULT nextval('jam_tags_id_seq'::regclass) NOT NULL,
    "jam_id"     bigint NOT NULL,
    "tag_id"     bigint NOT NULL,
    "created_at" timestamp with time zone DEFAULT now() NOT NULL,
    PRIMARY KEY ("id")
);
ALTER SEQUENCE "jam_tags_id_seq" OWNED BY "jam_tags"."id";
DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_jam_tags_jam') THEN
        ALTER TABLE "jam_tags" ADD CONSTRAINT "fk_jam_tags_jam"
            FOREIGN KEY ("jam_id") REFERENCES "jams" ("id");
    END IF;
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_jam_tags_tag') THEN
        ALTER TABLE "jam_tags" ADD CONSTRAINT "fk_jam_tags_tag"
            FOREIGN KEY ("tag_id") REFERENCES "tags" ("id");
    END IF;
END
$$;
CREATE UNIQUE INDEX IF NOT EXISTS "uq_jam_tags" ON "jam_tags" ("jam_id", "tag_id");
CREATE INDEX IF NOT EXISTS "idx_jam_tags_tag" ON "jam_tags" ("tag_id");

-- 4) 방문 로그 (dedupe 가능)
CREATE SEQUENCE IF NOT EXISTS "game_views_id_seq";
CREATE TABLE IF NOT EXISTS "game_views" (
    "id"         bigint DEFAULT nextval('game_views_id_seq'::regclass) NOT NULL,
    "game_id"    bigint NOT NULL,
    "viewer_key" character varying(200) NOT NULL,
    "viewed_at"  timestamp with time zone DEFAULT now() NOT NULL,
    PRIMARY KEY ("id")
);
ALTER SEQUENCE "game_views_id_seq" OWNED BY "game_views"."id";
DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_game_views_game') THEN
        ALTER TABLE "game_views" ADD CONSTRAINT "fk_game_views_game"
            FOREIGN KEY ("game_id") REFERENCES "games" ("id");
    END IF;
END
$$;
CREATE INDEX IF NOT EXISTS "idx_game_views_game" ON "game_views" ("game_id");
CREATE INDEX IF NOT EXISTS "idx_game_views_dedupe" ON "game_views" ("game_id", "viewer_key", "viewed_at");

-- 5) games 비정규화 방문수 카운터
ALTER TABLE "games" ADD COLUMN IF NOT EXISTS "view_count" integer DEFAULT 0 NOT NULL;
CREATE INDEX IF NOT EXISTS "idx_games_view_count" ON "games" ("view_count");

-- ---------------------------------------------------------------------------
-- W3-3 포스팅 보드: post_categories / posts / unity_feed_sources / unity_feed_items
-- (권위 DDL — docs/board-ddl.sql 와 동일)
-- ---------------------------------------------------------------------------

-- 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;

-- W3-5 Unity WebGL 업로드 감사 로그 (docs/game-upload-ddl.sql 동기 사본)
CREATE SEQUENCE IF NOT EXISTS "game_upload_audit_log_id_seq";

CREATE TABLE IF NOT EXISTS "game_upload_audit_log" (
    "id"            bigint DEFAULT nextval('game_upload_audit_log_id_seq'::regclass) NOT NULL,
    "actor_id"      bigint NOT NULL,                 -- 업로드 수행 사용자 users.id
    "game_uuid"     character varying(36),           -- 생성/교체된 UUID(거부 시 null 가능)
    "outcome"       character varying(20) NOT NULL,  -- SUCCESS / REJECTED
    "reject_reason" character varying(40),           -- REJECTED 시 사유코드(아래 reason 카탈로그)
    "original_name" character varying(255),          -- 업로드 파일 원본명(sanitize 후 저장)
    "upload_bytes"  bigint,                          -- zip 원본 크기
    "entry_count"   integer,                         -- 추출 엔트리 수(성공 시)
    "extracted_bytes" bigint,                        -- 누적 해제 크기(성공 시)
    "created_at"    timestamp with time zone DEFAULT now() NOT NULL,
    PRIMARY KEY ("id")
);
ALTER SEQUENCE "game_upload_audit_log_id_seq" OWNED BY "game_upload_audit_log"."id";

DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'game_upload_audit_log_actor_id_fkey') THEN
        ALTER TABLE "game_upload_audit_log"
            ADD CONSTRAINT "game_upload_audit_log_actor_id_fkey"
            FOREIGN KEY ("actor_id") REFERENCES "users" ("id");
    END IF;
END
$$;

DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'game_upload_audit_log_outcome_check') THEN
        ALTER TABLE "game_upload_audit_log"
            ADD CONSTRAINT "game_upload_audit_log_outcome_check"
            CHECK ("outcome" IN ('SUCCESS', 'REJECTED'));
    END IF;
END
$$;

CREATE INDEX IF NOT EXISTS "idx_game_upload_audit_actor"
    ON "game_upload_audit_log" ("actor_id", "created_at" DESC);
CREATE INDEX IF NOT EXISTS "idx_game_upload_audit_outcome"
    ON "game_upload_audit_log" ("outcome", "created_at" DESC);

COMMENT ON TABLE "game_upload_audit_log" IS 'WebGL 업로드 감사 로그(성공/거부 추적, W3-5)';
COMMENT ON COLUMN "game_upload_audit_log"."reject_reason" IS 'REJECTED 사유코드: NOT_ZIP/MAGIC_FAIL/TOO_LARGE/TOO_MANY_ENTRIES/ENTRY_TOO_LARGE/EXTRACTED_TOO_LARGE/ZIP_SLIP/SYMLINK/BAD_ENTRY_NAME/NO_INDEX/INCOMPLETE_BUILD';

-- W4 유저 배지/평판 (권위: docs/badge-ddl.sql 동기 사본)

-- 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");
