diff --git a/src/frontend/src/screens/public/PublicGatewayPage.tsx b/src/frontend/src/screens/public/PublicGatewayPage.tsx
index 40afda0..880ff88 100644
--- a/src/frontend/src/screens/public/PublicGatewayPage.tsx
+++ b/src/frontend/src/screens/public/PublicGatewayPage.tsx
@@ -6,6 +6,7 @@
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { PublicShell } from './PublicShell';
+import { PublicHeroSlides } from './PublicHeroSlides';
import { usePublicEvents, EventGrid } from './trackShared';
import {
IconSparkles,
@@ -32,13 +33,15 @@ export function PublicGatewayPage() {
return (
- {/* 제품 히어로 — 배경 실사진은 후속(별도). 현재 V-4 그라디언트+패턴 폴백(CLS 0). */}
+ {/* 제품 히어로 — 배경 실사진 5장 크로스페이드 회전(그라디언트+패턴은 로드 실패 폴백). */}
diff --git a/src/frontend/src/screens/public/PublicHeroSlides.tsx b/src/frontend/src/screens/public/PublicHeroSlides.tsx
new file mode 100644
index 0000000..1d58a42
--- /dev/null
+++ b/src/frontend/src/screens/public/PublicHeroSlides.tsx
@@ -0,0 +1,39 @@
+/*
+ * 공개 히어로 배경 회전 슬라이드 (§3C-V 확장) — 실사진 5장 크로스페이드.
+ * 각 히어로 배경 레이어(.kxp-hero__bg / .kxp-vhero__bg) 내부에 절대배치 자식으로 삽입한다.
+ * 회전 로직은 usePublicMotion.ts([data-hero-slides])가 담당(간격·정지조건·prefers-reduced-motion).
+ * 장식 배경이므로 aria-hidden + alt="" (헤드라인/스크림이 의미 전달). 로드 실패 시 그라디언트 폴백 노출.
+ * 이미지 alt(감사용): _workspace/hero_images.md — hero-1 곡면유리 파사드 … hero-5 제2전시장 돔.
+ */
+const HERO_COUNT = 5;
+
+export function PublicHeroSlides() {
+ return (
+
+ {Array.from({ length: HERO_COUNT }, (_, i) => {
+ const n = i + 1;
+ const first = i === 0;
+ return (
+

{
+ // 로드 실패: 회전 대상에서 제외하고 그라디언트 폴백이 비치도록 숨김(비파괴).
+ const img = e.currentTarget;
+ img.dataset.failed = '1';
+ img.classList.remove('is-active');
+ img.style.display = 'none';
+ }}
+ />
+ );
+ })}
+
+ );
+}
diff --git a/src/frontend/src/screens/public/VisitorTrackPage.tsx b/src/frontend/src/screens/public/VisitorTrackPage.tsx
index ab12d60..1fe8207 100644
--- a/src/frontend/src/screens/public/VisitorTrackPage.tsx
+++ b/src/frontend/src/screens/public/VisitorTrackPage.tsx
@@ -12,6 +12,7 @@ import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { PublicShell } from './PublicShell';
+import { PublicHeroSlides } from './PublicHeroSlides';
import { usePublicEvents, EventGrid } from './trackShared';
import { AiAssistant } from './AiAssistant';
import { AiPlanningBriefing } from './AiPlanningBriefing';
@@ -149,9 +150,11 @@ export function VisitorTrackPage() {
return (
{/* ── 1. 풀블리드 히어로 + AI 관람 도우미(중심) ── STITCH-REPLACE:hero ── */}
- {/* 히어로 배경 실사진은 후속(별도) — 현재 V-4 그라디언트+패턴 폴백 렌더(CLS 0). */}
+ {/* 히어로 배경 실사진 5장 크로스페이드 회전(그라디언트+도트 패턴은 로드 실패 폴백). */}
-
+
{t('track.visitor.aiWatermark')}
diff --git a/src/frontend/src/screens/public/public.css b/src/frontend/src/screens/public/public.css
index 3e43374..38f6e79 100644
--- a/src/frontend/src/screens/public/public.css
+++ b/src/frontend/src/screens/public/public.css
@@ -4113,6 +4113,31 @@ a.kxp-ai__cite:hover {
transform: none;
}
+/* 히어로 배경 회전 슬라이드(크로스페이드) — .kxp-hero__bg/.kxp-vhero__bg 내부 절대배치.
+ 폴백(그라디언트+패턴)은 부모 bg 레이어가 유지 → 이미지 위에 겹친다(로드 실패 시 비침). */
+.pub-hero__slides {
+ position: absolute;
+ inset: 0;
+ overflow: hidden;
+ z-index: 0;
+}
+.pub-hero__slide {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ object-position: center;
+ opacity: 0;
+ transition: opacity 1200ms var(--ease-standard);
+ will-change: opacity;
+ pointer-events: none;
+ user-select: none;
+}
+.pub-hero__slide.is-active {
+ opacity: 1;
+}
+
/* ── ④ 포스터 카드 리프트 + 이미지 줌 ──────────────────────────────────────── */
.pub-card--lift {
position: relative;
@@ -4222,6 +4247,11 @@ a.kxp-ai__cite:hover {
bottom: 0;
will-change: auto;
}
+ /* 회전 정지(첫 장 고정) + 크로스페이드 전환 무효화 */
+ .kxp .pub-hero__slide {
+ transition: none !important;
+ will-change: auto;
+ }
.kxp .pub-card--lift,
.kxp .pub-card__media img {
transition: none;
diff --git a/src/frontend/src/screens/public/usePublicMotion.ts b/src/frontend/src/screens/public/usePublicMotion.ts
index 155a414..84cdb4a 100644
--- a/src/frontend/src/screens/public/usePublicMotion.ts
+++ b/src/frontend/src/screens/public/usePublicMotion.ts
@@ -191,6 +191,80 @@ export function usePublicMotion(rootRef: RefObject): void {
cleanups.push(() => cancelAnimationFrame(rafId));
}
+ // ── ⑥ 히어로 배경 회전 슬라이드(크로스페이드) ──────────────────────────────────
+ // prefers-reduced-motion 시 첫 장 고정(회전 미배선). 정지: hover/focus·문서 숨김·오프스크린.
+ if (!reduce) {
+ root.querySelectorAll('[data-hero-slides]').forEach((box) => {
+ const slides = Array.from(box.querySelectorAll('img[data-slide]'));
+ if (slides.length < 2) return; // 폴백/단일 이미지는 회전 없음
+
+ const interval = 5500; // ~5.5s 노출 후 다음 장
+ let idx = slides.findIndex((s) => s.classList.contains('is-active'));
+ if (idx < 0) {
+ idx = 0;
+ slides[0].classList.add('is-active');
+ }
+
+ let hovered = false;
+ let offscreen = false;
+ const isPaused = () => hovered || offscreen || document.hidden;
+
+ const nextVisible = (from: number): number => {
+ for (let step = 1; step <= slides.length; step++) {
+ const j = (from + step) % slides.length;
+ if (slides[j].dataset.failed !== '1') return j;
+ }
+ return from;
+ };
+ const tick = () => {
+ if (isPaused()) return;
+ const nx = nextVisible(idx);
+ if (nx === idx) return; // 표시 가능한 다음 장 없음
+ slides[idx].classList.remove('is-active');
+ slides[nx].classList.add('is-active');
+ idx = nx;
+ };
+ const timer = window.setInterval(tick, interval);
+
+ const hero =
+ (box.closest('.pub-hero, .kxp-hero, .kxp-vhero') as HTMLElement | null) ??
+ (box.closest('section') as HTMLElement | null) ??
+ box;
+ const onEnter = () => {
+ hovered = true;
+ };
+ const onLeave = () => {
+ hovered = false;
+ };
+ hero.addEventListener('mouseenter', onEnter);
+ hero.addEventListener('mouseleave', onLeave);
+ hero.addEventListener('focusin', onEnter);
+ hero.addEventListener('focusout', onLeave);
+
+ let io: IntersectionObserver | null = null;
+ if (supportsIO) {
+ io = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((e) => {
+ offscreen = !e.isIntersecting;
+ });
+ },
+ { threshold: 0 },
+ );
+ io.observe(box);
+ }
+
+ cleanups.push(() => {
+ window.clearInterval(timer);
+ hero.removeEventListener('mouseenter', onEnter);
+ hero.removeEventListener('mouseleave', onLeave);
+ hero.removeEventListener('focusin', onEnter);
+ hero.removeEventListener('focusout', onLeave);
+ io?.disconnect();
+ });
+ });
+ }
+
return () => cleanups.forEach((fn) => fn());
}, [rootRef]);
}