feat(mypage): hero 축소 + "구매한 팩" 탭 신설 + 빠른 메뉴 AI 스튜디오 추가

- Hero: bg-[#04102b] → kx-surface, py-10→py-8, 아바타 보라 액센트, 가입일 톤 다운,
  로그아웃 버튼 제거 (TopNav가 담당)
- Tab type에 'packs' 추가, 결제 내역 다음 위치에 "구매한 팩" 탭
- packOrders 계산: orders.service 에서 extractPackTier로 Music 팩만 필터
- 신규 탭 JSX: status별 분기(완료/처리중/대기) + 자료 리스트 + 비활성 다운로드 버튼
  + 카톡 안내. Phase 2에서 다운로드 활성화 예정
- 빠른 메뉴: AI 스튜디오 카드 1개 추가 (사주·외주 옆), grid-cols-2→sm:grid-cols-3
- 탭 컨테이너 flex-wrap 적용 (모바일 7개 wrap)
- handleLogout 함수 제거 (사용처 없어짐)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-28 03:58:34 +09:00
parent 11bbd00d88
commit 754d81139e

View File

@@ -6,6 +6,7 @@ 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';
function buildSajuResultUrl(rec: SajuRecord) {
const { birth_year, birth_month, birth_day, birth_hour, gender } = rec.saju_data;
@@ -15,7 +16,7 @@ function buildSajuResultUrl(rec: SajuRecord) {
return url;
}
type Tab = 'profile' | 'projects' | 'subscription' | 'saju' | 'payments' | 'orders';
type Tab = 'profile' | 'projects' | 'subscription' | 'saju' | 'payments' | 'orders' | 'packs';
type TelegramLinkState = 'idle' | 'generating' | 'waiting' | 'disconnecting';
interface SajuRecord {
@@ -162,12 +163,6 @@ export default function MyPage() {
init();
}, []);
const handleLogout = async () => {
await supabase.auth.signOut();
router.push('/');
router.refresh();
};
// ── 구독 해지 ──
const handleCancelSubscription = async (subId: string) => {
if (!confirm('구독을 해지하시겠습니까?\n만료일까지는 서비스를 계속 이용할 수 있습니다.')) return;
@@ -283,10 +278,15 @@ export default function MyPage() {
const activeSubs = activeSubscriptions.filter((s) => s.status === 'active' || s.status === 'cancelled');
const packOrders = orders
.map((o) => ({ order: o, tier: extractPackTier(o.service) }))
.filter((x): x is { order: Order; tier: PackTier } => x.tier !== null);
const tabs: { key: Tab; label: string; count?: number }[] = [
{ key: 'projects', label: '프로젝트 현황', count: projects.length || undefined },
{ key: 'orders', label: '의뢰 내역', count: orders.length || undefined },
{ key: 'payments', label: '결제 내역', count: payments.length || undefined },
{ key: 'packs', label: '구매한 팩', count: packOrders.length || undefined },
{ key: 'profile', label: '내 정보' },
{ key: 'subscription', label: '구독 관리', count: activeSubs.length || undefined },
{ key: 'saju', label: '사주 기록', count: sajuRecords.length || undefined },
@@ -299,34 +299,35 @@ export default function MyPage() {
<TelegramGuideModal onClose={() => setShowTelegramGuide(false)} />
)}
{/* 헤더 */}
<div className="bg-[#04102b] px-6 py-10" style={{ backgroundImage: 'repeating-linear-gradient(135deg, rgba(255,255,255,0.015) 0px, rgba(255,255,255,0.015) 1px, transparent 1px, transparent 40px)' }}>
{/* 헤더 — kx-surface 다크 톤, 축소판. 로그아웃은 TopNav에서 담당 */}
<div
className="px-6 py-8 border-b border-white/5"
style={{
background: 'var(--kx-surface)',
backgroundImage: 'repeating-linear-gradient(135deg, rgba(255,255,255,0.015) 0px, rgba(255,255,255,0.015) 1px, transparent 1px, transparent 40px)',
}}
>
<div className="max-w-4xl mx-auto">
<div className="flex items-center gap-4">
<div className="w-14 h-14 rounded-full bg-[#1a56db] flex items-center justify-center text-white text-xl font-bold flex-shrink-0">
<div
className="w-12 h-12 rounded-full flex items-center justify-center text-white text-lg font-bold flex-shrink-0"
style={{ background: 'var(--kx-primary)' }}
>
{user.email?.[0].toUpperCase()}
</div>
<div>
<div className="text-white font-bold text-lg leading-tight">{user.email}</div>
<div className="text-blue-300/60 text-sm mt-0.5">
: {new Date(user.created_at).toLocaleDateString('ko-KR')}
<div className="kx-display text-white font-bold text-lg leading-tight">{user.email}</div>
<div className="text-white/50 text-xs mt-0.5">
{new Date(user.created_at).toLocaleDateString('ko-KR')}
</div>
</div>
<div className="ml-auto">
<button
onClick={handleLogout}
className="px-4 py-2 bg-white/5 border border-white/10 text-slate-300 text-sm rounded-xl hover:bg-white/10 transition"
>
</button>
</div>
</div>
</div>
</div>
<div className="px-6 py-8 max-w-4xl mx-auto">
{/* 탭 */}
<div className="flex gap-1 bg-white border border-[#dbe8ff] rounded-xl p-1 mb-6">
<div className="flex flex-wrap gap-1 bg-white border border-[#dbe8ff] rounded-xl p-1 mb-6">
{tabs.map((t) => (
<button
key={t.key}
@@ -509,7 +510,7 @@ export default function MyPage() {
<div className="w-1 h-5 bg-[#1a56db] rounded-full" />
</h2>
<div className="grid grid-cols-2 gap-3">
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
<Link href="/saju/input" className="flex items-center gap-3 p-4 rounded-xl border border-[#dbe8ff] hover:border-blue-300 hover:bg-blue-50/50 transition group">
<div className="w-9 h-9 rounded-xl bg-violet-50 border border-violet-200 flex items-center justify-center flex-shrink-0">
<svg className="w-5 h-5 text-violet-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -532,6 +533,20 @@ export default function MyPage() {
<div className="text-xs text-slate-500"> </div>
</div>
</Link>
<Link
href="/studio"
className="flex items-center gap-3 p-4 rounded-xl border border-[#dbe8ff] hover:border-blue-300 hover:bg-blue-50/50 transition group"
>
<div className="w-9 h-9 rounded-xl bg-violet-50 border border-violet-200 flex items-center justify-center flex-shrink-0">
<svg className="w-5 h-5 text-violet-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 19V6l12-3v13M9 19a3 3 0 11-6 0 3 3 0 016 0zm12-3a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<div>
<div className="text-sm font-semibold text-[#04102b]">AI </div>
<div className="text-xs text-slate-500"> </div>
</div>
</Link>
</div>
</div>
</div>
@@ -748,6 +763,82 @@ export default function MyPage() {
</div>
)}
{/* 구매한 팩 */}
{tab === 'packs' && (
<div className="space-y-4">
{packOrders.length === 0 ? (
<EmptyState
icon="🎵"
title="구매한 팩이 없습니다"
desc="AI 음악 팩을 구매하시면 자료가 여기에 표시됩니다"
linkHref="/services/music"
linkLabel="Music 팩 보기"
/>
) : (
packOrders.map(({ order, tier }) => {
const asset = PACK_ASSETS[tier];
const statusLabel =
order.status === 'completed' ? '자료 발송 완료' :
order.status === 'in_progress' ? '결제 처리 중' :
'입금 대기';
const statusColor =
order.status === 'completed' ? 'bg-violet-50 text-violet-600 border-violet-200' :
order.status === 'in_progress' ? 'bg-amber-50 text-amber-600 border-amber-200' :
'bg-slate-100 text-slate-500 border-slate-200';
return (
<div key={order.id} className="bg-white rounded-2xl border border-slate-200 p-6">
<div className="flex items-start justify-between mb-4">
<div>
<div className="font-bold text-slate-900 text-base">{asset.name}</div>
<div className="text-xs text-slate-500 mt-1">
{new Date(order.created_at).toLocaleDateString('ko-KR')}
</div>
</div>
<span className={`text-xs font-bold px-2.5 py-1 rounded-full border ${statusColor}`}>
{statusLabel}
</span>
</div>
<div className="border-t border-slate-100 pt-4">
<div className="text-sm font-semibold text-slate-700 mb-3">
📦 ({asset.files.length})
</div>
<ul className="space-y-2 mb-5">
{asset.files.map((file, i) => (
<li key={i} className="flex items-center gap-2 text-sm text-slate-600">
<span className="text-slate-400">·</span>
<span>{file}</span>
</li>
))}
</ul>
<button
disabled
className="w-full py-3 rounded-xl text-sm font-bold bg-slate-100 text-slate-400 cursor-not-allowed"
>
</button>
<p className="text-xs text-slate-500 mt-2 text-center leading-relaxed">
1:1로 . .
<br />
<a
href="https://open.kakao.com/o/s9stoNvb"
target="_blank"
rel="noopener noreferrer"
className="text-violet-600 hover:underline font-semibold"
>
</a>
</p>
</div>
</div>
);
})
)}
</div>
)}
{/* 프로젝트 진행 현황 */}
{tab === 'projects' && (
<div className="space-y-4">