import { NextResponse } from 'next/server'; import { cookies } from 'next/headers'; import { createAdminClient } from '@/lib/supabase/admin'; import { verifyAdminTokenNode } from '@/lib/admin-auth'; import { getProductById } from '@/lib/supabase/product-files'; import { sendOrderPaidEmail } from '@/lib/order-emails'; export const runtime = 'nodejs'; async function checkAuth() { const cookieStore = await cookies(); const token = cookieStore.get('admin_token')?.value; return token && verifyAdminTokenNode(token); } // GET: 주문 목록 (최근 200건) — 상품명 + 주문자 이메일 포함 export async function GET() { if (!(await checkAuth())) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); const supabase = createAdminClient(); // 2-쿼리 방식: FK 관계 중첩 select 대신 명시적 조인으로 안전하게 const { data: orders, error } = await supabase .from('orders') .select('id, user_id, product_id, amount, status, metadata, created_at') .order('created_at', { ascending: false }) .limit(200); if (error) return NextResponse.json({ error: error.message }, { status: 500 }); if (!orders || orders.length === 0) { return NextResponse.json({ orders: [] }); } // 상품명 조회 const productIds = [...new Set(orders.map((o) => o.product_id).filter(Boolean))] as string[]; const userIds = [...new Set(orders.map((o) => o.user_id).filter(Boolean))] as string[]; // music_video 주문의 연결 트랙 id 모으기 const trackIds = [ ...new Set( orders .filter((o) => o.product_id === 'music_video') .map((o) => typeof (o.metadata as Record | null)?.music_track_id === 'string' ? ((o.metadata as Record).music_track_id as string) : null, ) .filter((v): v is string => v !== null), ), ]; const [productsRes, profilesRes, tracksRes] = await Promise.all([ productIds.length > 0 ? supabase.from('products').select('id, name').in('id', productIds) : Promise.resolve({ data: [] as { id: string; name: string }[] | null, error: null }), userIds.length > 0 ? supabase.from('profiles').select('id, email').in('id', userIds) : Promise.resolve({ data: [] as { id: string; email: string }[] | null, error: null }), trackIds.length > 0 ? supabase.from('music_tracks').select('id, title, audio_url, video_status, video_url').in('id', trackIds) : Promise.resolve({ data: [] as { id: string; title: string | null; audio_url: string | null; video_status: string; video_url: string | null }[] | null, error: null, }), ]); const productMap = Object.fromEntries((productsRes.data ?? []).map((p) => [p.id, p.name])); const profileMap = Object.fromEntries((profilesRes.data ?? []).map((p) => [p.id, p.email])); const trackMap = Object.fromEntries((tracksRes.data ?? []).map((t) => [t.id, t])); const enriched = orders.map((o) => { const trackId = o.product_id === 'music_video' && typeof (o.metadata as Record | null)?.music_track_id === 'string' ? ((o.metadata as Record).music_track_id as string) : null; return { ...o, product_name: o.product_id ? (productMap[o.product_id] ?? null) : null, customer_email: o.user_id ? (profileMap[o.user_id] ?? null) : null, musicTrack: trackId ? (trackMap[trackId] ?? null) : null, }; }); return NextResponse.json({ orders: enriched }); } // PATCH: 상태 변경('paid' 전환 시 고객에게 다운로드 활성화 메일) + music_video 영상화 납품(videoUrl/videoStatus) export async function PATCH(request: Request) { if (!(await checkAuth())) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); const { id, status, videoUrl, videoStatus } = await request.json(); if (typeof id !== 'string') { return NextResponse.json({ error: 'invalid request' }, { status: 400 }); } if (status !== undefined && !['pending', 'paid', 'cancelled'].includes(status)) { return NextResponse.json({ error: 'invalid status' }, { status: 400 }); } if (videoStatus !== undefined && !['none', 'requested', 'in_progress', 'delivered'].includes(videoStatus)) { return NextResponse.json({ error: 'invalid videoStatus' }, { status: 400 }); } if (videoUrl !== undefined && typeof videoUrl !== 'string') { return NextResponse.json({ error: 'invalid videoUrl' }, { status: 400 }); } if (status === undefined && videoUrl === undefined && videoStatus === undefined) { return NextResponse.json({ error: 'invalid request' }, { status: 400 }); } const supabase = createAdminClient(); let order: { id: string; product_id: string | null; user_id: string | null; metadata: Record | null }; if (status !== undefined) { const { data, error } = await supabase .from('orders') .update({ status, updated_at: new Date().toISOString() }) .eq('id', id) .select('id, product_id, user_id, metadata') .single(); if (error || !data) return NextResponse.json({ error: error?.message ?? 'not found' }, { status: 500 }); order = data; // paid 전환 시에만 메일 발송 — 실패해도 상태 변경은 이미 완료 if (status === 'paid' && order.product_id && order.user_id) { try { const product = await getProductById(supabase, order.product_id); const { data: profile } = await supabase .from('profiles') .select('email') .eq('id', order.user_id) .maybeSingle(); if (product && profile?.email) { await sendOrderPaidEmail({ product, customerEmail: profile.email }); } } catch (e) { console.error('paid email failed', e); } } } else { // status 변경 없이 영상화 트랙만 갱신하는 경우 — 주문 조회로 product_id/metadata 확보 const { data, error } = await supabase .from('orders') .select('id, product_id, user_id, metadata') .eq('id', id) .single(); if (error || !data) return NextResponse.json({ error: error?.message ?? 'not found' }, { status: 500 }); order = data; } // music_video 주문의 연결 트랙 영상화 상태 갱신 if (order.product_id === 'music_video') { const trackId = typeof order.metadata?.music_track_id === 'string' ? (order.metadata.music_track_id as string) : null; if (trackId) { if (status === 'paid' || videoUrl !== undefined) { const trackUpdate: Record = { video_status: 'delivered' }; if (videoUrl !== undefined) trackUpdate.video_url = videoUrl; const { error: trackError } = await supabase.from('music_tracks').update(trackUpdate).eq('id', trackId); if (trackError) console.error('music_tracks video delivery update failed', trackError); } else if (videoStatus === 'in_progress') { const { error: trackError } = await supabase .from('music_tracks') .update({ video_status: 'in_progress' }) .eq('id', trackId); if (trackError) console.error('music_tracks video in_progress update failed', trackError); } } } return NextResponse.json({ success: true }); }