카드 컴포넌트생성
This commit is contained in:
@@ -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, // 웹타입 정보 추가
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -698,4 +698,3 @@ export default function LayoutsPanel({ onDragStart }: LayoutsPanelProps) {
|
||||
- **재사용성**: 레이아웃 템플릿 재사용으로 개발 효율성 향상
|
||||
- **유연성**: 다양한 화면 요구사항에 대응 가능
|
||||
- **일관성**: 표준화된 레이아웃을 통한 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();
|
||||
93
frontend/lib/registry/components/card-display/README.md
Normal file
93
frontend/lib/registry/components/card-display/README.md
Normal 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)
|
||||
53
frontend/lib/registry/components/card-display/index.ts
Normal file
53
frontend/lib/registry/components/card-display/index.ts
Normal 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";
|
||||
82
frontend/lib/registry/components/card-display/types.ts
Normal file
82
frontend/lib/registry/components/card-display/types.ts
Normal 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;
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import "./image-display/ImageDisplayRenderer";
|
||||
import "./divider-line/DividerLineRenderer";
|
||||
import "./accordion-basic/AccordionBasicRenderer";
|
||||
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"),
|
||||
"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 컴포넌트 캐시
|
||||
|
||||
Reference in New Issue
Block a user