diff --git a/solution/frontend/src/features/marketing/ShowcaseGrid.tsx b/solution/frontend/src/features/marketing/ShowcaseGrid.tsx index eda4c63..e81ca5d 100644 --- a/solution/frontend/src/features/marketing/ShowcaseGrid.tsx +++ b/solution/frontend/src/features/marketing/ShowcaseGrid.tsx @@ -1,4 +1,4 @@ -import {useEffect, useState} from 'react'; +import {useEffect, useState, type CSSProperties} from 'react'; import {Building2, Coffee, ImageOff, Stethoscope, UtensilsCrossed} from 'lucide-react'; import {PlaceCategory} from '@o2o/shared'; import {fetchShowcase, type ShowcaseItem} from './showcaseApi'; @@ -100,3 +100,75 @@ function SkeletonCard() { ); } + +/** + * 히어로 아래 발행 사이트 마퀴 — 옆으로 계속 흐른다. + * + * ★ 애니메이션은 **CSS 만으로** 돈다(index.css `o2o-marquee`). setInterval 로 인덱스를 돌리면 + * 탭이 백그라운드일 때 프레임이 밀려 돌아왔을 때 툭 끊긴 것처럼 보인다. + * ★ 같은 목록을 두 벌 그린다 — 트랙을 -50% 까지만 밀면 이음매 없이 이어진다. + * 한 벌만 그리면 끝에서 빈 화면이 지나간다. + * ★ 여기도 실물이다. 발행된 사이트가 없으면 아무것도 그리지 않는다. + */ +export function ShowcasePeeks({limit = 10}: {limit?: number}) { + const [items, setItems] = useState([]); + + useEffect(() => { + let alive = true; + fetchShowcase(limit) + .then((rows) => alive && setItems(rows)) + .catch(() => undefined); + return () => { + alive = false; + }; + }, [limit]); + + if (items.length === 0) return null; + + // 카드 수에 비례해 시간을 늘린다 — 개수가 늘어도 흐르는 **속도**는 그대로여야 한다. + const duration = `${Math.max(24, items.length * 6)}s`; + + return ( +
+
+ {[...items, ...items].map((item, index) => ( + = items.length} + tabIndex={index >= items.length ? -1 : undefined} + className="w-52 shrink-0 overflow-hidden rounded-xl border border-border bg-card text-left transition-transform hover:-translate-y-0.5" + > +
+ {item.thumbnailUrl ? ( + {`${item.name} + ) : ( + + )} +
+
+

{item.name}

+

+ {[item.region, CATEGORY_LABEL[item.category]].filter(Boolean).join(' · ')} +

+
+
+ ))} +
+
+ ); +} diff --git a/solution/frontend/src/index.css b/solution/frontend/src/index.css index 92c927d..24e9b47 100644 --- a/solution/frontend/src/index.css +++ b/solution/frontend/src/index.css @@ -76,6 +76,22 @@ body { } } +/* 랜딩 히어로의 발행 사이트 마퀴. **CSS 만으로 돈다** — 타이머를 쓰면 탭이 백그라운드일 때 + 프레임이 밀려 끊긴 것처럼 보인다. 트랙에 같은 목록을 두 벌 넣고 -50% 까지 밀면 이음매가 없다. */ +@keyframes o2o-marquee { + from { transform: translateX(0); } + to { transform: translateX(-50%); } +} +.o2o-marquee-track { + animation: o2o-marquee var(--marquee-duration, 48s) linear infinite; +} +.o2o-marquee:hover .o2o-marquee-track { + animation-play-state: paused; +} +@media (prefers-reduced-motion: reduce) { + .o2o-marquee-track { animation: none; } +} + /* 수집 진행바 shimmer — 0%(빈 트랙) 구간에서도 '진행 중'이 보이게 한다. */ @keyframes o2o-shimmer { from { transform: translateX(-100%); } diff --git a/solution/frontend/src/pages/LandingPage.tsx b/solution/frontend/src/pages/LandingPage.tsx index 77ac1ec..2c48bce 100644 --- a/solution/frontend/src/pages/LandingPage.tsx +++ b/solution/frontend/src/pages/LandingPage.tsx @@ -3,7 +3,7 @@ import {Link, useNavigate} from 'react-router'; import {ArrowRight, Building2, Check, Coffee, Search, Stethoscope, UtensilsCrossed} from 'lucide-react'; import type {IndustryType} from '@o2o/shared'; import {MarketingShell, Section, SectionHead} from '@/components/layout/MarketingShell'; -import {ShowcaseGrid} from '@/features/marketing/ShowcaseGrid'; +import {ShowcaseGrid, ShowcasePeeks} from '@/features/marketing/ShowcaseGrid'; import {Button} from '@/components/ui/button'; const INDUSTRIES: {id: IndustryType; label: string; icon: typeof Building2}[] = [ @@ -67,7 +67,7 @@ export function LandingPage() { /> @@ -97,6 +97,9 @@ export function LandingPage() { ))} + + {/* 입력칸만 있으면 무엇이 나오는지 알 수 없다 — 결과물 두 장을 먼저 보여준다. */} +