docs(phase3a): 음악 서비스 공개화 설계 — 스토리→음악·무료·회원저장 (WS1~5)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AAtcmKKtqDUe4NyVgy1aLQ
This commit is contained in:
2026-07-03 12:33:55 +09:00
parent a5b47a0278
commit a1a281d059

View File

@@ -0,0 +1,90 @@
# Phase 3a 음악 서비스 공개화 — "나의 이야기를 음악으로" 설계
- 날짜: 2026-07-03
- 선행: Phase 2(사주/타로)·2.5/2.6(사주 재스킨) main 머지 완료
- 배경: 운영 비전 2축 3번째 서비스(음악). 숨김 상태의 Suno 스튜디오를 공개·무료화하고, "스토리→음악" 흐름과 회원 저장을 추가. 영상화 유료는 Phase 3b로 분리.
## 결정 사항 (CEO 확정, 2026-07-03)
| 결정 | 내용 |
|------|------|
| 영상화 | **Phase 3b로 분리** — 계좌이체 발주(관리자 수동 제작·납품). 이번 3a 범위 밖 |
| 음악 노출·과금 | **공개+무료** — 페이지 공개(숨김 해제), 생성은 로그인+일일제한(무료). 사주·타로 패턴 |
| 스토리→음악 | **Gemini가 스토리→{가사·스타일·무드} 변환** 후 Suno 생성 |
| 일일 제한 | 음악 생성 **1회/일**(Suno 비용) |
| callback | `/api/studio/callback` **폴링 전용 확정** — 최소 200 응답 route로 댕글링 해소, 회원 저장은 폴링 완료 후 클라 트리거 |
## 확인된 기존 구조
- `POST /api/studio/generate`: **무인증**, `SUNO_API_KEY` 미설정 시 503, custom/simple 모드, `callBackUrl=${origin}/api/studio/callback`(부재), Suno `/api/v1/generate` 호출 → task 반환
- `GET /api/studio/status?taskId=`: **무인증**, Suno `record-info` 폴링
- `lib/ai-usage.ts`: `AiService='saju'|'tarot'`, `getTodayUsage`/`recordUsage`, KST 일일 집계. `ai_usage_log` CHECK는 `('saju','tarot')`(phase2 마이그, auto-name `ai_usage_log_service_check`)
- `app/music/`: layout(가드 `isServiceVisible('music')`), page(72), samples(102), studio(543, 다크 테마 Suno UI)
- 저장·마이페이지 통합 패턴: 타로 `interpret→readings save`, 마이페이지 'AI 기록' 탭(사주·타로)
## 워크스트림 5개
### WS1. 음악 공개화 + 무료화
- `app/music/layout.tsx``isServiceVisible('music')`+`notFound()`+import 제거 → 공개
- `lib/service-visibility.ts` `HideableService`에서 `'music'` 제거 → `'gyeol'|'lotto'`
- `app/api/admin/services/route.ts` DEFAULT_SERVICES music 행 제거
- 마이그레이션에서 `service_settings` music 행 DELETE
- Suno 키 미설정 시 503 유지(예시 폴백 없음)
### WS2. 스토리 → 음악 (Gemini + Suno)
- 신규 `lib/music/story-prompt.ts`: `STORY_SYSTEM_PROMPT` + `buildStoryUserMessage(story)` + `parseStoryJson`/`validateStory`. Gemini가 스토리 텍스트 → `{ title, lyrics, style, mood }` strict JSON. 타로 prompt.ts 방어(코드블록 스트립·검증·reroll 1회, 45s 가드) 패턴 재사용
- 신규 `POST /api/studio/story`: 인증(401) → Gemini 변환(GEMINI_API_KEY, 미설정 503) → `{ title, lyrics, style, mood }` 반환(사용자 편집 가능). **story 단계는 일일제한 미집계**(값싼 단계)
- 기존 `POST /api/studio/generate`(Suno) 수정: 인증(401) 추가 + **일일제한(429, `MUSIC_DAILY_LIMIT`)** + Suno task 생성 성공 시에만 `recordUsage('music')`. body에 `title/lyrics/tags(style)`를 story 결과에서 채워 custom 모드로 호출
- `GET /api/studio/status` 유지(무인증 폴링 가능 — taskId만 필요, 민감정보 없음)
- **callback 정리**: 신규 `POST /api/studio/callback`이 최소 `{ ok: true }` 200 반환(Suno webhook 404 방지). 회원 저장은 콜백이 아니라 폴링 완료 후 클라가 `POST /api/studio/tracks` 트리거
### WS3. 회원 저장 + 일일제한
- 마이그레이션 `supabase/migrations/2026-07-03-phase3a-music.sql`:
```sql
CREATE TABLE IF NOT EXISTS music_tracks (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
title text,
story text,
lyrics text,
style text,
audio_url text,
task_id text,
created_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE music_tracks ENABLE ROW LEVEL SECURITY;
CREATE POLICY music_select_own ON music_tracks FOR SELECT USING (auth.uid() = user_id);
-- ai_usage_log CHECK에 'music' 추가 (phase2 마이그 적용 후 실행 전제)
ALTER TABLE ai_usage_log DROP CONSTRAINT IF EXISTS ai_usage_log_service_check;
ALTER TABLE ai_usage_log ADD CONSTRAINT ai_usage_log_service_check CHECK (service IN ('saju','tarot','music'));
DELETE FROM service_settings WHERE id = 'music';
```
**의존성**: phase2-saju-tarot 마이그(ai_usage_log 생성)가 먼저 적용돼야 함 — 스펙·플랜·CEO 안내에 명시
- `lib/ai-usage.ts`: `AiService`에 `'music'` 추가, `export const MUSIC_DAILY_LIMIT = 1;`
- 신규 `POST/GET /api/studio/tracks`: 저장(admin insert, user_id 세션)·본인 조회(세션 client RLS). 타로 readings 패턴
### WS4. 라이트 재스킨 + 스토리 UI
- `app/music/{page,samples,studio}.tsx` 다크 → `--jsm` 라이트 재스킨(사주 2.5/2.6 패턴, gradient/blur/보라/이모지 0건). navy 밴드 무테두리 flat 관용구
- **스토리 UI 흐름**(studio 재구성): ①스토리 textarea → `POST /studio/story` → ②가사·스타일 미리보기(편집 가능) → ③"음악 만들기" → `POST /studio/generate` → ④`status` 폴링 → ⑤플레이어 + (로그인 시) 자동 저장(`POST /studio/tracks`). 비로그인 생성 시도 → 로그인 CTA(`/login?next=/music/studio`)
- 셔플류 클라 이슈 없음(폼 기반)
### WS5. 진입점 + AI기록 통합 + 문서
- TopNav `음악` 링크 추가(외주/소프트웨어/제작사례/사주/타로/음악 = 6링크)
- 마이페이지 'AI 기록' 탭에 음악 트랙 통합(`GET /api/studio/tracks`) — 사주·타로·음악 3종 병합 리스트, 음악은 제목·스토리 요약·오디오 링크
- CLAUDE.md: 음악 공개 서비스·스토리→음악·music_tracks·studio API 반영, 숨김 서비스 표에서 music 제거
- 최종 검증: `grep -rnE "gradient|violet|purple|blur" app/music/**/*.tsx` 0건, build+test 30, sajumusic 라우트 존재
## 범위 밖 (Phase 3b)
- 영상화 유료 발주(계좌이체 order로 video 신청·관리자 제작·납품·다운로드)
- 음악 자동 영상 생성 API 연동
## 리스크·주의
- **Suno 실동작**은 `SUNO_API_KEY` 운영 설정 의존 — 로컬 미설정 시 503(사주 Gemini와 동일 정책). 수동 E2E는 운영 키 있는 환경에서
- `ai_usage_log` CHECK ALTER는 phase2 마이그 DB 적용 후 실행돼야 함(미적용 시 ALTER 실패) — CEO 안내
- studio 페이지가 큼(543줄) — 재스킨 + 스토리 UI 재구성은 태스크 분할(라이트 재스킨과 스토리 흐름 분리 가능)
- Suno 응답 스키마(task/record-info)는 기존 status route가 이미 다룸 — 저장 시 audio_url 추출 지점은 구현 시 record-info 응답 구조 확인
- 생성은 비동기(task) — recordUsage는 task 생성 성공(Suno 202/200) 시점 집계(완료 아님). 완료 실패해도 1회 소진되나 개인 서비스 규모에서 허용(사주·타로와 동일 기조)