203 lines
9.3 KiB
SQL
203 lines
9.3 KiB
SQL
-- Game reviews (single star rating 1~5 + free-text body, one per game per user).
|
|
-- PostgreSQL DDL aligned with the existing bibimbap table style (recruit-posts-ddl.sql).
|
|
-- W3-2: 댓글/리뷰 분리. game_reviews 신규 + game_comments user_id 컬럼 추가.
|
|
|
|
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,
|
|
"user_id" bigint NOT NULL,
|
|
"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")
|
|
);
|
|
|
|
ALTER SEQUENCE "game_reviews_id_seq" OWNED BY "game_reviews"."id";
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_constraint WHERE conname = 'game_reviews_game_id_fkey'
|
|
) THEN
|
|
ALTER TABLE "game_reviews"
|
|
ADD CONSTRAINT "game_reviews_game_id_fkey"
|
|
FOREIGN KEY ("game_id") REFERENCES "games" ("id");
|
|
END IF;
|
|
END
|
|
$$;
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_constraint WHERE conname = 'game_reviews_user_id_fkey'
|
|
) THEN
|
|
ALTER TABLE "game_reviews"
|
|
ADD CONSTRAINT "game_reviews_user_id_fkey"
|
|
FOREIGN KEY ("user_id") REFERENCES "users" ("id");
|
|
END IF;
|
|
END
|
|
$$;
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_constraint WHERE conname = 'game_reviews_rating_check'
|
|
) THEN
|
|
ALTER TABLE "game_reviews"
|
|
ADD CONSTRAINT "game_reviews_rating_check"
|
|
CHECK ("rating" BETWEEN 1 AND 5);
|
|
END IF;
|
|
END
|
|
$$;
|
|
|
|
-- 게임당 사용자 1회 (active 리뷰 한정 — soft-delete 후 재작성 허용).
|
|
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;
|
|
|
|
-- 목록 조회 + 후속 집계 SELECT 의 game_id 필터용. (C3 재분류: 집계뷰 game_review_stats 는 본 파일 하단에 W3-2 일반 DDL 로 신설됨 — roadmap.md:63,201,202. W2-3 동결 무관.)
|
|
CREATE INDEX IF NOT EXISTS "idx_game_reviews_game"
|
|
ON "game_reviews" ("game_id")
|
|
WHERE "is_delete" = false;
|
|
|
|
COMMENT ON TABLE "game_reviews" IS '게임 리뷰. 게임당 사용자 1회, 별점 1~5 + 서술 평가';
|
|
COMMENT ON COLUMN "game_reviews"."id" IS '리뷰 고유 ID';
|
|
COMMENT ON COLUMN "game_reviews"."game_id" IS '대상 게임 games.id';
|
|
COMMENT ON COLUMN "game_reviews"."user_id" IS '리뷰 작성자 users.id (로그인 필수)';
|
|
COMMENT ON COLUMN "game_reviews"."rating" IS '별점 5점 단일 (1~5). 향후 다축은 별도 game_review_axes 테이블로 분리';
|
|
COMMENT ON COLUMN "game_reviews"."body" IS '서술 평가 (앱레벨 1,000자 제한, DB 무제한)';
|
|
COMMENT ON COLUMN "game_reviews"."created_at" IS '리뷰 작성 시각';
|
|
COMMENT ON COLUMN "game_reviews"."updated_at" IS '리뷰 마지막 수정 시각. updated_at > created_at 이면 수정됨';
|
|
COMMENT ON COLUMN "game_reviews"."deleted_at" IS '리뷰 삭제 시각';
|
|
COMMENT ON COLUMN "game_reviews"."is_delete" IS '소프트 삭제 여부';
|
|
|
|
-- ===========================================================================
|
|
-- 기존 DB 적용용 idempotent ALTER (security-hardening-ddl.sql DO $$ 패턴)
|
|
-- ---------------------------------------------------------------------------
|
|
-- game_comments user_id 컬럼 (W3-2: 작성자 귀속, nullable 비파괴).
|
|
-- 위 game_reviews CREATE/제약/인덱스는 전부 IF NOT EXISTS / DO $$ 멱등 → 재실행 안전.
|
|
-- ===========================================================================
|
|
|
|
ALTER TABLE "game_comments" ADD COLUMN IF NOT EXISTS "user_id" bigint;
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_constraint WHERE conname = 'game_comments_user_id_fkey'
|
|
) THEN
|
|
ALTER TABLE "game_comments"
|
|
ADD CONSTRAINT "game_comments_user_id_fkey"
|
|
FOREIGN KEY ("user_id") REFERENCES "users" ("id");
|
|
END IF;
|
|
END
|
|
$$;
|
|
|
|
COMMENT ON COLUMN "game_comments"."user_id" IS '덧글 작성자 users.id (nullable — 레거시 닉네임 덧글 보존)';
|
|
|
|
-- ===========================================================================
|
|
-- W3-2 고도화: 다축 평점(game_review_axes) + 댓글 updated_at + is_rating_manual
|
|
-- + game_review_stats 집계뷰
|
|
-- C3 재분류: game_review_stats 는 W3-2 일반 읽기전용 집계뷰. W2-3 잼 평가 동결과 무관
|
|
-- (roadmap.md:63,201,202). 아래 위 'idx_game_reviews_game' 주석의 "집계뷰 미신설"
|
|
-- 보수 표기는 본 블록으로 갱신됨.
|
|
-- ===========================================================================
|
|
|
|
-- 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 행수(최대 6)만큼
|
|
-- 복제되어 COUNT(*)/AVG(rating) 이 왜곡된다. 축 평균은 서브쿼리에서 game 단위로
|
|
-- 선집계한 뒤 LEFT JOIN 한다 (리뷰 단위 집계는 r 한 테이블로만 계산).
|
|
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축평균';
|