195 lines
11 KiB
SQL
195 lines
11 KiB
SQL
-- W2-3 잼 평가 통합(심사점수/인기투표/시상집계). 멱등. db/apply-local-ddl.sh 로 실행 DB 비파괴 적용.
|
|
-- 선행: docs/jam-ddl.sql(jams/jam_entries — 알파벳 글롭 순 jam-ddl 먼저 적용).
|
|
-- game_reviews/game_review_stats(W3-2) 변경 없음 — 시상 USER_RATING 트랙이 VIEW 를 읽기만.
|
|
-- 평가단위 = (jam_id, game_id) 자연키 = 활성 출품작(jam_entries 1:1 대응). 추가만, 파괴 없음.
|
|
|
|
-- ===========================================================================
|
|
-- 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 선례).';
|