From f151af89f3078e8fe077347bb6b9ab62ba9f7b4f Mon Sep 17 00:00:00 2001 From: gahusb Date: Thu, 11 Jun 2026 08:47:32 +0900 Subject: [PATCH] =?UTF-8?q?feat(downloads):=20=EB=8B=A4=EC=9A=B4=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EA=B2=80=EC=A6=9D=EC=9D=84=20orders=20=EB=8B=A8?= =?UTF-8?q?=EC=9D=BC=20=EC=86=8C=EC=8A=A4=EB=A1=9C=20=EA=B5=90=EC=B2=B4=20?= =?UTF-8?q?+=20=EB=82=B4=20=EC=A0=9C=ED=92=88=20=EC=A0=9C=ED=92=88?= =?UTF-8?q?=EB=B3=84=20=EA=B7=B8=EB=A3=B9=ED=95=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- app/api/packs/list-mine/route.ts | 33 +++--- app/api/packs/sign-link/route.ts | 34 ++---- app/mypage/page.tsx | 189 ++++++++++++++++--------------- 3 files changed, 126 insertions(+), 130 deletions(-) diff --git a/app/api/packs/list-mine/route.ts b/app/api/packs/list-mine/route.ts index b9602e2..7cb118f 100644 --- a/app/api/packs/list-mine/route.ts +++ b/app/api/packs/list-mine/route.ts @@ -2,8 +2,7 @@ import { NextResponse } from 'next/server'; import { cookies } from 'next/headers'; import { createServerClient as createSSRClient } from '@supabase/ssr'; import { createAdminClient } from '@/lib/supabase/admin'; -import { extractPackTier, type PackTier } from '@/lib/pack-assets'; -import { tierIncludes, getPackFilesForTiers } from '@/lib/supabase/pack-files'; +import { getUserAccessibleProductIds, getFilesByProductIds } from '@/lib/supabase/product-files'; export const runtime = 'nodejs'; @@ -20,21 +19,25 @@ export async function GET() { }, ); const { data: { user } } = await supabase.auth.getUser(); - if (!user) return NextResponse.json({ files: [] }); + if (!user) return NextResponse.json({ products: [] }); const admin = createAdminClient(); - const { data: orders } = await admin - .from('contact_requests') - .select('service, status') - .eq('user_id', user.id) - .eq('status', 'completed'); + const productIds = await getUserAccessibleProductIds(admin, user.id); + if (productIds.length === 0) return NextResponse.json({ products: [] }); - const tiers = new Set(); - for (const o of (orders ?? [])) { - const t = extractPackTier(o.service); - if (t) tierIncludes(t).forEach((x) => tiers.add(x)); + const [files, { data: products }] = await Promise.all([ + getFilesByProductIds(admin, productIds), + admin.from('products').select('id, name').in('id', productIds), + ]); + + const nameMap = new Map((products ?? []).map((p) => [p.id, p.name as string])); + const grouped = new Map(); + for (const f of files) { + if (!f.product_id) continue; + if (!grouped.has(f.product_id)) { + grouped.set(f.product_id, { id: f.product_id, name: nameMap.get(f.product_id) ?? f.product_id, files: [] }); + } + grouped.get(f.product_id)!.files.push(f); } - - const files = await getPackFilesForTiers(admin, Array.from(tiers)); - return NextResponse.json({ files }); + return NextResponse.json({ products: Array.from(grouped.values()) }); } diff --git a/app/api/packs/sign-link/route.ts b/app/api/packs/sign-link/route.ts index a8fb02b..65a3f0f 100644 --- a/app/api/packs/sign-link/route.ts +++ b/app/api/packs/sign-link/route.ts @@ -2,8 +2,7 @@ import { NextResponse } from 'next/server'; import { cookies } from 'next/headers'; import { createServerClient as createSSRClient } from '@supabase/ssr'; import { createAdminClient } from '@/lib/supabase/admin'; -import { extractPackTier, type PackTier } from '@/lib/pack-assets'; -import { tierIncludes, getPackFileById } from '@/lib/supabase/pack-files'; +import { getUserAccessibleProductIds, getFileById } from '@/lib/supabase/product-files'; import { signLink } from '@/lib/web-backend'; export const runtime = 'nodejs'; @@ -33,33 +32,18 @@ export async function POST(request: Request) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - // 2) orders 조회 — completed Music 팩 구매 확인 + // 2) orders(paid) 단일 소스로 접근 가능한 product_id 확인 const admin = createAdminClient(); - const { data: orders } = await admin - .from('contact_requests') - .select('service, status') - .eq('user_id', user.id) - .eq('status', 'completed'); - - const tiers = new Set(); - for (const o of (orders ?? [])) { - const t = extractPackTier(o.service); - if (t) tierIncludes(t).forEach((x) => tiers.add(x)); + const accessible = await getUserAccessibleProductIds(admin, user.id); + if (accessible.length === 0) { + return NextResponse.json({ error: '구매 내역이 없거나 입금 확인 전입니다' }, { status: 403 }); } - if (tiers.size === 0) { - return NextResponse.json({ error: '구매 내역이 없거나 결제 미완료입니다' }, { status: 403 }); + const file = await getFileById(admin, fileId); + if (!file || file.deleted_at || !file.product_id || !accessible.includes(file.product_id)) { + return NextResponse.json({ error: '구매한 제품의 파일이 아닙니다' }, { status: 403 }); } - // 3) 파일 조회 + tier 매칭 - const file = await getPackFileById(admin, fileId); - if (!file) { - return NextResponse.json({ error: '파일을 찾을 수 없습니다' }, { status: 404 }); - } - if (!tiers.has(file.min_tier)) { - return NextResponse.json({ error: '구매 등급에서 접근할 수 없는 파일입니다' }, { status: 403 }); - } - - // 4) web-backend 호출 → DSM 공유 링크 + // 3) web-backend 호출 → DSM 공유 링크 try { const { url, expires_at } = await signLink({ file_path: file.file_path, diff --git a/app/mypage/page.tsx b/app/mypage/page.tsx index fe86b02..fdca5d1 100644 --- a/app/mypage/page.tsx +++ b/app/mypage/page.tsx @@ -6,8 +6,6 @@ import Link from 'next/link'; import { createClient } from '@/lib/supabase/client'; import type { User } from '@supabase/supabase-js'; import TelegramGuideModal from '@/app/components/TelegramGuideModal'; -import { PACK_TIER_NAMES, extractPackTier, type PackTier } from '@/lib/pack-assets'; -import type { PackFile } from '@/lib/supabase/pack-files'; import { KAKAO_OPENCHAT_URL } from '@/lib/contact'; // 마이페이지 — 4탭 재구성 (프로필 / 내 의뢰 / 내 제품 / 주문 내역). @@ -57,6 +55,25 @@ interface Order { status: string; } +// 구매 제품 자료 그룹 (/api/packs/list-mine 응답) +interface ProductFileItem { + id: string; + label: string; +} +interface ProductGroup { + id: string; + name: string; + files: ProductFileItem[]; +} + +// orders 테이블(결제 단일 소스) — pending 안내용 +interface ProductOrder { + id: string; + product_id: string | null; + status: string; + created_at: string; +} + function MyPageContent() { const router = useRouter(); const searchParams = useSearchParams(); @@ -66,7 +83,8 @@ function MyPageContent() { const [tab, setTab] = useState(() => resolveTab(searchParams.get('tab'))); const [payments, setPayments] = useState([]); const [orders, setOrders] = useState([]); - const [packFiles, setPackFiles] = useState([]); + const [productGroups, setProductGroups] = useState([]); + const [productOrders, setProductOrders] = useState([]); const [downloading, setDownloading] = useState(null); // 텔레그램 연동 상태 @@ -111,13 +129,22 @@ function MyPageContent() { .maybeSingle(); setTelegramChatId(profile?.telegram_chat_id ?? null); - // 구매한 팩 자료 파일 조회 + // 구매 제품 자료 그룹 조회 (orders paid 단일 소스) const filesRes = await fetch('/api/packs/list-mine'); if (filesRes.ok) { - const { files } = await filesRes.json(); - setPackFiles(files ?? []); + const { products } = await filesRes.json(); + setProductGroups(products ?? []); } + // 결제 주문(orders 테이블) 조회 — pending 안내 / 주문 내역 공유 + const { data: prodOrders } = await supabase + .from('orders') + .select('id, product_id, status, created_at') + .eq('user_id', user.id) + .order('created_at', { ascending: false }) + .limit(50); + setProductOrders(prodOrders || []); + setLoading(false); } init(); @@ -204,15 +231,13 @@ function MyPageContent() { if (!user) return null; - // contact_requests 중 팩 주문만 추려 '내 제품' 탭에서 다운로드 노출 - const packOrders = orders - .map((o) => ({ order: o, tier: extractPackTier(o.service) })) - .filter((x): x is { order: Order; tier: PackTier } => x.tier !== null); + // 입금 확인 대기 중인 주문 (orders 테이블 pending) + const pendingOrders = productOrders.filter((o) => o.status === 'pending'); const tabs: { key: Tab; label: string; count?: number }[] = [ { key: 'profile', label: '프로필' }, { key: 'requests', label: '내 의뢰', count: orders.length || undefined }, - { key: 'products', label: '내 제품', count: packOrders.length || undefined }, + { key: 'products', label: '내 제품', count: productGroups.length || undefined }, { key: 'orders', label: '주문 내역', count: (orders.length + payments.length) || undefined }, ]; @@ -501,105 +526,89 @@ function MyPageContent() { )} - {/* ===== 내 제품 (구매한 팩) ===== */} + {/* ===== 내 제품 (구매한 제품 자료) ===== */} {tab === 'products' && (
- {packOrders.length === 0 ? ( + {/* 입금 확인 대기 안내 */} + {pendingOrders.length > 0 && ( +
+ + 대기중 + +
+ 입금 확인 대기 중인 주문이 {pendingOrders.length}건 있습니다. 입금이 확인되면 자료 다운로드가 활성화됩니다. + {' '} + + 카톡 오픈채팅 → + +
+
+ )} + + {productGroups.length === 0 ? ( ) : ( - packOrders.map(({ order, tier }) => { - const completed = order.status === 'completed'; - const filesForTier = packFiles.filter((pf) => { - if (tier === 'starter') return pf.min_tier === 'starter'; - if (tier === 'pro') return pf.min_tier === 'starter' || pf.min_tier === 'pro'; - return true; // master - }); + productGroups.map((group) => ( + +
+
+ {group.name} +
+
- return ( - -
-
-
- {PACK_TIER_NAMES[tier]} -
-
- {new Date(order.created_at).toLocaleDateString('ko-KR')} 신청 -
-
- +
+
+ 자료 패키지 ({group.files.length}개)
-
-
- 자료 패키지 ({filesForTier.length}개) -
- - {filesForTier.length === 0 ? ( -

- 자료 준비 중입니다. 카톡 1:1로 문의해주세요. -

- ) : ( + {group.files.length === 0 ? ( +

+ 자료 준비 중입니다. 카톡 1:1로 문의해주세요. +

+ ) : ( + <>
    - {filesForTier.map((f) => ( + {group.files.map((f) => (
  • {f.label} - {completed ? ( - - ) : ( - - 대기 중 - - )} +
  • ))}
- )} - - {completed && filesForTier.length > 0 && (

다운로드 링크는 발급 후 4시간 동안 유효합니다.

- )} - - {!completed && ( -
- 입금 확인 후 다운로드가 활성화됩니다. - {order.status === 'in_progress' - ? ' 결제 처리 중입니다.' - : ' 입금 안내는 카톡 1:1로 드립니다.'} -
- - 카톡 오픈채팅 → - -
- )} -
- - ); - }) + + )} +
+ + )) )}
)}