카드 컴포넌트생성

This commit is contained in:
kjs
2025-09-15 17:10:46 +09:00
parent d609cc89b9
commit d8358d8234
11 changed files with 1050 additions and 3 deletions

View File

@@ -1517,6 +1517,34 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
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 = {
id: generateComponentId(),
type: "component", // ✅ 새 컴포넌트 시스템 사용
@@ -1524,7 +1552,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
widgetType: component.webType,
componentType: component.id, // 새 컴포넌트 시스템의 ID (DynamicComponentRenderer용)
position: snappedPosition,
size: component.defaultSize,
size: componentSize,
gridColumns: gridColumns, // 카드 디스플레이 컴포넌트는 기본 8그리드
componentConfig: {
type: component.id, // 새 컴포넌트 시스템의 ID 사용
webType: component.webType, // 웹타입 정보 추가

View File

@@ -195,7 +195,12 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
height: selectedComponent?.size?.height?.toString() || "0",
gridColumns:
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 || "",
labelFontSize: selectedComponent?.style?.labelFontSize || "12px",
labelColor: selectedComponent?.style?.labelColor || "#374151",

View File

@@ -698,4 +698,3 @@ export default function LayoutsPanel({ onDragStart }: LayoutsPanelProps) {
- **재사용성**: 레이아웃 템플릿 재사용으로 개발 효율성 향상
- **유연성**: 다양한 화면 요구사항에 대응 가능
- **일관성**: 표준화된 레이아웃을 통한 UI 일관성 확보

View File

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

View File

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

View File

@@ -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();

View File

@@ -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)

View File

@@ -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";

View File

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

View File

@@ -36,6 +36,7 @@ import "./image-display/ImageDisplayRenderer";
import "./divider-line/DividerLineRenderer";
import "./accordion-basic/AccordionBasicRenderer";
import "./table-list/TableListRenderer";
import "./card-display/CardDisplayRenderer";
/**
* 컴포넌트 초기화 함수

View File

@@ -22,6 +22,7 @@ const CONFIG_PANEL_MAP: Record<string, () => Promise<any>> = {
"divider-line": () => import("@/lib/registry/components/divider-line/DividerLineConfigPanel"),
"accordion-basic": () => import("@/lib/registry/components/accordion-basic/AccordionBasicConfigPanel"),
"table-list": () => import("@/lib/registry/components/table-list/TableListConfigPanel"),
"card-display": () => import("@/lib/registry/components/card-display/CardDisplayConfigPanel"),
};
// ConfigPanel 컴포넌트 캐시