feat(phase3b): 마이페이지 음악 영상화 신청·상태 + 모달 트랙 연결
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AAtcmKKtqDUe4NyVgy1aLQ
This commit is contained in:
@@ -6,6 +6,7 @@ import Link from 'next/link';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
import type { User } from '@supabase/supabase-js';
|
||||
import TelegramGuideModal from '@/app/components/TelegramGuideModal';
|
||||
import BankTransferModal from '@/app/components/BankTransferModal';
|
||||
import { KAKAO_OPENCHAT_URL } from '@/lib/contact';
|
||||
import { findCard } from '@/lib/tarot/cards';
|
||||
import {
|
||||
@@ -75,6 +76,8 @@ type MusicTrackRow = {
|
||||
story: string | null;
|
||||
audio_url: string | null;
|
||||
created_at: string;
|
||||
video_status?: 'none' | 'requested' | 'in_progress' | 'delivered';
|
||||
video_url?: string | null;
|
||||
};
|
||||
|
||||
// AI 기록 탭 — 사주·타로·음악 병합 렌더용 판별 유니언
|
||||
@@ -167,6 +170,9 @@ function MyPageContent() {
|
||||
const [sajuRecords, setSajuRecords] = useState<SajuRecordRow[]>([]);
|
||||
const [musicTracks, setMusicTracks] = useState<MusicTrackRow[]>([]);
|
||||
const [expandedAiCards, setExpandedAiCards] = useState<Set<string>>(new Set());
|
||||
// 음악 영상화 신청 — 계좌이체 모달에 전달할 상품·대상 트랙
|
||||
const [videoProduct, setVideoProduct] = useState<{ id: string; name: string; price: number } | null>(null);
|
||||
const [videoModalTrackId, setVideoModalTrackId] = useState<string | null>(null);
|
||||
|
||||
const loadProjects = useCallback(async () => {
|
||||
try {
|
||||
@@ -202,6 +208,23 @@ function MyPageContent() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// 음악 트랙 영상화 신청 — 상품 정보 로드 후 계좌이체 모달 오픈
|
||||
const handleRequestVideo = useCallback(async (trackId: string) => {
|
||||
try {
|
||||
const res = await fetch('/api/studio/video-product');
|
||||
if (!res.ok) {
|
||||
console.warn('영상화 상품을 불러오지 못했습니다', res.status);
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
if (!data?.product) return;
|
||||
setVideoProduct(data.product);
|
||||
setVideoModalTrackId(trackId);
|
||||
} catch (e) {
|
||||
console.warn('영상화 상품 조회 중 오류', e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
async function init() {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
@@ -965,7 +988,11 @@ function MyPageContent() {
|
||||
onToggle={() => toggleAiCard(item.data.id)}
|
||||
/>
|
||||
) : (
|
||||
<MusicAiCard key={`music-${item.data.id}`} track={item.data} />
|
||||
<MusicAiCard
|
||||
key={`music-${item.data.id}`}
|
||||
track={item.data}
|
||||
onRequestVideo={handleRequestVideo}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
@@ -973,6 +1000,17 @@ function MyPageContent() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 음악 영상화 신청 — 계좌이체 모달 (영상화 상품 로드 완료 시에만 오픈) */}
|
||||
<BankTransferModal
|
||||
isOpen={!!videoModalTrackId && !!videoProduct}
|
||||
product={videoProduct ?? { id: '', name: '', price: 0 }}
|
||||
musicTrackId={videoModalTrackId ?? undefined}
|
||||
onClose={() => {
|
||||
setVideoModalTrackId(null);
|
||||
loadAiRecords();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1594,8 +1632,14 @@ function TarotAiCard({
|
||||
);
|
||||
}
|
||||
|
||||
// AI 기록 — 음악 카드. 제목·날짜 + 스토리 요약 + 오디오 플레이어(생성 완료 시).
|
||||
function MusicAiCard({ track }: { track: MusicTrackRow }) {
|
||||
// AI 기록 — 음악 카드. 제목·날짜 + 스토리 요약 + 오디오 플레이어(생성 완료 시) + 영상화 신청/상태.
|
||||
function MusicAiCard({
|
||||
track,
|
||||
onRequestVideo,
|
||||
}: {
|
||||
track: MusicTrackRow;
|
||||
onRequestVideo: (trackId: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<Card compact>
|
||||
<div className="flex items-center justify-between gap-3 mb-3">
|
||||
@@ -1624,14 +1668,68 @@ function MusicAiCard({ track }: { track: MusicTrackRow }) {
|
||||
)}
|
||||
|
||||
{track.audio_url ? (
|
||||
<audio controls className="w-full" style={{ height: 36 }}>
|
||||
<audio controls className="w-full mb-3" style={{ height: 36 }}>
|
||||
<source src={track.audio_url} />
|
||||
</audio>
|
||||
) : (
|
||||
<p className="text-xs" style={{ color: 'var(--jsm-ink-faint)' }}>
|
||||
<p className="text-xs mb-3" style={{ color: 'var(--jsm-ink-faint)' }}>
|
||||
생성 준비 중입니다. 잠시 후 다시 확인해주세요.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="pt-3 border-t" style={{ borderColor: 'var(--jsm-line)' }}>
|
||||
{(!track.video_status || track.video_status === 'none') && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onRequestVideo(track.id)}
|
||||
className="px-4 py-2 rounded-lg text-xs font-semibold text-white transition-colors hover:bg-[var(--jsm-accent-hover)]"
|
||||
style={{ background: 'var(--jsm-accent)' }}
|
||||
>
|
||||
영상화 신청
|
||||
</button>
|
||||
)}
|
||||
|
||||
{track.video_status === 'requested' && (
|
||||
<span
|
||||
className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold"
|
||||
style={{ background: 'var(--jsm-surface-alt)', color: 'var(--jsm-ink-soft)' }}
|
||||
>
|
||||
영상화 접수됨 · 입금 확인 중
|
||||
</span>
|
||||
)}
|
||||
|
||||
{track.video_status === 'in_progress' && (
|
||||
<span
|
||||
className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold"
|
||||
style={{ background: 'var(--jsm-surface-alt)', color: 'var(--jsm-ink-soft)' }}
|
||||
>
|
||||
영상 제작 중
|
||||
</span>
|
||||
)}
|
||||
|
||||
{track.video_status === 'delivered' && (
|
||||
track.video_url ? (
|
||||
<div>
|
||||
<video controls src={track.video_url} className="w-full rounded-lg mb-2" />
|
||||
<a
|
||||
href={track.video_url}
|
||||
download
|
||||
className="text-xs font-semibold underline"
|
||||
style={{ color: 'var(--jsm-accent)' }}
|
||||
>
|
||||
영상 다운로드
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<span
|
||||
className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold"
|
||||
style={{ background: 'var(--jsm-surface-alt)', color: 'var(--jsm-ink-soft)' }}
|
||||
>
|
||||
영상 완료
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user