-- ============================================================================ -- Phase V2-2: 초기 스키마 마이그레이션 -- V1 SQLite schema를 PostgreSQL로 포팅 + 멀티테넌시(user_id/team_id) 추가 -- ============================================================================ -- Extensions ------------------------------------------------------------------ CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- ============================================================================ -- profiles: auth.users 확장 (1:1) -- ============================================================================ CREATE TABLE public.profiles ( id uuid PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, name text, avatar_url text, locale text NOT NULL DEFAULT 'ko', tier text NOT NULL DEFAULT 'free' CHECK (tier IN ('free', 'pro', 'team')), created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); COMMENT ON TABLE public.profiles IS 'auth.users 확장 — 유저 메타데이터와 구독 티어'; -- ============================================================================ -- teams / team_members: 팀 협업 -- ============================================================================ CREATE TABLE public.teams ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL, owner_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, avatar_url text, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX idx_teams_owner_id ON public.teams(owner_id); CREATE TABLE public.team_members ( team_id uuid NOT NULL REFERENCES public.teams(id) ON DELETE CASCADE, user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, role text NOT NULL CHECK (role IN ('owner', 'admin', 'member')) DEFAULT 'member', joined_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (team_id, user_id) ); CREATE INDEX idx_team_members_user_id ON public.team_members(user_id); -- ============================================================================ -- meetings: 회의 세션 (V1 meeting_sessions) -- ============================================================================ CREATE TABLE public.meetings ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, team_id uuid REFERENCES public.teams(id) ON DELETE SET NULL, title text, status text NOT NULL CHECK (status IN ('recording', 'processing', 'completed', 'error')) DEFAULT 'recording', started_at timestamptz NOT NULL DEFAULT now(), ended_at timestamptz, duration_ms bigint, raw_transcript text, edited_transcript text, minutes_markdown text, minutes_json jsonb, stt_model text, llm_model text, stt_latency_ms integer, llm_latency_ms integer, error_message text, audio_storage_key text, -- storage.audio 버킷 내 경로 created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX idx_meetings_user_id ON public.meetings(user_id); CREATE INDEX idx_meetings_team_id ON public.meetings(team_id); CREATE INDEX idx_meetings_status ON public.meetings(status); CREATE INDEX idx_meetings_started_at ON public.meetings(started_at DESC); -- ============================================================================ -- meeting_memos: 회의 중 타임스탬프 메모 -- ============================================================================ CREATE TABLE public.meeting_memos ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), meeting_id uuid NOT NULL REFERENCES public.meetings(id) ON DELETE CASCADE, user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, content text NOT NULL, timestamp_ms bigint NOT NULL, created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX idx_meeting_memos_meeting_id ON public.meeting_memos(meeting_id); CREATE INDEX idx_meeting_memos_timestamp ON public.meeting_memos(meeting_id, timestamp_ms); -- ============================================================================ -- meeting_documents: LLM 생성 문서 (minutes/report/mindmap/custom) -- ============================================================================ CREATE TABLE public.meeting_documents ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), meeting_id uuid NOT NULL REFERENCES public.meetings(id) ON DELETE CASCADE, user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, template_type text NOT NULL CHECK (template_type IN ('minutes', 'report', 'idea-note', 'custom', 'mindmap')), title text NOT NULL, content text NOT NULL DEFAULT '', prompt_used text, llm_model text, llm_latency_ms integer, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX idx_meeting_documents_meeting_id ON public.meeting_documents(meeting_id); -- ============================================================================ -- transcripts: 전사 세그먼트 (실시간 동기화 대상) -- ============================================================================ CREATE TABLE public.transcripts ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), meeting_id uuid NOT NULL REFERENCES public.meetings(id) ON DELETE CASCADE, segment_index integer NOT NULL, timestamp_ms bigint NOT NULL, duration_ms integer, text text NOT NULL, speaker text, -- "화자 1", "화자 2" ... (Phase 15.5) edited boolean NOT NULL DEFAULT false, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX idx_transcripts_meeting_id ON public.transcripts(meeting_id); CREATE UNIQUE INDEX idx_transcripts_segment ON public.transcripts(meeting_id, segment_index); -- Realtime publication에 추가 (회의 중 실시간 동기화) ALTER PUBLICATION supabase_realtime ADD TABLE public.transcripts; -- ============================================================================ -- history: 음성 입력 이력 (V1 history — dictation/translate 등) -- ============================================================================ CREATE TABLE public.history ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, title text, original_text text NOT NULL, polished_text text, focused_app text, focused_app_name text, focused_app_window_title text, mode text NOT NULL CHECK (mode IN ('dictation', 'translate', 'command', 'caption', 'file-transcription')) DEFAULT 'dictation', status text NOT NULL CHECK (status IN ('completed', 'cancelled', 'error')) DEFAULT 'completed', error_code text, audio_storage_key text, duration double precision NOT NULL, detected_language text, mic_device text, word_count integer NOT NULL DEFAULT 0, stt_model text, llm_model text, stt_latency_ms integer, llm_latency_ms integer, app_version text NOT NULL DEFAULT '1.0.0', summary_text text, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX idx_history_user_id ON public.history(user_id); CREATE INDEX idx_history_created_at ON public.history(user_id, created_at DESC); CREATE INDEX idx_history_status ON public.history(user_id, status); CREATE INDEX idx_history_mode ON public.history(user_id, mode); CREATE INDEX idx_history_detected_language ON public.history(detected_language); -- ============================================================================ -- dictionary: 사용자 사전 -- ============================================================================ CREATE TABLE public.dictionary ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, word text NOT NULL, pronunciation text, category text NOT NULL CHECK (category IN ('user', 'auto', 'technical')) DEFAULT 'user', usage_count integer NOT NULL DEFAULT 0, last_used_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE (user_id, word, category) ); CREATE INDEX idx_dictionary_user_id ON public.dictionary(user_id); CREATE INDEX idx_dictionary_usage_count ON public.dictionary(user_id, usage_count DESC); -- ============================================================================ -- memo_tags: history 태그 -- ============================================================================ CREATE TABLE public.memo_tags ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, history_id uuid NOT NULL REFERENCES public.history(id) ON DELETE CASCADE, tag text NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), UNIQUE (history_id, tag) ); CREATE INDEX idx_memo_tags_user_id ON public.memo_tags(user_id); CREATE INDEX idx_memo_tags_tag ON public.memo_tags(user_id, tag); -- ============================================================================ -- daily_usage: 기능별 일일 사용량 (쿼터 집계) -- ============================================================================ CREATE TABLE public.daily_usage ( id bigserial PRIMARY KEY, user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, date date NOT NULL, feature text NOT NULL, count integer NOT NULL DEFAULT 0, UNIQUE (user_id, date, feature) ); CREATE INDEX idx_daily_usage_user_date ON public.daily_usage(user_id, date); -- ============================================================================ -- subscriptions: Stripe 구독 연동 -- ============================================================================ CREATE TABLE public.subscriptions ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL UNIQUE REFERENCES auth.users(id) ON DELETE CASCADE, tier text NOT NULL CHECK (tier IN ('free', 'pro', 'team')) DEFAULT 'free', stripe_customer_id text, stripe_subscription_id text, status text, -- active / canceled / past_due ... current_period_start timestamptz, current_period_end timestamptz, cancel_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX idx_subscriptions_stripe_customer ON public.subscriptions(stripe_customer_id);