diff --git a/app/api/orders/route.ts b/app/api/orders/route.ts new file mode 100644 index 0000000..8345419 --- /dev/null +++ b/app/api/orders/route.ts @@ -0,0 +1,105 @@ +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 } 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 }); + } + + // 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(); + const product = await getProductById(admin, productId); + + if (!product || !product.is_active) { + 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(); + + 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, + }, + }) + .select('id') + .single(); + + if (insertError || !order) { + console.error('[Orders] insert error:', insertError); + return NextResponse.json({ error: '주문 생성에 실패했습니다' }, { status: 500 }); + } + + const orderId = order.id as string; + + // 6) 메일 발송 (실패해도 주문 유효) + try { + await sendOrderReceivedEmails({ + orderId, + product, + customerEmail: user.email ?? '', + depositorName, + }); + } catch (mailError) { + console.error('[Orders] email send error:', mailError); + } + + // 7) 응답 + return NextResponse.json({ orderId }); +} diff --git a/lib/order-emails.ts b/lib/order-emails.ts new file mode 100644 index 0000000..0917e2a --- /dev/null +++ b/lib/order-emails.ts @@ -0,0 +1,71 @@ +import { Resend } from 'resend'; +import { escapeHtml } from '@/lib/security'; +import type { ProductRow } from '@/lib/supabase/product-files'; + +const FROM = '쟁승메이드 '; +const ADMIN_EMAIL = 'bgg8988@gmail.com'; +const BANK_INFO = '케이뱅크 100-116-337157 (예금주: 박재오)'; + +function resend() { + return new Resend(process.env.RESEND_API_KEY); +} + +const won = (n: number) => `₩${n.toLocaleString('ko-KR')}`; + +/** 주문 접수: 고객 안내 + 관리자 알림 (실패해도 주문은 유효 — 호출부에서 try/catch) */ +export async function sendOrderReceivedEmails(opts: { + orderId: string; + product: ProductRow; + customerEmail: string; + depositorName: string; +}) { + const { orderId, product, customerEmail, depositorName } = opts; + + // XSS 방지: 사용자 입력값 이스케이프 + const safeDepositorName = escapeHtml(depositorName); + const safeCustomerEmail = escapeHtml(customerEmail); + + const r = resend(); + await r.emails.send({ + from: FROM, + to: [customerEmail], + subject: `[쟁승메이드] 주문 접수 — ${product.name}`, + html: ` +

주문이 접수되었습니다

+

${product.name} · ${won(product.price)}

+

아래 계좌로 입금해 주시면, 확인 후 마이페이지에서 바로 다운로드하실 수 있습니다.

+

${BANK_INFO}

+

입금자명: ${safeDepositorName}

+
+

주문번호 ${orderId} · 입금 확인은 영업시간 기준 최대 24시간 소요됩니다.

+ `, + }); + await r.emails.send({ + from: FROM, + to: [ADMIN_EMAIL], + subject: `[쟁승메이드] 신규 주문(입금 대기) — ${product.name}`, + html: ` +

신규 계좌이체 주문

+

상품: ${product.name} (${won(product.price)})

+

주문자 이메일: ${safeCustomerEmail} / 입금자명: ${safeDepositorName}

+

주문번호: ${orderId}

+

입금 확인 후 관리자 주문 페이지에서 [입금 확인]을 눌러주세요.

+ `, + }); +} + +/** 입금 확인: 고객에게 다운로드 활성화 안내 */ +export async function sendOrderPaidEmail(opts: { product: ProductRow; customerEmail: string }) { + const { product, customerEmail } = opts; + await resend().emails.send({ + from: FROM, + to: [customerEmail], + subject: `[쟁승메이드] 입금 확인 완료 — ${product.name} 다운로드 안내`, + html: ` +

입금이 확인되었습니다

+

${product.name} 다운로드가 활성화되었습니다.

+

마이페이지 → 내 제품에서 바로 받으실 수 있습니다.

+

다운로드 링크는 클릭 시 4시간 동안 유효하며, 만료되면 다시 누르면 됩니다.

+ `, + }); +}