From e5b907dc3896507d583fb44ebb07e5b02db7e2b9 Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 30 Jun 2026 14:32:18 +0900 Subject: [PATCH 01/10] =?UTF-8?q?docs(redesign):=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EA=B3=A0craft=20=EC=9E=AC=EC=84=A4=EA=B3=84=20?= =?UTF-8?q?=EC=84=A4=EA=B3=84=20=EB=AC=B8=EC=84=9C=20=ED=99=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 홈·외주·제품 3면을 라이트 --jsm-* 단일 시스템으로 통일. Deep Field 다크/파티클 폐기, 히어로에 코드 UI 목업(MockWindow) 도입, 가짜 그래디언트 쇼케이스 → 실화면 느낌 목업 그리드, 죽은 CSS 정리. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01A2N6SziVSPfavx1j5rAs52 --- .../2026-06-30-jsm-light-redesign-design.md | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-30-jsm-light-redesign-design.md diff --git a/docs/superpowers/specs/2026-06-30-jsm-light-redesign-design.md b/docs/superpowers/specs/2026-06-30-jsm-light-redesign-design.md new file mode 100644 index 0000000..d4712c3 --- /dev/null +++ b/docs/superpowers/specs/2026-06-30-jsm-light-redesign-design.md @@ -0,0 +1,158 @@ +# 쟁승메이드 라이트 고craft 재설계 — 설계 문서 + +> 작성 2026-06-30 · brainstorming 산출물 (승인 완료) +> 대상: `app/page.tsx`(홈) · `app/outsourcing/page.tsx` · `app/products/page.tsx` + 공통 시스템 + +--- + +## 1. 배경 / 문제 정의 + +최근 "Deep Field" 다크 캔버스 재스킨이 검증 없이 얹히면서 다음 문제가 발생했다. + +1. **문서 ↔ 코드 충돌** — `CLAUDE.md` 가드레일(라이트·gradient/blur/보라 금지·`--jsm-*`)을 실제 메인/외주 코드가 정면으로 위반(다크 배경 + WebGL 파티클 + radial gradient + 보라 팔레트). +2. **반복된 사후 패치** — 최근 커밋 2개가 전부 "히어로 텍스트 대비 복구" 류 → 다크 파티클 히어로가 픽셀 단위 튜닝에 실패. +3. **톤 단절** — 홈·외주는 다크, `/products`는 라이트. 첫 클릭에서 톤이 깨진다. +4. **가짜 포트폴리오** — 쇼케이스 8슬롯이 실작업 이미지가 아닌 그래디언트 타일(보라 포함). "AI가 뽑은 가짜" 인상. +5. **사이트 정체성 누락** — CLAUDE.md가 규정한 "외주+완성SW 2축" 소개가 홈에 없고 바로 쇼케이스로 점프. +6. **죽은 CSS** — `kx-*`(blur), `gradient-text`(보라), `kx-orb/glow`, `--jsm-dark-*`, `--kx-*` 잔존. + +### 타깃·포지셔닝 (의사결정 근거) +- 고객: 크몽·숨고·위시캣 트래픽 = 다수가 비개발자 소상공인/실무자. +- 무기: "실서비스 15+ 직접 운영"이라는 **운영 실증** (경력 어필 금지 — `feedback_copy_no_career`). +- 결론: 다크 스펙터클이 아니라 **라이트·명료 + 진짜 목업**이 신뢰·전환에 유리. + +--- + +## 2. 확정된 방향 (승인됨) + +| 결정 | 값 | +|------|-----| +| 비주얼 방향 | 라이트 기반 고(高)craft + 강조면 1곳 | +| 강조면 위치 | **히어로의 코드 제품 목업** (운영 실증을 이미지로) | +| 소재 확보 | **코드로 디자인한 UI 목업** (실데이터 0, `--jsm-*` 라이트/navy) | +| 범위 | 홈 + 외주 + 제품 3면 통일 + 공통 시스템 정리 | +| 가드레일 | 라이트 복귀 = **CLAUDE.md 컴플라이언스 회복** (개정 불필요, 다크 토큰 언급만 정리) | + +--- + +## 3. 디자인 시스템 기반 (3면 공통) + +### 색 (─ `--jsm-*` 만) +``` +bg #f8fafc · surface #fff · surface-alt #f1f5f9 +ink #0f172a · ink-soft #475569 · ink-faint #94a3b8 · line #e2e8f0 +navy #0b1f3a (푸터 + CTA 1곳, 사이트 유일 다크면) +accent #1d4ed8 (유일 포인트) · accent-hover #1e40af · accent-soft #dbeafe +금지: 보라/violet · gradient · blur (navy CTA 밴드도 평면 navy로 — radial 광원 제거) +``` + +### 타이포 (Pretendard) +| 역할 | 스펙 | +|------|------| +| 디스플레이 h1 | `clamp(2.4rem, 7vw, 4rem)` · w800 · `-0.03em` · `break-keep` · lh 1.08 | +| 섹션 h2 | `clamp(1.7rem, 4vw, 2.4rem)` · w700 · `-0.02em` | +| 모노 라벨(eyebrow) | 11px · UPPER · `0.2em` · accent — 편집 디자인 시그니처 | +| 본문 | 16–18px · ink-soft · `-0.01em` · leading-relaxed | + +### 레이아웃·여백·리듬 +- 컨테이너 `max-w-6xl`(1152) · 패딩 `px-6 lg:px-8`. **3면 동일** (현재 제품은 max-w-5xl로 어긋남 → 통일). +- **여백 변주**: 현재 전부 `py-24/32` 단조 → 히어로 큰 호흡, 이후 섹션 `py-20 / py-24 / py-28`로 리듬. +- **교차 배경**: `surface`(#fff) ↔ `surface-alt`(#f1f5f9) 교차로 섹션 구분. `border-t` 단독 의존 탈피. +- 카드: `rounded-2xl` · `border line` · `shadow-sm` · hover `translateY(-2px)` + border accent. + +### 모션 +- `ScrollReveal`(fade+rise) 유지. `prefers-reduced-motion` 가드(기존 `.reveal` CSS 활용). 절제. +- `CountUp` 유지 (운영 실증 스탯). + +--- + +## 4. 핵심 신규 컴포넌트 — `MockWindow` 목업 시스템 + +파티클(HeroField)을 대체하는 craft의 핵심. **재사용 가능한 라이트 UI 목업.** + +``` +app/components/mock/ + MockWindow.tsx 브라우저/앱 크롬 프레임 (● ● ● 신호등 + 타이틀바 + 본문 슬롯) + screens/ + DashboardMock 스탯 카드 3 + 막대/라인 차트 (주식 리포트 톤) + FeedMock 텔레그램풍 메시지 피드 (봇 알림) + MatchMock 매물/항목 카드 + 매칭률 배지 (부동산 청약) + CommerceMock 상품 그리드 + 장바구니/가격 + SiteMock 기업 사이트 히어로 와이어 (corporate/portfolio/editorial) + BookingMock 예약 캘린더/슬롯 (로컬 매장) +``` +- 전부 SVG/CSS, `--jsm-*` 라이트 + navy 헤더, accent 포인트. **실데이터 없음.** +- 결정적 렌더(난수 시드 불필요 — 정적 마크업). SSR-safe(클라이언트 캔버스 의존 제거 → 서버 컴포넌트로 렌더 가능). +- 용도: **히어로 1개**(대표 = DashboardMock/FeedMock) + **쇼케이스 N개**. + +--- + +## 5. 페이지별 설계 + +### 5.1 홈 `/` +섹션 순서 (배경 교차 표기): +1. **HERO** (surface) — 비대칭 2단: 좌 텍스트(eyebrow·h1·sub·CTA 2) / 우 `MockWindow`(대표 목업). 하단 **신뢰 스트립**(15+ 실서비스 · 24/7 · 원스톱). +2. **2축 소개** (surface-alt) — 신규. `01 OUTSOURCING` / `02 SOFTWARE` 2카드. 사이트 정체성 복원. +3. **SHOWCASE** (surface) — `ShowcaseGrid` 재작성: 그래디언트 타일 → `MockWindow` 그리드. 홈 6장. +4. **운영 실증** (surface-alt) — 3종 카드 + 스탯(CountUp 15+/24·7/원스톱). 라이트 카드 통일. +5. **PROCESS** (surface) — 4단계 + 가로 연결선. +6. **완성 SW** (surface-alt) — featured 3종(DB, `getListedProducts`). 0개면 coming-soon 폴백(라이트). +7. **CTA 밴드** (navy) — 사이트 유일 다크면. 평면 navy(radial gradient 제거). "프로젝트, 이야기부터". + +삭제: `HeroField` 사용, 좌측 스크림/비네트, `-mt-16` 다크 풀블리드 트릭(라이트라 불필요). + +### 5.2 외주 `/outsourcing` — 다크→라이트 전환 (구조 유지) +``` +HERO(라이트, 소형 MockWindow 1개) → 제공 분야 6 → 운영 실사례 6(라이트 카드) +→ SHOWCASE 풀그리드 8(MockWindow) → PROCESS 6단계 → FAQ(아코디언) → 의뢰 폼 +``` +- 의뢰 폼: 라이트 스킨. `.jsm-dark-form` placeholder 규칙 제거/라이트화. `OutsourcingRequestForm` 입력 가독성 복구. +- 앵커(`#showcase`/`#portfolio`/`#process`/`#contact`) 유지. + +### 5.3 제품 `/products` — 이미 라이트, craft 격상 +``` +HERO → 카탈로그(2열 카드) → 구매방식 3단계 → CTA +``` +- `max-w-5xl` → `max-w-6xl`, 타입 스케일·여백을 홈과 동일 언어로 정렬. +- 카드 hover·라운드·그림자를 공통 카드 스펙에 맞춤. + +--- + +## 6. 공통 셸 + +- **TopNav** — "다크 인지형" 라우트 분기 제거 → 단일 라이트 네비(흰 배경 + 하단 line, 스크롤 시 미세 shadow) + 우측 `프로젝트 문의` CTA. (구현 시 현 코드 확인 후 최소 수정.) +- **Footer** — navy 유지. 사이트 유일 다크면(CTA 밴드와 함께). + +--- + +## 7. 정리·마이그레이션 + +- `app/globals.css`: + - 제거: `--jsm-dark-*` 토큰, `--kx-*` 매핑, `.kx-*`(glass/orb/glow/folder/...), `.gradient-text`(보라), `.kx-gradient-text`, `.jsm-dark-form`, `.df-scroll-dot`(파티클 전용). + - 유지: `--jsm-*` 라이트, `.reveal*`, `.marquee*`(사용처 확인 후), 스크롤바, `.scrollbar-hide`. +- `lib/showcase.ts`: `palette/accent` 그래디언트 스펙 → **목업 타입 스펙**(`mock: 'dashboard'|'commerce'|...`)으로 교체. 보라 4슬롯 제거/치환. +- `app/components/deepfield/`: + - `ShowcaseCard.tsx` → `MockWindow` 기반으로 재작성(또는 `app/components/mock/`로 이전). + - `ShowcaseGrid.tsx` 유지(레이아웃 로직) — 카드만 교체. + - `HeroField.tsx`·`useFieldMode.ts` — 홈/외주에서 import 제거. 파일은 보존만(미사용). three 의존 트리셰이킹 확인. +- `CLAUDE.md`: 디자인 시스템 섹션에서 다크 토큰 언급 정리(가드레일 본문은 이미 라이트 → 변경 불필요). + +--- + +## 8. 비목표 (YAGNI) +- 다크 모드 토글/테마 시스템 (불필요). +- 실제 스크린샷 수집·마스킹 파이프라인 (코드 목업으로 대체). +- admin/mypage/legal 등 비공개·내부 페이지 재설계 (이번 범위 밖 — 이미 라이트). +- 카피 전면 재작성 (기존 카피 유지, 구조·톤만 변경. 단 경력 어필 카피는 금지 유지). + +--- + +## 9. 검증 기준 +- [ ] 3면 모두 라이트 `--jsm-*`만 사용, 다크 토큰/보라/blur/임의 색 0건 (grep). +- [ ] 홈→외주→제품 클릭 시 톤 단절 없음. +- [ ] 쇼케이스가 코드 목업(실화면 느낌)으로 렌더, 그래디언트 타일 0건. +- [ ] 홈에 "2축 소개" 섹션 존재. +- [ ] 의뢰 폼 입력 텍스트·placeholder 가독성 정상(라이트). +- [ ] `npm run build` 성공 + `npm test`(lib 단위) 통과. +- [ ] 죽은 CSS(`kx-*`/`gradient-text` 등) 제거 확인. +- [ ] `prefers-reduced-motion` 시 모션 정지. From b2bd7b1b310876e91d20c3b941d4b4e7ac0c854d Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 30 Jun 2026 14:35:40 +0900 Subject: [PATCH 02/10] =?UTF-8?q?docs(redesign):=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=AC=EC=84=A4=EA=B3=84=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EA=B3=84=ED=9A=8D=20(7=20Task)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MockWindow 목업 시스템 → 쇼케이스 전환 → TopNav 단일화 → 홈/외주/제품 3면 라이트 재작성 → 죽은 CSS 제거·검증. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01A2N6SziVSPfavx1j5rAs52 --- .../plans/2026-06-30-jsm-light-redesign.md | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-30-jsm-light-redesign.md diff --git a/docs/superpowers/plans/2026-06-30-jsm-light-redesign.md b/docs/superpowers/plans/2026-06-30-jsm-light-redesign.md new file mode 100644 index 0000000..3556f74 --- /dev/null +++ b/docs/superpowers/plans/2026-06-30-jsm-light-redesign.md @@ -0,0 +1,235 @@ +# 쟁승메이드 라이트 고craft 재설계 — 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:** 홈·외주·제품 3면을 라이트 `--jsm-*` 단일 시스템으로 통일하고, 히어로·쇼케이스를 코드 UI 목업(MockWindow)으로 재구성한다. + +**Architecture:** 파티클(HeroField)·다크 토큰을 폐기하고, 재사용 가능한 라이트 `MockWindow` 목업 시스템을 craft의 핵심 비주얼로 삼는다. 3면이 동일한 컨테이너·타입 스케일·여백 리듬·카드 스펙을 공유한다. TopNav의 다크 라우트 분기를 제거해 전 페이지 단일 라이트 셸로 통일한다. + +**Tech Stack:** Next.js 16 (App Router, 서버 컴포넌트 우선), TypeScript, Tailwind v4, Pretendard, vitest. + +설계 문서: `docs/superpowers/specs/2026-06-30-jsm-light-redesign-design.md` + +## Global Constraints + +- 색: `--jsm-*` 라이트 토큰만. **금지** — `--jsm-dark-*`, `--kx-*`, 보라/violet, gradient, blur, 이모지. +- navy(`--jsm-navy`)는 푸터 + 홈 CTA 밴드 **2곳에서만** (평면, radial 없음). +- 컨테이너: `max-w-6xl mx-auto px-6 lg:px-8` (3면 동일). +- 한글: 헤딩·본문 `break-keep`. `KOR_TIGHT = letterSpacing -0.02em`, `KOR_BODY = -0.01em`. +- 타이포: h1 `clamp(2.4rem,7vw,4rem)` w800 -0.03em / h2 `clamp(1.7rem,4vw,2.4rem)` w700 -0.02em / eyebrow 11px UPPER 0.2em accent / 본문 16–18px ink-soft. +- 카피: 경력 어필("대기업 7년차" 류) 금지 → 운영 실증 표현 유지. +- 모션: `ScrollReveal`·`.reveal` CSS 유지, `prefers-reduced-motion` 가드. +- 각 Task 종료 시 `npm run build` 통과 + 커밋. 브랜치 `redesign/jsm-light-craft` (생성됨). +- 빌드 명령(Windows): `npm run build`. 테스트: `npm test`. + +> **계획 altitude 주석:** 본 계획은 *재사용 빌딩블록*(MockWindow API·showcase 타입·테스트)은 완전한 코드로, *페이지 재작성*은 섹션 구조 + 정확한 토큰/클래스 규약 + 검증 게이트로 명세한다. 페이지 JSX 전문을 계획에 박지 않는 것은 의도된 결정이다(시각 레이아웃은 토큰·구조 제약으로 충분히 결정되며, 전문 박제는 중복·열화를 유발). + +--- + +### Task 1: MockWindow 목업 시스템 + +**Files:** +- Create: `app/components/mock/MockWindow.tsx` +- Create: `app/components/mock/screens.tsx` (6 스크린 목업 한 파일 — 함께 변경되므로 동거) +- Create: `app/components/mock/registry.ts` (mock key → 컴포넌트 + 메타) + +**Interfaces:** +- Produces: + - `MockWindow({ title, accent?, children, className? }): JSX` — 브라우저 크롬 프레임(● ● ● 신호등 + 타이틀바 + 본문 슬롯). 서버 컴포넌트. 라이트(surface) + navy 타이틀바 옵션. + - 스크린 컴포넌트(전부 서버, props 없음, 정적 마크업): `DashboardMock`, `FeedMock`, `MatchMock`, `CommerceMock`, `SiteMock`, `BookingMock`. + - `MOCK_REGISTRY: Record` 및 `type MockKey = 'dashboard'|'feed'|'match'|'commerce'|'site'|'booking'`. + +**MockWindow 규약 (완전 코드):** +```tsx +// app/components/mock/MockWindow.tsx +interface MockWindowProps { + title: string; // 타이틀바 텍스트 (예: 'stock-report', 'realestate-match') + children: React.ReactNode; + className?: string; +} +export default function MockWindow({ title, children, className }: MockWindowProps) { + return ( +
+ {/* 타이틀바 */} +
+ + + + + + + {title} + +
+ {/* 본문 */} +
{children}
+
+ ); +} +``` + +**스크린 목업 시각 명세** (`screens.tsx` — 각 컴포넌트가 그릴 요소; 전부 `--jsm-*`, SVG/div, 실데이터 0): +- `DashboardMock` — 상단 스탯 3칸(라벨+숫자, 1칸 accent 강조) + 막대 차트(div 높이 배열) 1개. "주식 리포트" 톤. +- `FeedMock` — 메시지 버블 3~4개(좌측 정렬, 시각·텍스트·체결/알림 배지). "텔레그램 봇" 톤. +- `MatchMock` — 리스트 행 3개(항목명 + 매칭률 배지 `92%` accent-soft) + 상단 필터칩. "부동산 청약" 톤. +- `CommerceMock` — 상품 카드 그리드 4(썸네일 박스 + 가격) + 장바구니 바. +- `SiteMock` — 기업 사이트 와이어(네비 바 + 큰 헤드라인 라인 2 + CTA 버튼 + 카드 3). "corporate/portfolio". +- `BookingMock` — 주간 캘린더 헤더(요일 7) + 슬롯 그리드(일부 accent 채움) + 예약 버튼. + +**Steps:** +- [ ] **Step 1:** `MockWindow.tsx` 작성 (위 완전 코드). +- [ ] **Step 2:** `screens.tsx`에 6개 스크린 컴포넌트 작성 (위 시각 명세 따름, 각 `
...` 라이트 마크업). +- [ ] **Step 3:** `registry.ts` 작성 — `MockKey` 타입 + `MOCK_REGISTRY` 매핑 export. +- [ ] **Step 4:** 빌드 검증. Run: `npm run build` — Expected: 성공(타입 에러 0). +- [ ] **Step 5:** 커밋. `git add app/components/mock && git commit -m "feat(redesign): MockWindow 라이트 목업 시스템(프레임+6스크린+레지스트리)"` + +--- + +### Task 2: 쇼케이스 라이트 전환 + +**Files:** +- Modify: `lib/showcase.ts` (슬롯 타입을 mock 기반으로 교체) +- Modify: `app/components/deepfield/ShowcaseCard.tsx` (그래디언트/캔버스 → MockWindow 라이트 카드 재작성) +- Keep: `app/components/deepfield/ShowcaseGrid.tsx` (레이아웃 로직 유지, 카드만 교체) +- Test: `lib/__tests__/showcase.test.ts` (신규 — 가드레일 데이터 테스트) + +**Interfaces:** +- Consumes: Task 1의 `MockKey`, `MOCK_REGISTRY`. +- Produces: `ShowcaseSlot { slug; label; title; desc; mock: MockKey; href? }` (palette/accent 제거). `SHOWCASE_SLOTS: ShowcaseSlot[]` (8슬롯, 보라 0). + +**신규 슬롯 매핑** (보라 제거, mock 배정): +``` +corporate → site | commerce → commerce | dashboard → dashboard | bakery → booking +portfolio → site | game → site | interior → site | reading → site +``` +> 메모: site 목업이 다수 → 시각 단조 방지 위해 `SiteMock`에 variant prop(헤드라인 색/레이아웃 미세 차이) 추가 가능(선택). 1차는 단일 SiteMock로 진행, Task 7 검증 시 단조하면 variant 보강. + +**Steps:** +- [ ] **Step 1 (테스트 먼저):** `lib/__tests__/showcase.test.ts` 작성 — 각 슬롯이 (a) `mock`이 유효한 MockKey, (b) `slug/title/desc` 비어있지 않음, (c) 어떤 필드에도 보라 hex(`#c4b5fd`,`#f0abfc`,`#341a4f`,`#4a1342`) 부재. (palette 필드 자체가 사라지므로 타입+값 검증.) +- [ ] **Step 2:** Run `npm test` — Expected: FAIL (showcase 타입에 mock 없음 / palette 잔존). +- [ ] **Step 3:** `lib/showcase.ts` 인터페이스·데이터를 mock 기반으로 교체. +- [ ] **Step 4:** `ShowcaseCard.tsx` 재작성 — 카드 = `MockWindow`(상단) + 하단 텍스트(eyebrow label·title·desc, href면 "데모 보기"). 캔버스/시드/그래디언트/보라 전량 제거. 라이트 카드. `'use client'` 불필요면 서버 컴포넌트로. +- [ ] **Step 5:** Run `npm test` — Expected: PASS. 이어서 `npm run build` — Expected: 성공. +- [ ] **Step 6:** 커밋. `git commit -am "feat(redesign): 쇼케이스 그래디언트 타일 → 라이트 MockWindow 카드 + 가드레일 테스트"` + +--- + +### Task 3: TopNav 라이트 단일화 + +**Files:** +- Modify: `app/components/TopNav.tsx` + +**Interfaces:** +- Consumes: 없음. Produces: 단일 라이트 네비(전 라우트 동일). + +**변경:** +- `DARK_ROUTES`/`isDark` 분기 + 다크 팔레트 헬퍼(`ink/inkSoft/surface/line/accent/accentBg`의 isDark 삼항) 전량 제거 → 라이트 고정값. +- 최상단(미스크롤): 배경 transparent 유지(라이트 히어로 위 dark ink 텍스트로 가독) / 스크롤 시: `--jsm-surface` + `--jsm-line` border + 미세 shadow. +- 모바일 드로어 `surface` = `--jsm-surface` 고정. + +**Steps:** +- [ ] **Step 1:** `isDark` 및 다크 분기 제거, 팔레트를 라이트 토큰 고정으로 치환. +- [ ] **Step 2:** Run `npm run build` — Expected: 성공. +- [ ] **Step 3:** 커밋. `git commit -am "feat(redesign): TopNav 다크 라우트 분기 제거 → 단일 라이트 네비"` + +--- + +### Task 4: 홈 라이트 재구성 (`app/page.tsx`) + +**Files:** +- Modify: `app/page.tsx` (전면 재작성) + +**Interfaces:** +- Consumes: Task 1 `MockWindow`+스크린, Task 2 `ShowcaseGrid`/`SHOWCASE_SLOTS`, 기존 `getListedProducts`·`CountUp`·`ScrollReveal`. + +**섹션 구조(배경 교차):** +1. HERO (surface) — 비대칭 2단: 좌(eyebrow `OUTSOURCING · SOFTWARE` / h1 "생각을 / 동작하는 소프트웨어로." / sub / CTA 2개: filled accent `프로젝트 문의`→`/outsourcing#contact`, ghost `소프트웨어 보기`→`/products`) · 우(`MockWindow title="stock-report"` 안에 `DashboardMock`). `-mt-16`/스크림/HeroField 전량 제거. 하단 신뢰 스트립(15+ 실서비스 · 24/7 · 원스톱) border-y row. +2. 2축 소개 (surface-alt) — `01 OUTSOURCING`/`02 SOFTWARE` 2카드(라벨·제목·요약·링크). +3. SHOWCASE (surface) — `ShowcaseGrid slots variant="home"` (6). +4. 운영 실증 (surface-alt) — PROOF 3카드 + 스탯(CountUp 15+/24·7/원스톱). 라이트 카드. +5. PROCESS (surface) — 4단계 + 가로 연결선. +6. 완성 SW (surface-alt) — featured 3(DB) / 0개 coming-soon 폴백, 라이트 카드. +7. CTA 밴드 (navy 평면) — "프로젝트, 이야기부터 시작하세요" + 흰 버튼. + +**Steps:** +- [ ] **Step 1:** 다크 래퍼/HeroField/스크림 제거, 위 7섹션을 라이트 토큰으로 재작성. 모든 `--jsm-dark-*`/`accent-bright` → 라이트 대응(`--jsm-ink`/`ink-soft`/`accent`). +- [ ] **Step 2:** Run `npm run build` — Expected: 성공. (DB 0개 폴백 경로도 타입 통과 확인.) +- [ ] **Step 3:** 커밋. `git commit -am "feat(redesign): 홈 라이트 재구성 + 2축 복원 + 히어로 목업"` + +--- + +### Task 5: 외주 라이트 전환 (`app/outsourcing/page.tsx` + 폼) + +**Files:** +- Modify: `app/outsourcing/page.tsx` +- Modify: `app/components/OutsourcingRequestForm.tsx` + +**Interfaces:** +- Consumes: Task 1·2 컴포넌트, 기존 `ScrollReveal`. + +**변경:** +- 페이지: 다크 래퍼/HeroField/스크림 제거. 섹션 구조 유지(HERO·SHOWCASE 8·운영 실사례 6·제공분야 6·PROCESS 6·FAQ·CONTACT)를 라이트 토큰으로. 앵커(`#showcase`/`#portfolio`/`#process`/`#contact`) 유지. HERO 우측에 소형 `MockWindow`(`FeedMock` 등) 1개 추가(선택, 2단 비대칭). +- 폼: `INPUT_STYLE`·각 `--jsm-dark-*`/`accent-bright`/`rgba(96,165,250,..)` → 라이트(`--jsm-surface`/`--jsm-line`/`--jsm-ink`/`--jsm-accent`/`--jsm-accent-soft`). 래퍼 `className="jsm-dark-form"` 제거. 에러 박스(이미 라이트 `#fef2f2`)는 유지. + +**Steps:** +- [ ] **Step 1:** `OutsourcingRequestForm.tsx`의 다크 토큰 전량 라이트 치환 + `jsm-dark-form` 제거. +- [ ] **Step 2:** `outsourcing/page.tsx` 라이트 재작성(구조 유지). +- [ ] **Step 3:** Run `npm run build` — Expected: 성공. +- [ ] **Step 4:** 커밋. `git commit -am "feat(redesign): 외주 페이지 + 의뢰폼 라이트 전환"` + +--- + +### Task 6: 제품 craft 정렬 (`app/products/page.tsx`) + +**Files:** +- Modify: `app/products/page.tsx` + +**변경:** 이미 라이트 → `max-w-5xl`→`max-w-6xl`, 타입 스케일(h1 clamp·eyebrow·h2)·여백 리듬·카드(rounded-2xl·shadow-sm·hover) 를 홈과 동일 언어로 정렬. 교차 배경(surface↔surface-alt) 적용. 구조·카피 유지. + +**Steps:** +- [ ] **Step 1:** 컨테이너·타입·카드 스펙을 공통 언어로 정렬. +- [ ] **Step 2:** Run `npm run build` — Expected: 성공. +- [ ] **Step 3:** 커밋. `git commit -am "feat(redesign): 제품 페이지 craft 정렬(공통 언어)"` + +--- + +### Task 7: 죽은 CSS 제거 + 전체 검증 + 문서 정리 + +**Files:** +- Modify: `app/globals.css` +- Modify: `CLAUDE.md` (다크 토큰 언급 정리 — 가드레일 본문 변경 없음) + +**변경 (globals.css 제거 대상):** `--jsm-dark-*` 토큰, `--kx-*` 매핑, `.kx-section/.kx-display/.kx-label/.kx-folder/.kx-glass/.kx-glow/.kx-btn-*/.kx-gradient-text/.kx-orb`, `.gradient-text`(보라), `.jsm-dark-form`, `.df-scroll-dot` + `@keyframes df-scroll-cue`. **유지:** `--jsm-*` 라이트, `@font-face`, `.reveal*`, `.marquee*`(사용처 grep 후 미사용이면 제거), 스크롤바, `.scrollbar-hide`, `.service-card`. + +**Steps:** +- [ ] **Step 1:** `HeroField`/`useFieldMode` import 잔존 grep — Run: `grep -rn "HeroField\|useFieldMode\|jsm-dark\|--kx-\|gradient-text" app lib` — Expected: 코드(컴포넌트 파일 제외)에서 0건. 잔존 시 해당 파일 수정. +- [ ] **Step 2:** `globals.css`에서 위 제거 대상 삭제. +- [ ] **Step 3:** 가드레일 grep — Run: `grep -rn "jsm-dark\|--kx-\|#7c3aed\|#c4b5fd\|#f0abfc\|backdrop-filter\|blur(" app lib` — Expected: 0건(`globals.css` `.kx`/dark 제거 후). +- [ ] **Step 4:** Run `npm test` — Expected: PASS. 이어서 `npm run build` — Expected: 성공. +- [ ] **Step 5:** `CLAUDE.md` 디자인 시스템 섹션에서 다크 토큰 잔재 언급 정리(있다면). +- [ ] **Step 6:** 커밋. `git commit -am "chore(redesign): 죽은 다크/kx/보라 CSS 제거 + 가드레일 검증 통과"` + +--- + +## Self-Review + +**Spec coverage:** +- §3 시스템 기반 → Global Constraints + 각 Task. ✓ +- §4 MockWindow → Task 1. ✓ +- §5.1 홈 → Task 4. ✓ / §5.2 외주 → Task 5. ✓ / §5.3 제품 → Task 6. ✓ +- §6 셸(TopNav/Footer) → Task 3 (Footer는 이미 navy 유지, 변경 없음 명시). ✓ +- §7 정리 → Task 7. ✓ +- §9 검증 기준 → Task 2(테스트)·Task 7(grep/build/test). ✓ + +**Placeholder scan:** 페이지 JSX 전문 미기재는 의도(계획 altitude 주석). 스크린 목업은 시각 명세로 구체화. 빌딩블록(MockWindow)·테스트는 완전 코드. TBD 없음. + +**Type consistency:** `MockKey`/`MOCK_REGISTRY`(Task1) → `ShowcaseSlot.mock`(Task2)에서 동일 사용. `ShowcaseGrid`의 `variant`/`size`(home|full / feature|standard) 기존 시그니처 유지. ✓ From c1afb58bcd1cbd4f44b410daedac6494e45f82d0 Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 30 Jun 2026 14:38:07 +0900 Subject: [PATCH 03/10] =?UTF-8?q?feat(redesign):=20MockWindow=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EB=AA=A9=EC=97=85=20=EC=8B=9C=EC=8A=A4?= =?UTF-8?q?=ED=85=9C(=ED=94=84=EB=A0=88=EC=9E=84+6=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=B0+=EB=A0=88=EC=A7=80=EC=8A=A4=ED=8A=B8=EB=A6=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 파티클 대체 craft 핵심. 실데이터 0, --jsm-* 라이트 토큰만. dashboard/feed/match/commerce/site/booking 6종 + 레지스트리. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01A2N6SziVSPfavx1j5rAs52 --- app/components/mock/MockWindow.tsx | 51 ++++++ app/components/mock/registry.ts | 30 ++++ app/components/mock/screens.tsx | 250 +++++++++++++++++++++++++++++ 3 files changed, 331 insertions(+) create mode 100644 app/components/mock/MockWindow.tsx create mode 100644 app/components/mock/registry.ts create mode 100644 app/components/mock/screens.tsx diff --git a/app/components/mock/MockWindow.tsx b/app/components/mock/MockWindow.tsx new file mode 100644 index 0000000..7d40c5b --- /dev/null +++ b/app/components/mock/MockWindow.tsx @@ -0,0 +1,51 @@ +// 라이트 UI 목업의 공용 크롬 프레임 (서버 컴포넌트). +// 실데이터 없이 "운영 중인 화면" 인상을 주는 craft 요소. --jsm-* 토큰만 사용. +import type { ReactNode } from 'react'; + +interface MockWindowProps { + /** 타이틀바 텍스트 — 파일/서비스명 느낌 (예: 'stock-report', 'realestate-match') */ + title: string; + children: ReactNode; + className?: string; +} + +export default function MockWindow({ title, children, className }: MockWindowProps) { + return ( +
+ {/* 타이틀바 — 신호등 + 모노 파일명 + 라이브 점 */} +
+ + + + + + + {title} + + + + + live + + +
+ {/* 본문 슬롯 */} +
{children}
+
+ ); +} diff --git a/app/components/mock/registry.ts b/app/components/mock/registry.ts new file mode 100644 index 0000000..745a46a --- /dev/null +++ b/app/components/mock/registry.ts @@ -0,0 +1,30 @@ +// 목업 스크린 레지스트리 — showcase 슬롯의 mock 키를 컴포넌트로 해석. +import type { ComponentType } from 'react'; + +import { + DashboardMock, + FeedMock, + MatchMock, + CommerceMock, + SiteMock, + BookingMock, +} from './screens'; + +export type MockKey = + | 'dashboard' + | 'feed' + | 'match' + | 'commerce' + | 'site' + | 'booking'; + +export const MOCK_REGISTRY: Record = { + dashboard: DashboardMock, + feed: FeedMock, + match: MatchMock, + commerce: CommerceMock, + site: SiteMock, + booking: BookingMock, +}; + +export const MOCK_KEYS = Object.keys(MOCK_REGISTRY) as MockKey[]; diff --git a/app/components/mock/screens.tsx b/app/components/mock/screens.tsx new file mode 100644 index 0000000..73a0f91 --- /dev/null +++ b/app/components/mock/screens.tsx @@ -0,0 +1,250 @@ +// 라이트 UI 목업 스크린 6종 (서버 컴포넌트, props 없음, 정적 마크업). +// MockWindow 본문에 들어가 "운영 중인 화면" 인상을 만든다. 실데이터 0, --jsm-* 만. + +const ACCENT = 'var(--jsm-accent)'; +const INK = 'var(--jsm-ink)'; +const SOFT = 'var(--jsm-ink-soft)'; +const FAINT = 'var(--jsm-ink-faint)'; +const LINE = 'var(--jsm-line)'; +const ALT = 'var(--jsm-surface-alt)'; +const SOFTBG = 'var(--jsm-accent-soft)'; + +/** 1. 대시보드 — 주식 리포트 톤: 스탯 3 + 막대 차트 */ +export function DashboardMock() { + const bars = [38, 54, 30, 62, 46, 72, 58]; + return ( +
+
+
+

+ 오늘 손익 +

+

+ +2.4% +

+
+
+

+ 체결 +

+

+ 12건 +

+
+
+

+ 승률 +

+

+ 68% +

+
+
+
+ {bars.map((h, i) => ( + + ))} +
+
+ ); +} + +/** 2. 피드 — 텔레그램 봇 톤: 메시지 버블 3 */ +export function FeedMock() { + const rows = [ + { t: '09:01', m: '매수 체결 · 삼성전자 12,400', tag: '체결', on: true }, + { t: '11:24', m: '목표가 도달 — 익절 알림', tag: '알림', on: false }, + { t: '15:30', m: '일일 손익 리포트 전송 완료', tag: '리포트', on: false }, + ]; + return ( +
+ {rows.map((r) => ( +
+ + {r.t} + +

+ {r.m} +

+ + {r.tag} + +
+ ))} +
+ ); +} + +/** 3. 매칭 — 부동산 청약 톤: 필터칩 + 매칭률 리스트 3 */ +export function MatchMock() { + const chips = ['강남구', '85㎡↑', '신축']; + const rows = [ + { n: '래미안 원베일리', s: '92%' }, + { n: '디에이치 퍼스티어', s: '88%' }, + { n: '아크로 포레스트', s: '81%' }, + ]; + return ( +
+
+ {chips.map((c, i) => ( + + {c} + + ))} +
+
+ {rows.map((r) => ( +
+ + {r.n} + + + {r.s} + +
+ ))} +
+
+ ); +} + +/** 4. 커머스 — 상품 그리드 4 + 장바구니 바 */ +export function CommerceMock() { + const items = [ + { p: '₩28,000' }, + { p: '₩45,000' }, + { p: '₩19,000' }, + { p: '₩36,000' }, + ]; + return ( +
+
+ {items.map((it, i) => ( +
+
+

+ {it.p} +

+
+ ))} +
+
+ 장바구니 3 · ₩128,000 + + 결제 + +
+
+ ); +} + +/** 5. 사이트 — 기업/포트폴리오 와이어: 네비 + 헤드라인 + 카드 3 */ +export function SiteMock() { + return ( +
+
+ +
+ + + +
+ +
+
+ + +
+
+ {[0, 1, 2].map((i) => ( +
+
+ +
+ ))} +
+
+ ); +} + +/** 6. 예약 — 로컬 매장 톤: 주간 캘린더 + 슬롯 그리드 */ +export function BookingMock() { + const days = ['월', '화', '수', '목', '금', '토', '일']; + // 0=빈 1=예약됨(accent) 2=불가(alt) + const slots = [ + 1, 0, 0, 1, 0, 2, 2, + 0, 1, 0, 0, 1, 1, 2, + 0, 0, 1, 0, 0, 1, 0, + ]; + return ( +
+
+ {days.map((d) => ( + + {d} + + ))} +
+
+ {slots.map((s, i) => ( + + ))} +
+
+ 예약 확정 · 금 19:00 +
+
+ ); +} From 989cc254650b4274b5fa48854f4f359baee83eab Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 30 Jun 2026 14:40:56 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat(redesign):=20=EC=87=BC=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=EA=B7=B8=EB=9E=98=EB=94=94=EC=96=B8?= =?UTF-8?q?=ED=8A=B8=20=ED=83=80=EC=9D=BC=20=E2=86=92=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20MockWindow=20=EC=B9=B4=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lib/showcase.ts를 mock 키 기반으로 교체(보라 4슬롯 제거, 목업 6종 다양화). ShowcaseCard 캔버스/시드/그래디언트 제거 → surface-alt 스테이지 + 흰 MockWindow. 키 목록을 JSX-free keys.ts로 분리해 vitest 가드레일 테스트 추가. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01A2N6SziVSPfavx1j5rAs52 --- app/components/deepfield/ShowcaseCard.tsx | 315 ++-------------------- app/components/mock/keys.ts | 17 ++ app/components/mock/registry.ts | 12 +- lib/__tests__/showcase.test.ts | 40 +++ lib/showcase.ts | 33 +-- 5 files changed, 105 insertions(+), 312 deletions(-) create mode 100644 app/components/mock/keys.ts create mode 100644 lib/__tests__/showcase.test.ts diff --git a/app/components/deepfield/ShowcaseCard.tsx b/app/components/deepfield/ShowcaseCard.tsx index 6f98825..0043c75 100644 --- a/app/components/deepfield/ShowcaseCard.tsx +++ b/app/components/deepfield/ShowcaseCard.tsx @@ -1,9 +1,8 @@ -'use client'; - import Link from 'next/link'; -import { useEffect, useRef, useState } from 'react'; import type { ShowcaseSlot } from '@/lib/showcase'; +import MockWindow from '@/app/components/mock/MockWindow'; +import { MOCK_REGISTRY } from '@/app/components/mock/registry'; interface Props { slot: ShowcaseSlot; @@ -11,316 +10,58 @@ interface Props { index: number; } -// ───────────────────────── 시드 PRNG (결정적) ───────────────────────── - -/** slug → 32bit 정수 시드 (cyrb-ish 해시) */ -function hashSlug(s: string): number { - let h = 2166136261 >>> 0; - for (let i = 0; i < s.length; i++) { - h ^= s.charCodeAt(i); - h = Math.imul(h, 16777619); - } - return h >>> 0; -} - -/** mulberry32 — 시드 하나로 결정적 난수열 생성 */ -function mulberry32(seed: number): () => number { - let a = seed >>> 0; - return () => { - a |= 0; - a = (a + 0x6d2b79f5) | 0; - let t = Math.imul(a ^ (a >>> 15), 1 | a); - t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; - return ((t ^ (t >>> 14)) >>> 0) / 4294967296; - }; -} - -/** #rrggbb → {r,g,b} */ -function hexToRgb(hex: string): { r: number; g: number; b: number } { - const h = hex.replace('#', ''); - return { - r: parseInt(h.slice(0, 2), 16), - g: parseInt(h.slice(2, 4), 16), - b: parseInt(h.slice(4, 6), 16), - }; -} - -// ───────────────────────── 패턴 3종 (정적 텍스처) ───────────────────────── - -type RGB = { r: number; g: number; b: number }; - -/** 1. 등고선 — accent 동심 곡선 흐름 */ -function drawContour( - ctx: CanvasRenderingContext2D, - w: number, - h: number, - rng: () => number, - c: RGB, -) { - const cx = w * (0.3 + rng() * 0.4); - const cy = h * (0.3 + rng() * 0.4); - const rings = 9 + Math.floor(rng() * 5); - const step = Math.max(w, h) / rings; - const wobble = 0.12 + rng() * 0.1; - const phase = rng() * Math.PI * 2; - ctx.lineWidth = 1.25; - for (let i = 1; i <= rings; i++) { - const baseR = i * step; - const alpha = 0.1 + (1 - i / rings) * 0.08; // 0.10~0.18 - ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${alpha})`; - ctx.beginPath(); - const segs = 72; - for (let s = 0; s <= segs; s++) { - const a = (s / segs) * Math.PI * 2; - const r = baseR * (1 + Math.sin(a * 3 + phase + i * 0.6) * wobble); - const x = cx + Math.cos(a) * r; - const y = cy + Math.sin(a) * r * 0.82; - if (s === 0) ctx.moveTo(x, y); - else ctx.lineTo(x, y); - } - ctx.stroke(); - } -} - -/** 2. 격자 왜곡 — 미세하게 휘어진 그리드 라인 */ -function drawGrid( - ctx: CanvasRenderingContext2D, - w: number, - h: number, - rng: () => number, - c: RGB, -) { - const cols = 7 + Math.floor(rng() * 4); - const rows = 5 + Math.floor(rng() * 4); - const amp = 6 + rng() * 10; - const fx = 1.5 + rng() * 2; - const fy = 1.5 + rng() * 2; - const phase = rng() * Math.PI * 2; - ctx.lineWidth = 1; - ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},0.14)`; - // 세로선 - for (let i = 0; i <= cols; i++) { - const baseX = (i / cols) * w; - ctx.beginPath(); - for (let j = 0; j <= 40; j++) { - const ny = j / 40; - const y = ny * h; - const x = baseX + Math.sin(ny * Math.PI * fy + phase + i * 0.4) * amp; - if (j === 0) ctx.moveTo(x, y); - else ctx.lineTo(x, y); - } - ctx.stroke(); - } - // 가로선 - for (let i = 0; i <= rows; i++) { - const baseY = (i / rows) * h; - ctx.beginPath(); - for (let j = 0; j <= 40; j++) { - const nx = j / 40; - const x = nx * w; - const y = baseY + Math.cos(nx * Math.PI * fx + phase + i * 0.4) * amp; - if (j === 0) ctx.moveTo(x, y); - else ctx.lineTo(x, y); - } - ctx.stroke(); - } -} - -/** 3. 도트 필드 — 밀도 그라데이션 도트 */ -function drawDots( - ctx: CanvasRenderingContext2D, - w: number, - h: number, - rng: () => number, - c: RGB, -) { - const gap = 16 + rng() * 8; - const ox = rng() * Math.PI * 2; - const oy = rng() * Math.PI * 2; - // 밀도 중심 (가장 진한 지점) - const dcx = w * (0.2 + rng() * 0.6); - const dcy = h * (0.2 + rng() * 0.6); - const maxD = Math.hypot(w, h); - for (let y = gap * 0.5; y < h; y += gap) { - for (let x = gap * 0.5; x < w; x += gap) { - const jx = Math.sin((y / gap) * 1.3 + ox) * 2.5; - const jy = Math.cos((x / gap) * 1.3 + oy) * 2.5; - const px = x + jx; - const py = y + jy; - const d = Math.hypot(px - dcx, py - dcy) / maxD; // 0~~1 - const density = 1 - d; // 중심부 1 - const alpha = 0.06 + density * 0.16; // 0.06~0.22 - const radius = 0.9 + density * 1.8; - ctx.fillStyle = `rgba(${c.r},${c.g},${c.b},${alpha.toFixed(3)})`; - ctx.beginPath(); - ctx.arc(px, py, radius, 0, Math.PI * 2); - ctx.fill(); - } - } -} - -const PATTERNS = [drawContour, drawGrid, drawDots]; - -// ───────────────────────── 컴포넌트 ───────────────────────── - -export default function ShowcaseCard({ slot, size = 'standard', index }: Props) { - const canvasRef = useRef(null); - const wrapRef = useRef(null); - const [hovered, setHovered] = useState(false); - - // 슬러그 시드 — 결정적 패턴 선택/파라미터 - const seed = hashSlug(slot.slug); - const patternType = seed % PATTERNS.length; - - // 캔버스 1회 정적 렌더 (DPR 반영, 애니메이션 루프 없음) - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) return; - - const draw = () => { - const rect = canvas.getBoundingClientRect(); - const w = rect.width; - const h = rect.height; - if (w === 0 || h === 0) return; - const dpr = Math.min(window.devicePixelRatio || 1, 2); - canvas.width = Math.round(w * dpr); - canvas.height = Math.round(h * dpr); - const ctx = canvas.getContext('2d'); - if (!ctx) return; - ctx.setTransform(dpr, 0, 0, dpr, 0, 0); - ctx.clearRect(0, 0, w, h); - const rng = mulberry32(seed); - const c = hexToRgb(slot.accent); - PATTERNS[patternType](ctx, w, h, rng, c); - }; - - draw(); - - // 컨테이너 리사이즈 시 재렌더 (정적 — 루프 아님) - const ro = new ResizeObserver(() => draw()); - ro.observe(canvas); - return () => ro.disconnect(); - }, [seed, patternType, slot.accent]); - - // 호버 시차 — 리스너/rAF는 hover 중에만 가동 (상시 rAF 금지) - useEffect(() => { - if (!hovered) return; - if (typeof window === 'undefined') return; - if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; - const canvas = canvasRef.current; - const wrap = wrapRef.current; - if (!canvas || !wrap) return; - - let rafId = 0; - let tx = 0; - let ty = 0; - - const apply = () => { - rafId = 0; - canvas.style.transform = `translate(${tx.toFixed(2)}px, ${ty.toFixed(2)}px)`; - }; - - const onMove = (e: MouseEvent) => { - const rect = wrap.getBoundingClientRect(); - const nx = (e.clientX - rect.left) / rect.width - 0.5; // -0.5~0.5 - const ny = (e.clientY - rect.top) / rect.height - 0.5; - tx = nx * 12; // ±6px - ty = ny * 12; - if (!rafId) rafId = requestAnimationFrame(apply); - }; - - wrap.addEventListener('mousemove', onMove); - return () => { - wrap.removeEventListener('mousemove', onMove); - if (rafId) cancelAnimationFrame(rafId); - canvas.style.transform = ''; - }; - }, [hovered]); - +// 라이트 쇼케이스 카드 — surface-alt 스테이지 위에 흰 MockWindow가 떠 있는 "framed screen". +// 서버 컴포넌트 (캔버스/시드/그래디언트 전량 제거). +export default function ShowcaseCard({ slot, size = 'standard' }: Props) { + const Mock = MOCK_REGISTRY[slot.mock]; const isFeature = size === 'feature'; const isLink = Boolean(slot.href); - // 타일 본체 (링크/div 공통) - const tile = ( + const body = (
setHovered(true)} - onMouseLeave={() => setHovered(false)} className={[ - 'group/card relative isolate h-full w-full overflow-hidden rounded-2xl', - 'transition-[transform,box-shadow] duration-500', + 'group/card flex h-full flex-col rounded-2xl border p-5 lg:p-6', + 'transition-[transform,box-shadow,border-color] duration-300', '[transition-timing-function:cubic-bezier(0.16,1,0.3,1)]', - 'motion-safe:hover:scale-[1.03]', - isLink ? 'cursor-pointer' : 'cursor-default', - isFeature ? 'aspect-[16/10]' : 'aspect-[4/3]', + 'motion-safe:hover:-translate-y-1 hover:shadow-[0_24px_60px_-32px_rgba(15,23,42,0.4)]', + isLink ? 'cursor-pointer' : '', ].join(' ')} - style={ - { - '--card-accent': slot.accent, - backgroundImage: `linear-gradient(135deg, ${slot.palette[0]}, ${slot.palette[1]})`, - // 기본 보더는 다크 라인, hover 시 accent 점등 + 코너 글로우 (인라인 hover는 className으로) - boxShadow: hovered - ? `0 0 0 1px ${slot.accent}, 0 18px 50px -20px ${slot.accent}66, inset 0 0 60px -30px ${slot.accent}80` - : '0 0 0 1px var(--jsm-dark-line, rgba(148,163,184,0.14))', - } as React.CSSProperties - } + style={{ background: 'var(--jsm-surface-alt)', borderColor: 'var(--jsm-line)' }} > - {/* 제너러티브 텍스처 (정적) */} -