talk / backend /sql /026_care_recipients.sql
user
Deploy StyleVoiceChat backend
aa9892b
Raw History Blame Contribute Delete
6.07 kB
-- μ•ˆλΆ€ λ©”μ‹œμ§€ κ°€μ‘± μˆ˜μ‹ μž + care_messages ν™•μž₯ (2026-05-12, care_redesign Phase 2)
--
-- Phase 2 핡심:
-- - κ°€μ‘± μˆ˜μ‹ μž (`care_recipients`) β€” 라벨/μ „ν™”λ²ˆν˜Έ/μŒμ„±μŠ¬λ‘―/λ°œμ†‘μ‹œκ°/1νƒ­|0νƒ­ λͺ¨λ“œ.
-- - 맀일 KST T-30λΆ„ μžλ™ 본문생성+TTS β†’ care_messages ν–‰ INSERT (status='ready').
-- - 1νƒ­ λͺ¨λ“œ: T-0 λΆ„ μ‚¬μš©μžμ—κ²Œ T1 μ•Œλ¦Όν†‘ β†’ μ‚¬μš©μžκ°€ [보내기] λˆ„λ₯΄λ©΄ T3 μ•Œλ¦Όν†‘ λ°œμ†‘.
-- - 가쑱이 /listen/<care_id> 클릭 β†’ listened_at μ±„μ›Œμ§ β†’ λ°œμ‹ μžμ—κ²Œ T4 μ•Œλ¦Όν†‘.
--
-- νšŒκ·€ κ°€λ“œ λ³€κ²½:
-- - κ°€μ‘± 번호 μˆ˜μ§‘ OK (이전 μ •μ±… 폐기, doc/runbook/care_redesign.md μ°Έμ‘°)
-- - 우리 μ‚¬μ—…μž μ•Œλ¦Όν†‘μœΌλ‘œ κ°€μ‘±μ—κ²Œ μ§λ°œμ†‘ OK
--
-- μ•ˆν‹°-μŠ€ν† ν‚Ή (Phase 4 μ—μ„œ κ°•ν™”): care_blacklist μΆ”κ°€ μ˜ˆμ •, λ³Έ migration 에선
-- ꡬ쑰만 μ€€λΉ„.
CREATE TABLE IF NOT EXISTS public.care_recipients (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
-- κ°€μ‘± 식별
label TEXT NOT NULL, -- 'μ—„λ§ˆ', 'μ•„λΉ ', 'μ‹œμ–΄λ¨Έλ‹ˆ' λ“± 자유
phone_e164 TEXT NOT NULL, -- +8210xxxxxxxx (μ •κ·œν™” ν›„ μ €μž₯)
-- λ°œμ†‘ μ„€μ •
voice_slot_kind TEXT NOT NULL DEFAULT 'self'
CHECK (voice_slot_kind IN ('self','son','daughter')),
schedule_mode TEXT NOT NULL DEFAULT '1tap'
CHECK (schedule_mode IN ('1tap','0tap')),
send_time_kst TIME NOT NULL DEFAULT '09:00', -- KST κΈ°μ€€ 일일 λ°œμ†‘ μ‹œκ°
-- κ°€μ‘± λ™μ˜
consent_status TEXT NOT NULL DEFAULT 'active'
CHECK (consent_status IN ('active','opted_out','blocked')),
social_consent_confirmed BOOLEAN NOT NULL DEFAULT FALSE, -- 등둝 μ‹œ "λΆ€λͺ¨λ‹˜κ»˜ μ–‘ν•΄ ꡬ함" 체크
-- μ˜΅νŠΈμ•„μ›ƒ (Phase 4)
optout_token TEXT NOT NULL DEFAULT encode(gen_random_bytes(16), 'hex') UNIQUE,
opted_out_at TIMESTAMPTZ,
-- ν™œμ„± ν† κΈ€ (μ‚¬μš©μžκ°€ μΌμ‹œ μ •μ§€ κ°€λŠ₯)
active BOOLEAN NOT NULL DEFAULT TRUE,
-- λ‹€μŒ λ°œμ†‘ μ˜ˆμ • μ‹œκ° (UTC). cron tick 이 이걸 보고 due λ°œμ‚¬.
next_prep_at TIMESTAMPTZ, -- 본문생성+TTS μ‹œμž‘ μ‹œκ° (T-30λΆ„)
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_care_recipients_user
ON public.care_recipients (user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_care_recipients_due_prep
ON public.care_recipients (active, next_prep_at)
WHERE active = TRUE AND consent_status = 'active' AND next_prep_at IS NOT NULL;
-- ν•œ μ‚¬μš©μž + 같은 μ „ν™”λ²ˆν˜Έ 쀑볡 등둝 λ°©μ§€ (라벨만 λ‹€λ₯΄λ©΄ 별도 등둝 κ°€λŠ₯ν•˜κ²Œ λ‘λŠ” 게
-- λ³΄ν†΅μ΄μ§€λ§Œ, μ•ˆν‹°-μŠ€ν† ν‚Ή κ΄€μ μ—μ„œ 같은 κ°€μ‘±μ—κ²Œ 2개 row 보낼 일은 μ—†μŒ).
CREATE UNIQUE INDEX IF NOT EXISTS care_recipients_user_phone_uniq
ON public.care_recipients (user_id, phone_e164);
ALTER TABLE public.care_recipients ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS care_recipients_owner ON public.care_recipients;
CREATE POLICY care_recipients_owner ON public.care_recipients
FOR ALL
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
CREATE OR REPLACE FUNCTION public._care_recipients_touch_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS care_recipients_touch_updated_at ON public.care_recipients;
CREATE TRIGGER care_recipients_touch_updated_at
BEFORE UPDATE ON public.care_recipients
FOR EACH ROW EXECUTE FUNCTION public._care_recipients_touch_updated_at();
-- ───────── care_messages ν™•μž₯ ─────────
--
-- κΈ°μ‘΄ 컬럼 (recipient_label, generated_text, audio_file_*, scheduled_at, active λ“±) μœ μ§€.
-- μƒˆ 흐름은 `recipient_id` κ°€ μ±„μ›Œμ§€λ©΄ "Phase 2 μžλ™ 흐름" 으둜 κ°„μ£Ό, NULL 이면 κΈ°μ‘΄
-- μ¦‰μ‹œ-생성 흐름 (μ‚¬μš©μž 카톑 picker) 으둜 ν˜Έν™˜ μœ μ§€.
ALTER TABLE public.care_messages
ADD COLUMN IF NOT EXISTS recipient_id UUID REFERENCES public.care_recipients(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS scheduled_for TIMESTAMPTZ, -- κ°€μ‘± λ°œμ†‘ μ˜ˆμ • μ‹œκ° (UTC)
ADD COLUMN IF NOT EXISTS listened_at TIMESTAMPTZ, -- κ°€μ‘± 클릭 β†’ /listen μ§„μž… μ‹œκ°
ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'ready'
CHECK (status IN ('ready','sent','canceled','failed')),
ADD COLUMN IF NOT EXISTS sent_at TIMESTAMPTZ, -- T3 λ°œμ†‘ 성곡 μ‹œκ°
ADD COLUMN IF NOT EXISTS listened_notified_at TIMESTAMPTZ; -- T4 λ°œμ‹ μž μ•Œλ¦Ό λ°œμ†‘ μ‹œκ°
CREATE INDEX IF NOT EXISTS idx_care_messages_recipient_status
ON public.care_messages (recipient_id, status, created_at DESC)
WHERE recipient_id IS NOT NULL;
-- 가쑱별 + λ‚ μ§œλ³„ 1ν–‰λ§Œ μœ μ§€ (같은 λ‚  쀑볡 prep λ°©μ§€).
-- KST μžμ • 기쀀이 μ•„λ‹ˆλΌ created_at 의 date λ‹¨μœ„ β€” 맀일 cron 이 ν•œ 번만 λ§Œλ“ λ‹€.
CREATE UNIQUE INDEX IF NOT EXISTS care_messages_recipient_day_uniq
ON public.care_messages (recipient_id, (date_trunc('day', created_at AT TIME ZONE 'Asia/Seoul')))
WHERE recipient_id IS NOT NULL;
COMMENT ON TABLE public.care_recipients IS
'μ•ˆλΆ€ λ©”μ‹œμ§€ κ°€μ‘± μˆ˜μ‹ μž. 우리 μ‚¬μ—…μž μ•Œλ¦Όν†‘μœΌλ‘œ 직접 λ°œμ†‘. μ˜΅νŠΈμ•„μ›ƒ 토큰 포함.';
COMMENT ON COLUMN public.care_messages.recipient_id IS
'NULL = κΈ°μ‘΄ μ¦‰μ‹œ-생성 흐름 (μ‚¬μš©μž 카톑 picker). NOT NULL = Phase 2 μžλ™ 흐름.';
COMMENT ON COLUMN public.care_messages.status IS
'ready: prep μ™„λ£Œ, λ°œμ†‘ λŒ€κΈ° | sent: T3 λ°œμ†‘ μ™„λ£Œ | canceled: μ‚¬μš©μž μ·¨μ†Œ | failed: λ°œμ†‘ μ‹€νŒ¨';