import { NextResponse } from 'next/server'; import { cookies } from 'next/headers'; import { createServerClient as createSSRClient } from '@supabase/ssr'; import { createAdminClient } from '@/lib/supabase/admin'; import { getProductById } from '@/lib/supabase/product-files'; import { sanitizeStr, checkRateLimit } from '@/lib/security'; import { sendOrderReceivedEmails } from '@/lib/order-emails'; export const runtime = 'nodejs'; export async function POST(request: Request) { // 1) 인증 확인 (SSR 쿠키 클라이언트) const cookieStore = await cookies(); const supabase = createSSRClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { getAll: () => cookieStore.getAll(), setAll: () => {}, }, }, ); const { data: { user } } = await supabase.auth.getUser(); if (!user) { return NextResponse.json({ error: '로그인이 필요합니다' }, { status: 401 }); } // 1-b) Rate Limit: user 기준 분당 5회 const rl = checkRateLimit(`orders:${user.id}`, 60_000, 5); if (!rl.allowed) { return NextResponse.json( { error: '요청이 너무 잦습니다. 잠시 후 다시 시도해주세요' }, { status: 429, headers: { 'Retry-After': String(Math.ceil(rl.retryAfterMs / 1000)) }, }, ); } // 2) body 검증 let body: unknown; try { body = await request.json(); } catch { return NextResponse.json({ error: '잘못된 요청입니다' }, { status: 400 }); } const rawProductId = (body as Record).productId; const rawDepositorName = (body as Record).depositorName; const productId = sanitizeStr(rawProductId, 64); const depositorName = sanitizeStr(rawDepositorName, 40); if (!productId || !depositorName) { return NextResponse.json({ error: 'productId와 depositorName이 필요합니다' }, { status: 400 }); } // 3) 상품 조회 및 활성 상태 확인 const admin = createAdminClient(); let product; try { product = await getProductById(admin, productId); } catch (dbErr) { console.error('[Orders] product lookup error:', dbErr); return NextResponse.json({ error: '상품 조회에 실패했습니다' }, { status: 500 }); } if (!product || !product.is_active) { 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, error: trackLookupError } = await admin .from('music_tracks').select('id').eq('id', musicTrackId).eq('user_id', user.id).maybeSingle(); if (trackLookupError) { console.error('[Orders] music_tracks ownership lookup error:', trackLookupError); } if (!track) return NextResponse.json({ error: '본인 음악 트랙만 영상화할 수 있습니다' }, { status: 404 }); } // 4) 중복 pending 방지 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 }); } // 5) 주문 생성 (가격은 DB 소스) const { data: order, error: insertError } = await admin .from('orders') .insert({ user_id: user.id, product_id: productId, amount: product.price, status: 'pending', metadata: { method: 'bank_transfer', depositor_name: depositorName, ...(musicTrackId ? { music_track_id: musicTrackId } : {}), }, }) .select('id') .single(); if (insertError || !order) { console.error('[Orders] insert error:', insertError); return NextResponse.json({ error: '주문 생성에 실패했습니다' }, { status: 500 }); } const orderId = order.id as string; if (productId === 'music_video' && musicTrackId) { const { error: trackUpdateError } = await admin.from('music_tracks') .update({ video_status: 'requested', video_order_id: orderId }) .eq('id', musicTrackId).eq('user_id', user.id); if (trackUpdateError) { console.error('[Orders] music_tracks video_status update error:', trackUpdateError); } } // 6) 메일 발송 (실패해도 주문 유효) try { await sendOrderReceivedEmails({ orderId, product, customerEmail: user.email ?? '', depositorName, }); } catch (mailError) { console.error('[Orders] email send error:', mailError); } // 7) 응답 return NextResponse.json({ orderId }); }