-- ============================================ -- 음성 꼬맨틀 데이터베이스 스키마 -- ============================================ -- ============================================ -- 1. 퍼즐 정보 테이블 (마스터 데이터) -- ============================================ CREATE TABLE puzzles ( puzzle_number INT PRIMARY KEY, -- 정답 정보 answer_word VARCHAR(100) NOT NULL, -- 퍼즐 메타데이터 difficulty VARCHAR(20), -- 'easy', 'medium', 'hard' category VARCHAR(50), -- 카테고리 (예: 'meme', 'movie', 'song') -- 날짜 정보 puzzle_date DATE NOT NULL UNIQUE, -- 퍼즐 날짜 (YYYY-MM-DD) -- 타임스탬프 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 제약 조건 CONSTRAINT positive_puzzle_number CHECK (puzzle_number > 0) ); CREATE INDEX idx_puzzle_date ON puzzles(puzzle_date); COMMENT ON TABLE puzzles IS '일일 퍼즐 정보 - 정답 단어'; -- ============================================ -- 2. 추측 기록 테이블 (음성 파일 없이 메타데이터만) -- ============================================ CREATE TABLE guess_records ( -- 기본 키 id BIGSERIAL PRIMARY KEY, -- 게임 컨텍스트 session_id VARCHAR(100) NOT NULL, puzzle_number INT NOT NULL, -- AI 분석 결과 pitch DECIMAL(5, 2), -- AI 음높이 분석 (0-100) rhythm DECIMAL(5, 2), -- AI 리듬감 분석 (0-100) energy DECIMAL(5, 2), -- AI 에너지 분석 (0-100) pronunciation DECIMAL(5, 2), -- AI 발음 분석 (0-100) transcript TEXT, -- AI 분석 대사 (score, kept for compatibility) user_text TEXT, -- STT transcription from MCP overall DECIMAL(5, 2), -- AI 총점 (0-100) advice TEXT, -- AI 조언 -- 정답 여부 is_correct BOOLEAN DEFAULT FALSE, -- 타임스탬프 guess_timestamp BIGINT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 제약 조건 CONSTRAINT pitch_range CHECK (pitch IS NULL OR (pitch >= 0 AND pitch <= 100)), CONSTRAINT rhythm_range CHECK (rhythm IS NULL OR (rhythm >= 0 AND rhythm <= 100)), CONSTRAINT energy_range CHECK (energy IS NULL OR (energy >= 0 AND energy <= 100)), CONSTRAINT pronunciation_range CHECK (pronunciation IS NULL OR (pronunciation >= 0 AND pronunciation <= 100)), CONSTRAINT overall_range CHECK (overall IS NULL OR (overall >= 0 AND overall <= 100)) ); -- ============================================ -- 인덱스 -- ============================================ CREATE INDEX idx_guess_session ON guess_records(session_id); CREATE INDEX idx_guess_puzzle ON guess_records(puzzle_number); CREATE INDEX idx_guess_created ON guess_records(created_at); CREATE INDEX idx_guess_correct ON guess_records(is_correct); CREATE INDEX idx_guess_overall ON guess_records(overall); COMMENT ON TABLE guess_records IS '사용자 추측 기록 - AI 분석 결과만 저장 (음성 파일 없음)'; -- ============================================ -- 3. 날짜별 전체 통계 VIEW (대시보드용) -- ============================================ CREATE OR REPLACE VIEW daily_statistics AS SELECT p.puzzle_date, -- 참여자 수 (해당 날짜에 시도한 고유 세션 수) COUNT(DISTINCT gr.session_id) as participants, -- 정답자 수 (해당 날짜에 정답 맞춘 고유 세션 수) COUNT(DISTINCT CASE WHEN gr.is_correct = true THEN gr.session_id END) as winners, -- 정답률 ROUND( CASE WHEN COUNT(DISTINCT gr.session_id) > 0 THEN (COUNT(DISTINCT CASE WHEN gr.is_correct = true THEN gr.session_id END)::NUMERIC / COUNT(DISTINCT gr.session_id) * 100) ELSE 0 END, 1 ) as success_rate, -- 시도 횟수 (해당 날짜 총 음성 시도 수) COUNT(*) as total_attempts, -- 평균 시도 횟수 (참여자당) ROUND( CASE WHEN COUNT(DISTINCT gr.session_id) > 0 THEN COUNT(*)::NUMERIC / COUNT(DISTINCT gr.session_id) ELSE 0 END, 1 ) as avg_attempts_per_user FROM puzzles p LEFT JOIN guess_records gr ON p.puzzle_number = gr.puzzle_number GROUP BY p.puzzle_date ORDER BY p.puzzle_date DESC; COMMENT ON VIEW daily_statistics IS '날짜별 전체 게임 통계 - 대시보드용'; -- ============================================ -- 4. 전체 누적 통계 VIEW (대시보드용) -- ============================================ CREATE OR REPLACE VIEW overall_statistics AS SELECT -- 전체 참여자 수 (모든 시간 동안 참여한 고유 세션 수) COUNT(DISTINCT session_id) as total_participants, -- 전체 정답자 수 (한 번이라도 정답 맞춘 고유 세션 수) COUNT(DISTINCT CASE WHEN is_correct = true THEN session_id END) as total_winners, -- 전체 정답률 (정답자 수 / 참여자 수) ROUND( CASE WHEN COUNT(DISTINCT session_id) > 0 THEN (COUNT(DISTINCT CASE WHEN is_correct = true THEN session_id END)::NUMERIC / COUNT(DISTINCT session_id) * 100) ELSE 0 END, 1 ) as overall_success_rate, -- 전체 시도 횟수 COUNT(*) as total_attempts, -- 전체 퍼즐 수 (SELECT COUNT(*) FROM puzzles) as total_puzzles, -- 전체 평균 시도 횟수 ROUND( CASE WHEN COUNT(DISTINCT session_id) > 0 THEN COUNT(*)::NUMERIC / COUNT(DISTINCT session_id) ELSE 0 END, 1 ) as avg_attempts_per_user FROM guess_records; COMMENT ON VIEW overall_statistics IS '전체 누적 게임 통계 - 대시보드용'; -- ============================================ -- 5. 개별 사용자 통계 VIEW -- ============================================ CREATE OR REPLACE VIEW user_statistics AS WITH daily_activity AS ( SELECT gr.session_id, p.puzzle_date, COUNT(*) as attempts, MAX(CASE WHEN gr.is_correct = true THEN 1 ELSE 0 END) as won FROM guess_records gr JOIN puzzles p ON gr.puzzle_number = p.puzzle_number GROUP BY gr.session_id, p.puzzle_date ), streaks AS ( SELECT session_id, puzzle_date, won, puzzle_date - (ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY puzzle_date))::int AS play_streak_group FROM daily_activity ), current_play_streak AS ( SELECT session_id, COUNT(*) as play_streak FROM streaks WHERE play_streak_group = ( SELECT play_streak_group FROM streaks s2 WHERE s2.session_id = streaks.session_id ORDER BY puzzle_date DESC LIMIT 1 ) GROUP BY session_id ), win_streaks AS ( SELECT session_id, COUNT(*) as win_streak FROM ( SELECT session_id, puzzle_date, won, SUM(CASE WHEN won = 0 THEN 1 ELSE 0 END) OVER (PARTITION BY session_id ORDER BY puzzle_date DESC) as loss_count FROM daily_activity ) ws WHERE loss_count = 0 AND won = 1 GROUP BY session_id ) SELECT da.session_id, COUNT(DISTINCT da.puzzle_date) as total_plays, SUM(da.attempts) as total_attempts, SUM(da.won) as wins, ROUND( CASE WHEN COUNT(DISTINCT da.puzzle_date) > 0 THEN (SUM(da.won)::NUMERIC / COUNT(DISTINCT da.puzzle_date) * 100) ELSE 0 END, 1 ) as win_rate, COALESCE(cps.play_streak, 0) as play_streak, COALESCE(ws.win_streak, 0) as win_streak FROM daily_activity da LEFT JOIN current_play_streak cps ON da.session_id = cps.session_id LEFT JOIN win_streaks ws ON da.session_id = ws.session_id GROUP BY da.session_id, cps.play_streak, ws.win_streak; COMMENT ON VIEW user_statistics IS '개별 사용자 게임 통계'; -- ============================================ -- 샘플 데이터 (테스트용) -- ============================================ INSERT INTO puzzles (puzzle_number, answer_word, puzzle_date, difficulty, category) VALUES (1, 'Tralalero Tralala', '2024-11-24', 'easy', 'meme'), (2, 'Wingardium Leviosa', '2024-11-25', 'medium', 'movie'), (3, 'Baby shark, doo doo doo doo doo doo', '2024-11-26', 'hard', 'song');