155 lines
4.4 KiB
TypeScript
155 lines
4.4 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* TemplateResponsivePreview
|
|
* ============================================================================
|
|
* 템플릿 한 개를 실제 대시보드 카드 크기와 동일한 컨테이너(Full/Half/Quarter)
|
|
* 안에서 미리 렌더링하는 패널. 화면 디자이너에서 편집 → "반응 미리보기" 로
|
|
* 열어서 카드가 축소될 때 각 컴포넌트가 정책대로 반응하는지 즉시 확인한다.
|
|
*
|
|
* 관련 문서:
|
|
* notes/gbpark/2026-04-19-template-responsive-policy.md
|
|
* lib/registry/responsive-policy.ts
|
|
* ============================================================================
|
|
*/
|
|
|
|
import { useMemo, useState } from 'react';
|
|
import type { Template } from '@/types/invyone-component';
|
|
import { TemplateRenderer, type TemplateRenderContext } from './TemplateRenderer';
|
|
|
|
export type PreviewSize = 'full' | 'half' | 'quarter';
|
|
|
|
const SIZE_PRESETS: Record<PreviewSize, { width: number; label: string }> = {
|
|
full: { width: 1200, label: '1/1 (Full)' },
|
|
half: { width: 600, label: '1/2 (Half)' },
|
|
quarter: { width: 300, label: '1/4 (Quarter)' },
|
|
};
|
|
|
|
interface Props {
|
|
template: Template;
|
|
/** 미리보기용 mock context — 실제 데이터 바인딩은 빌더에서 전달 */
|
|
context?: Partial<TemplateRenderContext>;
|
|
/** 기본 프리셋 (기본 full) */
|
|
initialSize?: PreviewSize;
|
|
}
|
|
|
|
const DEFAULT_CONTEXT: TemplateRenderContext = {
|
|
fields: [],
|
|
data: [],
|
|
loading: false,
|
|
selectedRow: null,
|
|
totalCount: 0,
|
|
page: 1,
|
|
pageSize: 20,
|
|
searchParams: {},
|
|
onSearch: () => {},
|
|
onRowSelect: () => {},
|
|
onPageChange: () => {},
|
|
onAdd: () => {},
|
|
onEdit: () => {},
|
|
onDelete: () => {},
|
|
};
|
|
|
|
export function TemplateResponsivePreview({
|
|
template,
|
|
context,
|
|
initialSize = 'full',
|
|
}: Props) {
|
|
const [size, setSize] = useState<PreviewSize>(initialSize);
|
|
|
|
const mergedContext = useMemo<TemplateRenderContext>(
|
|
() => ({ ...DEFAULT_CONTEXT, ...(context ?? {}) }),
|
|
[context],
|
|
);
|
|
|
|
const preset = SIZE_PRESETS[size];
|
|
|
|
return (
|
|
<div className="tpl-rp-root">
|
|
<style>{`
|
|
.tpl-rp-root {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
gap: 12px;
|
|
padding: 12px;
|
|
box-sizing: border-box;
|
|
}
|
|
.tpl-rp-toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.tpl-rp-btn {
|
|
padding: 4px 12px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
border-radius: 6px;
|
|
border: 1px solid hsl(var(--border));
|
|
background: hsl(var(--card));
|
|
color: hsl(var(--foreground));
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
}
|
|
.tpl-rp-btn:hover {
|
|
background: hsl(var(--muted));
|
|
}
|
|
.tpl-rp-btn.active {
|
|
background: hsl(var(--primary));
|
|
color: hsl(var(--primary-foreground, var(--card)));
|
|
border-color: hsl(var(--primary));
|
|
}
|
|
.tpl-rp-info {
|
|
margin-left: auto;
|
|
font-size: 11px;
|
|
color: hsl(var(--muted-foreground));
|
|
}
|
|
.tpl-rp-stage {
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: flex-start;
|
|
overflow: auto;
|
|
background: hsl(var(--muted) / 0.3);
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
}
|
|
.tpl-rp-card {
|
|
background: hsl(var(--card));
|
|
border: 1px solid hsl(var(--border));
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 16px rgba(0,0,0,0.06);
|
|
overflow: hidden;
|
|
transition: width 0.25s ease;
|
|
height: 640px;
|
|
}
|
|
`}</style>
|
|
|
|
<div className="tpl-rp-toolbar">
|
|
{(Object.keys(SIZE_PRESETS) as PreviewSize[]).map((key) => (
|
|
<button
|
|
key={key}
|
|
type="button"
|
|
className={`tpl-rp-btn ${size === key ? 'active' : ''}`}
|
|
onClick={() => setSize(key)}
|
|
>
|
|
{SIZE_PRESETS[key].label}
|
|
</button>
|
|
))}
|
|
<span className="tpl-rp-info">
|
|
카드 폭 <strong>{preset.width}px</strong> — 템플릿:{' '}
|
|
{template.name ?? template.templateId}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="tpl-rp-stage">
|
|
<div className="tpl-rp-card" style={{ width: preset.width }}>
|
|
<TemplateRenderer template={template} context={mergedContext} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|