From d8358d823462ae325dfcf0358ca79ac3109192fa Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 15 Sep 2025 17:10:46 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/screen/ScreenDesigner.tsx | 31 +- .../screen/panels/PropertiesPanel.tsx | 7 +- frontend/docs/레이아웃_기능_설계서.md | 1 - .../card-display/CardDisplayComponent.tsx | 406 ++++++++++++++++++ .../card-display/CardDisplayConfigPanel.tsx | 327 ++++++++++++++ .../card-display/CardDisplayRenderer.tsx | 51 +++ .../components/card-display/README.md | 93 ++++ .../registry/components/card-display/index.ts | 53 +++ .../registry/components/card-display/types.ts | 82 ++++ frontend/lib/registry/components/index.ts | 1 + .../lib/utils/getComponentConfigPanel.tsx | 1 + 11 files changed, 1050 insertions(+), 3 deletions(-) create mode 100644 frontend/lib/registry/components/card-display/CardDisplayComponent.tsx create mode 100644 frontend/lib/registry/components/card-display/CardDisplayConfigPanel.tsx create mode 100644 frontend/lib/registry/components/card-display/CardDisplayRenderer.tsx create mode 100644 frontend/lib/registry/components/card-display/README.md create mode 100644 frontend/lib/registry/components/card-display/index.ts create mode 100644 frontend/lib/registry/components/card-display/types.ts diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index ecffbad6..de2bf9a9 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -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, // 웹타입 정보 추가 diff --git a/frontend/components/screen/panels/PropertiesPanel.tsx b/frontend/components/screen/panels/PropertiesPanel.tsx index 19ce1691..2fdb0735 100644 --- a/frontend/components/screen/panels/PropertiesPanel.tsx +++ b/frontend/components/screen/panels/PropertiesPanel.tsx @@ -195,7 +195,12 @@ const PropertiesPanelComponent: React.FC = ({ 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", diff --git a/frontend/docs/레이아웃_기능_설계서.md b/frontend/docs/레이아웃_기능_설계서.md index f4360b63..acd6e627 100644 --- a/frontend/docs/레이아웃_기능_설계서.md +++ b/frontend/docs/레이아웃_기능_설계서.md @@ -698,4 +698,3 @@ export default function LayoutsPanel({ onDragStart }: LayoutsPanelProps) { - **재사용성**: 레이아웃 템플릿 재사용으로 개발 효율성 향상 - **유연성**: 다양한 화면 요구사항에 대응 가능 - **일관성**: 표준화된 레이아웃을 통한 UI 일관성 확보 - diff --git a/frontend/lib/registry/components/card-display/CardDisplayComponent.tsx b/frontend/lib/registry/components/card-display/CardDisplayComponent.tsx new file mode 100644 index 00000000..89bcb9ba --- /dev/null +++ b/frontend/lib/registry/components/card-display/CardDisplayComponent.tsx @@ -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 = ({ + component, + isDesignMode = false, + isSelected = false, + isInteractive = false, + onClick, + onDragStart, + onDragEnd, + config, + className, + style, + formData, + onFormDataChange, + screenId, + tableName, + tableData = [], + tableColumns = [], + ...props +}) => { + // 테이블 데이터 상태 관리 + const [loadedTableData, setLoadedTableData] = useState([]); + const [loadedTableColumns, setLoadedTableColumns] = useState([]); + 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 ( +
+
테이블 데이터를 로드하는 중...
+
+ ); + } + + // 컨테이너 스타일 (원래 카드 레이아웃과 완전히 동일) + 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 ( + <> + +
+
+ {displayData.length === 0 ? ( +
+ 표시할 데이터가 없습니다. +
+ ) : ( + 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 ( +
handleCardClick(data)} + > + {/* 카드 이미지 */} + {componentConfig.cardStyle?.showImage && componentConfig.columnMapping?.imageColumn && ( +
+
+ 👤 +
+
+ )} + + {/* 카드 타이틀 */} + {componentConfig.cardStyle?.showTitle && ( +
+

{titleValue}

+
+ )} + + {/* 카드 서브타이틀 */} + {componentConfig.cardStyle?.showSubtitle && ( +
+

{subtitleValue}

+
+ )} + + {/* 카드 설명 */} + {componentConfig.cardStyle?.showDescription && ( +
+

+ {truncateText(descriptionValue, componentConfig.cardStyle?.maxDescriptionLength || 100)} +

+
+ )} + + {/* 추가 표시 컬럼들 */} + {componentConfig.columnMapping?.displayColumns && + componentConfig.columnMapping.displayColumns.length > 0 && ( +
+ {componentConfig.columnMapping.displayColumns.map((columnName, idx) => { + const value = getColumnValue(data, columnName); + if (!value) return null; + + return ( +
+ {getColumnLabel(columnName)}: + {value} +
+ ); + })} +
+ )} + + {/* 카드 액션 (선택사항) */} +
+ + +
+
+ ); + }) + )} +
+
+ + ); +}; diff --git a/frontend/lib/registry/components/card-display/CardDisplayConfigPanel.tsx b/frontend/lib/registry/components/card-display/CardDisplayConfigPanel.tsx new file mode 100644 index 00000000..dc993238 --- /dev/null +++ b/frontend/lib/registry/components/card-display/CardDisplayConfigPanel.tsx @@ -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 = ({ + 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 ( +
+
카드 디스플레이 설정
+ + {/* 테이블이 선택된 경우 컬럼 매핑 설정 */} + {tableColumns && tableColumns.length > 0 && ( +
+
컬럼 매핑
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + {/* 동적 표시 컬럼 추가 */} +
+
+ + +
+ +
+ {(config.columnMapping?.displayColumns || []).map((column: string, index: number) => ( +
+ + +
+ ))} + + {(!config.columnMapping?.displayColumns || config.columnMapping.displayColumns.length === 0) && ( +
+ "컬럼 추가" 버튼을 클릭하여 표시할 컬럼을 추가하세요 +
+ )} +
+
+
+ )} + + {/* 카드 스타일 설정 */} +
+
카드 스타일
+ +
+
+ + handleChange("cardsPerRow", parseInt(e.target.value))} + className="w-full rounded border border-gray-300 px-2 py-1 text-sm" + /> +
+ +
+ + handleChange("cardSpacing", parseInt(e.target.value))} + className="w-full rounded border border-gray-300 px-2 py-1 text-sm" + /> +
+
+ +
+
+ handleNestedChange("cardStyle.showTitle", e.target.checked)} + className="rounded border-gray-300" + /> + +
+ +
+ handleNestedChange("cardStyle.showSubtitle", e.target.checked)} + className="rounded border-gray-300" + /> + +
+ +
+ handleNestedChange("cardStyle.showDescription", e.target.checked)} + className="rounded border-gray-300" + /> + +
+ +
+ handleNestedChange("cardStyle.showImage", e.target.checked)} + className="rounded border-gray-300" + /> + +
+ +
+ handleNestedChange("cardStyle.showActions", e.target.checked)} + className="rounded border-gray-300" + /> + +
+
+ +
+ + handleNestedChange("cardStyle.maxDescriptionLength", parseInt(e.target.value))} + className="w-full rounded border border-gray-300 px-2 py-1 text-sm" + /> +
+
+ + {/* 공통 설정 */} +
+
공통 설정
+ +
+ handleChange("disabled", e.target.checked)} + className="rounded border-gray-300" + /> + +
+ +
+ handleChange("readonly", e.target.checked)} + className="rounded border-gray-300" + /> + +
+
+
+ ); +}; diff --git a/frontend/lib/registry/components/card-display/CardDisplayRenderer.tsx b/frontend/lib/registry/components/card-display/CardDisplayRenderer.tsx new file mode 100644 index 00000000..79b0cea9 --- /dev/null +++ b/frontend/lib/registry/components/card-display/CardDisplayRenderer.tsx @@ -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 ; + } + + /** + * 컴포넌트별 특화 메서드들 + */ + + // text 타입 특화 속성 처리 + protected getCardDisplayProps() { + const baseProps = this.getWebTypeProps(); + + // text 타입에 특화된 추가 속성들 + return { + ...baseProps, + // 여기에 text 타입 특화 속성들 추가 + }; + } + + // 값 변경 처리 + protected handleValueChange = (value: any) => { + this.updateComponent({ value }); + }; + + // 포커스 처리 + protected handleFocus = () => { + // 포커스 로직 + }; + + // 블러 처리 + protected handleBlur = () => { + // 블러 로직 + }; +} + +// 자동 등록 실행 +CardDisplayRenderer.registerSelf(); diff --git a/frontend/lib/registry/components/card-display/README.md b/frontend/lib/registry/components/card-display/README.md new file mode 100644 index 00000000..e2811a52 --- /dev/null +++ b/frontend/lib/registry/components/card-display/README.md @@ -0,0 +1,93 @@ +# CardDisplay 컴포넌트 + +테이블 데이터를 카드 형태로 표시하는 컴포넌트 + +## 개요 + +- **ID**: `card-display` +- **카테고리**: display +- **웹타입**: text +- **작성자**: 개발팀 +- **버전**: 1.0.0 + +## 특징 + +- ✅ 자동 등록 시스템 +- ✅ 타입 안전성 +- ✅ Hot Reload 지원 +- ✅ 설정 패널 제공 +- ✅ 반응형 디자인 + +## 사용법 + +### 기본 사용법 + +```tsx +import { CardDisplayComponent } from "@/lib/registry/components/card-display"; + + +``` + +### 설정 옵션 + +| 속성 | 타입 | 기본값 | 설명 | +|------|------|--------|------| +| 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 +// 기본 예시 + +``` + +## 개발자 정보 + +- **생성일**: 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) diff --git a/frontend/lib/registry/components/card-display/index.ts b/frontend/lib/registry/components/card-display/index.ts new file mode 100644 index 00000000..1caab621 --- /dev/null +++ b/frontend/lib/registry/components/card-display/index.ts @@ -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"; diff --git a/frontend/lib/registry/components/card-display/types.ts b/frontend/lib/registry/components/card-display/types.ts new file mode 100644 index 00000000..c711125a --- /dev/null +++ b/frontend/lib/registry/components/card-display/types.ts @@ -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; +} diff --git a/frontend/lib/registry/components/index.ts b/frontend/lib/registry/components/index.ts index 7c466134..80f690f3 100644 --- a/frontend/lib/registry/components/index.ts +++ b/frontend/lib/registry/components/index.ts @@ -36,6 +36,7 @@ import "./image-display/ImageDisplayRenderer"; import "./divider-line/DividerLineRenderer"; import "./accordion-basic/AccordionBasicRenderer"; import "./table-list/TableListRenderer"; +import "./card-display/CardDisplayRenderer"; /** * 컴포넌트 초기화 함수 diff --git a/frontend/lib/utils/getComponentConfigPanel.tsx b/frontend/lib/utils/getComponentConfigPanel.tsx index 621b00c3..8ff6fd55 100644 --- a/frontend/lib/utils/getComponentConfigPanel.tsx +++ b/frontend/lib/utils/getComponentConfigPanel.tsx @@ -22,6 +22,7 @@ const CONFIG_PANEL_MAP: Record Promise> = { "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 컴포넌트 캐시