597 lines
29 KiB
SQL
597 lines
29 KiB
SQL
-- =============================================================================
|
|
-- 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);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 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 컬럼 없음)
|
|
-- ---------------------------------------------------------------------------
|
|
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")
|
|
);
|
|
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");
|