Sync main → gbpark-node: AI 모듈 JSONB 파싱 + audit-log fix #1

Merged
johngreen merged 33 commits from main into gbpark-node 2026-05-02 10:20:35 +00:00
3 changed files with 108 additions and 2 deletions
Showing only changes of commit 9755869754 - Show all commits
@@ -3,7 +3,7 @@
import { useState, useEffect } from 'react';
import { Search, X } from 'lucide-react';
import { getTemplateList } from '@/lib/api/template';
import { TemplateThumbnail } from './TemplateThumbnail';
import { TemplateMiniPreview } from './TemplateMiniPreview';
interface TemplateLibraryModalProps {
open: boolean;
@@ -143,7 +143,7 @@ export function TemplateLibraryModal({ open, onClose, onSelectTemplate }: Templa
className="dash-lib-card"
onClick={() => onSelectTemplate(t)}
>
<TemplateThumbnail views={t.views ?? t.VIEWS} />
<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' }}>
@@ -0,0 +1,97 @@
'use client';
/**
* 라이브러리 카드용 라이브 미니 프리뷰.
*
* 실제 TemplateRenderer 를 mock empty context 로 띄운 뒤 transform: scale 로
* 카드 썸네일 사이즈에 맞춤. 사용자가 빌더에서 그린 그대로의 레이아웃이
* 읽힘 (테이블/폼/버튼/구분선 등 실제 컴포넌트 모양). 데이터는 비어있으니
* 추가 API 호출이나 회사 전환 부담 없음.
*
* - pointer-events: none — 카드 클릭 영역은 부모가 처리
* - overflow: hidden — scale 후 박스 밖 픽셀 클립
* - ResizeObserver 로 카드 폭에 따라 scale 자동 조정
*
* 빈 템플릿이면 TemplateThumbnail 폴백.
*/
import { useLayoutEffect, useRef, useState, useMemo } from 'react';
import { TemplateRenderer, type TemplateRenderContext } from './TemplateRenderer';
import { TemplateThumbnail } from './TemplateThumbnail';
interface TemplateMiniPreviewProps {
template: any;
}
const EMPTY_CTX: TemplateRenderContext = {
fields: [],
data: [],
loading: false,
selectedRow: null,
totalCount: 0,
page: 1,
pageSize: 20,
searchParams: {},
onSearch: () => {},
onRowSelect: () => {},
onPageChange: () => {},
onAdd: () => {},
onEdit: () => {},
onDelete: () => {},
};
const BASE_WIDTH = 1200;
const BASE_HEIGHT = 750; // 16:10
function hasContent(template: any): boolean {
const v = template?.views ?? template?.VIEWS;
if (!v) return false;
const lst = v?.list;
if (Array.isArray(lst) && lst.length > 0) return true;
if (Array.isArray(lst?.blocks) && lst.blocks.length > 0) return true;
if (Array.isArray(lst?.components) && lst.components.length > 0) return true;
if (Array.isArray(lst?.layers) &&
lst.layers.some((l: any) => Array.isArray(l?.components) && l.components.length > 0)) {
return true;
}
return false;
}
export function TemplateMiniPreview({ template }: TemplateMiniPreviewProps) {
const wrapRef = useRef<HTMLDivElement>(null);
const [scale, setScale] = useState(0.15);
const empty = useMemo(() => !hasContent(template), [template]);
useLayoutEffect(() => {
if (empty || !wrapRef.current) return;
const el = wrapRef.current;
const update = () => {
const w = el.clientWidth;
if (w > 0) setScale(w / BASE_WIDTH);
};
update();
const ro = new ResizeObserver(update);
ro.observe(el);
return () => ro.disconnect();
}, [empty]);
if (empty) {
return <TemplateThumbnail views={template?.views ?? template?.VIEWS} />;
}
return (
<div ref={wrapRef} className="dash-lib-card-thumb dash-lib-card-thumb--live" aria-hidden="true">
<div
className="dash-lib-card-thumb-stage"
style={{
width: BASE_WIDTH,
height: BASE_HEIGHT,
transform: `scale(${scale})`,
}}
>
<TemplateRenderer template={template} context={EMPTY_CTX} view="list" />
</div>
</div>
);
}
+9
View File
@@ -487,6 +487,15 @@
.dash-lib-card-thumb--empty {
display: flex; align-items: center; justify-content: center;
}
.dash-lib-card-thumb--live {
padding: 0;
background: var(--v5-surface-solid);
}
.dash-lib-card-thumb-stage {
transform-origin: top left;
pointer-events: none;
user-select: none;
}
.dash-lib-card-thumb-canvas {
position: relative; width: 100%; height: 100%;
}