From 692fb504d92c8cf5cafc4013d27fad7108aaf7b3 Mon Sep 17 00:00:00 2001 From: gahusb Date: Thu, 11 Jun 2026 08:32:44 +0900 Subject: [PATCH] =?UTF-8?q?feat(products):=20orders=20=EA=B8=B0=EB=B0=98?= =?UTF-8?q?=20=EC=A0=9C=ED=92=88/=ED=8C=8C=EC=9D=BC=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=ED=97=AC=ED=8D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/supabase/product-files.ts | 84 +++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 lib/supabase/product-files.ts diff --git a/lib/supabase/product-files.ts b/lib/supabase/product-files.ts new file mode 100644 index 0000000..01eda78 --- /dev/null +++ b/lib/supabase/product-files.ts @@ -0,0 +1,84 @@ +import type { SupabaseClient } from '@supabase/supabase-js'; +import { expandProductAccess } from '@/lib/product-access'; + +export interface ProductRow { + id: string; + name: string; + description: string | null; + description_long: string | null; + price: number; + category: string; + is_active: boolean; + is_listed: boolean; + sort_order: number; + features: string[] | null; + pay_method: string; +} + +export interface ProductFile { + id: string; + product_id: string | null; + label: string; + file_path: string; + filename: string; + size_bytes: number; + sort_order: number; + uploaded_at: string; + deleted_at: string | null; + min_tier: string; // 레거시 컬럼 — 신규 로직에서는 미사용 +} + +/** 카탈로그 노출 제품 (is_listed && is_active, sort_order 순) */ +export async function getListedProducts(supabase: SupabaseClient): Promise { + const { data, error } = await supabase + .from('products') + .select('*') + .eq('is_listed', true) + .eq('is_active', true) + .order('sort_order') + .order('id'); + if (error) throw error; + return (data ?? []) as ProductRow[]; +} + +export async function getProductById(supabase: SupabaseClient, id: string): Promise { + const { data, error } = await supabase.from('products').select('*').eq('id', id).maybeSingle(); + if (error) throw error; + return (data as ProductRow) ?? null; +} + +/** 사용자의 결제 완료 product_id 목록 (orders 단일 소스) */ +export async function getUserPaidProductIds(supabase: SupabaseClient, userId: string): Promise { + const { data, error } = await supabase + .from('orders') + .select('product_id') + .eq('user_id', userId) + .eq('status', 'paid') + .not('product_id', 'is', null); + if (error) throw error; + return Array.from(new Set((data ?? []).map((r) => r.product_id as string))); +} + +/** 접근 확장 포함 — 사용자가 다운로드 가능한 product_id 집합 */ +export async function getUserAccessibleProductIds(supabase: SupabaseClient, userId: string): Promise { + return expandProductAccess(await getUserPaidProductIds(supabase, userId)); +} + +export async function getFilesByProductIds(supabase: SupabaseClient, productIds: string[]): Promise { + if (productIds.length === 0) return []; + const { data, error } = await supabase + .from('pack_files') + .select('*') + .in('product_id', productIds) + .is('deleted_at', null) + .order('product_id') + .order('sort_order'); + if (error) throw error; + return (data ?? []) as ProductFile[]; +} + +export async function getFileById(supabase: SupabaseClient, id: string): Promise { + const { data, error } = await supabase.from('pack_files').select('*').eq('id', id).maybeSingle(); + if (error) throw error; + return (data as ProductFile) ?? null; +}