From d2bdc6a854d85503430318509ded12bddb4bc835 Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 28 Apr 2026 03:37:49 +0900 Subject: [PATCH] =?UTF-8?q?docs(plan):=20mypage=20Liquid=20Glass=20Phase?= =?UTF-8?q?=201=20=E2=80=94=208=20task=20implementation=20plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spec docs/superpowers/specs/2026-04-27-mypage-liquidglass-redesign.md 의 7개 섹션 모두 task로 매핑. 검증 인프라 부재 → lint + build + 시각 회귀 3단계 검증. Task 순서 안전 분석(부록 A): 각 commit 후 mypage 로그아웃 경로 + 카카오 진입 항상 유지. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...26-04-27-mypage-liquidglass-redesign-p1.md | 1300 +++++++++++++++++ 1 file changed, 1300 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-27-mypage-liquidglass-redesign-p1.md diff --git a/docs/superpowers/plans/2026-04-27-mypage-liquidglass-redesign-p1.md b/docs/superpowers/plans/2026-04-27-mypage-liquidglass-redesign-p1.md new file mode 100644 index 0000000..917806c --- /dev/null +++ b/docs/superpowers/plans/2026-04-27-mypage-liquidglass-redesign-p1.md @@ -0,0 +1,1300 @@ +# mypage Liquid Glass 리뉴얼 (Phase 1) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** mypage를 메인 surface(Liquid Glass + Jua)와 시각·구조적으로 조화시키고, "구매한 팩" 탭 placeholder + TopNav 로그인 상태 반영을 추가한다. NAS 기반 자료 다운로드는 Phase 2로 분리. + +**Architecture:** 기존 Sidebar 레이아웃을 폐기하고 PublicShell + TopNav로 통합. mypage 자체 hero는 축소(가입일·아바타 유지·로그아웃 버튼 제거)하고 본문은 보라/시안 액센트의 light card 톤으로 전환. 신원·로그아웃은 TopNav 한 곳에서 담당. Music 팩 자료는 정적 매핑(`lib/pack-assets.ts`)으로 노출하되 다운로드 버튼은 Phase 2까지 비활성. + +**Tech Stack:** Next.js 16 (App Router), TypeScript, Tailwind v4, Supabase Auth (`@supabase/ssr`), 자체 디자인 토큰(`globals.css`의 `--kx-*` + slate/violet Tailwind 팔레트) + +**Spec:** `docs/superpowers/specs/2026-04-27-mypage-liquidglass-redesign.md` + +--- + +## P1에서 다루지 않는 항목 (의도적 제외) + +| 항목 | 이유 | 다음 단계 | +|---|---|---| +| NAS `/media/packs/` 인프라 + HMAC 토큰 + Next API | Phase 2 별도 spec | 운영상 자료 자동 다운로드 수요 발생 시 | +| Studio 트랙 DB 저장 (음악 통합 옵션 A) | 별도 plan | 백로그 | +| `/admin` shell Liquid Glass | 별도 surface, 우선순위 낮음 | 백로그 | +| 사주 1,000원 PG 결제 결정 | P0 brainstorm 부록 A 보류 | CEO 결정 후 | +| URL 마이그레이션 (`/freelance` → `/work/freelance` 등) | P1 home-restructure 별도 plan | 후속 P1 | + +--- + +## File Structure + +| 파일 | 종류 | 책임 | +|---|---|---| +| `lib/pack-assets.ts` | Create | Music 팩 3티어 정적 자료 매핑 + `extractPackTier(service)` 함수 | +| `app/components/TopNav.tsx` | Modify | supabase auth 구독 + 로그인 토글 (데스크톱 + 모바일) | +| `app/components/PublicShell.tsx` | Modify | 카카오 플로팅 버튼 마운트 (DashboardShell에서 이동) | +| `app/mypage/page.tsx` | Modify | hero 축소(로그아웃 제거), Tab type 확장, "구매한 팩" 탭 JSX, body 토큰 일괄 마이그레이션 | +| `app/components/DashboardShell.tsx` | Modify | 사이드바 분기 + 카카오 버튼 + Sidebar import 통째 제거 | +| `app/components/Sidebar.tsx` | Delete | 사용처 0 | + +검증 인프라: 이 프로젝트는 jest/vitest/playwright 미설치. 각 task 검증 = `npx eslint <변경 파일>` + 마지막 task에서 `npm run build` + 시각 회귀(사용자 수동). + +--- + +## Task 1: `lib/pack-assets.ts` 신규 파일 + +**Files:** +- Create: `C:\Users\jaeoh\Desktop\workspace\jaengseung-made\lib\pack-assets.ts` + +- [ ] **Step 1: 새 파일 작성** + +다음 내용으로 `lib/pack-assets.ts` 신규 작성: + +```ts +export interface PackAsset { + name: string; + files: string[]; +} + +export type PackTier = 'starter' | 'pro' | 'master'; + +export const PACK_ASSETS: Record = { + starter: { + name: 'AI 음악 마스터 팩 (입문)', + files: [ + 'Suno 프롬프트 북 PDF (40p)', + '구조 템플릿 PDF', + '저작권 가이드 기본판', + ], + }, + pro: { + name: 'AI 음악 마스터 팩 (프로)', + files: [ + '입문 자료 전체', + 'MV 워크플로우 가이드 (Runway · Luma · Pika)', + '샘플 프로젝트 1개 (.prj 파일 + 영상)', + '유튜브 SEO 템플릿', + '1:1 Q&A 1회 (이메일 응답)', + ], + }, + master: { + name: 'AI 음악 마스터 팩 (마스터)', + files: [ + '프로 자료 전체', + '샘플 프로젝트 장르별 3종', + '저작권 심화판 + 상업 이용 체크리스트', + '제작 레시피 영상 (우선 공개)', + ], + }, +}; + +/** + * orders.service ("구매 신청: AI 음악 마스터 팩 · 프로") → tier key. + * 매칭 안 되면 null 반환 (Music 팩 외 의뢰). + */ +export function extractPackTier(service: string): PackTier | null { + if (!service.startsWith('구매 신청:')) return null; + if (service.includes('마스터')) return 'master'; + if (service.includes('프로')) return 'pro'; + if (service.includes('입문')) return 'starter'; + return null; +} +``` + +**주의 — extractPackTier 분기 순서**: `'마스터'`가 `'프로'`보다 먼저 검사되어야 함. `name` 필드가 `"AI 음악 마스터 팩 (프로)"` 처럼 "마스터"와 "프로"가 동시 등장하지만 `service`는 `"구매 신청: AI 음악 마스터 팩 · 프로"` 형식 → "마스터" + "프로" 둘 다 포함됨 → 분기 순서가 중요. **마스터 → 프로 → 입문** 순서로 검사하면 tier 분리 정확: +- "구매 신청: AI 음악 마스터 팩 · 입문" → master 매칭? "마스터" 단어 포함되어 master로 분류됨 → **틀림** + +→ 더 안전한 분기: tier 단어가 `·` 뒤에 와야 함: + +```ts +export function extractPackTier(service: string): PackTier | null { + if (!service.startsWith('구매 신청:')) return null; + // service 예시: "구매 신청: AI 음악 마스터 팩 · 프로" + // 마지막 "·" 뒤가 tier 이름 + const dotIdx = service.lastIndexOf('·'); + if (dotIdx === -1) return null; + const tierName = service.slice(dotIdx + 1).trim(); + if (tierName === '입문') return 'starter'; + if (tierName === '프로') return 'pro'; + if (tierName === '마스터') return 'master'; + return null; +} +``` + +이 형태로 작성. + +- [ ] **Step 2: 린트 통과 확인** + +```bash +npx eslint lib/pack-assets.ts +``` +Expected: exit 0, 출력 없음. + +- [ ] **Step 3: 빠른 동작 검증 (Node REPL or 임시 console)** + +다음 명령으로 함수 분기 검증: + +```bash +node --input-type=module -e " +const { extractPackTier } = await import('./lib/pack-assets.ts').catch(() => null) ?? {}; +" +``` + +이 프로젝트는 `.ts` 직접 실행이 안 되므로, 대신 임시로 Node에서 함수 로직만 복사해서 검증: + +```bash +node -e " +function extractPackTier(service) { + if (!service.startsWith('구매 신청:')) return null; + const dotIdx = service.lastIndexOf('·'); + if (dotIdx === -1) return null; + const tierName = service.slice(dotIdx + 1).trim(); + if (tierName === '입문') return 'starter'; + if (tierName === '프로') return 'pro'; + if (tierName === '마스터') return 'master'; + return null; +} +console.log('starter:', extractPackTier('구매 신청: AI 음악 마스터 팩 · 입문')); +console.log('pro: ', extractPackTier('구매 신청: AI 음악 마스터 팩 · 프로')); +console.log('master: ', extractPackTier('구매 신청: AI 음악 마스터 팩 · 마스터')); +console.log('null1: ', extractPackTier('구매 신청: 외주 개발')); +console.log('null2: ', extractPackTier('일반 문의')); +" +``` + +Expected output: +``` +starter: starter +pro: pro +master: master +null1: null +null2: null +``` + +- [ ] **Step 4: 커밋** + +```bash +git add lib/pack-assets.ts +git commit -m "$(cat <<'EOF' +feat(packs): Music 팩 3티어 정적 자료 매핑 + tier 추출 함수 + +- PACK_ASSETS: starter/pro/master 각 자료 리스트 (Phase 1 placeholder, 실제 파일 URL은 Phase 2) +- extractPackTier(): orders.service "구매 신청: AI 음악 마스터 팩 · {tier}" → tier key + · "·" 뒤의 마지막 단어로 매칭하여 "마스터 팩" + "프로" 같은 충돌 회피 + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Task 2: TopNav supabase auth 구독 + 로그인 토글 + +**Files:** +- Modify: `C:\Users\jaeoh\Desktop\workspace\jaengseung-made\app\components\TopNav.tsx` + +- [ ] **Step 1: imports 추가** + +`app/components/TopNav.tsx` 의 현재 1-5행: +```tsx +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { useState, useEffect } from 'react'; +``` + +다음으로 변경: +```tsx +'use client'; + +import Link from 'next/link'; +import { usePathname, useRouter } from 'next/navigation'; +import { useState, useEffect } from 'react'; +import { createClient } from '@/lib/supabase/client'; +import type { User } from '@supabase/supabase-js'; +``` + +- [ ] **Step 2: state + auth 구독 + 로그아웃 핸들러 추가** + +현재 `export default function TopNav()` 함수 본문 (15-41행)에서 state 선언 직후에 router/supabase/user state + auth effect + handleLogout 추가. + +현재 (15-25행): +```tsx +export default function TopNav() { + const pathname = usePathname(); + const [open, setOpen] = useState(false); + const [scrolled, setScrolled] = useState(false); + + useEffect(() => { + const onScroll = () => setScrolled(window.scrollY > 8); + onScroll(); + window.addEventListener('scroll', onScroll, { passive: true }); + return () => window.removeEventListener('scroll', onScroll); + }, []); +``` + +변경 후: +```tsx +export default function TopNav() { + const pathname = usePathname(); + const router = useRouter(); + const supabase = createClient(); + const [open, setOpen] = useState(false); + const [scrolled, setScrolled] = useState(false); + const [user, setUser] = useState(null); + + useEffect(() => { + const onScroll = () => setScrolled(window.scrollY > 8); + onScroll(); + window.addEventListener('scroll', onScroll, { passive: true }); + return () => window.removeEventListener('scroll', onScroll); + }, []); + + // Supabase auth state subscription (Sidebar.tsx:93-103 패턴) + useEffect(() => { + let mounted = true; + supabase.auth.getUser().then(({ data }) => { + if (mounted) setUser(data.user ?? null); + }); + const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => { + if (mounted) setUser(session?.user ?? null); + }); + return () => { + mounted = false; + subscription.unsubscribe(); + }; + }, [supabase]); + + const handleLogout = async () => { + await supabase.auth.signOut(); + setOpen(false); + router.push('/'); + router.refresh(); + }; +``` + +- [ ] **Step 3: 데스크톱 우측 영역 — 로그인 상태 토글** + +현재 91-105행: +```tsx +
+ + 로그인 + + + Try now + +``` + +변경 후: +```tsx +
+ {user ? ( + <> + + 마이페이지 + + + + ) : ( + <> + + 로그인 + + + Try now + + + )} +``` + +이후 ` + + ) : ( + <> + + 로그인 + + + Try now + + + )} +
+``` + +- [ ] **Step 5: 린트 통과 확인** + +```bash +npx eslint app/components/TopNav.tsx +``` +Expected: 새 경고/에러 없음. 사전 존재하던 `react-hooks/set-state-in-effect` (line 27) 경고는 그대로 (out of scope). + +- [ ] **Step 6: 커밋** + +```bash +git add app/components/TopNav.tsx +git commit -m "$(cat <<'EOF' +feat(nav): TopNav supabase auth 구독 + 로그인 상태 토글 + +- 로그아웃 시: "로그인" link + "Try now" 버튼 (기존) +- 로그인 시: "마이페이지" link + "로그아웃" 버튼 (신규) +- 데스크톱 + 모바일 오버레이 둘 다 동일 패턴 +- Sidebar.tsx:93-103 의 auth 구독 패턴 차용 + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Task 3: PublicShell에 카카오 플로팅 버튼 추가 + +**Files:** +- Modify: `C:\Users\jaeoh\Desktop\workspace\jaengseung-made\app\components\PublicShell.tsx` + +- [ ] **Step 1: 카카오 버튼 JSX + style 블록 추가** + +현재 `PublicShell.tsx` 마지막은 `` 닫고 `` 로 fragment 종료. footer는 `
` 안에 있음. 카카오 버튼은 footer 닫히고 main 닫히기 전, 또는 main 닫힌 후 fragment 안에 mount. + +**위치**: 현재 `
...
` 닫는 태그(line ~113) 다음, `
` 직전. + +현재 구조 (단순화): +```tsx +return ( + <> + +
+ {children} +
+ ... +
+
+ +); +``` + +변경 후: +```tsx +return ( + <> + +
+ {children} +
+ ... +
+
+ + {/* 카카오 오픈채팅 플로팅 버튼 */} + + + + + 1:1 상담 + + + + +); +``` + +- [ ] **Step 2: 린트 통과 확인** + +```bash +npx eslint app/components/PublicShell.tsx +``` +Expected: exit 0. + +- [ ] **Step 3: 시각적 잠시 확인 (수동, 선택적)** + +`npm run dev` 후 메인 페이지(`/`) 우측 하단에 노란 카카오 플로팅 버튼 떠있는지 빠르게 확인. 본격 회귀 검증은 Task 8. + +- [ ] **Step 4: 커밋** + +```bash +git add app/components/PublicShell.tsx +git commit -m "$(cat <<'EOF' +feat(shell): PublicShell에 카카오 1:1 상담 플로팅 버튼 추가 + +DashboardShell 사이드바 분기에서 mypage 전용으로만 노출되던 카카오 버튼을 +모든 공개 페이지(메인/서비스/외주/사주/결제/legal/mypage 등)에서 노출되도록 이동. +DashboardShell 쪽 원본은 Task 6에서 사이드바 분기 제거와 함께 자연 삭제 예정. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Task 4: mypage hero 축소 + Tab type 확장 + "구매한 팩" 탭 신설 + +**Files:** +- Modify: `C:\Users\jaeoh\Desktop\workspace\jaengseung-made\app\mypage\page.tsx` + +본 task는 mypage에 추가/구조변경 성격의 변경만 적용 (색 토큰 일괄 마이그레이션은 Task 5). + +- [ ] **Step 1: imports 추가** + +`app/mypage/page.tsx` 파일 상단 import 영역에서 `import TelegramGuideModal` 다음에 추가: + +```tsx +import { PACK_ASSETS, extractPackTier, type PackTier } from '@/lib/pack-assets'; +``` + +- [ ] **Step 2: Tab type 확장** + +현재 18행: +```tsx +type Tab = 'profile' | 'projects' | 'subscription' | 'saju' | 'payments' | 'orders'; +``` + +변경 후: +```tsx +type Tab = 'profile' | 'projects' | 'subscription' | 'saju' | 'payments' | 'orders' | 'packs'; +``` + +- [ ] **Step 3: tabs 배열에 "구매한 팩" 항목 추가 (결제 내역 다음 위치)** + +현재 286-293행: +```tsx + 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: 'profile', label: '내 정보' }, + { key: 'subscription', label: '구독 관리', count: activeSubs.length || undefined }, + { key: 'saju', label: '사주 기록', count: sajuRecords.length || undefined }, + ]; +``` + +변경 후 (`결제 내역` 다음에 `구매한 팩` 추가, count는 packOrders로 계산): +```tsx + 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 }, + ]; +``` + +- [ ] **Step 4: hero JSX 축소 + 로그아웃 버튼 제거 + 토큰 변경** + +현재 302-325행 헤더: +```tsx + {/* 헤더 */} +
+
+
+
+ {user.email?.[0].toUpperCase()} +
+
+
{user.email}
+
+ 가입일: {new Date(user.created_at).toLocaleDateString('ko-KR')} +
+
+
+ +
+
+
+
+``` + +변경 후: +```tsx + {/* 헤더 — kx-surface 다크 톤, 축소판. 로그아웃은 TopNav에서 담당 */} +
+
+
+
+ {user.email?.[0].toUpperCase()} +
+
+
{user.email}
+
+ 가입일 {new Date(user.created_at).toLocaleDateString('ko-KR')} +
+
+
+
+
+``` + +변경 사항 요약: +- `bg-[#04102b]` → CSS var `var(--kx-surface)` (#060e20) +- 패딩 `py-10` → `py-8` +- 하단 보더 추가 `border-b border-white/5` +- 아바타 크기 `w-14 h-14 text-xl` → `w-12 h-12 text-lg` +- 아바타 배경 `bg-[#1a56db]` → `var(--kx-primary)` (#cc97ff 보라) +- 이메일 텍스트에 `kx-display` 클래스 추가 (Jua + letter-spacing) +- 가입일 폰트 `text-sm` → `text-xs`, 색 `text-blue-300/60` → `text-white/50` +- 가입일 `: ` 콜론 → 공백 +- **로그아웃 버튼 통째 제거** (`
...
` 블록) + +- [ ] **Step 5: handleLogout 함수 제거 (사용처 없어짐)** + +현재 165-169행: +```tsx + const handleLogout = async () => { + await supabase.auth.signOut(); + router.push('/'); + router.refresh(); + }; +``` + +이 함수는 hero에서만 호출됐고 hero에서 로그아웃 버튼이 제거되므로 unused. **함수 통째 삭제**. + +⚠️ 참고: 만약 다른 위치에서 `handleLogout` 호출이 남아있는지 확인 필요. 검증: + +```bash +grep -n "handleLogout" app/mypage/page.tsx +``` +Expected: 검색 결과 없음 (또는 함수 정의 한 줄만). 호출처 있으면 함께 제거. + +- [ ] **Step 6: "구매한 팩" 탭 JSX 추가** + +`{/* 결제 내역 */}` 섹션과 `{/* 프로젝트 진행 현황 */}` 섹션 사이에 새 섹션 삽입. + +현재 mypage page.tsx 의 `{tab === 'payments' && (...)}` 블록과 `{tab === 'projects' && (...)}` 블록 사이. + +새 섹션 JSX: +```tsx + {/* 구매한 팩 */} + {tab === 'packs' && ( +
+ {packOrders.length === 0 ? ( + + ) : ( + 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 ( +
+
+
+
{asset.name}
+
+ {new Date(order.created_at).toLocaleDateString('ko-KR')} 신청 +
+
+ + {statusLabel} + +
+ +
+
+ 📦 자료 패키지 ({asset.files.length}개) +
+
    + {asset.files.map((file, i) => ( +
  • + · + {file} +
  • + ))} +
+ + +

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

+
+
+ ); + }) + )} +
+ )} +``` + +- [ ] **Step 7: '내 정보' 탭 빠른 메뉴에 AI 스튜디오 카드 추가** + +현재 mypage `{tab === 'profile' && (...)}` 블록 안에 "빠른 메뉴" 섹션이 있음 (현재 line ~512-535). 두 카드: 사주 분석(`/saju/input`) + 외주 의뢰(`/freelance`). 음악 통합 강화를 위해 **AI 스튜디오 카드 1개 추가**. + +현재 빠른 메뉴 그리드: +```tsx +
+ ... + ... +
+``` + +변경 후 — `grid-cols-2` → `grid-cols-3`, AI 스튜디오 카드 추가: +```tsx +
+ + {/* 기존 사주 카드 그대로 */} + + + {/* 기존 외주 카드 그대로 */} + + +
+ + + +
+
+
AI 스튜디오
+
새 트랙 만들기
+
+ +
+``` + +**주의 — 토큰 표기**: 위 새 카드의 `border-[#dbe8ff]`, `text-[#04102b]`, `hover:bg-blue-50/50`, `hover:border-blue-300` 같은 brand blue 토큰은 Task 5의 일괄 마이그레이션이 자동으로 처리하므로 **이번 task에서는 그대로 두기**. (Task 5에서 grep-치환 매핑이 새 카드도 함께 마이그레이션함.) + +**모바일 grid 고려**: `grid-cols-3` 으로 모바일에서 3 column이 좁아질 수 있음. 보수적으로 `grid-cols-2 sm:grid-cols-3` 적용: +```tsx +
+``` +(모바일 기본 2 column, sm+ 에서 3 column) + +- [ ] **Step 8: 탭 한 줄 → wrap 처리 (모바일 7개 대응)** + +현재 329-329행 탭 컨테이너: +```tsx +
+``` + +변경 후 (`flex-wrap` 추가, 각 탭 최소 폭 확보 위해 `flex-1 min-w-[100px]`): +```tsx +
+``` + +탭 버튼 className은 Task 5의 토큰 마이그레이션에서 `flex-1` → `flex-1 min-w-[100px]` 추가. 이번 task에서는 컨테이너만 `flex-wrap`. + +- [ ] **Step 9: 린트 통과 확인** + +```bash +npx eslint app/mypage/page.tsx +``` +Expected: exit 0. 새 import 사용 X 경고 등 없어야 함. + +- [ ] **Step 10: 빌드 통과 확인 (구조적 변경이라 TS 검증 권장)** + +```bash +npm run build +``` +Expected: 성공. PackTier 타입, packOrders 타입 추론 정상. + +- [ ] **Step 11: 커밋** + +```bash +git add app/mypage/page.tsx +git commit -m "$(cat <<'EOF' +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) +EOF +)" +``` + +--- + +## Task 5: mypage 본문 색 토큰 마이그레이션 + +**Files:** +- Modify: `C:\Users\jaeoh\Desktop\workspace\jaengseung-made\app\mypage\page.tsx` + +본 task는 색 토큰만 일괄 치환. 구조 변경 없음. Task 4 직후 같은 파일에 적용. + +⚠️ **주의**: 의미 색(emerald/orange/amber/red/rose/pink/cyan/sky 등 status·메타 시그널)은 그대로 유지. 변경은 brand 색(`#1a56db`, `#04102b`, `#f0f5ff`, `#dbe8ff`, `blue-50/200/600` 같은 brand blue)만. + +- [ ] **Step 1: 본문 외곽 + 탭 active 토큰 변경** + +현재 296행: +```tsx +
+``` +변경 후: +```tsx +
+``` + +현재 329-339행 (탭 바): +```tsx +
+ {tabs.map((t) => ( +