feat(insta): 슬레이트 캐러셀 + 반응형 레이아웃 전면 개선
문제: - 페이지 1~10 미리보기가 가로 overflow인데 시각 affordance 없어서 page 2~10 못 봄 - 슬레이트 목록(.ic-slates-grid)이 모바일에서 어색 + 카드 자체가 viewport 밖으로 밀림 수정: - PagesStrip 컴포넌트 신설: 좌/우 chevron + page 인디케이터(3/10) + 양옆 fade gradient + 키보드 ←/→ + scroll-snap + 클릭 페이지 이동 + 활성 카드 핑크 테두리/scale - .ic-page-img width를 clamp(140px, 42vw, 220px)로 viewport 비례 - .ic-slates-grid 모바일 2칸 강제, 640px+ 부터 auto-fill - .ic-detail에 min-width: 0 + max-width: 100% (자식이 부모 안 밀게) - .ic-layout grid-template-columns에 minmax(0, 1fr) — 자식 overflow 시 부모 안정 - .ic 모바일 좌우 padding 12px (768px+ 16px)
This commit is contained in:
@@ -689,6 +689,91 @@ function SlatesPanel({ selectedId, onSelect }) {
|
||||
);
|
||||
}
|
||||
|
||||
/* ══════════════════════ 페이지 스트립 (chevron + indicator) ═══════════ */
|
||||
function PagesStrip({ slateId, pageCount }) {
|
||||
const stripRef = useRef(null);
|
||||
const [activePage, setActivePage] = useState(1);
|
||||
|
||||
const scrollToPage = useCallback((pageNo) => {
|
||||
const strip = stripRef.current;
|
||||
if (!strip) return;
|
||||
const next = Math.max(1, Math.min(pageCount, pageNo));
|
||||
const child = strip.children[next - 1];
|
||||
if (child) {
|
||||
child.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
|
||||
setActivePage(next);
|
||||
}
|
||||
}, [pageCount]);
|
||||
|
||||
// 스크롤/드래그 시 가운데 카드 감지
|
||||
const onScroll = useCallback(() => {
|
||||
const strip = stripRef.current;
|
||||
if (!strip) return;
|
||||
const rect = strip.getBoundingClientRect();
|
||||
const centerX = rect.left + rect.width / 2;
|
||||
let best = 1, bestDist = Infinity;
|
||||
Array.from(strip.children).forEach((child, i) => {
|
||||
const cRect = child.getBoundingClientRect();
|
||||
const cCenter = cRect.left + cRect.width / 2;
|
||||
const dist = Math.abs(cCenter - centerX);
|
||||
if (dist < bestDist) { bestDist = dist; best = i + 1; }
|
||||
});
|
||||
if (best !== activePage) setActivePage(best);
|
||||
}, [activePage]);
|
||||
|
||||
// 키보드 ←/→
|
||||
useEffect(() => {
|
||||
const onKey = (e) => {
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
||||
if (e.key === 'ArrowLeft') { scrollToPage(activePage - 1); e.preventDefault(); }
|
||||
else if (e.key === 'ArrowRight') { scrollToPage(activePage + 1); e.preventDefault(); }
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [activePage, scrollToPage]);
|
||||
|
||||
return (
|
||||
<div className="ic-pages-wrap">
|
||||
<button
|
||||
className="ic-pages-nav ic-pages-nav--prev"
|
||||
onClick={() => scrollToPage(activePage - 1)}
|
||||
disabled={activePage <= 1}
|
||||
aria-label="이전 페이지"
|
||||
type="button"
|
||||
>‹</button>
|
||||
|
||||
<div className="ic-pages-strip" ref={stripRef} onScroll={onScroll}>
|
||||
{Array.from({ length: pageCount }, (_, i) => i + 1).map((page) => (
|
||||
<img
|
||||
key={page}
|
||||
className={`ic-page-img ${activePage === page ? 'is-active' : ''}`}
|
||||
src={getInstaAssetUrl(slateId, page)}
|
||||
alt={`Page ${page}`}
|
||||
loading="lazy"
|
||||
onClick={() => scrollToPage(page)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="ic-pages-nav ic-pages-nav--next"
|
||||
onClick={() => scrollToPage(activePage + 1)}
|
||||
disabled={activePage >= pageCount}
|
||||
aria-label="다음 페이지"
|
||||
type="button"
|
||||
>›</button>
|
||||
|
||||
<div className="ic-pages-indicator-row">
|
||||
<span className="ic-pages-indicator">
|
||||
<span className="ic-pages-indicator__current">{activePage}</span>
|
||||
<span className="ic-pages-indicator__sep">/</span>
|
||||
<span className="ic-pages-indicator__total">{pageCount}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ══════════════════════ 슬레이트 상세 ══════════════════════════════════ */
|
||||
function SlateDetail({ slate, onDelete, onRender }) {
|
||||
const pages = slate.assets || [];
|
||||
@@ -712,19 +797,9 @@ function SlateDetail({ slate, onDelete, onRender }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 페이지 이미지 스트립 */}
|
||||
{/* 페이지 이미지 스트립 (캐러셀: chevron + indicator + ←/→ 키보드) */}
|
||||
{(slate.status === 'rendered' || slate.status === 'sent') ? (
|
||||
<div className="ic-pages-strip">
|
||||
{Array.from({ length: pageCount }, (_, i) => i + 1).map((page) => (
|
||||
<img
|
||||
key={page}
|
||||
className="ic-page-img"
|
||||
src={getInstaAssetUrl(slate.id, page)}
|
||||
alt={`Page ${page}`}
|
||||
loading="lazy"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<PagesStrip slateId={slate.id} pageCount={pageCount} />
|
||||
) : (
|
||||
<div className="ic-empty" style={{ padding: '20px 0' }}>
|
||||
{slate.status === 'failed' ? '렌더 실패 — 재렌더를 시도하세요.' : '렌더링 전입니다.'}
|
||||
|
||||
Reference in New Issue
Block a user