From 774835a37af4b64428bf12cc02bca680465a7ad8 Mon Sep 17 00:00:00 2001 From: gahusb Date: Sat, 2 May 2026 09:11:08 +0900 Subject: [PATCH] =?UTF-8?q?feat(mypage):=20=EB=8B=A4=EC=9A=B4=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EB=B2=84=ED=8A=BC=20=ED=99=9C=EC=84=B1=ED=99=94=20?= =?UTF-8?q?(Phase=202)=20+=20status=20=EB=B6=84=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - packFiles state + /api/packs/list-mine fetch (RLS 우회 위해 admin client 라우트) - handleDownload: /api/packs/sign-link 호출 → window.location 이동 - 카드: 자료 리스트 DB SSOT (PACK_ASSETS.files 폐기) - order.status === 'completed' 만 다운로드 활성, 그 외는 Phase 1 placeholder 유지 - 4시간 만료 안내 추가 빌드 복구: B3에서 깨진 mypage 빌드를 이번 commit이 복구. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/api/packs/list-mine/route.ts | 40 ++++++++++ app/mypage/page.tsx | 124 ++++++++++++++++++++++--------- 2 files changed, 130 insertions(+), 34 deletions(-) create mode 100644 app/api/packs/list-mine/route.ts diff --git a/app/api/packs/list-mine/route.ts b/app/api/packs/list-mine/route.ts new file mode 100644 index 0000000..b9602e2 --- /dev/null +++ b/app/api/packs/list-mine/route.ts @@ -0,0 +1,40 @@ +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'; + +export const runtime = 'nodejs'; + +export async function GET() { + 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({ files: [] }); + + 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 files = await getPackFilesForTiers(admin, Array.from(tiers)); + return NextResponse.json({ files }); +} diff --git a/app/mypage/page.tsx b/app/mypage/page.tsx index eda9f27..d9eec89 100644 --- a/app/mypage/page.tsx +++ b/app/mypage/page.tsx @@ -6,7 +6,8 @@ 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_ASSETS, extractPackTier, type PackTier } from '@/lib/pack-assets'; +import { PACK_TIER_NAMES, extractPackTier, type PackTier } from '@/lib/pack-assets'; +import type { PackFile } from '@/lib/supabase/pack-files'; function buildSajuResultUrl(rec: SajuRecord) { const { birth_year, birth_month, birth_day, birth_hour, gender } = rec.saju_data; @@ -92,6 +93,8 @@ export default function MyPage() { const [linkToken, setLinkToken] = useState(''); const [linking, setLinking] = useState(false); const [linkMessage, setLinkMessage] = useState(''); + const [packFiles, setPackFiles] = useState([]); + const [downloading, setDownloading] = useState(null); // 텔레그램 연동 상태 const [telegramChatId, setTelegramChatId] = useState(null); @@ -158,6 +161,13 @@ export default function MyPage() { setProjects(projData.projects ?? []); } + // 구매한 팩 자료 파일 조회 + const filesRes = await fetch('/api/packs/list-mine'); + if (filesRes.ok) { + const { files } = await filesRes.json(); + setPackFiles(files ?? []); + } + setLoading(false); } init(); @@ -240,6 +250,26 @@ export default function MyPage() { setTelegramLinkState('idle'); }; + async function handleDownload(fileId: string) { + setDownloading(fileId); + try { + const res = await fetch('/api/packs/sign-link', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ fileId }), + }); + const data = await res.json(); + if (!res.ok || !data.url) { + throw new Error(data.error ?? '링크 발급 실패'); + } + window.location.href = data.url; + } catch (e) { + alert(e instanceof Error ? e.message : '다운로드 준비 중 오류가 발생했습니다'); + } finally { + setDownloading(null); + } + } + const handleLinkProject = async (e: React.FormEvent) => { e.preventDefault(); if (!linkToken.trim()) return; @@ -777,7 +807,6 @@ export default function MyPage() { /> ) : ( packOrders.map(({ order, tier }) => { - const asset = PACK_ASSETS[tier]; const statusLabel = order.status === 'completed' ? '자료 발송 완료' : order.status === 'in_progress' ? '결제 처리 중' : @@ -791,7 +820,7 @@ export default function MyPage() {
-
{asset.name}
+
{PACK_TIER_NAMES[tier]}
{new Date(order.created_at).toLocaleDateString('ko-KR')} 신청
@@ -801,38 +830,65 @@ export default function MyPage() {
-
-
- 📦 자료 패키지 ({asset.files.length}개) -
-
    - {asset.files.map((file, i) => ( -
  • - · - {file} -
  • - ))} -
+ {/* 자료 리스트 — DB가 SSOT */} + {(() => { + 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 + }); - -

- 현재는 카톡 1:1로 자료를 보내드립니다. 자동 다운로드는 곧 활성화됩니다. -
- - 카톡 오픈채팅 → - -

-
+ return ( +
+
+ 📦 자료 패키지 ({filesForTier.length}개) +
+ {filesForTier.length === 0 ? ( +

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

+ ) : ( +
    + {filesForTier.map((f) => ( +
  • + {f.label} + {order.status === 'completed' ? ( + + ) : ( + 대기 중 + )} +
  • + ))} +
+ )} + + {order.status === 'completed' && filesForTier.length > 0 && ( +

+ ※ 다운로드 링크는 4시간 동안 유효합니다. +

+ )} + + {order.status !== 'completed' && ( +

+ {order.status === 'in_progress' ? '결제 처리 중. 자료는 결제 확인 후 활성화됩니다.' : '입금 대기 중. 카톡 1:1로 안내드립니다.'} +
+ + 카톡 오픈채팅 → + +

+ )} +
+ ); + })()}
); })