diff --git a/frontend/components/dash/TemplateLibraryModal.tsx b/frontend/components/dash/TemplateLibraryModal.tsx index 436d730d..194f7b42 100644 --- a/frontend/components/dash/TemplateLibraryModal.tsx +++ b/frontend/components/dash/TemplateLibraryModal.tsx @@ -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)} > - +
{name}
{desc &&
{desc}
}
diff --git a/frontend/components/dash/TemplateMiniPreview.tsx b/frontend/components/dash/TemplateMiniPreview.tsx new file mode 100644 index 00000000..b6448525 --- /dev/null +++ b/frontend/components/dash/TemplateMiniPreview.tsx @@ -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(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 ; + } + + return ( + + ); +} diff --git a/frontend/styles/dashboard.css b/frontend/styles/dashboard.css index 7b68fa29..119e2e4f 100644 --- a/frontend/styles/dashboard.css +++ b/frontend/styles/dashboard.css @@ -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%; }