9755869754
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>
164 lines
6.2 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|