테이블 리스트 기능 수정
This commit is contained in:
parent
c4bf8b727a
commit
c243137a91
|
|
@ -0,0 +1,4 @@
|
|||
회사 코드: COMPANY_4
|
||||
생성일: 2025-09-15T01:39:42.042Z
|
||||
폴더 구조: YYYY/MM/DD/파일명
|
||||
관리자: 시스템 자동 생성
|
||||
|
|
@ -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);
|
||||
// 개별 속성별로 업데이트하여 다른 속성과의 충돌 방지
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
@ -35,6 +35,7 @@ import "./toggle-switch/ToggleSwitchRenderer";
|
|||
import "./image-display/ImageDisplayRenderer";
|
||||
import "./divider-line/DividerLineRenderer";
|
||||
import "./accordion-basic/AccordionBasicRenderer";
|
||||
import "./table-list/TableListRenderer";
|
||||
|
||||
/**
|
||||
* 컴포넌트 초기화 함수
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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} />;
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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();
|
||||
|
|
@ -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";
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 컴포넌트 캐시
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue