Files
invyone/frontend/components/dash/TemplateLibraryModal.tsx
T
johngreen 9755869754
Build & Deploy to K8s / build-and-deploy (push) Successful in 4m5s
feat(template-thumb): 라이브 미니 프리뷰 (실제 TemplateRenderer scale-down)
기존 와이어프레임 박스(테두리 + 투명 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>
2026-04-30 08:31:34 +09:00

164 lines
6.2 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { Search, X } from 'lucide-react';
import { getTemplateList } from '@/lib/api/template';
import { TemplateMiniPreview } from './TemplateMiniPreview';
interface TemplateLibraryModalProps {
open: boolean;
onClose: () => void;
onSelectTemplate: (template: Record<string, any>) => void;
}
const CATEGORIES = [
{ id: '', label: '전체', icon: '📋' },
{ id: 'sales', label: '영업/CRM', icon: '💰' },
{ id: 'production', label: '생산/공정', icon: '🏭' },
{ id: 'hr', label: '인사/급여', icon: '👥' },
{ id: 'inventory', label: '재고/물류', icon: '📦' },
{ id: 'finance', label: '재무/회계', icon: '💳' },
{ id: 'admin', label: '관리자', icon: '⚙' },
];
export function TemplateLibraryModal({ open, onClose, onSelectTemplate }: TemplateLibraryModalProps) {
const [templates, setTemplates] = useState<Record<string, any>[]>([]);
const [activeCategory, setActiveCategory] = useState('');
const [keyword, setKeyword] = useState('');
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!open) return;
const load = async () => {
setLoading(true);
try {
const result = await getTemplateList();
setTemplates(result?.list ?? []);
} catch (err) {
console.error('[TemplateLibrary] Load failed:', err);
} finally {
setLoading(false);
}
};
load();
}, [open]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
if (open) document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [open, onClose]);
const filtered = templates.filter((t) => {
const name = (t.name ?? t.NAME ?? '').toLowerCase();
const cat = (t.category ?? t.CATEGORY ?? '').toLowerCase();
const desc = (t.description ?? t.DESCRIPTION ?? '').toLowerCase();
const matchKeyword = !keyword || name.includes(keyword.toLowerCase()) || desc.includes(keyword.toLowerCase());
const matchCategory = !activeCategory || cat === activeCategory;
return matchKeyword && matchCategory;
});
return (
<>
<div
className={`dash-lib-backdrop${open ? ' open' : ''}`}
onClick={onClose}
/>
<div className={`dash-lib-modal${open ? ' open' : ''}`}>
{/* 헤더 */}
<div className="dash-lib-head">
<div style={{ display: 'flex', alignItems: 'center', gap: '.7rem' }}>
<span className="dash-lib-title">릿 </span>
<span style={{ fontSize: '.6rem', color: 'var(--v5-text-muted)' }}>
{filtered.length}
</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '.5rem' }}>
<div style={{
display: 'flex', alignItems: 'center', gap: '.5rem',
padding: '.45rem .75rem', borderRadius: '10px',
background: 'var(--v5-surface)', border: '1px solid var(--v5-glass-border)',
width: '220px',
}}>
<Search size={13} style={{ color: 'var(--v5-text-muted)', flexShrink: 0 }} />
<input
type="text"
placeholder="템플릿 검색..."
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
style={{
flex: 1, border: 'none', background: 'transparent',
color: 'var(--v5-text)', fontSize: '.7rem', fontFamily: 'inherit', outline: 'none',
}}
/>
</div>
<button className="dash-lib-close" onClick={onClose}>
<X size={14} />
</button>
</div>
</div>
{/* 본문 */}
<div className="dash-lib-body">
{/* 카테고리 */}
<div className="dash-lib-cats">
{CATEGORIES.map((cat) => (
<div
key={cat.id}
className={`dash-lib-cat${activeCategory === cat.id ? ' on' : ''}`}
onClick={() => setActiveCategory(cat.id)}
>
<span className="ic">{cat.icon}</span>
<span>{cat.label}</span>
</div>
))}
</div>
{/* 카드 그리드 */}
<div className="dash-lib-grid">
{loading ? (
<div style={{ padding: '3rem', textAlign: 'center', color: 'var(--v5-text-muted)', fontSize: '.7rem' }}>
릿 ...
</div>
) : filtered.length === 0 ? (
<div style={{ padding: '3rem', textAlign: 'center', color: 'var(--v5-text-muted)', fontSize: '.7rem' }}>
{templates.length === 0
? '게시된 템플릿이 없습니다. 개발자 빌더에서 템플릿을 만들고 게시하세요.'
: '검색 결과가 없습니다.'}
</div>
) : (
<div className="dash-lib-cards">
{filtered.map((t) => {
const tid = t.template_id ?? t.TEMPLATE_ID;
const name = t.name ?? t.NAME ?? '템플릿';
const cat = t.category ?? t.CATEGORY ?? '';
const desc = t.description ?? t.DESCRIPTION ?? '';
const table = t.primary_table ?? t.PRIMARY_TABLE ?? '';
return (
<div
key={tid}
className="dash-lib-card"
onClick={() => onSelectTemplate(t)}
>
<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' }}>
{cat && <span className="dash-lib-card-tag">{cat}</span>}
{table && <span className="dash-lib-card-tag">{table}</span>}
</div>
</div>
);
})}
</div>
)}
</div>
</div>
</div>
</>
);
}