Files
invyone/frontend/components/dash/TemplateLibraryModal.tsx
T
johngreen df9a539017
Build & Deploy to K8s / build-and-deploy (push) Successful in 4m26s
feat(template): 라이브러리 카드에 wireframe 썸네일 표시
템플릿 목록 카드의 정적 📋 아이콘을 실제 view 구조 기반의
미니 와이어프레임으로 교체. 사용자가 카드만 보고도 템플릿이
어떤 화면인지(테이블 위주 / 폼 위주 / 단순 버튼 등) 파악 가능.

- backend: getTemplateList SQL 에 VIEWS 컬럼 추가, list 응답 각
  row 의 views jsonb 를 객체로 파싱
- frontend: TemplateThumbnail 컴포넌트 신설 — v2(BlockV2.xPct/yPct
  /wPct/hPct) 정규화 좌표 우선, v1(order/row) 폴백, 컴포넌트
  종류별 색상(table=primary, form=cyan, button=pink)
- TemplateLibraryModal 카드 아이콘 자리 교체
- dashboard.css 에 .dash-lib-card-thumb / -block 스타일 추가
  (v5 토큰 준수 — solid + glow, blur 없음)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 08:00:10 +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 { TemplateThumbnail } from './TemplateThumbnail';
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)}
>
<TemplateThumbnail views={t.views ?? t.VIEWS} />
<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>
</>
);
}