File size: 3,596 Bytes
aa9892b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
-- 카페 μŒμ„± 선물쿠폰 MVP β€” voice_letters (2026-05-28, P1a Phase B)
--
-- μ†λ‹˜μ΄ μž‘μ„±ν•œ μŒμ„±νŽΈμ§€ (ν…μŠ€νŠΈ + 보이슀 + λͺ¨λ”λ ˆμ΄μ…˜ + ν•©μ„±λœ audio path).
-- μš”μ²­μ„œ: doc/runbook/req_cafe_gift_mvp.md Β§1
-- 섀계: doc/runbook/cafe_gift_p1a.md Β§2 sql/047
--
-- 흐름:
--   1) μ†λ‹˜ ν…μŠ€νŠΈ μž…λ ₯ (≀400자, λ°±μ—”λ“œ 검증)
--   2) moderation_service β†’ approved/blocked
--   3) approved λ©΄ TTS ν•©μ„± β†’ 'voice-letters' Storage μ—…λ‘œλ“œ β†’ audio_path μ„ΈνŒ…
--   4) gift_orders κ°€ voice_letter_id FK 둜 μ—°κ²° (1:1)
--
-- λΉ„λ‘œκ·ΈμΈ μ†λ‹˜μ΄ λ§Œλ“€ 수 μžˆμ–΄μ•Ό ν•˜λ―€λ‘œ λ°±μ—”λ“œ service-role 만 INSERT.
-- RLS 곡개 SELECT μ—†μŒ β€” reveal 은 별도 token (gift_coupons.reveal_token) 으둜 λ°±μ—”λ“œ λΌμš°ν„°κ°€ service-role 쑰회.
--
-- β›” νšŒκ·€ κ°€λ“œ:
--   - μ‹ κ·œ ν…Œμ΄λΈ”, κΈ°μ‘΄ CHECK 영ν–₯ 0 (sql/043 사고 κ°€λ“œ)
--   - voice_clone 호좜 0 (μ‹œμŠ€ν…œ UID + λ‚΄μž₯ν™”μžλ§Œ, P2 ν™•μž₯)
--   - is_preview=True (chat broadcast leak 차단)

CREATE TABLE IF NOT EXISTS public.voice_letters (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- gift_orders.id 와 μ–‘λ°©ν–₯ 1:1 (FK λŠ” sql/048 μ—μ„œ 보강)
    -- νŽΈμ§€ λ¨Όμ € β†’ λͺ¨λ”λ ˆμ΄μ…˜ 톡과 β†’ μ£Όλ¬Έ attach κ°€λŠ₯. λ˜λŠ” λ™μ‹œ 생성.
    order_id            UUID,

    sender_name         TEXT NOT NULL DEFAULT '',
    -- λ°±μ—”λ“œκ°€ 400자 cap 검증 (μ‚¬μš©μž κ²°μ • #3). DB 에 hard cap CHECK μ•ˆ λ‘  β€” μΆ”ν›„ μ •μ±… λ³€κ²½ μ‹œ λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ 없이 κ°€λŠ₯.
    text                TEXT NOT NULL,
    voice_kind          TEXT NOT NULL DEFAULT '__default__',
                        -- '__default__' (Sohee) | '__spk__<code>' (λ‚΄μž₯ν™”μž 8μ’…)

    -- ν•©μ„± κ²°κ³Ό (λͺ¨λ”λ ˆμ΄μ…˜ approved μ‹œμ—λ§Œ μ±„μ›Œμ§)
    audio_path          TEXT,                       -- Storage 'voice-letters' 버킷 path
    audio_duration_sec  NUMERIC(6,2),

    moderation_status   TEXT NOT NULL DEFAULT 'pending'
                        CHECK (moderation_status IN ('pending','approved','blocked','manual_review')),
    moderation_reason   TEXT,                       -- blocked μ‹œ μ‚¬μš©μž λ…ΈμΆœμš© 짧은 이유

    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_voice_letters_status
    ON public.voice_letters (moderation_status, created_at DESC);

ALTER TABLE public.voice_letters ENABLE ROW LEVEL SECURITY;

-- 곡개 INSERT/UPDATE μ •μ±… μ—†μŒ. λ°±μ—”λ“œ service-role ν‚€λ§Œ μž‘μ—….
-- (λΌμš°ν„°κ°€ λΉ„λ‘œκ·ΈμΈ μ†λ‹˜ μž…λ ₯ λ°›μ•„ service-role 둜 INSERT, owner_user_id 컬럼 μ—†μŒ.)

CREATE OR REPLACE FUNCTION public._voice_letters_touch_updated_at()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

DROP TRIGGER IF EXISTS voice_letters_touch_updated_at ON public.voice_letters;
CREATE TRIGGER voice_letters_touch_updated_at
    BEFORE UPDATE ON public.voice_letters
    FOR EACH ROW EXECUTE FUNCTION public._voice_letters_touch_updated_at();

COMMENT ON TABLE public.voice_letters IS
    '카페 선물쿠폰 μŒμ„±νŽΈμ§€. ≀400자, λ‚΄μž₯ν™”μžλ§Œ (P1a). λͺ¨λ”λ ˆμ΄μ…˜ 톡과 ν›„ audio ν•©μ„±.';
COMMENT ON COLUMN public.voice_letters.voice_kind IS
    '__default__ (Sohee) λ˜λŠ” __spk__<code> (λ‚΄μž₯ν™”μž 8μ’…). voice_clone(❀️) 은 P2.';
COMMENT ON COLUMN public.voice_letters.moderation_status IS
    'pending: 검사 μ „ | approved: 톡과 (audio ν•©μ„± OK) | blocked: 차단 (결제 μ§„μž… λΆˆκ°€) | manual_review: 운영자 κ²€ν†  (Phase E)';