feat(template-thumb): 라이브 미니 프리뷰 (실제 TemplateRenderer scale-down)
Build & Deploy to K8s / build-and-deploy (push) Successful in 4m5s

기존 와이어프레임 박스(테두리 + 투명 fill) 대신 실제 TemplateRenderer
를 mock empty context 로 띄워 transform: scale 로 축소 → 사용자가
빌더에서 그린 그대로의 레이아웃이 카드에서 그대로 읽힘.

- TemplateMiniPreview 컴포넌트 신설:
  - DEFAULT empty context (data:[], callbacks no-op) 로 데이터 fetch 0회
  - BASE_WIDTH=1200, 16:10 stage → ResizeObserver 로 카드 폭 변화 자동 추종
  - pointer-events: none / user-select: none / overflow: hidden
  - views 가 비어있으면 기존 TemplateThumbnail (와이어프레임) 폴백
- TemplateLibraryModal 카드 아이콘 자리 교체
- dashboard.css 에 .dash-lib-card-thumb--live / -stage 추가

향후 템플릿 50+ 로 늘어 모달 첫 오픈이 무거워지면 lazy mount(
intersection observer) 또는 background 스크린샷 캐싱으로 전환.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-30 08:31:34 +09:00
parent b7ebc69755
commit 9755869754
3 changed files with 108 additions and 2 deletions
@@ -3,7 +3,7 @@
import { useState, useEffect } from 'react';
import { Search, X } from 'lucide-react';
import { getTemplateList } from '@/lib/api/template';
import { TemplateThumbnail } from './TemplateThumbnail';
import { TemplateMiniPreview } from './TemplateMiniPreview';
interface TemplateLibraryModalProps {
open: boolean;
@@ -143,7 +143,7 @@ export function TemplateLibraryModal({ open, onClose, onSelectTemplate }: Templa
className="dash-lib-card"
onClick={() => onSelectTemplate(t)}
>
<TemplateThumbnail views={t.views ?? t.VIEWS} />
<TemplateMiniPreview template={t} />
<div className="dash-lib-card-name">{name}</div>
{desc && <div className="dash-lib-card-desc">{desc}</div>}
<div style={{ display: 'flex', gap: '.2rem', marginTop: 'auto', flexWrap: 'wrap' }}>
@@ -0,0 +1,97 @@
'use client';
/**
* 라이브러리 카드용 라이브 미니 프리뷰.
*
* 실제 TemplateRenderer 를 mock empty context 로 띄운 뒤 transform: scale 로
* 카드 썸네일 사이즈에 맞춤. 사용자가 빌더에서 그린 그대로의 레이아웃이
* 읽힘 (테이블/폼/버튼/구분선 등 실제 컴포넌트 모양). 데이터는 비어있으니
* 추가 API 호출이나 회사 전환 부담 없음.
*
* - pointer-events: none — 카드 클릭 영역은 부모가 처리
* - overflow: hidden — scale 후 박스 밖 픽셀 클립
* - ResizeObserver 로 카드 폭에 따라 scale 자동 조정
*
* 빈 템플릿이면 TemplateThumbnail 폴백.
*/
import { useLayoutEffect, useRef, useState, useMemo } from 'react';
import { TemplateRenderer, type TemplateRenderContext } from './TemplateRenderer';
import { TemplateThumbnail } from './TemplateThumbnail';
interface TemplateMiniPreviewProps {
template: any;
}
const EMPTY_CTX: TemplateRenderContext = {
fields: [],
data: [],
loading: false,
selectedRow: null,
totalCount: 0,
page: 1,
pageSize: 20,
searchParams: {},
onSearch: () => {},
onRowSelect: () => {},
onPageChange: () => {},
onAdd: () => {},
onEdit: () => {},
onDelete: () => {},
};
const BASE_WIDTH = 1200;
const BASE_HEIGHT = 750; // 16:10
function hasContent(template: any): boolean {
const v = template?.views ?? template?.VIEWS;
if (!v) return false;
const lst = v?.list;
if (Array.isArray(lst) && lst.length > 0) return true;
if (Array.isArray(lst?.blocks) && lst.blocks.length > 0) return true;
if (Array.isArray(lst?.components) && lst.components.length > 0) return true;
if (Array.isArray(lst?.layers) &&
lst.layers.some((l: any) => Array.isArray(l?.components) && l.components.length > 0)) {
return true;
}
return false;
}
export function TemplateMiniPreview({ template }: TemplateMiniPreviewProps) {
const wrapRef = useRef<HTMLDivElement>(null);
const [scale, setScale] = useState(0.15);
const empty = useMemo(() => !hasContent(template), [template]);
useLayoutEffect(() => {
if (empty || !wrapRef.current) return;
const el = wrapRef.current;
const update = () => {
const w = el.clientWidth;
if (w > 0) setScale(w / BASE_WIDTH);
};
update();
const ro = new ResizeObserver(update);
ro.observe(el);
return () => ro.disconnect();
}, [empty]);
if (empty) {
return <TemplateThumbnail views={template?.views ?? template?.VIEWS} />;
}
return (
<div ref={wrapRef} className="dash-lib-card-thumb dash-lib-card-thumb--live" aria-hidden="true">
<div
className="dash-lib-card-thumb-stage"
style={{
width: BASE_WIDTH,
height: BASE_HEIGHT,
transform: `scale(${scale})`,
}}
>
<TemplateRenderer template={template} context={EMPTY_CTX} view="list" />
</div>
</div>
);
}
+9
View File
@@ -487,6 +487,15 @@
.dash-lib-card-thumb--empty {
display: flex; align-items: center; justify-content: center;
}
.dash-lib-card-thumb--live {
padding: 0;
background: var(--v5-surface-solid);
}
.dash-lib-card-thumb-stage {
transform-origin: top left;
pointer-events: none;
user-select: none;
}
.dash-lib-card-thumb-canvas {
position: relative; width: 100%; height: 100%;
}