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:
@@ -41,7 +41,7 @@ export async function GET() {
|
||||
// 세션 클라이언트로 본인 것만(RLS music_select_own)
|
||||
const { data, error } = await supabase
|
||||
.from('music_tracks')
|
||||
.select('id, title, story, lyrics, style, audio_url, task_id, created_at')
|
||||
.select('id, title, story, lyrics, style, audio_url, task_id, created_at, video_status, video_url')
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
|
||||
@@ -19,6 +19,7 @@ interface Props {
|
||||
product: { id: string; name: string; price: number };
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
musicTrackId?: string;
|
||||
}
|
||||
|
||||
type AuthState = 'checking' | 'guest' | 'user';
|
||||
@@ -29,7 +30,7 @@ interface SuccessInfo {
|
||||
reused: boolean;
|
||||
}
|
||||
|
||||
export default function BankTransferModal({ product, isOpen, onClose }: Props) {
|
||||
export default function BankTransferModal({ product, isOpen, onClose, musicTrackId }: Props) {
|
||||
const [authState, setAuthState] = useState<AuthState>('checking');
|
||||
const [depositorName, setDepositorName] = useState('');
|
||||
const [agreed, setAgreed] = useState(false);
|
||||
@@ -98,7 +99,11 @@ export default function BankTransferModal({ product, isOpen, onClose }: Props) {
|
||||
const res = await fetch('/api/orders', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ productId: product.id, depositorName: name }),
|
||||
body: JSON.stringify({
|
||||
productId: product.id,
|
||||
depositorName: name,
|
||||
...(musicTrackId ? { musicTrackId } : {}),
|
||||
}),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
@@ -122,7 +127,7 @@ export default function BankTransferModal({ product, isOpen, onClose }: Props) {
|
||||
setSubmitting(false);
|
||||
}
|
||||
},
|
||||
[depositorName, agreed, submitting, product.id],
|
||||
[depositorName, agreed, submitting, product.id, musicTrackId],
|
||||
);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
@@ -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