diff --git a/CLAUDE.md b/CLAUDE.md index 8dd4079..9b3a21c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,10 +21,11 @@ | `/showcase` | 제작 사례 — 웹 데모 8종 + 실서비스 운영 사례 | | `/work/saju` | 사주 분석 — 공개 AI 사주 (로그인 시 무료 해석 1회/일) | | `/tarot` | 타로 — 3카드 셔플·해석 (비로그인 카드 리딩, 로그인 AI 인사이트) | +| `/music` | 공개 음악 — 스토리→음악 AI 스튜디오 (studio·samples, 로그인 시 생성·저장) | | `/track/[token]` | 비회원 의뢰 진행 추적 | | `/quote/[token]` | 공개 견적 — 고객 수락/거절 | | `/login` | 로그인 (`?next=` 리다이렉트 지원) | -| `/mypage` | 4탭: 프로필 / 발주·진행(발주서·마일스톤·견적코드 연결) / 내 제품(다운로드) / 주문 내역 | +| `/mypage` | 5탭: 프로필 / 발주·진행(발주서·마일스톤·견적코드 연결) / 내 제품(다운로드) / 주문 내역 / AI 기록(사주·타로·음악 병합) | | `/legal/*` | 이용약관 · 개인정보처리방침 · 환불정책 | ## 숨김 서비스 (admin_token 세션 전용) @@ -33,7 +34,6 @@ admin/services 패널에서 ON/OFF 전환 가능. | 경로 | 서비스 | |------|--------| -| `/music/*` | 음악 팩 (단, `/music/packs`는 `/products`로 308 리다이렉트) | | `/gyeol` | CONTOUR PMF 설문 | ## 기술 스택 @@ -93,9 +93,12 @@ app/ saju/analyze/route.ts — 사주 AI 분석 (Gemini) tarot/interpret/route.ts — 타로 AI 인사이트 (로그인·일 3회 제한) tarot/readings/route.ts — 타로 리딩 저장·조회 (tarot_readings) + studio/story/route.ts — POST: 스토리→가사 생성 (Gemini, 로그인 필요) + studio/tracks/route.ts — GET/POST: 음악 트랙 저장·조회 (music_tracks, 본인 것만) + studio/callback/route.ts — POST: Suno webhook 수신용 최소 엔드포인트 work/saju/ — 공개: 사주 서비스 (로그인 시 AI 해석 무료 1회/일) tarot/ — 공개: 타로 3카드 (셔플·reference·AI 해석) - music/ — 숨김: 음악 팩 (packs는 /products로 308) + music/ — 공개: 스토리→음악 AI 스튜디오 (studio·samples, packs는 /products로 308) gyeol/ — 숨김: CONTOUR PMF 설문 lib/ @@ -116,6 +119,8 @@ lib/ shuffle.ts — 셔플·3카드 드로우 로직 reference.ts — 카드 의미 레퍼런스 prompt.ts — AI 해석 프롬프트 + music/ + story-prompt.ts — 스토리→가사 AI 프롬프트 (시스템 프롬프트·JSON 파싱·검증) ``` --- diff --git a/app/components/TopNav.tsx b/app/components/TopNav.tsx index 7fbbe45..a7e50d8 100644 --- a/app/components/TopNav.tsx +++ b/app/components/TopNav.tsx @@ -12,6 +12,7 @@ const LINKS = [ { href: '/showcase', label: '제작 사례' }, { href: '/work/saju', label: '사주' }, { href: '/tarot', label: '타로' }, + { href: '/music', label: '음악' }, ]; export default function TopNav() { diff --git a/app/mypage/page.tsx b/app/mypage/page.tsx index 5d67015..bb5eff9 100644 --- a/app/mypage/page.tsx +++ b/app/mypage/page.tsx @@ -68,10 +68,20 @@ type SajuRecordRow = { is_paid: boolean; }; -// AI 기록 탭 — 사주·타로 병합 렌더용 판별 유니언 +// AI 기록 탭 — 음악 트랙 (app/api/studio/tracks 응답) +type MusicTrackRow = { + id: string; + title: string | null; + story: string | null; + audio_url: string | null; + created_at: string; +}; + +// AI 기록 탭 — 사주·타로·음악 병합 렌더용 판별 유니언 type AiRecordItem = | { kind: 'saju'; data: SajuRecordRow } - | { kind: 'tarot'; data: TarotReadingRow }; + | { kind: 'tarot'; data: TarotReadingRow } + | { kind: 'music'; data: MusicTrackRow }; interface Payment { id: string; @@ -152,9 +162,10 @@ function MyPageContent() { const [telegramLinkExpiry, setTelegramLinkExpiry] = useState(''); const [showTelegramGuide, setShowTelegramGuide] = useState(false); - // AI 기록 탭 — 타로 리딩 / 사주 기록 + // AI 기록 탭 — 타로 리딩 / 사주 기록 / 음악 트랙 const [tarotReadings, setTarotReadings] = useState([]); const [sajuRecords, setSajuRecords] = useState([]); + const [musicTracks, setMusicTracks] = useState([]); const [expandedAiCards, setExpandedAiCards] = useState>(new Set()); const loadProjects = useCallback(async () => { @@ -166,7 +177,7 @@ function MyPageContent() { } catch { /* 미로그인/네트워크 — 무시 */ } }, []); - // 사주·타로 결과 통합 로드 — 둘 다 실패해도 서로 영향 없이 무시(best-effort) + // 사주·타로·음악 결과 통합 로드 — 셋 다 실패해도 서로 영향 없이 무시(best-effort) const loadAiRecords = useCallback(async () => { try { const tr = await fetch('/api/tarot/readings'); @@ -184,6 +195,10 @@ function MyPageContent() { setSajuRecords(data ?? []); } } catch { /* 무시 */ } + try { + const mt = await fetch('/api/studio/tracks'); + if (mt.ok) setMusicTracks((await mt.json()).tracks ?? []); + } catch { /* 무시 */ } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -369,10 +384,11 @@ function MyPageContent() { // 입금 확인 대기 중인 주문 (orders 테이블 pending) const pendingOrders = productOrders.filter((o) => o.status === 'pending'); - // AI 기록 탭 — 사주·타로 결과를 created_at 기준 내림차순 병합 + // AI 기록 탭 — 사주·타로·음악 결과를 created_at 기준 내림차순 병합 const aiRecords: AiRecordItem[] = [ ...sajuRecords.map((r): AiRecordItem => ({ kind: 'saju', data: r })), ...tarotReadings.map((r): AiRecordItem => ({ kind: 'tarot', data: r })), + ...musicTracks.map((r): AiRecordItem => ({ kind: 'music', data: r })), ].sort((a, b) => new Date(b.data.created_at).getTime() - new Date(a.data.created_at).getTime()); const tabs: { key: Tab; label: string; count?: number }[] = [ @@ -380,7 +396,7 @@ function MyPageContent() { { key: 'requests', label: '발주·진행', count: orders.length || undefined }, { key: 'products', label: '내 제품', count: productGroups.length || undefined }, { key: 'orders', label: '주문 내역', count: (orders.length + payments.length) || undefined }, - { key: 'ai', label: 'AI 기록', count: (sajuRecords.length + tarotReadings.length) || undefined }, + { key: 'ai', label: 'AI 기록', count: (sajuRecords.length + tarotReadings.length + musicTracks.length) || undefined }, ]; function selectTab(key: Tab) { @@ -910,7 +926,7 @@ function MyPageContent() { AI 기록이 없습니다
- 사주 분석·타로 리딩 결과가 여기에 모아서 표시됩니다. + 사주 분석·타로 리딩·음악 생성 결과가 여기에 모아서 표시됩니다.
타로 리딩 하기 → + + 음악 만들기 → +
) : ( @@ -934,13 +957,15 @@ function MyPageContent() { {aiRecords.map((item) => item.kind === 'saju' ? ( - ) : ( + ) : item.kind === 'tarot' ? ( toggleAiCard(item.data.id)} /> + ) : ( + ) )} @@ -1569,6 +1594,48 @@ function TarotAiCard({ ); } +// AI 기록 — 음악 카드. 제목·날짜 + 스토리 요약 + 오디오 플레이어(생성 완료 시). +function MusicAiCard({ track }: { track: MusicTrackRow }) { + return ( + +
+ + 음악 + + + {new Date(track.created_at).toLocaleDateString('ko-KR')} + +
+ +
+ {track.title || '제목 없는 트랙'} +
+ + {track.story && ( +

+ {track.story} +

+ )} + + {track.audio_url ? ( + + ) : ( +

+ 생성 준비 중입니다. 잠시 후 다시 확인해주세요. +

+ )} +
+ ); +} + function TelegramIcon({ className, style }: { className?: string; style?: React.CSSProperties }) { return (