카드 컴포넌트생성
This commit is contained in:
parent
d609cc89b9
commit
d8358d8234
|
|
@ -1517,6 +1517,34 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
defaultConfig: component.defaultConfig,
|
defaultConfig: component.defaultConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 카드 디스플레이 컴포넌트의 경우 gridColumns에 맞는 width 계산
|
||||||
|
let componentSize = component.defaultSize;
|
||||||
|
const isCardDisplay = component.id === "card-display";
|
||||||
|
const gridColumns = isCardDisplay ? 8 : 1;
|
||||||
|
|
||||||
|
if (isCardDisplay && layout.gridSettings?.snapToGrid && gridInfo) {
|
||||||
|
// gridColumns에 맞는 정확한 너비 계산
|
||||||
|
const calculatedWidth = calculateWidthFromColumns(
|
||||||
|
gridColumns,
|
||||||
|
gridInfo,
|
||||||
|
layout.gridSettings as GridUtilSettings,
|
||||||
|
);
|
||||||
|
|
||||||
|
componentSize = {
|
||||||
|
...component.defaultSize,
|
||||||
|
width: calculatedWidth,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("📐 카드 디스플레이 초기 크기 자동 조정:", {
|
||||||
|
componentId: component.id,
|
||||||
|
gridColumns,
|
||||||
|
defaultWidth: component.defaultSize.width,
|
||||||
|
calculatedWidth,
|
||||||
|
gridInfo,
|
||||||
|
gridSettings: layout.gridSettings,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const newComponent: ComponentData = {
|
const newComponent: ComponentData = {
|
||||||
id: generateComponentId(),
|
id: generateComponentId(),
|
||||||
type: "component", // ✅ 새 컴포넌트 시스템 사용
|
type: "component", // ✅ 새 컴포넌트 시스템 사용
|
||||||
|
|
@ -1524,7 +1552,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
widgetType: component.webType,
|
widgetType: component.webType,
|
||||||
componentType: component.id, // 새 컴포넌트 시스템의 ID (DynamicComponentRenderer용)
|
componentType: component.id, // 새 컴포넌트 시스템의 ID (DynamicComponentRenderer용)
|
||||||
position: snappedPosition,
|
position: snappedPosition,
|
||||||
size: component.defaultSize,
|
size: componentSize,
|
||||||
|
gridColumns: gridColumns, // 카드 디스플레이 컴포넌트는 기본 8그리드
|
||||||
componentConfig: {
|
componentConfig: {
|
||||||
type: component.id, // 새 컴포넌트 시스템의 ID 사용
|
type: component.id, // 새 컴포넌트 시스템의 ID 사용
|
||||||
webType: component.webType, // 웹타입 정보 추가
|
webType: component.webType, // 웹타입 정보 추가
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,12 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||||
height: selectedComponent?.size?.height?.toString() || "0",
|
height: selectedComponent?.size?.height?.toString() || "0",
|
||||||
gridColumns:
|
gridColumns:
|
||||||
selectedComponent?.gridColumns?.toString() ||
|
selectedComponent?.gridColumns?.toString() ||
|
||||||
(selectedComponent?.type === "layout" && (selectedComponent as any)?.layoutType === "card-layout" ? "8" : "1"),
|
(selectedComponent?.type === "layout" && (selectedComponent as any)?.layoutType === "card-layout"
|
||||||
|
? "8"
|
||||||
|
: selectedComponent?.type === "component" &&
|
||||||
|
(selectedComponent as any)?.componentConfig?.type === "card-display"
|
||||||
|
? "8"
|
||||||
|
: "1"),
|
||||||
labelText: selectedComponent?.style?.labelText || selectedComponent?.label || "",
|
labelText: selectedComponent?.style?.labelText || selectedComponent?.label || "",
|
||||||
labelFontSize: selectedComponent?.style?.labelFontSize || "12px",
|
labelFontSize: selectedComponent?.style?.labelFontSize || "12px",
|
||||||
labelColor: selectedComponent?.style?.labelColor || "#374151",
|
labelColor: selectedComponent?.style?.labelColor || "#374151",
|
||||||
|
|
|
||||||
|
|
@ -698,4 +698,3 @@ export default function LayoutsPanel({ onDragStart }: LayoutsPanelProps) {
|
||||||
- **재사용성**: 레이아웃 템플릿 재사용으로 개발 효율성 향상
|
- **재사용성**: 레이아웃 템플릿 재사용으로 개발 효율성 향상
|
||||||
- **유연성**: 다양한 화면 요구사항에 대응 가능
|
- **유연성**: 다양한 화면 요구사항에 대응 가능
|
||||||
- **일관성**: 표준화된 레이아웃을 통한 UI 일관성 확보
|
- **일관성**: 표준화된 레이아웃을 통한 UI 일관성 확보
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,406 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useState, useMemo } from "react";
|
||||||
|
import { ComponentRendererProps } from "@/types/component";
|
||||||
|
import { CardDisplayConfig } from "./types";
|
||||||
|
import { tableTypeApi } from "@/lib/api/screen";
|
||||||
|
|
||||||
|
export interface CardDisplayComponentProps extends ComponentRendererProps {
|
||||||
|
config?: CardDisplayConfig;
|
||||||
|
tableData?: any[];
|
||||||
|
tableColumns?: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CardDisplay 컴포넌트
|
||||||
|
* 테이블 데이터를 카드 형태로 표시하는 컴포넌트
|
||||||
|
*/
|
||||||
|
export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
||||||
|
component,
|
||||||
|
isDesignMode = false,
|
||||||
|
isSelected = false,
|
||||||
|
isInteractive = false,
|
||||||
|
onClick,
|
||||||
|
onDragStart,
|
||||||
|
onDragEnd,
|
||||||
|
config,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
formData,
|
||||||
|
onFormDataChange,
|
||||||
|
screenId,
|
||||||
|
tableName,
|
||||||
|
tableData = [],
|
||||||
|
tableColumns = [],
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
// 테이블 데이터 상태 관리
|
||||||
|
const [loadedTableData, setLoadedTableData] = useState<any[]>([]);
|
||||||
|
const [loadedTableColumns, setLoadedTableColumns] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// 테이블 데이터 로딩
|
||||||
|
useEffect(() => {
|
||||||
|
const loadTableData = async () => {
|
||||||
|
// 디자인 모드에서는 테이블 데이터를 로드하지 않음
|
||||||
|
if (isDesignMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// tableName 확인 (props에서 전달받은 tableName 사용)
|
||||||
|
const tableNameToUse = tableName || component.componentConfig?.tableName;
|
||||||
|
|
||||||
|
if (!tableNameToUse) {
|
||||||
|
console.log("📋 CardDisplay: 테이블명이 설정되지 않음", {
|
||||||
|
tableName,
|
||||||
|
componentTableName: component.componentConfig?.tableName,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
console.log(`📋 CardDisplay: ${tableNameToUse} 테이블 데이터 로딩 시작`);
|
||||||
|
|
||||||
|
// 테이블 데이터와 컬럼 정보를 병렬로 로드
|
||||||
|
const [dataResponse, columnsResponse] = await Promise.all([
|
||||||
|
tableTypeApi.getTableData(tableNameToUse, {
|
||||||
|
page: 1,
|
||||||
|
size: 50, // 카드 표시용으로 적당한 개수
|
||||||
|
}),
|
||||||
|
tableTypeApi.getColumns(tableNameToUse),
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log(`📋 CardDisplay: ${tableNameToUse} 데이터 로딩 완료`, {
|
||||||
|
total: dataResponse.total,
|
||||||
|
dataLength: dataResponse.data.length,
|
||||||
|
columnsLength: columnsResponse.length,
|
||||||
|
sampleData: dataResponse.data.slice(0, 2),
|
||||||
|
sampleColumns: columnsResponse.slice(0, 3),
|
||||||
|
});
|
||||||
|
|
||||||
|
setLoadedTableData(dataResponse.data);
|
||||||
|
setLoadedTableColumns(columnsResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ CardDisplay: ${tableNameToUse} 데이터 로딩 실패`, error);
|
||||||
|
setLoadedTableData([]);
|
||||||
|
setLoadedTableColumns([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadTableData();
|
||||||
|
}, [isDesignMode, tableName, component.componentConfig?.tableName]);
|
||||||
|
|
||||||
|
// 컴포넌트 설정 (기본값 보장)
|
||||||
|
const componentConfig = {
|
||||||
|
cardsPerRow: 3, // 기본값 3 (한 행당 카드 수)
|
||||||
|
cardSpacing: 16,
|
||||||
|
cardStyle: {
|
||||||
|
showTitle: true,
|
||||||
|
showSubtitle: true,
|
||||||
|
showDescription: true,
|
||||||
|
showImage: false,
|
||||||
|
showActions: true,
|
||||||
|
maxDescriptionLength: 100,
|
||||||
|
imagePosition: "top",
|
||||||
|
imageSize: "medium",
|
||||||
|
},
|
||||||
|
columnMapping: {},
|
||||||
|
dataSource: "table",
|
||||||
|
staticData: [],
|
||||||
|
...config,
|
||||||
|
...component.config,
|
||||||
|
...component.componentConfig,
|
||||||
|
} as CardDisplayConfig;
|
||||||
|
|
||||||
|
// 컴포넌트 기본 스타일
|
||||||
|
const componentStyle: React.CSSProperties = {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
position: "relative",
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isDesignMode) {
|
||||||
|
componentStyle.border = "1px dashed #cbd5e1";
|
||||||
|
componentStyle.borderColor = isSelected ? "#3b82f6" : "#cbd5e1";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 표시할 데이터 결정 (로드된 테이블 데이터 우선 사용)
|
||||||
|
const displayData = useMemo(() => {
|
||||||
|
console.log("📋 CardDisplay: displayData 결정 중", {
|
||||||
|
dataSource: componentConfig.dataSource,
|
||||||
|
loadedTableDataLength: loadedTableData.length,
|
||||||
|
tableDataLength: tableData.length,
|
||||||
|
staticDataLength: componentConfig.staticData?.length || 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 로드된 테이블 데이터가 있으면 항상 우선 사용 (dataSource 설정 무시)
|
||||||
|
if (loadedTableData.length > 0) {
|
||||||
|
console.log("📋 CardDisplay: 로드된 테이블 데이터 사용", loadedTableData.slice(0, 2));
|
||||||
|
return loadedTableData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// props로 전달받은 테이블 데이터가 있으면 사용
|
||||||
|
if (tableData.length > 0) {
|
||||||
|
console.log("📋 CardDisplay: props 테이블 데이터 사용", tableData.slice(0, 2));
|
||||||
|
return tableData;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (componentConfig.staticData && componentConfig.staticData.length > 0) {
|
||||||
|
console.log("📋 CardDisplay: 정적 데이터 사용", componentConfig.staticData.slice(0, 2));
|
||||||
|
return componentConfig.staticData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 데이터가 없으면 빈 배열 반환
|
||||||
|
console.log("📋 CardDisplay: 표시할 데이터가 없음");
|
||||||
|
return [];
|
||||||
|
}, [componentConfig.dataSource, loadedTableData, tableData, componentConfig.staticData]);
|
||||||
|
|
||||||
|
// 실제 사용할 테이블 컬럼 정보 (로드된 컬럼 우선 사용)
|
||||||
|
const actualTableColumns = loadedTableColumns.length > 0 ? loadedTableColumns : tableColumns;
|
||||||
|
|
||||||
|
// 로딩 중인 경우 로딩 표시
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
...componentStyle,
|
||||||
|
...style,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
padding: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="text-gray-500">테이블 데이터를 로드하는 중...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 컨테이너 스타일 (원래 카드 레이아웃과 완전히 동일)
|
||||||
|
const containerStyle: React.CSSProperties = {
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: `repeat(${componentConfig.cardsPerRow || 3}, 1fr)`, // 기본값 3 (한 행당 카드 수)
|
||||||
|
gridAutoRows: "min-content", // 자동 행 생성으로 모든 데이터 표시
|
||||||
|
gap: `${componentConfig.cardSpacing || 16}px`,
|
||||||
|
padding: "16px",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
background: "transparent",
|
||||||
|
overflow: "auto",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 카드 스타일 (원래 카드 레이아웃과 완전히 동일)
|
||||||
|
const cardStyle: React.CSSProperties = {
|
||||||
|
backgroundColor: "white",
|
||||||
|
border: "1px solid #e5e7eb",
|
||||||
|
borderRadius: "8px",
|
||||||
|
padding: "16px",
|
||||||
|
boxShadow: "0 1px 3px 0 rgba(0, 0, 0, 0.1)",
|
||||||
|
transition: "all 0.2s ease-in-out",
|
||||||
|
overflow: "hidden",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
position: "relative",
|
||||||
|
minHeight: "200px",
|
||||||
|
cursor: isDesignMode ? "pointer" : "default",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 텍스트 자르기 함수
|
||||||
|
const truncateText = (text: string, maxLength: number) => {
|
||||||
|
if (!text) return "";
|
||||||
|
if (text.length <= maxLength) return text;
|
||||||
|
return text.substring(0, maxLength) + "...";
|
||||||
|
};
|
||||||
|
|
||||||
|
// 컬럼 매핑에서 값 가져오기
|
||||||
|
const getColumnValue = (data: any, columnName?: string) => {
|
||||||
|
if (!columnName) return "";
|
||||||
|
return data[columnName] || "";
|
||||||
|
};
|
||||||
|
|
||||||
|
// 컬럼명을 라벨로 변환하는 헬퍼 함수
|
||||||
|
const getColumnLabel = (columnName: string) => {
|
||||||
|
if (!actualTableColumns || actualTableColumns.length === 0) return columnName;
|
||||||
|
const column = actualTableColumns.find((col) => col.columnName === columnName);
|
||||||
|
return column?.columnLabel || columnName;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 자동 폴백 로직 - 컬럼이 설정되지 않은 경우 적절한 기본값 찾기
|
||||||
|
const getAutoFallbackValue = (data: any, type: "title" | "subtitle" | "description") => {
|
||||||
|
const keys = Object.keys(data);
|
||||||
|
switch (type) {
|
||||||
|
case "title":
|
||||||
|
// 이름 관련 필드 우선 검색
|
||||||
|
return data.name || data.title || data.label || data[keys[0]] || "제목 없음";
|
||||||
|
case "subtitle":
|
||||||
|
// 직책, 부서, 카테고리 관련 필드 검색
|
||||||
|
return data.position || data.role || data.department || data.category || data.type || "";
|
||||||
|
case "description":
|
||||||
|
// 설명, 내용 관련 필드 검색
|
||||||
|
return data.description || data.content || data.summary || data.memo || "";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 이벤트 핸들러
|
||||||
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onClick?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCardClick = (data: any) => {
|
||||||
|
if (componentConfig.onCardClick) {
|
||||||
|
componentConfig.onCardClick(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// DOM에 전달하면 안 되는 React-specific props 필터링
|
||||||
|
const {
|
||||||
|
selectedScreen,
|
||||||
|
onZoneComponentDrop,
|
||||||
|
onZoneClick,
|
||||||
|
componentConfig: _componentConfig,
|
||||||
|
component: _component,
|
||||||
|
isSelected: _isSelected,
|
||||||
|
onClick: _onClick,
|
||||||
|
onDragStart: _onDragStart,
|
||||||
|
onDragEnd: _onDragEnd,
|
||||||
|
size: _size,
|
||||||
|
position: _position,
|
||||||
|
style: _style,
|
||||||
|
...domProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<style jsx>{`
|
||||||
|
.card-hover {
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
.card-hover:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow:
|
||||||
|
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||||
|
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
border-color: #3b82f6;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
<div
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
...componentStyle,
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
onClick={handleClick}
|
||||||
|
onDragStart={onDragStart}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
{...domProps}
|
||||||
|
>
|
||||||
|
<div style={containerStyle}>
|
||||||
|
{displayData.length === 0 ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
gridColumn: "1 / -1",
|
||||||
|
textAlign: "center",
|
||||||
|
padding: "40px 20px",
|
||||||
|
color: "#6b7280",
|
||||||
|
fontSize: "14px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
표시할 데이터가 없습니다.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
displayData.map((data, index) => {
|
||||||
|
// 타이틀, 서브타이틀, 설명 값 결정 (원래 카드 레이아웃과 동일한 로직)
|
||||||
|
const titleValue =
|
||||||
|
getColumnValue(data, componentConfig.columnMapping?.titleColumn) || getAutoFallbackValue(data, "title");
|
||||||
|
|
||||||
|
const subtitleValue =
|
||||||
|
getColumnValue(data, componentConfig.columnMapping?.subtitleColumn) ||
|
||||||
|
getAutoFallbackValue(data, "subtitle");
|
||||||
|
|
||||||
|
const descriptionValue =
|
||||||
|
getColumnValue(data, componentConfig.columnMapping?.descriptionColumn) ||
|
||||||
|
getAutoFallbackValue(data, "description");
|
||||||
|
|
||||||
|
const imageValue = componentConfig.columnMapping?.imageColumn
|
||||||
|
? getColumnValue(data, componentConfig.columnMapping.imageColumn)
|
||||||
|
: data.avatar || data.image || "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={data.id || index}
|
||||||
|
style={cardStyle}
|
||||||
|
className="card-hover"
|
||||||
|
onClick={() => handleCardClick(data)}
|
||||||
|
>
|
||||||
|
{/* 카드 이미지 */}
|
||||||
|
{componentConfig.cardStyle?.showImage && componentConfig.columnMapping?.imageColumn && (
|
||||||
|
<div className="mb-3 flex justify-center">
|
||||||
|
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-gray-200">
|
||||||
|
<span className="text-xl text-gray-500">👤</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 카드 타이틀 */}
|
||||||
|
{componentConfig.cardStyle?.showTitle && (
|
||||||
|
<div className="mb-2">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">{titleValue}</h3>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 카드 서브타이틀 */}
|
||||||
|
{componentConfig.cardStyle?.showSubtitle && (
|
||||||
|
<div className="mb-2">
|
||||||
|
<p className="text-sm font-medium text-blue-600">{subtitleValue}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 카드 설명 */}
|
||||||
|
{componentConfig.cardStyle?.showDescription && (
|
||||||
|
<div className="mb-3 flex-1">
|
||||||
|
<p className="text-sm leading-relaxed text-gray-600">
|
||||||
|
{truncateText(descriptionValue, componentConfig.cardStyle?.maxDescriptionLength || 100)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 추가 표시 컬럼들 */}
|
||||||
|
{componentConfig.columnMapping?.displayColumns &&
|
||||||
|
componentConfig.columnMapping.displayColumns.length > 0 && (
|
||||||
|
<div className="space-y-1 border-t border-gray-100 pt-3">
|
||||||
|
{componentConfig.columnMapping.displayColumns.map((columnName, idx) => {
|
||||||
|
const value = getColumnValue(data, columnName);
|
||||||
|
if (!value) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={idx} className="flex justify-between text-xs">
|
||||||
|
<span className="text-gray-500 capitalize">{getColumnLabel(columnName)}:</span>
|
||||||
|
<span className="font-medium text-gray-700">{value}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 카드 액션 (선택사항) */}
|
||||||
|
<div className="mt-3 flex justify-end space-x-2">
|
||||||
|
<button className="text-xs font-medium text-blue-600 hover:text-blue-800">상세보기</button>
|
||||||
|
<button className="text-xs font-medium text-gray-500 hover:text-gray-700">편집</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,327 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface CardDisplayConfigPanelProps {
|
||||||
|
config: any;
|
||||||
|
onChange: (config: any) => void;
|
||||||
|
screenTableName?: string;
|
||||||
|
tableColumns?: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CardDisplay 설정 패널
|
||||||
|
* 카드 레이아웃과 동일한 설정 UI 제공
|
||||||
|
*/
|
||||||
|
export const CardDisplayConfigPanel: React.FC<CardDisplayConfigPanelProps> = ({
|
||||||
|
config,
|
||||||
|
onChange,
|
||||||
|
screenTableName,
|
||||||
|
tableColumns = [],
|
||||||
|
}) => {
|
||||||
|
const handleChange = (key: string, value: any) => {
|
||||||
|
onChange({ ...config, [key]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNestedChange = (path: string, value: any) => {
|
||||||
|
const keys = path.split(".");
|
||||||
|
let newConfig = { ...config };
|
||||||
|
let current = newConfig;
|
||||||
|
|
||||||
|
// 중첩 객체 생성
|
||||||
|
for (let i = 0; i < keys.length - 1; i++) {
|
||||||
|
if (!current[keys[i]]) {
|
||||||
|
current[keys[i]] = {};
|
||||||
|
}
|
||||||
|
current = current[keys[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
current[keys[keys.length - 1]] = value;
|
||||||
|
onChange(newConfig);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 표시 컬럼 추가
|
||||||
|
const addDisplayColumn = () => {
|
||||||
|
const currentColumns = config.columnMapping?.displayColumns || [];
|
||||||
|
const newColumns = [...currentColumns, ""];
|
||||||
|
handleNestedChange("columnMapping.displayColumns", newColumns);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 표시 컬럼 삭제
|
||||||
|
const removeDisplayColumn = (index: number) => {
|
||||||
|
const currentColumns = [...(config.columnMapping?.displayColumns || [])];
|
||||||
|
currentColumns.splice(index, 1);
|
||||||
|
handleNestedChange("columnMapping.displayColumns", currentColumns);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 표시 컬럼 값 변경
|
||||||
|
const updateDisplayColumn = (index: number, value: string) => {
|
||||||
|
const currentColumns = [...(config.columnMapping?.displayColumns || [])];
|
||||||
|
currentColumns[index] = value;
|
||||||
|
handleNestedChange("columnMapping.displayColumns", currentColumns);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="text-sm font-medium text-gray-700">카드 디스플레이 설정</div>
|
||||||
|
|
||||||
|
{/* 테이블이 선택된 경우 컬럼 매핑 설정 */}
|
||||||
|
{tableColumns && tableColumns.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h5 className="text-xs font-medium text-gray-700">컬럼 매핑</h5>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-xs font-medium text-gray-600">타이틀 컬럼</label>
|
||||||
|
<select
|
||||||
|
value={config.columnMapping?.titleColumn || ""}
|
||||||
|
onChange={(e) => handleNestedChange("columnMapping.titleColumn", e.target.value)}
|
||||||
|
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
||||||
|
>
|
||||||
|
<option value="">컬럼을 선택하세요</option>
|
||||||
|
{tableColumns.map((column) => (
|
||||||
|
<option key={column.columnName} value={column.columnName}>
|
||||||
|
{column.columnLabel || column.columnName} ({column.dataType})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-xs font-medium text-gray-600">서브타이틀 컬럼</label>
|
||||||
|
<select
|
||||||
|
value={config.columnMapping?.subtitleColumn || ""}
|
||||||
|
onChange={(e) => handleNestedChange("columnMapping.subtitleColumn", e.target.value)}
|
||||||
|
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
||||||
|
>
|
||||||
|
<option value="">컬럼을 선택하세요</option>
|
||||||
|
{tableColumns.map((column) => (
|
||||||
|
<option key={column.columnName} value={column.columnName}>
|
||||||
|
{column.columnLabel || column.columnName} ({column.dataType})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-xs font-medium text-gray-600">설명 컬럼</label>
|
||||||
|
<select
|
||||||
|
value={config.columnMapping?.descriptionColumn || ""}
|
||||||
|
onChange={(e) => handleNestedChange("columnMapping.descriptionColumn", e.target.value)}
|
||||||
|
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
||||||
|
>
|
||||||
|
<option value="">컬럼을 선택하세요</option>
|
||||||
|
{tableColumns.map((column) => (
|
||||||
|
<option key={column.columnName} value={column.columnName}>
|
||||||
|
{column.columnLabel || column.columnName} ({column.dataType})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-xs font-medium text-gray-600">이미지 컬럼</label>
|
||||||
|
<select
|
||||||
|
value={config.columnMapping?.imageColumn || ""}
|
||||||
|
onChange={(e) => handleNestedChange("columnMapping.imageColumn", e.target.value)}
|
||||||
|
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
||||||
|
>
|
||||||
|
<option value="">컬럼을 선택하세요</option>
|
||||||
|
{tableColumns.map((column) => (
|
||||||
|
<option key={column.columnName} value={column.columnName}>
|
||||||
|
{column.columnLabel || column.columnName} ({column.dataType})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 동적 표시 컬럼 추가 */}
|
||||||
|
<div>
|
||||||
|
<div className="mb-2 flex items-center justify-between">
|
||||||
|
<label className="text-xs font-medium text-gray-600">표시 컬럼들</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={addDisplayColumn}
|
||||||
|
className="rounded bg-blue-500 px-2 py-1 text-xs text-white hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
+ 컬럼 추가
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{(config.columnMapping?.displayColumns || []).map((column: string, index: number) => (
|
||||||
|
<div key={index} className="flex items-center space-x-2">
|
||||||
|
<select
|
||||||
|
value={column}
|
||||||
|
onChange={(e) => updateDisplayColumn(index, e.target.value)}
|
||||||
|
className="flex-1 rounded border border-gray-300 px-2 py-1 text-sm"
|
||||||
|
>
|
||||||
|
<option value="">컬럼을 선택하세요</option>
|
||||||
|
{tableColumns.map((col) => (
|
||||||
|
<option key={col.columnName} value={col.columnName}>
|
||||||
|
{col.columnLabel || col.columnName} ({col.dataType})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeDisplayColumn(index)}
|
||||||
|
className="rounded bg-red-500 px-2 py-1 text-xs text-white hover:bg-red-600"
|
||||||
|
>
|
||||||
|
삭제
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{(!config.columnMapping?.displayColumns || config.columnMapping.displayColumns.length === 0) && (
|
||||||
|
<div className="rounded border border-dashed border-gray-300 py-2 text-center text-xs text-gray-500">
|
||||||
|
"컬럼 추가" 버튼을 클릭하여 표시할 컬럼을 추가하세요
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 카드 스타일 설정 */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h5 className="text-xs font-medium text-gray-700">카드 스타일</h5>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-xs font-medium text-gray-600">한 행당 카드 수</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="6"
|
||||||
|
value={config.cardsPerRow || 3}
|
||||||
|
onChange={(e) => handleChange("cardsPerRow", parseInt(e.target.value))}
|
||||||
|
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-xs font-medium text-gray-600">카드 간격 (px)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="50"
|
||||||
|
value={config.cardSpacing || 16}
|
||||||
|
onChange={(e) => handleChange("cardSpacing", parseInt(e.target.value))}
|
||||||
|
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="showTitle"
|
||||||
|
checked={config.cardStyle?.showTitle ?? true}
|
||||||
|
onChange={(e) => handleNestedChange("cardStyle.showTitle", e.target.checked)}
|
||||||
|
className="rounded border-gray-300"
|
||||||
|
/>
|
||||||
|
<label htmlFor="showTitle" className="text-xs text-gray-600">
|
||||||
|
타이틀 표시
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="showSubtitle"
|
||||||
|
checked={config.cardStyle?.showSubtitle ?? true}
|
||||||
|
onChange={(e) => handleNestedChange("cardStyle.showSubtitle", e.target.checked)}
|
||||||
|
className="rounded border-gray-300"
|
||||||
|
/>
|
||||||
|
<label htmlFor="showSubtitle" className="text-xs text-gray-600">
|
||||||
|
서브타이틀 표시
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="showDescription"
|
||||||
|
checked={config.cardStyle?.showDescription ?? true}
|
||||||
|
onChange={(e) => handleNestedChange("cardStyle.showDescription", e.target.checked)}
|
||||||
|
className="rounded border-gray-300"
|
||||||
|
/>
|
||||||
|
<label htmlFor="showDescription" className="text-xs text-gray-600">
|
||||||
|
설명 표시
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="showImage"
|
||||||
|
checked={config.cardStyle?.showImage ?? false}
|
||||||
|
onChange={(e) => handleNestedChange("cardStyle.showImage", e.target.checked)}
|
||||||
|
className="rounded border-gray-300"
|
||||||
|
/>
|
||||||
|
<label htmlFor="showImage" className="text-xs text-gray-600">
|
||||||
|
이미지 표시
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="showActions"
|
||||||
|
checked={config.cardStyle?.showActions ?? true}
|
||||||
|
onChange={(e) => handleNestedChange("cardStyle.showActions", e.target.checked)}
|
||||||
|
className="rounded border-gray-300"
|
||||||
|
/>
|
||||||
|
<label htmlFor="showActions" className="text-xs text-gray-600">
|
||||||
|
액션 버튼 표시
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-xs font-medium text-gray-600">설명 최대 길이</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="10"
|
||||||
|
max="500"
|
||||||
|
value={config.cardStyle?.maxDescriptionLength || 100}
|
||||||
|
onChange={(e) => handleNestedChange("cardStyle.maxDescriptionLength", parseInt(e.target.value))}
|
||||||
|
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 공통 설정 */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h5 className="text-xs font-medium text-gray-700">공통 설정</h5>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="disabled"
|
||||||
|
checked={config.disabled || false}
|
||||||
|
onChange={(e) => handleChange("disabled", e.target.checked)}
|
||||||
|
className="rounded border-gray-300"
|
||||||
|
/>
|
||||||
|
<label htmlFor="disabled" className="text-xs text-gray-600">
|
||||||
|
비활성화
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="readonly"
|
||||||
|
checked={config.readonly || false}
|
||||||
|
onChange={(e) => handleChange("readonly", e.target.checked)}
|
||||||
|
className="rounded border-gray-300"
|
||||||
|
/>
|
||||||
|
<label htmlFor="readonly" className="text-xs text-gray-600">
|
||||||
|
읽기 전용
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
|
||||||
|
import { CardDisplayDefinition } from "./index";
|
||||||
|
import { CardDisplayComponent } from "./CardDisplayComponent";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CardDisplay 렌더러
|
||||||
|
* 자동 등록 시스템을 사용하여 컴포넌트를 레지스트리에 등록
|
||||||
|
*/
|
||||||
|
export class CardDisplayRenderer extends AutoRegisteringComponentRenderer {
|
||||||
|
static componentDefinition = CardDisplayDefinition;
|
||||||
|
|
||||||
|
render(): React.ReactElement {
|
||||||
|
return <CardDisplayComponent {...this.props} renderer={this} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컴포넌트별 특화 메서드들
|
||||||
|
*/
|
||||||
|
|
||||||
|
// text 타입 특화 속성 처리
|
||||||
|
protected getCardDisplayProps() {
|
||||||
|
const baseProps = this.getWebTypeProps();
|
||||||
|
|
||||||
|
// text 타입에 특화된 추가 속성들
|
||||||
|
return {
|
||||||
|
...baseProps,
|
||||||
|
// 여기에 text 타입 특화 속성들 추가
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 값 변경 처리
|
||||||
|
protected handleValueChange = (value: any) => {
|
||||||
|
this.updateComponent({ value });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 포커스 처리
|
||||||
|
protected handleFocus = () => {
|
||||||
|
// 포커스 로직
|
||||||
|
};
|
||||||
|
|
||||||
|
// 블러 처리
|
||||||
|
protected handleBlur = () => {
|
||||||
|
// 블러 로직
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 자동 등록 실행
|
||||||
|
CardDisplayRenderer.registerSelf();
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
# CardDisplay 컴포넌트
|
||||||
|
|
||||||
|
테이블 데이터를 카드 형태로 표시하는 컴포넌트
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
- **ID**: `card-display`
|
||||||
|
- **카테고리**: display
|
||||||
|
- **웹타입**: text
|
||||||
|
- **작성자**: 개발팀
|
||||||
|
- **버전**: 1.0.0
|
||||||
|
|
||||||
|
## 특징
|
||||||
|
|
||||||
|
- ✅ 자동 등록 시스템
|
||||||
|
- ✅ 타입 안전성
|
||||||
|
- ✅ Hot Reload 지원
|
||||||
|
- ✅ 설정 패널 제공
|
||||||
|
- ✅ 반응형 디자인
|
||||||
|
|
||||||
|
## 사용법
|
||||||
|
|
||||||
|
### 기본 사용법
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { CardDisplayComponent } from "@/lib/registry/components/card-display";
|
||||||
|
|
||||||
|
<CardDisplayComponent
|
||||||
|
component={{
|
||||||
|
id: "my-card-display",
|
||||||
|
type: "widget",
|
||||||
|
webType: "text",
|
||||||
|
position: { x: 100, y: 100, z: 1 },
|
||||||
|
size: { width: 200, height: 36 },
|
||||||
|
config: {
|
||||||
|
// 설정값들
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isDesignMode={false}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 설정 옵션
|
||||||
|
|
||||||
|
| 속성 | 타입 | 기본값 | 설명 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| placeholder | string | "" | 플레이스홀더 텍스트 |
|
||||||
|
| maxLength | number | 255 | 최대 입력 길이 |
|
||||||
|
| minLength | number | 0 | 최소 입력 길이 |
|
||||||
|
| disabled | boolean | false | 비활성화 여부 |
|
||||||
|
| required | boolean | false | 필수 입력 여부 |
|
||||||
|
| readonly | boolean | false | 읽기 전용 여부 |
|
||||||
|
|
||||||
|
## 이벤트
|
||||||
|
|
||||||
|
- `onChange`: 값 변경 시
|
||||||
|
- `onFocus`: 포커스 시
|
||||||
|
- `onBlur`: 포커스 해제 시
|
||||||
|
- `onClick`: 클릭 시
|
||||||
|
|
||||||
|
## 스타일링
|
||||||
|
|
||||||
|
컴포넌트는 다음과 같은 스타일 옵션을 제공합니다:
|
||||||
|
|
||||||
|
- `variant`: "default" | "outlined" | "filled"
|
||||||
|
- `size`: "sm" | "md" | "lg"
|
||||||
|
|
||||||
|
## 예시
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 기본 예시
|
||||||
|
<CardDisplayComponent
|
||||||
|
component={{
|
||||||
|
id: "sample-card-display",
|
||||||
|
config: {
|
||||||
|
placeholder: "입력하세요",
|
||||||
|
required: true,
|
||||||
|
variant: "outlined"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 개발자 정보
|
||||||
|
|
||||||
|
- **생성일**: 2025-09-15
|
||||||
|
- **CLI 명령어**: `node scripts/create-component.js card-display "카드 디스플레이" "테이블 데이터를 카드 형태로 표시하는 컴포넌트" display text`
|
||||||
|
- **경로**: `lib/registry/components/card-display/`
|
||||||
|
|
||||||
|
## 관련 문서
|
||||||
|
|
||||||
|
- [컴포넌트 시스템 가이드](../../docs/컴포넌트_시스템_가이드.md)
|
||||||
|
- [개발자 문서](https://docs.example.com/components/card-display)
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { createComponentDefinition } from "../../utils/createComponentDefinition";
|
||||||
|
import { ComponentCategory } from "@/types/component";
|
||||||
|
import type { WebType } from "@/types/screen";
|
||||||
|
import { CardDisplayComponent } from "./CardDisplayComponent";
|
||||||
|
import { CardDisplayConfigPanel } from "./CardDisplayConfigPanel";
|
||||||
|
import { CardDisplayConfig } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CardDisplay 컴포넌트 정의
|
||||||
|
* 테이블 데이터를 카드 형태로 표시하는 컴포넌트
|
||||||
|
*/
|
||||||
|
export const CardDisplayDefinition = createComponentDefinition({
|
||||||
|
id: "card-display",
|
||||||
|
name: "카드 디스플레이",
|
||||||
|
nameEng: "CardDisplay Component",
|
||||||
|
description: "테이블 데이터를 카드 형태로 표시하는 컴포넌트",
|
||||||
|
category: ComponentCategory.DISPLAY,
|
||||||
|
webType: "text",
|
||||||
|
component: CardDisplayComponent,
|
||||||
|
defaultConfig: {
|
||||||
|
cardsPerRow: 3, // 기본값 3 (한 행당 카드 수)
|
||||||
|
cardSpacing: 16,
|
||||||
|
cardStyle: {
|
||||||
|
showTitle: true,
|
||||||
|
showSubtitle: true,
|
||||||
|
showDescription: true,
|
||||||
|
showImage: false,
|
||||||
|
showActions: true,
|
||||||
|
maxDescriptionLength: 100,
|
||||||
|
imagePosition: "top",
|
||||||
|
imageSize: "medium",
|
||||||
|
},
|
||||||
|
columnMapping: {},
|
||||||
|
dataSource: "table",
|
||||||
|
staticData: [],
|
||||||
|
},
|
||||||
|
defaultSize: { width: 800, height: 400 },
|
||||||
|
configPanel: CardDisplayConfigPanel,
|
||||||
|
icon: "Grid3x3",
|
||||||
|
tags: ["card", "display", "table", "grid"],
|
||||||
|
version: "1.0.0",
|
||||||
|
author: "개발팀",
|
||||||
|
documentation:
|
||||||
|
"테이블 데이터를 카드 형태로 표시하는 컴포넌트입니다. 레이아웃과 다르게 컴포넌트로서 재사용 가능하며, 다양한 설정이 가능합니다.",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 컴포넌트는 CardDisplayRenderer에서 자동 등록됩니다
|
||||||
|
|
||||||
|
// 타입 내보내기
|
||||||
|
export type { CardDisplayConfig } from "./types";
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ComponentConfig } from "@/types/component";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 카드 스타일 설정
|
||||||
|
*/
|
||||||
|
export interface CardStyleConfig {
|
||||||
|
showTitle?: boolean;
|
||||||
|
showSubtitle?: boolean;
|
||||||
|
showDescription?: boolean;
|
||||||
|
showImage?: boolean;
|
||||||
|
maxDescriptionLength?: number;
|
||||||
|
imagePosition?: "top" | "left" | "right";
|
||||||
|
imageSize?: "small" | "medium" | "large";
|
||||||
|
showActions?: boolean; // 액션 버튼 표시 여부
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컬럼 매핑 설정
|
||||||
|
*/
|
||||||
|
export interface ColumnMappingConfig {
|
||||||
|
titleColumn?: string;
|
||||||
|
subtitleColumn?: string;
|
||||||
|
descriptionColumn?: string;
|
||||||
|
imageColumn?: string;
|
||||||
|
displayColumns?: string[];
|
||||||
|
actionColumns?: string[]; // 액션 버튼으로 표시할 컬럼들
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CardDisplay 컴포넌트 설정 타입
|
||||||
|
*/
|
||||||
|
export interface CardDisplayConfig extends ComponentConfig {
|
||||||
|
// 카드 레이아웃 설정
|
||||||
|
cardsPerRow?: number;
|
||||||
|
cardSpacing?: number;
|
||||||
|
|
||||||
|
// 카드 스타일 설정
|
||||||
|
cardStyle?: CardStyleConfig;
|
||||||
|
|
||||||
|
// 컬럼 매핑 설정
|
||||||
|
columnMapping?: ColumnMappingConfig;
|
||||||
|
|
||||||
|
// 테이블 데이터 설정
|
||||||
|
dataSource?: "static" | "table" | "api";
|
||||||
|
tableId?: string;
|
||||||
|
staticData?: any[];
|
||||||
|
|
||||||
|
// 공통 설정
|
||||||
|
disabled?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
readonly?: boolean;
|
||||||
|
helperText?: string;
|
||||||
|
|
||||||
|
// 스타일 관련
|
||||||
|
variant?: "default" | "outlined" | "filled";
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
|
||||||
|
// 이벤트 관련
|
||||||
|
onChange?: (value: any) => void;
|
||||||
|
onCardClick?: (data: any) => void;
|
||||||
|
onCardHover?: (data: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CardDisplay 컴포넌트 Props 타입
|
||||||
|
*/
|
||||||
|
export interface CardDisplayProps {
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
value?: any;
|
||||||
|
config?: CardDisplayConfig;
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
|
||||||
|
// 이벤트 핸들러
|
||||||
|
onChange?: (value: any) => void;
|
||||||
|
onFocus?: () => void;
|
||||||
|
onBlur?: () => void;
|
||||||
|
onClick?: () => void;
|
||||||
|
}
|
||||||
|
|
@ -36,6 +36,7 @@ import "./image-display/ImageDisplayRenderer";
|
||||||
import "./divider-line/DividerLineRenderer";
|
import "./divider-line/DividerLineRenderer";
|
||||||
import "./accordion-basic/AccordionBasicRenderer";
|
import "./accordion-basic/AccordionBasicRenderer";
|
||||||
import "./table-list/TableListRenderer";
|
import "./table-list/TableListRenderer";
|
||||||
|
import "./card-display/CardDisplayRenderer";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 컴포넌트 초기화 함수
|
* 컴포넌트 초기화 함수
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ const CONFIG_PANEL_MAP: Record<string, () => Promise<any>> = {
|
||||||
"divider-line": () => import("@/lib/registry/components/divider-line/DividerLineConfigPanel"),
|
"divider-line": () => import("@/lib/registry/components/divider-line/DividerLineConfigPanel"),
|
||||||
"accordion-basic": () => import("@/lib/registry/components/accordion-basic/AccordionBasicConfigPanel"),
|
"accordion-basic": () => import("@/lib/registry/components/accordion-basic/AccordionBasicConfigPanel"),
|
||||||
"table-list": () => import("@/lib/registry/components/table-list/TableListConfigPanel"),
|
"table-list": () => import("@/lib/registry/components/table-list/TableListConfigPanel"),
|
||||||
|
"card-display": () => import("@/lib/registry/components/card-display/CardDisplayConfigPanel"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// ConfigPanel 컴포넌트 캐시
|
// ConfigPanel 컴포넌트 캐시
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue