Files
invyone/frontend/components/dash/TemplateResponsivePreview.tsx
T
DDD1542 3eda684787
Build & Deploy to K8s / build-and-deploy (push) Successful in 4m22s
사용자 대시보드 기능강화 및 인비온 스튜디오 메뉴관리 자잘한수정
2026-04-22 18:27:06 +09:00

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>
);
}