diff --git a/backend-node/uploads/company_COMPANY_4/README.txt b/backend-node/uploads/company_COMPANY_4/README.txt new file mode 100644 index 00000000..1217fc78 --- /dev/null +++ b/backend-node/uploads/company_COMPANY_4/README.txt @@ -0,0 +1,4 @@ +회사 코드: COMPANY_4 +생성일: 2025-09-15T01:39:42.042Z +폴더 구조: YYYY/MM/DD/파일명 +관리자: 시스템 자동 생성 \ No newline at end of file diff --git a/frontend/components/screen/panels/DetailSettingsPanel.tsx b/frontend/components/screen/panels/DetailSettingsPanel.tsx index 1ac65dde..8543cc71 100644 --- a/frontend/components/screen/panels/DetailSettingsPanel.tsx +++ b/frontend/components/screen/panels/DetailSettingsPanel.tsx @@ -45,6 +45,13 @@ export const DetailSettingsPanel: React.FC = ({ // 데이터베이스에서 입력 가능한 웹타입들을 동적으로 가져오기 const { webTypes } = useWebTypes({ active: "Y" }); + console.log(`🔍 DetailSettingsPanel props:`, { + selectedComponent: selectedComponent?.id, + componentType: selectedComponent?.type, + currentTableName, + currentTable: currentTable?.tableName, + selectedComponentTableName: selectedComponent?.tableName, + }); console.log(`🔍 DetailSettingsPanel webTypes 로드됨:`, webTypes?.length, "개"); console.log(`🔍 webTypes:`, webTypes); console.log(`🔍 DetailSettingsPanel selectedComponent:`, selectedComponent); @@ -1001,7 +1008,14 @@ export const DetailSettingsPanel: React.FC = ({ componentId={componentId} config={selectedComponent.componentConfig || {}} screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName} - tableColumns={currentTable?.columns || []} + tableColumns={(() => { + console.log("🔍 DetailSettingsPanel tableColumns 전달:", { + currentTable, + columns: currentTable?.columns, + columnsLength: currentTable?.columns?.length, + }); + return currentTable?.columns || []; + })()} onChange={(newConfig) => { console.log("🔧 컴포넌트 설정 변경:", newConfig); // 개별 속성별로 업데이트하여 다른 속성과의 충돌 방지 diff --git a/frontend/components/ui/scroll-area.tsx b/frontend/components/ui/scroll-area.tsx new file mode 100644 index 00000000..8e4fa13f --- /dev/null +++ b/frontend/components/ui/scroll-area.tsx @@ -0,0 +1,58 @@ +"use client" + +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/lib/utils" + +function ScrollArea({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + + ) +} + +function ScrollBar({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { ScrollArea, ScrollBar } diff --git a/frontend/lib/registry/components/index.ts b/frontend/lib/registry/components/index.ts index b0892054..7c466134 100644 --- a/frontend/lib/registry/components/index.ts +++ b/frontend/lib/registry/components/index.ts @@ -35,6 +35,7 @@ import "./toggle-switch/ToggleSwitchRenderer"; import "./image-display/ImageDisplayRenderer"; import "./divider-line/DividerLineRenderer"; import "./accordion-basic/AccordionBasicRenderer"; +import "./table-list/TableListRenderer"; /** * 컴포넌트 초기화 함수 diff --git a/frontend/lib/registry/components/table-list/README.md b/frontend/lib/registry/components/table-list/README.md new file mode 100644 index 00000000..dcd9af62 --- /dev/null +++ b/frontend/lib/registry/components/table-list/README.md @@ -0,0 +1,241 @@ +# TableList 컴포넌트 + +데이터베이스 테이블의 데이터를 목록으로 표시하는 고급 테이블 컴포넌트 + +## 개요 + +- **ID**: `table-list` +- **카테고리**: display +- **웹타입**: table +- **작성자**: 개발팀 +- **버전**: 1.0.0 + +## 특징 + +- ✅ **동적 테이블 연동**: 데이터베이스 테이블 자동 로드 +- ✅ **고급 페이지네이션**: 대용량 데이터 효율적 처리 +- ✅ **실시간 검색**: 빠른 데이터 검색 및 필터링 +- ✅ **컬럼 커스터마이징**: 표시/숨김, 순서 변경, 정렬 +- ✅ **정렬 기능**: 컬럼별 오름차순/내림차순 정렬 +- ✅ **반응형 디자인**: 다양한 화면 크기 지원 +- ✅ **다양한 테마**: 기본, 줄무늬, 테두리, 미니멀 테마 +- ✅ **실시간 새로고침**: 데이터 자동/수동 새로고침 + +## 사용법 + +### 기본 사용법 + +```tsx +import { TableListComponent } from "@/lib/registry/components/table-list"; + +; +``` + +## 주요 설정 옵션 + +### 기본 설정 + +| 속성 | 타입 | 기본값 | 설명 | +| ------------- | ------------------------------- | ------ | ---------------------------- | +| selectedTable | string | - | 표시할 데이터베이스 테이블명 | +| title | string | - | 테이블 제목 | +| showHeader | boolean | true | 헤더 표시 여부 | +| showFooter | boolean | true | 푸터 표시 여부 | +| autoLoad | boolean | true | 자동 데이터 로드 | +| height | "auto" \| "fixed" \| "viewport" | "auto" | 높이 설정 모드 | +| fixedHeight | number | 400 | 고정 높이 (px) | + +### 페이지네이션 설정 + +| 속성 | 타입 | 기본값 | 설명 | +| --------------------------- | -------- | -------------- | ----------------------- | +| pagination.enabled | boolean | true | 페이지네이션 사용 여부 | +| pagination.pageSize | number | 20 | 페이지당 표시 항목 수 | +| pagination.showSizeSelector | boolean | true | 페이지 크기 선택기 표시 | +| pagination.showPageInfo | boolean | true | 페이지 정보 표시 | +| pagination.pageSizeOptions | number[] | [10,20,50,100] | 선택 가능한 페이지 크기 | + +### 컬럼 설정 + +| 속성 | 타입 | 설명 | +| --------------------- | ------------------------------------------------------- | ------------------- | +| columns | ColumnConfig[] | 컬럼 설정 배열 | +| columns[].columnName | string | 데이터베이스 컬럼명 | +| columns[].displayName | string | 화면 표시명 | +| columns[].visible | boolean | 표시 여부 | +| columns[].sortable | boolean | 정렬 가능 여부 | +| columns[].searchable | boolean | 검색 가능 여부 | +| columns[].align | "left" \| "center" \| "right" | 텍스트 정렬 | +| columns[].format | "text" \| "number" \| "date" \| "currency" \| "boolean" | 데이터 형식 | +| columns[].width | number | 컬럼 너비 (px) | +| columns[].order | number | 표시 순서 | + +### 필터 설정 + +| 속성 | 타입 | 기본값 | 설명 | +| ------------------------ | -------- | ------ | ------------------- | +| filter.enabled | boolean | true | 필터 기능 사용 여부 | +| filter.quickSearch | boolean | true | 빠른 검색 사용 여부 | +| filter.advancedFilter | boolean | false | 고급 필터 사용 여부 | +| filter.filterableColumns | string[] | [] | 필터 가능 컬럼 목록 | + +### 스타일 설정 + +| 속성 | 타입 | 기본값 | 설명 | +| ------------------------ | ------------------------------------------------- | --------- | ------------------- | +| tableStyle.theme | "default" \| "striped" \| "bordered" \| "minimal" | "default" | 테이블 테마 | +| tableStyle.headerStyle | "default" \| "dark" \| "light" | "default" | 헤더 스타일 | +| tableStyle.rowHeight | "compact" \| "normal" \| "comfortable" | "normal" | 행 높이 | +| tableStyle.alternateRows | boolean | true | 교대로 행 색상 변경 | +| tableStyle.hoverEffect | boolean | true | 마우스 오버 효과 | +| tableStyle.borderStyle | "none" \| "light" \| "heavy" | "light" | 테두리 스타일 | +| stickyHeader | boolean | false | 헤더 고정 | + +## 이벤트 + +- `onRowClick`: 행 클릭 시 +- `onRowDoubleClick`: 행 더블클릭 시 +- `onSelectionChange`: 선택 변경 시 +- `onPageChange`: 페이지 변경 시 +- `onSortChange`: 정렬 변경 시 +- `onFilterChange`: 필터 변경 시 +- `onRefresh`: 새로고침 시 + +## API 연동 + +### 테이블 목록 조회 + +``` +GET /api/tables +``` + +### 테이블 컬럼 정보 조회 + +``` +GET /api/tables/{tableName}/columns +``` + +### 테이블 데이터 조회 + +``` +GET /api/tables/{tableName}/data?page=1&limit=20&search=&sortBy=&sortDirection= +``` + +## 사용 예시 + +### 1. 기본 사용자 목록 + +```tsx + +``` + +### 2. 매출 데이터 (통화 형식) + +```tsx + +``` + +### 3. 고정 높이 테이블 + +```tsx + +``` + +## 상세설정 패널 + +컴포넌트 설정 패널은 5개의 탭으로 구성되어 있습니다: + +1. **기본 탭**: 테이블 선택, 제목, 표시 설정, 높이, 페이지네이션 +2. **컬럼 탭**: 컬럼 추가/제거, 표시 설정, 순서 변경, 형식 지정 +3. **필터 탭**: 검색 및 필터 옵션 설정 +4. **액션 탭**: 행 액션 버튼, 일괄 액션 설정 +5. **스타일 탭**: 테마, 행 높이, 색상, 효과 설정 + +## 개발자 정보 + +- **생성일**: 2025-09-12 +- **CLI 명령어**: `node scripts/create-component.js table-list "테이블 리스트" "데이터베이스 테이블의 데이터를 목록으로 표시하는 컴포넌트" display` +- **경로**: `lib/registry/components/table-list/` + +## API 요구사항 + +이 컴포넌트가 정상 작동하려면 다음 API 엔드포인트가 구현되어 있어야 합니다: + +- `GET /api/tables` - 사용 가능한 테이블 목록 +- `GET /api/tables/{tableName}/columns` - 테이블 컬럼 정보 +- `GET /api/tables/{tableName}/data` - 테이블 데이터 (페이징, 검색, 정렬 지원) + +## 관련 문서 + +- [컴포넌트 시스템 가이드](../../docs/컴포넌트_시스템_가이드.md) +- [API 문서](https://docs.example.com/api/tables) +- [개발자 문서](https://docs.example.com/components/table-list) diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx new file mode 100644 index 00000000..929c56ea --- /dev/null +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -0,0 +1,616 @@ +"use client"; + +import React, { useState, useEffect, useMemo } from "react"; +import { ComponentRendererProps } from "@/types/component"; +import { TableListConfig, ColumnConfig, TableDataResponse } from "./types"; +import { tableTypeApi } from "@/lib/api/screen"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + ChevronLeft, + ChevronRight, + ChevronsLeft, + ChevronsRight, + Search, + RefreshCw, + ArrowUpDown, + ArrowUp, + ArrowDown, + TableIcon, +} from "lucide-react"; +import { cn } from "@/lib/utils"; + +export interface TableListComponentProps { + component: any; + isDesignMode?: boolean; + isSelected?: boolean; + isInteractive?: boolean; + onClick?: () => void; + onDragStart?: (e: React.DragEvent) => void; + onDragEnd?: (e: React.DragEvent) => void; + className?: string; + style?: React.CSSProperties; + formData?: Record; + onFormDataChange?: (data: any) => void; + config?: TableListConfig; + + // 추가 props (DOM에 전달되지 않음) + size?: { width: number; height: number }; + position?: { x: number; y: number; z?: number }; + componentConfig?: any; + selectedScreen?: any; + onZoneComponentDrop?: any; + onZoneClick?: any; + tableName?: string; + onRefresh?: () => void; + onClose?: () => void; + screenId?: string; +} + +/** + * TableList 컴포넌트 + * 데이터베이스 테이블의 데이터를 목록으로 표시하는 컴포넌트 + */ +export const TableListComponent: React.FC = ({ + component, + isDesignMode = false, + isSelected = false, + isInteractive = false, + onClick, + onDragStart, + onDragEnd, + config, + className, + style, + formData, + onFormDataChange, + screenId, + size, + position, + componentConfig, + selectedScreen, + onZoneComponentDrop, + onZoneClick, + tableName, + onRefresh, + onClose, +}) => { + // 컴포넌트 설정 + const tableConfig = { + ...config, + ...component.config, + ...componentConfig, + } as TableListConfig; + + // 상태 관리 + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(0); + const [totalItems, setTotalItems] = useState(0); + const [searchTerm, setSearchTerm] = useState(""); + const [sortColumn, setSortColumn] = useState(null); + const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); + const [columnLabels, setColumnLabels] = useState>({}); + const [tableLabel, setTableLabel] = useState(""); + const [localPageSize, setLocalPageSize] = useState(tableConfig.pagination?.pageSize || 20); // 로컬 페이지 크기 상태 + const [selectedSearchColumn, setSelectedSearchColumn] = useState(""); // 선택된 검색 컬럼 + + // 높이 계산 함수 + const calculateOptimalHeight = () => { + // 50개 이상일 때는 20개 기준으로 높이 고정 + const displayPageSize = localPageSize >= 50 ? 20 : localPageSize; + const headerHeight = 48; // 테이블 헤더 + const rowHeight = 40; // 각 행 높이 (normal) + const searchHeight = tableConfig.filter?.enabled ? 48 : 0; // 검색 영역 + const footerHeight = tableConfig.showFooter ? 56 : 0; // 페이지네이션 + const padding = 8; // 여백 + + return headerHeight + displayPageSize * rowHeight + searchHeight + footerHeight + padding; + }; + + // 스타일 계산 + const componentStyle: React.CSSProperties = { + width: "100%", + height: + tableConfig.height === "fixed" + ? `${tableConfig.fixedHeight || calculateOptimalHeight()}px` + : tableConfig.height === "auto" + ? `${calculateOptimalHeight()}px` + : "100%", + ...component.style, + ...style, + display: "flex", + flexDirection: "column", + }; + + // 디자인 모드 스타일 + if (isDesignMode) { + componentStyle.border = "1px dashed #cbd5e1"; + componentStyle.borderColor = isSelected ? "#3b82f6" : "#cbd5e1"; + componentStyle.minHeight = "200px"; + } + + // 컬럼 라벨 정보 가져오기 + const fetchColumnLabels = async () => { + if (!tableConfig.selectedTable) return; + + try { + const columns = await tableTypeApi.getColumns(tableConfig.selectedTable); + const labels: Record = {}; + columns.forEach((column: any) => { + labels[column.columnName] = column.displayName || column.columnName; + }); + setColumnLabels(labels); + } catch (error) { + console.log("컬럼 라벨 정보를 가져올 수 없습니다:", error); + } + }; + + // 테이블 라벨명 가져오기 + const fetchTableLabel = async () => { + if (!tableConfig.selectedTable) return; + + try { + const tables = await tableTypeApi.getTables(); + const table = tables.find((t: any) => t.tableName === tableConfig.selectedTable); + if (table && table.displayName && table.displayName !== table.tableName) { + setTableLabel(table.displayName); + } else { + setTableLabel(tableConfig.selectedTable); + } + } catch (error) { + console.log("테이블 라벨 정보를 가져올 수 없습니다:", error); + setTableLabel(tableConfig.selectedTable); + } + }; + + // 테이블 데이터 가져오기 + const fetchTableData = async () => { + if (!tableConfig.selectedTable) { + setData([]); + return; + } + + setLoading(true); + setError(null); + + try { + // tableTypeApi.getTableData 사용 (POST /api/table-management/tables/:tableName/data) + const result = await tableTypeApi.getTableData(tableConfig.selectedTable, { + page: currentPage, + size: localPageSize, + search: searchTerm + ? (() => { + // 스마트한 단일 컬럼 선택 로직 (서버가 OR 검색을 지원하지 않음) + let searchColumn = selectedSearchColumn || sortColumn; // 사용자 선택 컬럼 최우선, 그 다음 정렬된 컬럼 + + if (!searchColumn) { + // 1순위: name 관련 컬럼 (가장 검색에 적합) + const nameColumns = visibleColumns.filter( + (col) => + col.columnName.toLowerCase().includes("name") || + col.columnName.toLowerCase().includes("title") || + col.columnName.toLowerCase().includes("subject"), + ); + + // 2순위: text/varchar 타입 컬럼 + const textColumns = visibleColumns.filter( + (col) => col.dataType === "text" || col.dataType === "varchar", + ); + + // 3순위: description 관련 컬럼 + const descColumns = visibleColumns.filter( + (col) => + col.columnName.toLowerCase().includes("desc") || + col.columnName.toLowerCase().includes("comment") || + col.columnName.toLowerCase().includes("memo"), + ); + + // 우선순위에 따라 선택 + if (nameColumns.length > 0) { + searchColumn = nameColumns[0].columnName; + } else if (textColumns.length > 0) { + searchColumn = textColumns[0].columnName; + } else if (descColumns.length > 0) { + searchColumn = descColumns[0].columnName; + } else { + // 마지막 대안: 첫 번째 컬럼 + searchColumn = visibleColumns[0]?.columnName || "id"; + } + } + + console.log("🔍 선택된 검색 컬럼:", searchColumn); + console.log("🔍 검색어:", searchTerm); + console.log( + "🔍 사용 가능한 컬럼들:", + visibleColumns.map((col) => `${col.columnName}(${col.dataType || "unknown"})`), + ); + + return { [searchColumn]: searchTerm }; + })() + : {}, + sortBy: sortColumn || undefined, + sortOrder: sortDirection, + }); + + if (result) { + setData(result.data || []); + setTotalPages(result.totalPages || 1); + setTotalItems(result.total || 0); + + // 컬럼 정보가 없으면 첫 번째 데이터 행에서 추출 + if ((!tableConfig.columns || tableConfig.columns.length === 0) && result.data.length > 0) { + const autoColumns: ColumnConfig[] = Object.keys(result.data[0]).map((key, index) => ({ + columnName: key, + displayName: columnLabels[key] || key, // 라벨명 우선 사용 + visible: true, + sortable: true, + searchable: true, + align: "left", + format: "text", + order: index, + })); + + // 컴포넌트 설정 업데이트 (부모 컴포넌트에 알림) + if (onFormDataChange) { + onFormDataChange({ + ...component, + config: { + ...tableConfig, + columns: autoColumns, + }, + }); + } + } + } + } catch (err) { + console.error("테이블 데이터 로딩 오류:", err); + setError(err instanceof Error ? err.message : "데이터를 불러오는 중 오류가 발생했습니다."); + setData([]); + } finally { + setLoading(false); + } + }; + + // 페이지 변경 + const handlePageChange = (newPage: number) => { + setCurrentPage(newPage); + }; + + // 정렬 변경 + const handleSort = (column: string) => { + if (sortColumn === column) { + setSortDirection(sortDirection === "asc" ? "desc" : "asc"); + } else { + setSortColumn(column); + setSortDirection("asc"); + } + }; + + // 검색 + const handleSearch = (term: string) => { + setSearchTerm(term); + setCurrentPage(1); // 검색 시 첫 페이지로 이동 + }; + + // 새로고침 + const handleRefresh = () => { + fetchTableData(); + }; + + // 효과 + useEffect(() => { + if (tableConfig.selectedTable) { + fetchColumnLabels(); + fetchTableLabel(); + } + }, [tableConfig.selectedTable]); + + // 컬럼 라벨이 로드되면 기존 컬럼의 displayName을 업데이트 + useEffect(() => { + if (Object.keys(columnLabels).length > 0 && tableConfig.columns && tableConfig.columns.length > 0) { + const updatedColumns = tableConfig.columns.map((col) => ({ + ...col, + displayName: columnLabels[col.columnName] || col.displayName, + })); + + // 부모 컴포넌트에 업데이트된 컬럼 정보 전달 + if (onFormDataChange) { + onFormDataChange({ + ...component, + componentConfig: { + ...tableConfig, + columns: updatedColumns, + }, + }); + } + } + }, [columnLabels]); + + useEffect(() => { + if (tableConfig.autoLoad && !isDesignMode) { + fetchTableData(); + } + }, [tableConfig.selectedTable, localPageSize, currentPage, searchTerm, sortColumn, sortDirection, columnLabels]); + + // 표시할 컬럼 계산 + const visibleColumns = useMemo(() => { + if (!tableConfig.columns) return []; + return tableConfig.columns.filter((col) => col.visible).sort((a, b) => a.order - b.order); + }, [tableConfig.columns]); + + // 값 포맷팅 + const formatCellValue = (value: any, format?: string) => { + if (value === null || value === undefined) return ""; + + switch (format) { + case "number": + return typeof value === "number" ? value.toLocaleString() : value; + case "currency": + return typeof value === "number" ? `₩${value.toLocaleString()}` : value; + case "date": + return value instanceof Date ? value.toLocaleDateString() : value; + case "boolean": + return value ? "예" : "아니오"; + default: + return String(value); + } + }; + + // 이벤트 핸들러 + const handleClick = (e: React.MouseEvent) => { + e.stopPropagation(); + onClick?.(); + }; + + // 행 클릭 핸들러 + const handleRowClick = (row: any) => { + if (tableConfig.onRowClick) { + tableConfig.onRowClick(row); + } + }; + + // DOM에 전달할 수 있는 기본 props만 정의 + const domProps = { + onClick: handleClick, + onDragStart, + onDragEnd, + }; + + // 디자인 모드에서의 플레이스홀더 + if (isDesignMode && !tableConfig.selectedTable) { + return ( +
+
+
+ +
테이블 리스트
+
설정 패널에서 테이블을 선택해주세요
+
+
+
+ ); + } + + return ( +
+ {/* 헤더 */} + {tableConfig.showHeader && ( +
+
+ {(tableConfig.title || tableLabel) && ( +

{tableConfig.title || tableLabel}

+ )} +
+ +
+ {/* 검색 */} + {tableConfig.filter?.enabled && tableConfig.filter?.quickSearch && ( +
+
+ + handleSearch(e.target.value)} + className="w-64 pl-8" + /> +
+ {/* 검색 컬럼 선택 드롭다운 */} + {tableConfig.filter?.showColumnSelector && ( + + )} +
+ )} + + {/* 새로고침 */} + +
+
+ )} + + {/* 테이블 컨텐츠 */} +
= 50 ? "overflow-auto" : "overflow-hidden"}`}> + {loading ? ( +
+
+ +
데이터를 불러오는 중...
+
+
+ ) : error ? ( +
+
+
오류가 발생했습니다
+
{error}
+
+
+ ) : ( + + + + {visibleColumns.map((column) => ( + column.sortable && handleSort(column.columnName)} + > +
+ {columnLabels[column.columnName] || column.displayName} + {column.sortable && ( +
+ {sortColumn === column.columnName ? ( + sortDirection === "asc" ? ( + + ) : ( + + ) + ) : ( + + )} +
+ )} +
+
+ ))} +
+
+ + {data.length === 0 ? ( + + + 데이터가 없습니다 + + + ) : ( + data.map((row, index) => ( + handleRowClick(row)} + > + {visibleColumns.map((column) => ( + + {formatCellValue(row[column.columnName], column.format)} + + ))} + + )) + )} + +
+ )} +
+ + {/* 푸터/페이지네이션 */} + {tableConfig.showFooter && tableConfig.pagination?.enabled && ( +
+
+ {tableConfig.pagination?.showPageInfo && ( + + 전체 {totalItems.toLocaleString()}건 중 {(currentPage - 1) * localPageSize + 1}- + {Math.min(currentPage * localPageSize, totalItems)} 표시 + + )} +
+ +
+ {/* 페이지 크기 선택 */} + {tableConfig.pagination?.showSizeSelector && ( + + )} + + {/* 페이지네이션 버튼 */} +
+ + + + + {currentPage} / {totalPages} + + + + +
+
+
+ )} +
+ ); +}; + +/** + * TableList 래퍼 컴포넌트 + * 추가적인 로직이나 상태 관리가 필요한 경우 사용 + */ +export const TableListWrapper: React.FC = (props) => { + return ; +}; diff --git a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx new file mode 100644 index 00000000..8ce02cc3 --- /dev/null +++ b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx @@ -0,0 +1,771 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Separator } from "@/components/ui/separator"; +import { TableListConfig, ColumnConfig } from "./types"; +import { + Plus, + Trash2, + ArrowUp, + ArrowDown, + Eye, + EyeOff, + Settings, + Columns, + Filter, + Palette, + MousePointer, +} from "lucide-react"; + +export interface TableListConfigPanelProps { + config: TableListConfig; + onChange: (config: Partial) => void; + screenTableName?: string; // 화면에 연결된 테이블명 + tableColumns?: any[]; // 테이블 컬럼 정보 +} + +/** + * TableList 설정 패널 + * 컴포넌트의 설정값들을 편집할 수 있는 UI 제공 + */ +export const TableListConfigPanel: React.FC = ({ + config, + onChange, + screenTableName, + tableColumns, +}) => { + console.log("🔍 TableListConfigPanel props:", { config, screenTableName, tableColumns }); + + const [availableTables, setAvailableTables] = useState>([]); + const [loadingTables, setLoadingTables] = useState(false); + const [availableColumns, setAvailableColumns] = useState< + Array<{ columnName: string; dataType: string; label?: string }> + >([]); + + // 화면 테이블명이 있으면 자동으로 설정 + useEffect(() => { + if (screenTableName && (!config.selectedTable || config.selectedTable !== screenTableName)) { + console.log("🔄 화면 테이블명 자동 설정:", screenTableName); + onChange({ selectedTable: screenTableName }); + } + }, [screenTableName, config.selectedTable, onChange]); + + // 테이블 목록 가져오기 + useEffect(() => { + const fetchTables = async () => { + setLoadingTables(true); + try { + const response = await fetch("/api/tables"); + if (response.ok) { + const result = await response.json(); + if (result.success && result.data) { + setAvailableTables( + result.data.map((table: any) => ({ + tableName: table.tableName, + displayName: table.displayName || table.tableName, + })), + ); + } + } + } catch (error) { + console.error("테이블 목록 가져오기 실패:", error); + } finally { + setLoadingTables(false); + } + }; + + fetchTables(); + }, []); + + // 선택된 테이블의 컬럼 목록 설정 (tableColumns prop 우선 사용) + useEffect(() => { + console.log("🔍 useEffect 실행됨 - tableColumns:", tableColumns, "length:", tableColumns?.length); + if (tableColumns && tableColumns.length > 0) { + // tableColumns prop이 있으면 사용 + console.log("🔧 tableColumns prop 사용:", tableColumns); + console.log("🔧 첫 번째 컬럼 상세:", tableColumns[0]); + const mappedColumns = tableColumns.map((column: any) => ({ + columnName: column.columnName || column.name, + dataType: column.dataType || column.type || "text", + label: column.label || column.displayName || column.columnLabel || column.columnName || column.name, + })); + console.log("🏷️ availableColumns 설정됨:", mappedColumns); + console.log("🏷️ 첫 번째 mappedColumn:", mappedColumns[0]); + setAvailableColumns(mappedColumns); + } else if (config.selectedTable || screenTableName) { + // API에서 컬럼 정보 가져오기 + const fetchColumns = async () => { + const tableName = config.selectedTable || screenTableName; + if (!tableName) { + setAvailableColumns([]); + return; + } + + console.log("🔧 API에서 컬럼 정보 가져오기:", tableName); + try { + const response = await fetch(`/api/tables/${tableName}/columns`); + if (response.ok) { + const result = await response.json(); + if (result.success && result.data) { + console.log("🔧 API 응답 컬럼 데이터:", result.data); + setAvailableColumns( + result.data.map((col: any) => ({ + columnName: col.columnName, + dataType: col.dataType, + label: col.displayName || col.columnName, + })), + ); + } + } + } catch (error) { + console.error("컬럼 목록 가져오기 실패:", error); + } + }; + + fetchColumns(); + } else { + setAvailableColumns([]); + } + }, [config.selectedTable, screenTableName, tableColumns]); + + const handleChange = (key: keyof TableListConfig, value: any) => { + onChange({ [key]: value }); + }; + + const handleNestedChange = (parentKey: keyof TableListConfig, childKey: string, value: any) => { + const parentValue = config[parentKey] as any; + onChange({ + [parentKey]: { + ...parentValue, + [childKey]: value, + }, + }); + }; + + // 컬럼 추가 + const addColumn = (columnName: string) => { + const existingColumn = config.columns?.find((col) => col.columnName === columnName); + if (existingColumn) return; + + // tableColumns에서 해당 컬럼의 라벨 정보 찾기 + const columnInfo = tableColumns?.find((col: any) => (col.columnName || col.name) === columnName); + + // 라벨명 우선 사용, 없으면 컬럼명 사용 + const displayName = columnInfo?.label || columnInfo?.displayName || columnName; + + const newColumn: ColumnConfig = { + columnName, + displayName, + visible: true, + sortable: true, + searchable: true, + align: "left", + format: "text", + order: config.columns?.length || 0, + }; + + handleChange("columns", [...(config.columns || []), newColumn]); + }; + + // 컬럼 제거 + const removeColumn = (columnName: string) => { + const updatedColumns = config.columns?.filter((col) => col.columnName !== columnName) || []; + handleChange("columns", updatedColumns); + }; + + // 컬럼 업데이트 + const updateColumn = (columnName: string, updates: Partial) => { + const updatedColumns = + config.columns?.map((col) => (col.columnName === columnName ? { ...col, ...updates } : col)) || []; + handleChange("columns", updatedColumns); + }; + + // 컬럼 순서 변경 + const moveColumn = (columnName: string, direction: "up" | "down") => { + const columns = [...(config.columns || [])]; + const index = columns.findIndex((col) => col.columnName === columnName); + + if (index === -1) return; + + const targetIndex = direction === "up" ? index - 1 : index + 1; + if (targetIndex < 0 || targetIndex >= columns.length) return; + + [columns[index], columns[targetIndex]] = [columns[targetIndex], columns[index]]; + + // order 값 재정렬 + columns.forEach((col, idx) => { + col.order = idx; + }); + + handleChange("columns", columns); + }; + + return ( +
+
테이블 리스트 설정
+ + + + + + 기본 + + + + 컬럼 + + + + 필터 + + + + 액션 + + + + 스타일 + + + + {/* 기본 설정 탭 */} + + + + 연결된 테이블 + 화면에 연결된 테이블 정보가 자동으로 매핑됩니다 + + +
+ +
+
+ {screenTableName ? ( + {screenTableName} + ) : ( + 테이블이 연결되지 않았습니다 + )} +
+ {screenTableName && ( +
화면 설정에서 자동으로 연결된 테이블입니다
+ )} +
+
+ +
+ + handleChange("title", e.target.value)} + placeholder="테이블 제목 (선택사항)" + /> +
+
+
+ + + + 표시 설정 + + +
+ handleChange("showHeader", checked)} + /> + +
+ +
+ handleChange("showFooter", checked)} + /> + +
+ +
+ handleChange("autoLoad", checked)} + /> + +
+
+
+ + + + 높이 설정 + + +
+ + +
+ + {config.height === "fixed" && ( +
+ + handleChange("fixedHeight", parseInt(e.target.value) || 400)} + min={200} + max={1000} + /> +
+ )} +
+
+ + + + 페이지네이션 + + +
+ handleNestedChange("pagination", "enabled", checked)} + /> + +
+ + {config.pagination?.enabled && ( + <> +
+ + +
+ +
+ handleNestedChange("pagination", "showSizeSelector", checked)} + /> + +
+ +
+ handleNestedChange("pagination", "showPageInfo", checked)} + /> + +
+ + )} +
+
+
+ + {/* 컬럼 설정 탭 */} + + {!screenTableName ? ( + + +
+

테이블이 연결되지 않았습니다.

+

화면에 테이블을 연결한 후 컬럼을 설정할 수 있습니다.

+
+
+
+ ) : ( + <> + + + 컬럼 추가 - {screenTableName} + + {availableColumns.length > 0 + ? `${availableColumns.length}개의 사용 가능한 컬럼에서 선택하세요` + : "컬럼 정보를 불러오는 중..."} + + + + {availableColumns.length > 0 ? ( +
+ {availableColumns + .filter((col) => !config.columns?.find((c) => c.columnName === col.columnName)) + .map((column) => ( + + ))} +
+ ) : ( +
+

컬럼 정보를 불러오는 중입니다...

+
+ )} +
+
+ + )} + + {screenTableName && ( + + + 컬럼 설정 + 선택된 컬럼들의 표시 옵션을 설정하세요 + + + +
+ {config.columns?.map((column, index) => ( +
+
+
+ + updateColumn(column.columnName, { visible: checked as boolean }) + } + /> + + {availableColumns.find((col) => col.columnName === column.columnName)?.label || + column.displayName || + column.columnName} + +
+ +
+ + + +
+
+ + {column.visible && ( +
+
+ + col.columnName === column.columnName)?.label || + column.displayName || + column.columnName + } + onChange={(e) => updateColumn(column.columnName, { displayName: e.target.value })} + className="h-8" + /> +
+ +
+ + +
+ +
+ + +
+ +
+ + + updateColumn(column.columnName, { + width: e.target.value ? parseInt(e.target.value) : undefined, + }) + } + placeholder="자동" + className="h-8" + /> +
+ +
+
+ + updateColumn(column.columnName, { sortable: checked as boolean }) + } + /> + +
+
+ + updateColumn(column.columnName, { searchable: checked as boolean }) + } + /> + +
+
+
+ )} +
+ ))} +
+
+
+
+ )} +
+ + {/* 필터 설정 탭 */} + + + + 검색 및 필터 + + +
+ handleNestedChange("filter", "enabled", checked)} + /> + +
+ + {config.filter?.enabled && ( + <> +
+ handleNestedChange("filter", "quickSearch", checked)} + /> + +
+ + {config.filter?.quickSearch && ( +
+ handleNestedChange("filter", "showColumnSelector", checked)} + /> + +
+ )} + +
+ handleNestedChange("filter", "advancedFilter", checked)} + /> + +
+ + )} +
+
+
+ + {/* 액션 설정 탭 */} + + + + 행 액션 + + +
+ handleNestedChange("actions", "showActions", checked)} + /> + +
+ +
+ handleNestedChange("actions", "bulkActions", checked)} + /> + +
+
+
+
+ + {/* 스타일 설정 탭 */} + + + + 테이블 스타일 + + +
+ + +
+ +
+ + +
+ +
+ handleNestedChange("tableStyle", "alternateRows", checked)} + /> + +
+ +
+ handleNestedChange("tableStyle", "hoverEffect", checked)} + /> + +
+ +
+ handleChange("stickyHeader", checked)} + /> + +
+
+
+
+
+
+ ); +}; diff --git a/frontend/lib/registry/components/table-list/TableListRenderer.tsx b/frontend/lib/registry/components/table-list/TableListRenderer.tsx new file mode 100644 index 00000000..10fa959d --- /dev/null +++ b/frontend/lib/registry/components/table-list/TableListRenderer.tsx @@ -0,0 +1,51 @@ +"use client"; + +import React from "react"; +import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer"; +import { TableListDefinition } from "./index"; +import { TableListComponent } from "./TableListComponent"; + +/** + * TableList 렌더러 + * 자동 등록 시스템을 사용하여 컴포넌트를 레지스트리에 등록 + */ +export class TableListRenderer extends AutoRegisteringComponentRenderer { + static componentDefinition = TableListDefinition; + + render(): React.ReactElement { + return ; + } + + /** + * 컴포넌트별 특화 메서드들 + */ + + // text 타입 특화 속성 처리 + protected getTableListProps() { + const baseProps = this.getWebTypeProps(); + + // text 타입에 특화된 추가 속성들 + return { + ...baseProps, + // 여기에 text 타입 특화 속성들 추가 + }; + } + + // 값 변경 처리 + protected handleValueChange = (value: any) => { + this.updateComponent({ value }); + }; + + // 포커스 처리 + protected handleFocus = () => { + // 포커스 로직 + }; + + // 블러 처리 + protected handleBlur = () => { + // 블러 로직 + }; +} + +// 자동 등록 실행 +TableListRenderer.registerSelf(); diff --git a/frontend/lib/registry/components/table-list/index.ts b/frontend/lib/registry/components/table-list/index.ts new file mode 100644 index 00000000..45ad1596 --- /dev/null +++ b/frontend/lib/registry/components/table-list/index.ts @@ -0,0 +1,84 @@ +"use client"; + +import React from "react"; +import { createComponentDefinition } from "../../utils/createComponentDefinition"; +import { ComponentCategory } from "@/types/component"; +import type { WebType } from "@/types/screen"; +import { TableListWrapper } from "./TableListComponent"; +import { TableListConfigPanel } from "./TableListConfigPanel"; +import { TableListConfig } from "./types"; + +/** + * TableList 컴포넌트 정의 + * 데이터베이스 테이블의 데이터를 목록으로 표시하는 컴포넌트 + */ +export const TableListDefinition = createComponentDefinition({ + id: "table-list", + name: "테이블 리스트", + nameEng: "TableList Component", + description: "데이터베이스 테이블의 데이터를 목록으로 표시하는 컴포넌트", + category: ComponentCategory.DISPLAY, + webType: "table", + component: TableListWrapper, + defaultConfig: { + // 테이블 기본 설정 + showHeader: true, + showFooter: true, + height: "auto", + + // 컬럼 설정 + columns: [], + autoWidth: true, + stickyHeader: false, + + // 페이지네이션 + pagination: { + enabled: true, + pageSize: 20, + showSizeSelector: true, + showPageInfo: true, + pageSizeOptions: [10, 20, 50, 100], + }, + + // 필터 설정 + filter: { + enabled: true, + quickSearch: true, + advancedFilter: false, + filterableColumns: [], + }, + + // 액션 설정 + actions: { + showActions: false, + actions: [], + bulkActions: false, + bulkActionList: [], + }, + + // 스타일 설정 + tableStyle: { + theme: "default", + headerStyle: "default", + rowHeight: "normal", + alternateRows: true, + hoverEffect: true, + borderStyle: "light", + }, + + // 데이터 로딩 + autoLoad: true, + }, + defaultSize: { width: 800, height: 960 }, + configPanel: TableListConfigPanel, + icon: "Table", + tags: ["테이블", "데이터", "목록", "그리드"], + version: "1.0.0", + author: "개발팀", + documentation: "https://docs.example.com/components/table-list", +}); + +// 컴포넌트는 TableListRenderer에서 자동 등록됩니다 + +// 타입 내보내기 +export type { TableListConfig } from "./types"; diff --git a/frontend/lib/registry/components/table-list/types.ts b/frontend/lib/registry/components/table-list/types.ts new file mode 100644 index 00000000..17e54c0b --- /dev/null +++ b/frontend/lib/registry/components/table-list/types.ts @@ -0,0 +1,162 @@ +"use client"; + +import { ComponentConfig } from "@/types/component"; + +/** + * 테이블 컬럼 설정 + */ +export interface ColumnConfig { + columnName: string; + displayName: string; + visible: boolean; + sortable: boolean; + searchable: boolean; + width?: number; + align: "left" | "center" | "right"; + format?: "text" | "number" | "date" | "currency" | "boolean"; + order: number; + dataType?: string; // 컬럼 데이터 타입 (검색 컬럼 선택에 사용) +} + +/** + * 필터 설정 + */ +export interface FilterConfig { + enabled: boolean; + quickSearch: boolean; + showColumnSelector?: boolean; // 검색 컬럼 선택기 표시 여부 + advancedFilter: boolean; + filterableColumns: string[]; + defaultFilters?: Array<{ + column: string; + operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN"; + value: any; + }>; +} + +/** + * 액션 설정 + */ +export interface ActionConfig { + showActions: boolean; + actions: Array<{ + type: "view" | "edit" | "delete" | "custom"; + label: string; + icon?: string; + color?: string; + confirmMessage?: string; + targetScreen?: string; + }>; + bulkActions: boolean; + bulkActionList: string[]; +} + +/** + * 스타일 설정 + */ +export interface TableStyleConfig { + theme: "default" | "striped" | "bordered" | "minimal"; + headerStyle: "default" | "dark" | "light"; + rowHeight: "compact" | "normal" | "comfortable"; + alternateRows: boolean; + hoverEffect: boolean; + borderStyle: "none" | "light" | "heavy"; +} + +/** + * 페이지네이션 설정 + */ +export interface PaginationConfig { + enabled: boolean; + pageSize: number; + showSizeSelector: boolean; + showPageInfo: boolean; + pageSizeOptions: number[]; +} + +/** + * TableList 컴포넌트 설정 타입 + */ +export interface TableListConfig extends ComponentConfig { + // 테이블 기본 설정 + selectedTable?: string; + tableName?: string; + title?: string; + showHeader: boolean; + showFooter: boolean; + + // 높이 설정 + height: "auto" | "fixed" | "viewport"; + fixedHeight?: number; + + // 컬럼 설정 + columns: ColumnConfig[]; + autoWidth: boolean; + stickyHeader: boolean; + + // 페이지네이션 + pagination: PaginationConfig; + + // 필터 설정 + filter: FilterConfig; + + // 액션 설정 + actions: ActionConfig; + + // 스타일 설정 + tableStyle: TableStyleConfig; + + // 데이터 로딩 + autoLoad: boolean; + refreshInterval?: number; // 초 단위 + + // 이벤트 핸들러 + onRowClick?: (row: any) => void; + onRowDoubleClick?: (row: any) => void; + onSelectionChange?: (selectedRows: any[]) => void; + onPageChange?: (page: number, pageSize: number) => void; + onSortChange?: (column: string, direction: "asc" | "desc") => void; + onFilterChange?: (filters: any) => void; +} + +/** + * 테이블 데이터 응답 타입 + */ +export interface TableDataResponse { + data: any[]; + pagination: { + page: number; + pageSize: number; + total: number; + totalPages: number; + }; + columns?: Array<{ + name: string; + type: string; + nullable: boolean; + }>; +} + +/** + * TableList 컴포넌트 Props 타입 + */ +export interface TableListProps { + id?: string; + config?: TableListConfig; + className?: string; + style?: React.CSSProperties; + + // 데이터 관련 + data?: any[]; + loading?: boolean; + error?: string; + + // 이벤트 핸들러 + onRowClick?: (row: any) => void; + onRowDoubleClick?: (row: any) => void; + onSelectionChange?: (selectedRows: any[]) => void; + onPageChange?: (page: number, pageSize: number) => void; + onSortChange?: (column: string, direction: "asc" | "desc") => void; + onFilterChange?: (filters: any) => void; + onRefresh?: () => void; +} diff --git a/frontend/lib/utils/getComponentConfigPanel.tsx b/frontend/lib/utils/getComponentConfigPanel.tsx index dd3fbed7..621b00c3 100644 --- a/frontend/lib/utils/getComponentConfigPanel.tsx +++ b/frontend/lib/utils/getComponentConfigPanel.tsx @@ -21,6 +21,7 @@ const CONFIG_PANEL_MAP: Record Promise> = { "image-display": () => import("@/lib/registry/components/image-display/ImageDisplayConfigPanel"), "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"), }; // ConfigPanel 컴포넌트 캐시 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index be40d0b6..6209004f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,15 +24,16 @@ "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-progress": "^1.1.7", "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.6", - "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-tabs": "^1.1.13", "@tanstack/react-query": "^5.86.0", "@tanstack/react-table": "^8.21.3", - "@xyflow/react": "^12.8.4", "@types/react-window": "^1.8.8", + "@xyflow/react": "^12.8.4", "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -1907,6 +1908,37 @@ } } }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-select": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", diff --git a/frontend/package.json b/frontend/package.json index 343da391..ebf3326a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,15 +30,16 @@ "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-progress": "^1.1.7", "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.6", - "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-tabs": "^1.1.13", "@tanstack/react-query": "^5.86.0", "@tanstack/react-table": "^8.21.3", - "@xyflow/react": "^12.8.4", "@types/react-window": "^1.8.8", + "@xyflow/react": "^12.8.4", "axios": "^1.11.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1",