From c9b8d2df5ceee74f9bdb3b41932f2dbc4e1127ac Mon Sep 17 00:00:00 2001 From: gahusb Date: Thu, 9 Jul 2026 22:53:02 +0900 Subject: [PATCH] =?UTF-8?q?feat(phase3b):=20orders=EC=97=90=20=EC=98=81?= =?UTF-8?q?=EC=83=81=ED=99=94(music=5Fvideo)=20=EB=B0=9C=EC=A3=BC=20+=20vi?= =?UTF-8?q?deo-product=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/orders/route.ts | 29 ++++++++++++++++++++------- app/api/studio/video-product/route.ts | 16 +++++++++++++++ 2 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 app/api/studio/video-product/route.ts diff --git a/app/api/orders/route.ts b/app/api/orders/route.ts index c753f8b..6e45e7e 100644 --- a/app/api/orders/route.ts +++ b/app/api/orders/route.ts @@ -70,14 +70,22 @@ export async function POST(request: Request) { return NextResponse.json({ error: '판매 중인 상품이 아닙니다' }, { status: 404 }); } + // music_video 발주: 본인 트랙 검증 + 트랙별 중복 가드 + let musicTrackId: string | null = null; + if (productId === 'music_video') { + musicTrackId = sanitizeStr((body as Record).musicTrackId, 64) || null; + if (!musicTrackId) return NextResponse.json({ error: '영상화할 트랙이 필요합니다' }, { status: 400 }); + const { data: track } = await admin + .from('music_tracks').select('id').eq('id', musicTrackId).eq('user_id', user.id).maybeSingle(); + if (!track) return NextResponse.json({ error: '본인 음악 트랙만 영상화할 수 있습니다' }, { status: 404 }); + } + // 4) 중복 pending 방지 - const { data: existing } = await admin - .from('orders') - .select('id') - .eq('user_id', user.id) - .eq('product_id', productId) - .eq('status', 'pending') - .maybeSingle(); + const dupQuery = admin.from('orders').select('id') + .eq('user_id', user.id).eq('product_id', productId).eq('status', 'pending'); + const { data: existing } = productId === 'music_video' && musicTrackId + ? await dupQuery.eq('metadata->>music_track_id', musicTrackId).maybeSingle() + : await dupQuery.maybeSingle(); if (existing) { return NextResponse.json({ orderId: existing.id, reused: true }); @@ -94,6 +102,7 @@ export async function POST(request: Request) { metadata: { method: 'bank_transfer', depositor_name: depositorName, + ...(musicTrackId ? { music_track_id: musicTrackId } : {}), }, }) .select('id') @@ -106,6 +115,12 @@ export async function POST(request: Request) { const orderId = order.id as string; + if (productId === 'music_video' && musicTrackId) { + await admin.from('music_tracks') + .update({ video_status: 'requested', video_order_id: orderId }) + .eq('id', musicTrackId).eq('user_id', user.id); + } + // 6) 메일 발송 (실패해도 주문 유효) try { await sendOrderReceivedEmails({ diff --git a/app/api/studio/video-product/route.ts b/app/api/studio/video-product/route.ts new file mode 100644 index 0000000..3aad1d2 --- /dev/null +++ b/app/api/studio/video-product/route.ts @@ -0,0 +1,16 @@ +import { NextResponse } from 'next/server'; +import { createClient } from '@/lib/supabase/server'; +import { createAdminClient } from '@/lib/supabase/admin'; +import { getProductById } from '@/lib/supabase/product-files'; + +export const runtime = 'nodejs'; + +export async function GET() { + const supabase = await createClient(); + const { data: { user } } = await supabase.auth.getUser(); + if (!user) return NextResponse.json({ error: '로그인이 필요합니다.' }, { status: 401 }); + const admin = createAdminClient(); + const product = await getProductById(admin, 'music_video'); + if (!product || !product.is_active) return NextResponse.json({ error: '영상화 상품이 준비 중입니다.' }, { status: 404 }); + return NextResponse.json({ product: { id: product.id, name: product.name, price: product.price, is_active: product.is_active } }); +}