테이블 리스트 기능 수정

This commit is contained in:
kjs 2025-09-15 11:43:59 +09:00
parent c4bf8b727a
commit c243137a91
13 changed files with 2041 additions and 5 deletions

View File

@ -0,0 +1,4 @@
회사 코드: COMPANY_4
생성일: 2025-09-15T01:39:42.042Z
폴더 구조: YYYY/MM/DD/파일명
관리자: 시스템 자동 생성

View File

@ -45,6 +45,13 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
// 데이터베이스에서 입력 가능한 웹타입들을 동적으로 가져오기
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<DetailSettingsPanelProps> = ({
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);
// 개별 속성별로 업데이트하여 다른 속성과의 충돌 방지

View File

@ -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<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }

View File

@ -35,6 +35,7 @@ import "./toggle-switch/ToggleSwitchRenderer";
import "./image-display/ImageDisplayRenderer";
import "./divider-line/DividerLineRenderer";
import "./accordion-basic/AccordionBasicRenderer";
import "./table-list/TableListRenderer";
/**
*

View File

@ -0,0 +1,241 @@
# TableList 컴포넌트
데이터베이스 테이블의 데이터를 목록으로 표시하는 고급 테이블 컴포넌트
## 개요
- **ID**: `table-list`
- **카테고리**: display
- **웹타입**: table
- **작성자**: 개발팀
- **버전**: 1.0.0
## 특징
- ✅ **동적 테이블 연동**: 데이터베이스 테이블 자동 로드
- ✅ **고급 페이지네이션**: 대용량 데이터 효율적 처리
- ✅ **실시간 검색**: 빠른 데이터 검색 및 필터링
- ✅ **컬럼 커스터마이징**: 표시/숨김, 순서 변경, 정렬
- ✅ **정렬 기능**: 컬럼별 오름차순/내림차순 정렬
- ✅ **반응형 디자인**: 다양한 화면 크기 지원
- ✅ **다양한 테마**: 기본, 줄무늬, 테두리, 미니멀 테마
- ✅ **실시간 새로고침**: 데이터 자동/수동 새로고침
## 사용법
### 기본 사용법
```tsx
import { TableListComponent } from "@/lib/registry/components/table-list";
<TableListComponent
component={{
id: "my-table-list",
type: "widget",
webType: "table",
position: { x: 100, y: 100, z: 1 },
size: { width: 800, height: 400 },
config: {
selectedTable: "users",
title: "사용자 목록",
showHeader: true,
showFooter: true,
autoLoad: true,
pagination: {
enabled: true,
pageSize: 20,
showSizeSelector: true,
showPageInfo: true,
pageSizeOptions: [10, 20, 50, 100],
},
filter: {
enabled: true,
quickSearch: true,
advancedFilter: false,
},
},
}}
isDesignMode={false}
/>;
```
## 주요 설정 옵션
### 기본 설정
| 속성 | 타입 | 기본값 | 설명 |
| ------------- | ------------------------------- | ------ | ---------------------------- |
| 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
<TableListComponent
component={{
id: "user-list",
config: {
selectedTable: "users",
title: "사용자 관리",
pagination: { enabled: true, pageSize: 25 },
filter: { enabled: true, quickSearch: true },
columns: [
{ columnName: "id", displayName: "ID", visible: true, sortable: true },
{ columnName: "name", displayName: "이름", visible: true, sortable: true },
{ columnName: "email", displayName: "이메일", visible: true, sortable: true },
{ columnName: "created_at", displayName: "가입일", visible: true, format: "date" },
],
},
}}
/>
```
### 2. 매출 데이터 (통화 형식)
```tsx
<TableListComponent
component={{
id: "sales-list",
config: {
selectedTable: "sales",
title: "매출 현황",
tableStyle: { theme: "striped", rowHeight: "comfortable" },
columns: [
{ columnName: "product_name", displayName: "상품명", visible: true },
{ columnName: "amount", displayName: "금액", visible: true, format: "currency", align: "right" },
{ columnName: "quantity", displayName: "수량", visible: true, format: "number", align: "center" },
],
},
}}
/>
```
### 3. 고정 높이 테이블
```tsx
<TableListComponent
component={{
id: "fixed-table",
config: {
selectedTable: "products",
height: "fixed",
fixedHeight: 300,
stickyHeader: true,
pagination: { enabled: false },
},
}}
/>
```
## 상세설정 패널
컴포넌트 설정 패널은 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)

View File

@ -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<string, any>;
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<TableListComponentProps> = ({
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<any[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(0);
const [totalItems, setTotalItems] = useState(0);
const [searchTerm, setSearchTerm] = useState("");
const [sortColumn, setSortColumn] = useState<string | null>(null);
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({});
const [tableLabel, setTableLabel] = useState<string>("");
const [localPageSize, setLocalPageSize] = useState<number>(tableConfig.pagination?.pageSize || 20); // 로컬 페이지 크기 상태
const [selectedSearchColumn, setSelectedSearchColumn] = useState<string>(""); // 선택된 검색 컬럼
// 높이 계산 함수
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<string, string> = {};
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 (
<div style={componentStyle} className={className} {...domProps}>
<div className="flex h-full items-center justify-center rounded-lg border-2 border-dashed border-gray-300">
<div className="text-center text-gray-500">
<TableIcon className="mx-auto mb-2 h-8 w-8" />
<div className="text-sm font-medium"> </div>
<div className="text-xs text-gray-400"> </div>
</div>
</div>
</div>
);
}
return (
<div style={componentStyle} className={cn("rounded-lg border bg-white shadow-sm", className)} {...domProps}>
{/* 헤더 */}
{tableConfig.showHeader && (
<div className="flex items-center justify-between border-b p-4">
<div className="flex items-center space-x-4">
{(tableConfig.title || tableLabel) && (
<h3 className="text-lg font-medium">{tableConfig.title || tableLabel}</h3>
)}
</div>
<div className="flex items-center space-x-2">
{/* 검색 */}
{tableConfig.filter?.enabled && tableConfig.filter?.quickSearch && (
<div className="flex items-center space-x-2">
<div className="relative">
<Search className="absolute top-1/2 left-2 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
<Input
placeholder="검색..."
value={searchTerm}
onChange={(e) => handleSearch(e.target.value)}
className="w-64 pl-8"
/>
</div>
{/* 검색 컬럼 선택 드롭다운 */}
{tableConfig.filter?.showColumnSelector && (
<select
value={selectedSearchColumn}
onChange={(e) => setSelectedSearchColumn(e.target.value)}
className="min-w-[120px] rounded border px-2 py-1 text-sm"
>
<option value=""> </option>
{visibleColumns.map((column) => (
<option key={column.columnName} value={column.columnName}>
{columnLabels[column.columnName] || column.displayName || column.columnName}
</option>
))}
</select>
)}
</div>
)}
{/* 새로고침 */}
<Button variant="outline" size="sm" onClick={handleRefresh} disabled={loading}>
<RefreshCw className={cn("h-4 w-4", loading && "animate-spin")} />
</Button>
</div>
</div>
)}
{/* 테이블 컨텐츠 */}
<div className={`flex-1 ${localPageSize >= 50 ? "overflow-auto" : "overflow-hidden"}`}>
{loading ? (
<div className="flex h-full items-center justify-center">
<div className="text-center">
<RefreshCw className="mx-auto mb-2 h-8 w-8 animate-spin text-gray-400" />
<div className="text-sm text-gray-500"> ...</div>
</div>
</div>
) : error ? (
<div className="flex h-full items-center justify-center">
<div className="text-center text-red-500">
<div className="text-sm"> </div>
<div className="mt-1 text-xs text-gray-400">{error}</div>
</div>
</div>
) : (
<Table>
<TableHeader className={tableConfig.stickyHeader ? "sticky top-0 z-10 bg-white" : ""}>
<TableRow>
{visibleColumns.map((column) => (
<TableHead
key={column.columnName}
style={{ width: column.width ? `${column.width}px` : undefined }}
className={cn(
"cursor-pointer select-none",
`text-${column.align}`,
column.sortable && "hover:bg-gray-50",
)}
onClick={() => column.sortable && handleSort(column.columnName)}
>
<div className="flex items-center space-x-1">
<span>{columnLabels[column.columnName] || column.displayName}</span>
{column.sortable && (
<div className="flex flex-col">
{sortColumn === column.columnName ? (
sortDirection === "asc" ? (
<ArrowUp className="h-3 w-3" />
) : (
<ArrowDown className="h-3 w-3" />
)
) : (
<ArrowUpDown className="h-3 w-3 text-gray-400" />
)}
</div>
)}
</div>
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{data.length === 0 ? (
<TableRow>
<TableCell colSpan={visibleColumns.length} className="py-8 text-center text-gray-500">
</TableCell>
</TableRow>
) : (
data.map((row, index) => (
<TableRow
key={index}
className={cn(
"cursor-pointer",
tableConfig.tableStyle?.hoverEffect && "hover:bg-gray-50",
tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-50/50",
)}
onClick={() => handleRowClick(row)}
>
{visibleColumns.map((column) => (
<TableCell key={column.columnName} className={`text-${column.align}`}>
{formatCellValue(row[column.columnName], column.format)}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
)}
</div>
{/* 푸터/페이지네이션 */}
{tableConfig.showFooter && tableConfig.pagination?.enabled && (
<div className="flex items-center justify-between border-t p-4">
<div className="text-sm text-gray-500">
{tableConfig.pagination?.showPageInfo && (
<span>
{totalItems.toLocaleString()} {(currentPage - 1) * localPageSize + 1}-
{Math.min(currentPage * localPageSize, totalItems)}
</span>
)}
</div>
<div className="flex items-center space-x-2">
{/* 페이지 크기 선택 */}
{tableConfig.pagination?.showSizeSelector && (
<select
value={localPageSize}
onChange={(e) => {
const newPageSize = parseInt(e.target.value);
// 로컬 상태만 업데이트 (데이터베이스에 저장하지 않음)
setLocalPageSize(newPageSize);
// 페이지를 1로 리셋
setCurrentPage(1);
// 데이터는 useEffect에서 자동으로 다시 로드됨
}}
className="rounded border px-2 py-1 text-sm"
>
{tableConfig.pagination?.pageSizeOptions?.map((size) => (
<option key={size} value={size}>
{size}
</option>
))}
</select>
)}
{/* 페이지네이션 버튼 */}
<div className="flex items-center space-x-1">
<Button variant="outline" size="sm" onClick={() => handlePageChange(1)} disabled={currentPage === 1}>
<ChevronsLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<span className="px-3 py-1 text-sm">
{currentPage} / {totalPages}
</span>
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
>
<ChevronRight className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(totalPages)}
disabled={currentPage === totalPages}
>
<ChevronsRight className="h-4 w-4" />
</Button>
</div>
</div>
</div>
)}
</div>
);
};
/**
* TableList
*
*/
export const TableListWrapper: React.FC<TableListComponentProps> = (props) => {
return <TableListComponent {...props} />;
};

View File

@ -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<TableListConfig>) => void;
screenTableName?: string; // 화면에 연결된 테이블명
tableColumns?: any[]; // 테이블 컬럼 정보
}
/**
* TableList
* UI
*/
export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
config,
onChange,
screenTableName,
tableColumns,
}) => {
console.log("🔍 TableListConfigPanel props:", { config, screenTableName, tableColumns });
const [availableTables, setAvailableTables] = useState<Array<{ tableName: string; displayName: string }>>([]);
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<ColumnConfig>) => {
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 (
<div className="space-y-4">
<div className="text-sm font-medium"> </div>
<Tabs defaultValue="basic" className="w-full">
<TabsList className="grid w-full grid-cols-5">
<TabsTrigger value="basic" className="flex items-center gap-1">
<Settings className="h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="columns" className="flex items-center gap-1">
<Columns className="h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="filter" className="flex items-center gap-1">
<Filter className="h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="actions" className="flex items-center gap-1">
<MousePointer className="h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="style" className="flex items-center gap-1">
<Palette className="h-3 w-3" />
</TabsTrigger>
</TabsList>
{/* 기본 설정 탭 */}
<TabsContent value="basic" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label> </Label>
<div className="rounded-md bg-gray-50 p-3">
<div className="text-sm font-medium">
{screenTableName ? (
<span className="text-blue-600">{screenTableName}</span>
) : (
<span className="text-gray-500"> </span>
)}
</div>
{screenTableName && (
<div className="mt-1 text-xs text-gray-500"> </div>
)}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="title"></Label>
<Input
id="title"
value={config.title || ""}
onChange={(e) => handleChange("title", e.target.value)}
placeholder="테이블 제목 (선택사항)"
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
id="showHeader"
checked={config.showHeader}
onCheckedChange={(checked) => handleChange("showHeader", checked)}
/>
<Label htmlFor="showHeader"> </Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="showFooter"
checked={config.showFooter}
onCheckedChange={(checked) => handleChange("showFooter", checked)}
/>
<Label htmlFor="showFooter"> </Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="autoLoad"
checked={config.autoLoad}
onCheckedChange={(checked) => handleChange("autoLoad", checked)}
/>
<Label htmlFor="autoLoad"> </Label>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label> </Label>
<Select
value={config.height}
onValueChange={(value: "auto" | "fixed" | "viewport") => handleChange("height", value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto"></SelectItem>
<SelectItem value="fixed"></SelectItem>
<SelectItem value="viewport"> </SelectItem>
</SelectContent>
</Select>
</div>
{config.height === "fixed" && (
<div className="space-y-2">
<Label htmlFor="fixedHeight"> (px)</Label>
<Input
id="fixedHeight"
type="number"
value={config.fixedHeight || 400}
onChange={(e) => handleChange("fixedHeight", parseInt(e.target.value) || 400)}
min={200}
max={1000}
/>
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
id="paginationEnabled"
checked={config.pagination?.enabled}
onCheckedChange={(checked) => handleNestedChange("pagination", "enabled", checked)}
/>
<Label htmlFor="paginationEnabled"> </Label>
</div>
{config.pagination?.enabled && (
<>
<div className="space-y-2">
<Label htmlFor="pageSize"> </Label>
<Select
value={config.pagination?.pageSize?.toString() || "20"}
onValueChange={(value) => handleNestedChange("pagination", "pageSize", parseInt(value))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="10">10</SelectItem>
<SelectItem value="20">20</SelectItem>
<SelectItem value="50">50</SelectItem>
<SelectItem value="100">100</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="showSizeSelector"
checked={config.pagination?.showSizeSelector}
onCheckedChange={(checked) => handleNestedChange("pagination", "showSizeSelector", checked)}
/>
<Label htmlFor="showSizeSelector"> </Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="showPageInfo"
checked={config.pagination?.showPageInfo}
onCheckedChange={(checked) => handleNestedChange("pagination", "showPageInfo", checked)}
/>
<Label htmlFor="showPageInfo"> </Label>
</div>
</>
)}
</CardContent>
</Card>
</TabsContent>
{/* 컬럼 설정 탭 */}
<TabsContent value="columns" className="space-y-4">
{!screenTableName ? (
<Card>
<CardContent className="pt-6">
<div className="text-center text-gray-500">
<p> .</p>
<p className="text-sm"> .</p>
</div>
</CardContent>
</Card>
) : (
<>
<Card>
<CardHeader>
<CardTitle className="text-base"> - {screenTableName}</CardTitle>
<CardDescription>
{availableColumns.length > 0
? `${availableColumns.length}개의 사용 가능한 컬럼에서 선택하세요`
: "컬럼 정보를 불러오는 중..."}
</CardDescription>
</CardHeader>
<CardContent>
{availableColumns.length > 0 ? (
<div className="flex flex-wrap gap-2">
{availableColumns
.filter((col) => !config.columns?.find((c) => c.columnName === col.columnName))
.map((column) => (
<Button
key={column.columnName}
variant="outline"
size="sm"
onClick={() => addColumn(column.columnName)}
className="flex items-center gap-1"
>
<Plus className="h-3 w-3" />
{column.label || column.columnName}
<Badge variant="secondary" className="text-xs">
{column.dataType}
</Badge>
</Button>
))}
</div>
) : (
<div className="py-4 text-center text-gray-500">
<p> ...</p>
</div>
)}
</CardContent>
</Card>
</>
)}
{screenTableName && (
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent>
<ScrollArea className="h-64">
<div className="space-y-3">
{config.columns?.map((column, index) => (
<div key={column.columnName} className="space-y-3 rounded-lg border p-3">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Checkbox
checked={column.visible}
onCheckedChange={(checked) =>
updateColumn(column.columnName, { visible: checked as boolean })
}
/>
<span className="font-medium">
{availableColumns.find((col) => col.columnName === column.columnName)?.label ||
column.displayName ||
column.columnName}
</span>
</div>
<div className="flex items-center space-x-1">
<Button
variant="ghost"
size="sm"
onClick={() => moveColumn(column.columnName, "up")}
disabled={index === 0}
>
<ArrowUp className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => moveColumn(column.columnName, "down")}
disabled={index === (config.columns?.length || 0) - 1}
>
<ArrowDown className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => removeColumn(column.columnName)}
className="text-red-500 hover:text-red-600"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</div>
{column.visible && (
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<Label className="text-xs"></Label>
<Input
value={
availableColumns.find((col) => col.columnName === column.columnName)?.label ||
column.displayName ||
column.columnName
}
onChange={(e) => updateColumn(column.columnName, { displayName: e.target.value })}
className="h-8"
/>
</div>
<div className="space-y-1">
<Label className="text-xs"></Label>
<Select
value={column.align}
onValueChange={(value: "left" | "center" | "right") =>
updateColumn(column.columnName, { align: value })
}
>
<SelectTrigger className="h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="left"></SelectItem>
<SelectItem value="center"></SelectItem>
<SelectItem value="right"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label className="text-xs"></Label>
<Select
value={column.format}
onValueChange={(value: "text" | "number" | "date" | "currency" | "boolean") =>
updateColumn(column.columnName, { format: value })
}
>
<SelectTrigger className="h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="text"></SelectItem>
<SelectItem value="number"></SelectItem>
<SelectItem value="date"></SelectItem>
<SelectItem value="currency"></SelectItem>
<SelectItem value="boolean"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label className="text-xs"> (px)</Label>
<Input
type="number"
value={column.width || ""}
onChange={(e) =>
updateColumn(column.columnName, {
width: e.target.value ? parseInt(e.target.value) : undefined,
})
}
placeholder="자동"
className="h-8"
/>
</div>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-1">
<Checkbox
checked={column.sortable}
onCheckedChange={(checked) =>
updateColumn(column.columnName, { sortable: checked as boolean })
}
/>
<Label className="text-xs"> </Label>
</div>
<div className="flex items-center space-x-1">
<Checkbox
checked={column.searchable}
onCheckedChange={(checked) =>
updateColumn(column.columnName, { searchable: checked as boolean })
}
/>
<Label className="text-xs"> </Label>
</div>
</div>
</div>
)}
</div>
))}
</div>
</ScrollArea>
</CardContent>
</Card>
)}
</TabsContent>
{/* 필터 설정 탭 */}
<TabsContent value="filter" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
id="filterEnabled"
checked={config.filter?.enabled}
onCheckedChange={(checked) => handleNestedChange("filter", "enabled", checked)}
/>
<Label htmlFor="filterEnabled"> </Label>
</div>
{config.filter?.enabled && (
<>
<div className="flex items-center space-x-2">
<Checkbox
id="quickSearch"
checked={config.filter?.quickSearch}
onCheckedChange={(checked) => handleNestedChange("filter", "quickSearch", checked)}
/>
<Label htmlFor="quickSearch"> </Label>
</div>
{config.filter?.quickSearch && (
<div className="ml-6 flex items-center space-x-2">
<Checkbox
id="showColumnSelector"
checked={config.filter?.showColumnSelector}
onCheckedChange={(checked) => handleNestedChange("filter", "showColumnSelector", checked)}
/>
<Label htmlFor="showColumnSelector"> </Label>
</div>
)}
<div className="flex items-center space-x-2">
<Checkbox
id="advancedFilter"
checked={config.filter?.advancedFilter}
onCheckedChange={(checked) => handleNestedChange("filter", "advancedFilter", checked)}
/>
<Label htmlFor="advancedFilter"> </Label>
</div>
</>
)}
</CardContent>
</Card>
</TabsContent>
{/* 액션 설정 탭 */}
<TabsContent value="actions" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
id="showActions"
checked={config.actions?.showActions}
onCheckedChange={(checked) => handleNestedChange("actions", "showActions", checked)}
/>
<Label htmlFor="showActions"> </Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="bulkActions"
checked={config.actions?.bulkActions}
onCheckedChange={(checked) => handleNestedChange("actions", "bulkActions", checked)}
/>
<Label htmlFor="bulkActions"> </Label>
</div>
</CardContent>
</Card>
</TabsContent>
{/* 스타일 설정 탭 */}
<TabsContent value="style" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label></Label>
<Select
value={config.tableStyle?.theme}
onValueChange={(value: "default" | "striped" | "bordered" | "minimal") =>
handleNestedChange("tableStyle", "theme", value)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default"></SelectItem>
<SelectItem value="striped"></SelectItem>
<SelectItem value="bordered"></SelectItem>
<SelectItem value="minimal"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label> </Label>
<Select
value={config.tableStyle?.rowHeight}
onValueChange={(value: "compact" | "normal" | "comfortable") =>
handleNestedChange("tableStyle", "rowHeight", value)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="compact"></SelectItem>
<SelectItem value="normal"></SelectItem>
<SelectItem value="comfortable"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="alternateRows"
checked={config.tableStyle?.alternateRows}
onCheckedChange={(checked) => handleNestedChange("tableStyle", "alternateRows", checked)}
/>
<Label htmlFor="alternateRows"> </Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="hoverEffect"
checked={config.tableStyle?.hoverEffect}
onCheckedChange={(checked) => handleNestedChange("tableStyle", "hoverEffect", checked)}
/>
<Label htmlFor="hoverEffect"> </Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="stickyHeader"
checked={config.stickyHeader}
onCheckedChange={(checked) => handleChange("stickyHeader", checked)}
/>
<Label htmlFor="stickyHeader"> </Label>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
};

View File

@ -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 <TableListComponent {...this.props} renderer={this} />;
}
/**
*
*/
// text 타입 특화 속성 처리
protected getTableListProps() {
const baseProps = this.getWebTypeProps();
// text 타입에 특화된 추가 속성들
return {
...baseProps,
// 여기에 text 타입 특화 속성들 추가
};
}
// 값 변경 처리
protected handleValueChange = (value: any) => {
this.updateComponent({ value });
};
// 포커스 처리
protected handleFocus = () => {
// 포커스 로직
};
// 블러 처리
protected handleBlur = () => {
// 블러 로직
};
}
// 자동 등록 실행
TableListRenderer.registerSelf();

View File

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

View File

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

View File

@ -21,6 +21,7 @@ const CONFIG_PANEL_MAP: Record<string, () => Promise<any>> = {
"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 컴포넌트 캐시

View File

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

View File

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