테이블 리스트 기능 수정
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" });
|
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(`🔍 DetailSettingsPanel webTypes 로드됨:`, webTypes?.length, "개");
|
||||||
console.log(`🔍 webTypes:`, webTypes);
|
console.log(`🔍 webTypes:`, webTypes);
|
||||||
console.log(`🔍 DetailSettingsPanel selectedComponent:`, selectedComponent);
|
console.log(`🔍 DetailSettingsPanel selectedComponent:`, selectedComponent);
|
||||||
|
|
@ -1001,7 +1008,14 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
componentId={componentId}
|
componentId={componentId}
|
||||||
config={selectedComponent.componentConfig || {}}
|
config={selectedComponent.componentConfig || {}}
|
||||||
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName}
|
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) => {
|
onChange={(newConfig) => {
|
||||||
console.log("🔧 컴포넌트 설정 변경:", 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 "./image-display/ImageDisplayRenderer";
|
||||||
import "./divider-line/DividerLineRenderer";
|
import "./divider-line/DividerLineRenderer";
|
||||||
import "./accordion-basic/AccordionBasicRenderer";
|
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"),
|
"image-display": () => import("@/lib/registry/components/image-display/ImageDisplayConfigPanel"),
|
||||||
"divider-line": () => import("@/lib/registry/components/divider-line/DividerLineConfigPanel"),
|
"divider-line": () => import("@/lib/registry/components/divider-line/DividerLineConfigPanel"),
|
||||||
"accordion-basic": () => import("@/lib/registry/components/accordion-basic/AccordionBasicConfigPanel"),
|
"accordion-basic": () => import("@/lib/registry/components/accordion-basic/AccordionBasicConfigPanel"),
|
||||||
|
"table-list": () => import("@/lib/registry/components/table-list/TableListConfigPanel"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// ConfigPanel 컴포넌트 캐시
|
// ConfigPanel 컴포넌트 캐시
|
||||||
|
|
|
||||||
|
|
@ -24,15 +24,16 @@
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-progress": "^1.1.7",
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
"@radix-ui/react-radio-group": "^1.3.8",
|
"@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-select": "^2.2.6",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@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-query": "^5.86.0",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@xyflow/react": "^12.8.4",
|
|
||||||
"@types/react-window": "^1.8.8",
|
"@types/react-window": "^1.8.8",
|
||||||
|
"@xyflow/react": "^12.8.4",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.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": {
|
"node_modules/@radix-ui/react-select": {
|
||||||
"version": "2.2.6",
|
"version": "2.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
|
"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-popover": "^1.1.15",
|
||||||
"@radix-ui/react-progress": "^1.1.7",
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
"@radix-ui/react-radio-group": "^1.3.8",
|
"@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-select": "^2.2.6",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@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-query": "^5.86.0",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@xyflow/react": "^12.8.4",
|
|
||||||
"@types/react-window": "^1.8.8",
|
"@types/react-window": "^1.8.8",
|
||||||
|
"@xyflow/react": "^12.8.4",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue