2025-09-15 11:43:59 +09:00
|
|
|
|
"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";
|
2025-09-25 18:54:25 +09:00
|
|
|
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
2025-09-15 11:43:59 +09:00
|
|
|
|
import { TableListConfig, ColumnConfig } from "./types";
|
2025-09-16 18:02:19 +09:00
|
|
|
|
import { entityJoinApi } from "@/lib/api/entityJoin";
|
2025-09-23 17:08:52 +09:00
|
|
|
|
import { tableTypeApi } from "@/lib/api/screen";
|
2025-09-18 15:14:14 +09:00
|
|
|
|
import { Plus, Trash2, ArrowUp, ArrowDown, Settings, Columns, Filter, Palette, MousePointer } from "lucide-react";
|
2025-09-15 11:43:59 +09:00
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
}) => {
|
2025-09-25 16:22:02 +09:00
|
|
|
|
// console.log("🔍 TableListConfigPanel props:", {
|
|
|
|
|
|
// config,
|
|
|
|
|
|
// configType: typeof config,
|
|
|
|
|
|
// configSelectedTable: config?.selectedTable,
|
|
|
|
|
|
// configPagination: config?.pagination,
|
|
|
|
|
|
// paginationEnabled: config?.pagination?.enabled,
|
|
|
|
|
|
// paginationPageSize: config?.pagination?.pageSize,
|
|
|
|
|
|
// configKeys: typeof config === 'object' ? Object.keys(config || {}) : 'not object',
|
|
|
|
|
|
// screenTableName,
|
|
|
|
|
|
// tableColumns: tableColumns?.length,
|
|
|
|
|
|
// tableColumnsSample: tableColumns?.[0],
|
|
|
|
|
|
// });
|
2025-09-15 11:43:59 +09:00
|
|
|
|
|
|
|
|
|
|
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 }>
|
|
|
|
|
|
>([]);
|
2025-09-16 18:02:19 +09:00
|
|
|
|
const [entityJoinColumns, setEntityJoinColumns] = useState<{
|
|
|
|
|
|
availableColumns: Array<{
|
|
|
|
|
|
tableName: string;
|
|
|
|
|
|
columnName: string;
|
|
|
|
|
|
columnLabel: string;
|
|
|
|
|
|
dataType: string;
|
|
|
|
|
|
joinAlias: string;
|
|
|
|
|
|
suggestedLabel: string;
|
|
|
|
|
|
}>;
|
|
|
|
|
|
joinTables: Array<{
|
|
|
|
|
|
tableName: string;
|
|
|
|
|
|
currentDisplayColumn: string;
|
|
|
|
|
|
availableColumns: Array<{
|
|
|
|
|
|
columnName: string;
|
|
|
|
|
|
columnLabel: string;
|
|
|
|
|
|
dataType: string;
|
|
|
|
|
|
description?: string;
|
|
|
|
|
|
}>;
|
|
|
|
|
|
}>;
|
|
|
|
|
|
}>({ availableColumns: [], joinTables: [] });
|
2025-09-23 16:23:36 +09:00
|
|
|
|
|
2025-09-16 18:02:19 +09:00
|
|
|
|
const [loadingEntityJoins, setLoadingEntityJoins] = useState(false);
|
2025-09-15 11:43:59 +09:00
|
|
|
|
|
2025-09-25 16:22:02 +09:00
|
|
|
|
// 🔄 외부에서 config가 변경될 때 내부 상태 동기화 (표의 페이지네이션 변경 감지)
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
// console.log("🔄 TableListConfigPanel - 외부 config 변경 감지:", {
|
|
|
|
|
|
// configPagination: config?.pagination,
|
|
|
|
|
|
// configPageSize: config?.pagination?.pageSize,
|
|
|
|
|
|
// });
|
|
|
|
|
|
// 현재는 별도 내부 상태가 없어서 자동으로 UI가 업데이트됨
|
|
|
|
|
|
// 만약 내부 상태가 있다면 여기서 동기화 처리
|
|
|
|
|
|
}, [config]);
|
|
|
|
|
|
|
2025-09-23 16:23:36 +09:00
|
|
|
|
// 🎯 엔티티 컬럼 표시 설정을 위한 상태
|
2025-09-23 16:51:12 +09:00
|
|
|
|
const [entityDisplayConfigs, setEntityDisplayConfigs] = useState<
|
|
|
|
|
|
Record<
|
|
|
|
|
|
string,
|
|
|
|
|
|
{
|
|
|
|
|
|
sourceColumns: Array<{ columnName: string; displayName: string; dataType: string }>;
|
|
|
|
|
|
joinColumns: Array<{ columnName: string; displayName: string; dataType: string }>;
|
|
|
|
|
|
selectedColumns: string[];
|
|
|
|
|
|
separator: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
>({});
|
2025-09-23 16:23:36 +09:00
|
|
|
|
|
2025-10-17 15:31:23 +09:00
|
|
|
|
// 화면 테이블명이 있으면 자동으로 설정 (초기 한 번만)
|
2025-09-15 11:43:59 +09:00
|
|
|
|
useEffect(() => {
|
2025-10-17 15:31:23 +09:00
|
|
|
|
if (screenTableName && !config.selectedTable) {
|
|
|
|
|
|
// 기존 config의 모든 속성을 유지하면서 selectedTable만 추가/업데이트
|
|
|
|
|
|
const updatedConfig = {
|
|
|
|
|
|
...config,
|
|
|
|
|
|
selectedTable: screenTableName,
|
|
|
|
|
|
// 컬럼이 있으면 유지, 없으면 빈 배열
|
|
|
|
|
|
columns: config.columns || [],
|
|
|
|
|
|
};
|
|
|
|
|
|
onChange(updatedConfig);
|
2025-09-15 11:43:59 +09:00
|
|
|
|
}
|
2025-10-17 15:31:23 +09:00
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
|
}, [screenTableName]); // config.selectedTable이 없을 때만 실행되도록 의존성 최소화
|
2025-09-15 11:43:59 +09:00
|
|
|
|
|
|
|
|
|
|
// 테이블 목록 가져오기
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const fetchTables = async () => {
|
|
|
|
|
|
setLoadingTables(true);
|
|
|
|
|
|
try {
|
2025-09-23 17:08:52 +09:00
|
|
|
|
// API 클라이언트를 사용하여 올바른 포트로 호출
|
|
|
|
|
|
const response = await tableTypeApi.getTables();
|
|
|
|
|
|
setAvailableTables(
|
|
|
|
|
|
response.map((table: any) => ({
|
|
|
|
|
|
tableName: table.tableName,
|
|
|
|
|
|
displayName: table.displayName || table.tableName,
|
|
|
|
|
|
})),
|
|
|
|
|
|
);
|
2025-09-15 11:43:59 +09:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("테이블 목록 가져오기 실패:", error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoadingTables(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
fetchTables();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2025-09-23 14:26:18 +09:00
|
|
|
|
// 선택된 테이블의 컬럼 목록 설정
|
2025-09-15 11:43:59 +09:00
|
|
|
|
useEffect(() => {
|
2025-09-23 14:26:18 +09:00
|
|
|
|
console.log(
|
|
|
|
|
|
"🔍 useEffect 실행됨 - config.selectedTable:",
|
|
|
|
|
|
config.selectedTable,
|
|
|
|
|
|
"screenTableName:",
|
|
|
|
|
|
screenTableName,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-10-17 15:31:23 +09:00
|
|
|
|
// 컴포넌트에 명시적으로 테이블이 선택되었거나, 화면에 연결된 테이블이 있는 경우 컬럼 목록 표시
|
|
|
|
|
|
const shouldShowColumns = config.selectedTable || screenTableName;
|
2025-09-23 14:26:18 +09:00
|
|
|
|
|
|
|
|
|
|
if (!shouldShowColumns) {
|
2025-10-17 15:31:23 +09:00
|
|
|
|
console.log("🔧 컬럼 목록 숨김 - 테이블이 선택되지 않음");
|
2025-09-23 14:26:18 +09:00
|
|
|
|
setAvailableColumns([]);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-17 15:31:23 +09:00
|
|
|
|
// tableColumns prop을 우선 사용
|
|
|
|
|
|
if (tableColumns && tableColumns.length > 0) {
|
2025-09-15 11:43:59 +09:00
|
|
|
|
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,
|
|
|
|
|
|
}));
|
|
|
|
|
|
setAvailableColumns(mappedColumns);
|
2025-10-17 15:31:23 +09:00
|
|
|
|
|
|
|
|
|
|
// selectedTable이 없으면 screenTableName으로 설정
|
|
|
|
|
|
if (!config.selectedTable && screenTableName) {
|
|
|
|
|
|
onChange({
|
|
|
|
|
|
...config,
|
|
|
|
|
|
selectedTable: screenTableName,
|
|
|
|
|
|
columns: config.columns || [],
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-09-15 11:43:59 +09:00
|
|
|
|
} 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([]);
|
|
|
|
|
|
}
|
2025-10-17 15:31:23 +09:00
|
|
|
|
}, [config.selectedTable, screenTableName, tableColumns]);
|
2025-09-15 11:43:59 +09:00
|
|
|
|
|
2025-09-16 18:02:19 +09:00
|
|
|
|
// Entity 조인 컬럼 정보 가져오기
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const fetchEntityJoinColumns = async () => {
|
|
|
|
|
|
const tableName = config.selectedTable || screenTableName;
|
|
|
|
|
|
if (!tableName) {
|
|
|
|
|
|
setEntityJoinColumns({ availableColumns: [], joinTables: [] });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setLoadingEntityJoins(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log("🔗 Entity 조인 컬럼 정보 가져오기:", tableName);
|
|
|
|
|
|
const result = await entityJoinApi.getEntityJoinColumns(tableName);
|
|
|
|
|
|
console.log("✅ Entity 조인 컬럼 응답:", result);
|
|
|
|
|
|
|
|
|
|
|
|
setEntityJoinColumns({
|
|
|
|
|
|
availableColumns: result.availableColumns || [],
|
|
|
|
|
|
joinTables: result.joinTables || [],
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("❌ Entity 조인 컬럼 조회 오류:", error);
|
|
|
|
|
|
setEntityJoinColumns({ availableColumns: [], joinTables: [] });
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoadingEntityJoins(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
fetchEntityJoinColumns();
|
|
|
|
|
|
}, [config.selectedTable, screenTableName]);
|
|
|
|
|
|
|
2025-09-15 11:43:59 +09:00
|
|
|
|
const handleChange = (key: keyof TableListConfig, value: any) => {
|
|
|
|
|
|
onChange({ [key]: value });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleNestedChange = (parentKey: keyof TableListConfig, childKey: string, value: any) => {
|
2025-09-25 16:22:02 +09:00
|
|
|
|
// console.log("🔧 TableListConfigPanel handleNestedChange:", {
|
|
|
|
|
|
// parentKey,
|
|
|
|
|
|
// childKey,
|
|
|
|
|
|
// value,
|
|
|
|
|
|
// parentValue: config[parentKey],
|
|
|
|
|
|
// hasOnChange: !!onChange,
|
|
|
|
|
|
// onChangeType: typeof onChange,
|
|
|
|
|
|
// });
|
2025-10-17 15:31:23 +09:00
|
|
|
|
|
2025-09-15 11:43:59 +09:00
|
|
|
|
const parentValue = config[parentKey] as any;
|
2025-09-24 18:07:36 +09:00
|
|
|
|
const newConfig = {
|
2025-09-15 11:43:59 +09:00
|
|
|
|
[parentKey]: {
|
|
|
|
|
|
...parentValue,
|
|
|
|
|
|
[childKey]: value,
|
|
|
|
|
|
},
|
2025-09-24 18:07:36 +09:00
|
|
|
|
};
|
2025-10-17 15:31:23 +09:00
|
|
|
|
|
2025-09-25 16:22:02 +09:00
|
|
|
|
// console.log("📤 TableListConfigPanel onChange 호출:", newConfig);
|
2025-09-24 18:07:36 +09:00
|
|
|
|
onChange(newConfig);
|
2025-09-15 11:43:59 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼 추가
|
|
|
|
|
|
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]);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-24 10:33:54 +09:00
|
|
|
|
// 🎯 조인 컬럼 추가 (조인 탭에서 추가하는 컬럼들은 일반 컬럼으로 처리)
|
2025-09-23 16:23:36 +09:00
|
|
|
|
const addEntityColumn = (joinColumn: (typeof entityJoinColumns.availableColumns)[0]) => {
|
2025-10-17 15:31:23 +09:00
|
|
|
|
console.log("🔗 조인 컬럼 추가 요청:", {
|
|
|
|
|
|
joinColumn,
|
|
|
|
|
|
joinAlias: joinColumn.joinAlias,
|
|
|
|
|
|
columnLabel: joinColumn.columnLabel,
|
|
|
|
|
|
tableName: joinColumn.tableName,
|
|
|
|
|
|
columnName: joinColumn.columnName,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-16 18:02:19 +09:00
|
|
|
|
const existingColumn = config.columns?.find((col) => col.columnName === joinColumn.joinAlias);
|
2025-10-17 15:31:23 +09:00
|
|
|
|
if (existingColumn) {
|
|
|
|
|
|
console.warn("⚠️ 이미 존재하는 컬럼:", joinColumn.joinAlias);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🎯 joinTables에서 sourceColumn 찾기
|
|
|
|
|
|
const joinTableInfo = entityJoinColumns.joinTables?.find((jt: any) => jt.tableName === joinColumn.tableName);
|
|
|
|
|
|
const sourceColumn = joinTableInfo?.joinConfig?.sourceColumn || "";
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🔍 조인 정보 추출:", {
|
|
|
|
|
|
tableName: joinColumn.tableName,
|
|
|
|
|
|
foundJoinTable: !!joinTableInfo,
|
|
|
|
|
|
sourceColumn,
|
|
|
|
|
|
joinConfig: joinTableInfo?.joinConfig,
|
|
|
|
|
|
});
|
2025-09-16 18:02:19 +09:00
|
|
|
|
|
2025-09-24 10:33:54 +09:00
|
|
|
|
// 조인 탭에서 추가하는 컬럼들은 일반 컬럼으로 처리 (isEntityJoin: false)
|
2025-09-16 18:02:19 +09:00
|
|
|
|
const newColumn: ColumnConfig = {
|
|
|
|
|
|
columnName: joinColumn.joinAlias,
|
2025-09-23 16:23:36 +09:00
|
|
|
|
displayName: joinColumn.columnLabel,
|
2025-09-16 18:02:19 +09:00
|
|
|
|
visible: true,
|
|
|
|
|
|
sortable: true,
|
|
|
|
|
|
searchable: true,
|
|
|
|
|
|
align: "left",
|
|
|
|
|
|
format: "text",
|
|
|
|
|
|
order: config.columns?.length || 0,
|
2025-09-24 10:33:54 +09:00
|
|
|
|
isEntityJoin: false, // 조인 탭에서 추가하는 컬럼은 엔티티 타입이 아님
|
2025-10-17 15:31:23 +09:00
|
|
|
|
// 🎯 추가 조인 정보 저장
|
|
|
|
|
|
additionalJoinInfo: {
|
|
|
|
|
|
sourceTable: config.selectedTable || screenTableName || "", // 기준 테이블 (예: user_info)
|
|
|
|
|
|
sourceColumn: sourceColumn, // 기준 컬럼 (예: dept_code) - joinTables에서 추출
|
|
|
|
|
|
referenceTable: joinColumn.tableName, // 참조 테이블 (예: dept_info)
|
|
|
|
|
|
joinAlias: joinColumn.joinAlias, // 조인 별칭 (예: dept_code_company_name)
|
|
|
|
|
|
},
|
2025-09-16 18:02:19 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
handleChange("columns", [...(config.columns || []), newColumn]);
|
2025-10-17 15:31:23 +09:00
|
|
|
|
console.log("✅ 조인 컬럼 추가 완료:", {
|
|
|
|
|
|
columnName: newColumn.columnName,
|
|
|
|
|
|
displayName: newColumn.displayName,
|
|
|
|
|
|
totalColumns: (config.columns?.length || 0) + 1,
|
|
|
|
|
|
});
|
2025-09-16 18:02:19 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-15 11:43:59 +09:00
|
|
|
|
// 컬럼 제거
|
|
|
|
|
|
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);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-23 16:51:12 +09:00
|
|
|
|
// 🎯 기존 컬럼들을 체크하여 엔티티 타입인 경우 isEntityJoin 플래그 설정
|
2025-10-17 15:31:23 +09:00
|
|
|
|
// useRef로 이전 컬럼 개수를 추적하여 새 컬럼 추가 시에만 실행
|
|
|
|
|
|
const prevColumnsLengthRef = React.useRef<number>(0);
|
|
|
|
|
|
|
2025-09-23 16:51:12 +09:00
|
|
|
|
useEffect(() => {
|
2025-10-17 15:31:23 +09:00
|
|
|
|
const currentLength = config.columns?.length || 0;
|
|
|
|
|
|
const prevLength = prevColumnsLengthRef.current;
|
|
|
|
|
|
|
2025-09-23 16:51:12 +09:00
|
|
|
|
console.log("🔍 엔티티 컬럼 감지 useEffect 실행:", {
|
|
|
|
|
|
hasColumns: !!config.columns,
|
2025-10-17 15:31:23 +09:00
|
|
|
|
columnsCount: currentLength,
|
|
|
|
|
|
prevColumnsCount: prevLength,
|
2025-09-23 16:51:12 +09:00
|
|
|
|
hasTableColumns: !!tableColumns,
|
|
|
|
|
|
tableColumnsCount: tableColumns?.length || 0,
|
2025-09-23 16:59:12 +09:00
|
|
|
|
selectedTable: config.selectedTable,
|
2025-09-23 16:51:12 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-17 15:31:23 +09:00
|
|
|
|
if (!config.columns || !tableColumns || config.columns.length === 0) {
|
2025-09-23 16:51:12 +09:00
|
|
|
|
console.log("⚠️ 컬럼 또는 테이블 컬럼 정보가 없어서 엔티티 감지 스킵");
|
2025-10-17 15:31:23 +09:00
|
|
|
|
prevColumnsLengthRef.current = currentLength;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼 개수가 변경되지 않았고, 이미 체크한 적이 있으면 스킵
|
|
|
|
|
|
if (currentLength === prevLength && prevLength > 0) {
|
|
|
|
|
|
console.log("ℹ️ 컬럼 개수 변경 없음, 엔티티 감지 스킵");
|
2025-09-23 16:51:12 +09:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const updatedColumns = config.columns.map((column) => {
|
|
|
|
|
|
// 이미 isEntityJoin이 설정된 경우 스킵
|
|
|
|
|
|
if (column.isEntityJoin) {
|
|
|
|
|
|
console.log("✅ 이미 엔티티 플래그 설정됨:", column.columnName);
|
|
|
|
|
|
return column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 테이블 컬럼 정보에서 해당 컬럼 찾기
|
2025-09-23 16:59:12 +09:00
|
|
|
|
const tableColumn = tableColumns.find((tc) => tc.columnName === column.columnName);
|
2025-09-23 16:51:12 +09:00
|
|
|
|
console.log("🔍 컬럼 검색:", {
|
|
|
|
|
|
columnName: column.columnName,
|
|
|
|
|
|
found: !!tableColumn,
|
|
|
|
|
|
inputType: tableColumn?.input_type,
|
2025-09-23 16:59:12 +09:00
|
|
|
|
webType: tableColumn?.web_type,
|
2025-09-23 16:51:12 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 엔티티 타입인 경우 isEntityJoin 플래그 설정 (input_type 또는 web_type 확인)
|
|
|
|
|
|
if (tableColumn && (tableColumn.input_type === "entity" || tableColumn.web_type === "entity")) {
|
2025-09-23 17:08:52 +09:00
|
|
|
|
console.log("🎯 엔티티 컬럼 감지 및 플래그 설정:", {
|
|
|
|
|
|
columnName: column.columnName,
|
|
|
|
|
|
referenceTable: tableColumn.reference_table,
|
|
|
|
|
|
referenceTableAlt: tableColumn.referenceTable,
|
|
|
|
|
|
allTableColumnKeys: Object.keys(tableColumn),
|
|
|
|
|
|
});
|
2025-09-23 16:51:12 +09:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
...column,
|
|
|
|
|
|
isEntityJoin: true,
|
|
|
|
|
|
entityJoinInfo: {
|
2025-10-17 15:31:23 +09:00
|
|
|
|
sourceTable: config.selectedTable || screenTableName || "",
|
2025-09-23 16:51:12 +09:00
|
|
|
|
sourceColumn: column.columnName,
|
|
|
|
|
|
joinAlias: column.columnName,
|
|
|
|
|
|
},
|
|
|
|
|
|
entityDisplayConfig: {
|
|
|
|
|
|
displayColumns: [], // 빈 배열로 초기화
|
|
|
|
|
|
separator: " - ",
|
2025-10-17 15:31:23 +09:00
|
|
|
|
sourceTable: config.selectedTable || screenTableName || "",
|
2025-09-23 17:08:52 +09:00
|
|
|
|
joinTable: tableColumn.reference_table || tableColumn.referenceTable || "",
|
2025-09-23 16:51:12 +09:00
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return column;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 변경사항이 있는 경우에만 업데이트
|
|
|
|
|
|
const hasChanges = updatedColumns.some((col, index) => col.isEntityJoin !== config.columns![index].isEntityJoin);
|
|
|
|
|
|
|
|
|
|
|
|
if (hasChanges) {
|
|
|
|
|
|
console.log("🎯 엔티티 컬럼 플래그 업데이트:", updatedColumns);
|
|
|
|
|
|
handleChange("columns", updatedColumns);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log("ℹ️ 엔티티 컬럼 변경사항 없음");
|
|
|
|
|
|
}
|
2025-10-17 15:31:23 +09:00
|
|
|
|
|
|
|
|
|
|
// 현재 컬럼 개수를 저장
|
|
|
|
|
|
prevColumnsLengthRef.current = currentLength;
|
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
|
}, [config.columns?.length, tableColumns, config.selectedTable]); // 컬럼 개수 변경 시에만 실행
|
2025-09-23 16:51:12 +09:00
|
|
|
|
|
2025-09-23 16:23:36 +09:00
|
|
|
|
// 🎯 엔티티 컬럼의 표시 컬럼 정보 로드
|
|
|
|
|
|
const loadEntityDisplayConfig = async (column: ColumnConfig) => {
|
2025-09-23 17:43:24 +09:00
|
|
|
|
console.log("🔍 loadEntityDisplayConfig 시작:", {
|
|
|
|
|
|
columnName: column.columnName,
|
|
|
|
|
|
isEntityJoin: column.isEntityJoin,
|
|
|
|
|
|
entityJoinInfo: column.entityJoinInfo,
|
|
|
|
|
|
entityDisplayConfig: column.entityDisplayConfig,
|
|
|
|
|
|
configSelectedTable: config.selectedTable,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!column.isEntityJoin || !column.entityJoinInfo) {
|
|
|
|
|
|
console.log("⚠️ 엔티티 컬럼 조건 불만족:", {
|
|
|
|
|
|
isEntityJoin: column.isEntityJoin,
|
|
|
|
|
|
entityJoinInfo: column.entityJoinInfo,
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// entityDisplayConfig가 없으면 초기화
|
|
|
|
|
|
if (!column.entityDisplayConfig) {
|
|
|
|
|
|
console.log("🔧 entityDisplayConfig 초기화:", column.columnName);
|
2025-10-17 15:31:23 +09:00
|
|
|
|
|
|
|
|
|
|
// sourceTable을 결정: entityJoinInfo -> config.selectedTable -> screenTableName 순서
|
|
|
|
|
|
const initialSourceTable = column.entityJoinInfo?.sourceTable || config.selectedTable || screenTableName;
|
|
|
|
|
|
|
|
|
|
|
|
if (!initialSourceTable) {
|
|
|
|
|
|
console.warn("⚠️ sourceTable을 결정할 수 없어서 초기화 실패:", column.columnName);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-23 17:43:24 +09:00
|
|
|
|
const updatedColumns = config.columns?.map((col) => {
|
|
|
|
|
|
if (col.columnName === column.columnName) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
...col,
|
|
|
|
|
|
entityDisplayConfig: {
|
|
|
|
|
|
displayColumns: [],
|
|
|
|
|
|
separator: " - ",
|
2025-10-17 15:31:23 +09:00
|
|
|
|
sourceTable: initialSourceTable,
|
2025-09-23 17:43:24 +09:00
|
|
|
|
joinTable: "",
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return col;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (updatedColumns) {
|
|
|
|
|
|
handleChange("columns", updatedColumns);
|
|
|
|
|
|
// 업데이트된 컬럼으로 다시 시도
|
|
|
|
|
|
const updatedColumn = updatedColumns.find((col) => col.columnName === column.columnName);
|
|
|
|
|
|
if (updatedColumn) {
|
|
|
|
|
|
console.log("🔄 업데이트된 컬럼으로 재시도:", updatedColumn.entityDisplayConfig);
|
|
|
|
|
|
return loadEntityDisplayConfig(updatedColumn);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🔍 entityDisplayConfig 전체 구조:", column.entityDisplayConfig);
|
|
|
|
|
|
console.log("🔍 entityDisplayConfig 키들:", Object.keys(column.entityDisplayConfig));
|
|
|
|
|
|
|
2025-10-17 15:31:23 +09:00
|
|
|
|
// sourceTable 결정 우선순위:
|
|
|
|
|
|
// 1. entityDisplayConfig.sourceTable
|
|
|
|
|
|
// 2. entityJoinInfo.sourceTable
|
|
|
|
|
|
// 3. config.selectedTable
|
|
|
|
|
|
// 4. screenTableName
|
|
|
|
|
|
let sourceTable =
|
|
|
|
|
|
column.entityDisplayConfig.sourceTable ||
|
|
|
|
|
|
column.entityJoinInfo?.sourceTable ||
|
|
|
|
|
|
config.selectedTable ||
|
|
|
|
|
|
screenTableName;
|
|
|
|
|
|
|
2025-09-23 17:43:24 +09:00
|
|
|
|
let joinTable = column.entityDisplayConfig.joinTable;
|
|
|
|
|
|
|
2025-10-17 15:31:23 +09:00
|
|
|
|
// sourceTable이 여전히 비어있으면 에러
|
|
|
|
|
|
if (!sourceTable) {
|
|
|
|
|
|
console.error("❌ sourceTable이 비어있어서 처리 불가:", {
|
|
|
|
|
|
columnName: column.columnName,
|
|
|
|
|
|
entityDisplayConfig: column.entityDisplayConfig,
|
|
|
|
|
|
entityJoinInfo: column.entityJoinInfo,
|
|
|
|
|
|
configSelectedTable: config.selectedTable,
|
|
|
|
|
|
screenTableName,
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
2025-09-23 17:43:24 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-17 15:31:23 +09:00
|
|
|
|
console.log("✅ sourceTable 결정됨:", sourceTable);
|
|
|
|
|
|
|
|
|
|
|
|
if (!joinTable && sourceTable) {
|
2025-09-23 17:43:24 +09:00
|
|
|
|
// joinTable이 없으면 tableTypeApi로 조회해서 설정
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log("🔍 joinTable이 없어서 tableTypeApi로 조회:", sourceTable);
|
|
|
|
|
|
const columnList = await tableTypeApi.getColumns(sourceTable);
|
|
|
|
|
|
const columnInfo = columnList.find((col: any) => (col.column_name || col.columnName) === column.columnName);
|
|
|
|
|
|
|
|
|
|
|
|
if (columnInfo?.reference_table || columnInfo?.referenceTable) {
|
|
|
|
|
|
joinTable = columnInfo.reference_table || columnInfo.referenceTable;
|
|
|
|
|
|
console.log("✅ tableTypeApi에서 조인 테이블 정보 찾음:", joinTable);
|
|
|
|
|
|
|
|
|
|
|
|
// entityDisplayConfig 업데이트
|
|
|
|
|
|
const updatedConfig = {
|
|
|
|
|
|
...column.entityDisplayConfig,
|
|
|
|
|
|
sourceTable: sourceTable,
|
|
|
|
|
|
joinTable: joinTable,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼 설정 업데이트
|
|
|
|
|
|
const updatedColumns = config.columns?.map((col) =>
|
|
|
|
|
|
col.columnName === column.columnName ? { ...col, entityDisplayConfig: updatedConfig } : col,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (updatedColumns) {
|
|
|
|
|
|
handleChange("columns", updatedColumns);
|
|
|
|
|
|
}
|
2025-10-17 15:31:23 +09:00
|
|
|
|
} else {
|
|
|
|
|
|
console.warn("⚠️ tableTypeApi에서 조인 테이블 정보를 찾지 못함:", column.columnName);
|
2025-09-23 17:43:24 +09:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("tableTypeApi 컬럼 정보 조회 실패:", error);
|
2025-10-17 15:31:23 +09:00
|
|
|
|
console.log("❌ 조회 실패 상세:", { sourceTable, columnName: column.columnName });
|
2025-09-23 17:43:24 +09:00
|
|
|
|
}
|
2025-10-17 15:31:23 +09:00
|
|
|
|
} else if (!joinTable) {
|
|
|
|
|
|
console.warn("⚠️ sourceTable이 없어서 joinTable 조회 불가:", column.columnName);
|
2025-09-23 17:43:24 +09:00
|
|
|
|
}
|
2025-09-23 16:23:36 +09:00
|
|
|
|
|
2025-09-23 17:43:24 +09:00
|
|
|
|
console.log("🔍 최종 추출한 값:", { sourceTable, joinTable });
|
2025-09-23 16:23:36 +09:00
|
|
|
|
const configKey = `${column.columnName}`;
|
|
|
|
|
|
|
|
|
|
|
|
// 이미 로드된 경우 스킵
|
|
|
|
|
|
if (entityDisplayConfigs[configKey]) return;
|
|
|
|
|
|
|
2025-09-23 17:43:24 +09:00
|
|
|
|
// joinTable이 비어있으면 tableTypeApi로 컬럼 정보를 다시 가져와서 referenceTable 정보를 찾기
|
2025-09-23 17:11:07 +09:00
|
|
|
|
let actualJoinTable = joinTable;
|
|
|
|
|
|
if (!actualJoinTable && sourceTable) {
|
|
|
|
|
|
try {
|
2025-09-23 17:43:24 +09:00
|
|
|
|
console.log("🔍 tableTypeApi로 컬럼 정보 다시 조회:", {
|
|
|
|
|
|
tableName: sourceTable,
|
|
|
|
|
|
columnName: column.columnName,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const columnList = await tableTypeApi.getColumns(sourceTable);
|
|
|
|
|
|
const columnInfo = columnList.find((col: any) => (col.column_name || col.columnName) === column.columnName);
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🔍 컬럼 정보 조회 결과:", {
|
|
|
|
|
|
columnInfo: columnInfo,
|
|
|
|
|
|
referenceTable: columnInfo?.reference_table || columnInfo?.referenceTable,
|
|
|
|
|
|
referenceColumn: columnInfo?.reference_column || columnInfo?.referenceColumn,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (columnInfo?.reference_table || columnInfo?.referenceTable) {
|
|
|
|
|
|
actualJoinTable = columnInfo.reference_table || columnInfo.referenceTable;
|
|
|
|
|
|
console.log("✅ tableTypeApi에서 조인 테이블 정보 찾음:", actualJoinTable);
|
|
|
|
|
|
|
2025-09-23 17:11:07 +09:00
|
|
|
|
// entityDisplayConfig 업데이트
|
|
|
|
|
|
const updatedConfig = {
|
|
|
|
|
|
...column.entityDisplayConfig,
|
|
|
|
|
|
joinTable: actualJoinTable,
|
|
|
|
|
|
};
|
2025-09-23 17:43:24 +09:00
|
|
|
|
|
2025-09-23 17:11:07 +09:00
|
|
|
|
// 컬럼 설정 업데이트
|
|
|
|
|
|
const updatedColumns = config.columns?.map((col) =>
|
2025-09-23 17:43:24 +09:00
|
|
|
|
col.columnName === column.columnName ? { ...col, entityDisplayConfig: updatedConfig } : col,
|
2025-09-23 17:11:07 +09:00
|
|
|
|
);
|
2025-09-23 17:43:24 +09:00
|
|
|
|
|
2025-09-23 17:11:07 +09:00
|
|
|
|
if (updatedColumns) {
|
|
|
|
|
|
handleChange("columns", updatedColumns);
|
|
|
|
|
|
}
|
2025-09-23 17:43:24 +09:00
|
|
|
|
} else {
|
|
|
|
|
|
console.log("⚠️ tableTypeApi에서도 referenceTable을 찾을 수 없음:", {
|
|
|
|
|
|
columnName: column.columnName,
|
|
|
|
|
|
columnInfo: columnInfo,
|
|
|
|
|
|
});
|
2025-09-23 17:11:07 +09:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2025-09-23 17:43:24 +09:00
|
|
|
|
console.error("tableTypeApi 컬럼 정보 조회 실패:", error);
|
2025-09-23 17:11:07 +09:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-23 17:06:23 +09:00
|
|
|
|
// sourceTable과 joinTable이 모두 있어야 로드
|
2025-09-23 17:11:07 +09:00
|
|
|
|
if (!sourceTable || !actualJoinTable) {
|
|
|
|
|
|
console.log("⚠️ sourceTable 또는 joinTable이 비어있어서 로드 스킵:", { sourceTable, joinTable: actualJoinTable });
|
2025-09-23 17:06:23 +09:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-23 16:23:36 +09:00
|
|
|
|
try {
|
|
|
|
|
|
// 기본 테이블과 조인 테이블의 컬럼 정보를 병렬로 로드
|
|
|
|
|
|
const [sourceResult, joinResult] = await Promise.all([
|
|
|
|
|
|
entityJoinApi.getReferenceTableColumns(sourceTable),
|
2025-09-23 17:11:07 +09:00
|
|
|
|
entityJoinApi.getReferenceTableColumns(actualJoinTable),
|
2025-09-23 16:23:36 +09:00
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
const sourceColumns = sourceResult.columns || [];
|
|
|
|
|
|
const joinColumns = joinResult.columns || [];
|
|
|
|
|
|
|
2025-09-23 16:51:12 +09:00
|
|
|
|
setEntityDisplayConfigs((prev) => ({
|
2025-09-23 16:23:36 +09:00
|
|
|
|
...prev,
|
|
|
|
|
|
[configKey]: {
|
|
|
|
|
|
sourceColumns,
|
|
|
|
|
|
joinColumns,
|
|
|
|
|
|
selectedColumns: column.entityDisplayConfig?.displayColumns || [],
|
|
|
|
|
|
separator: column.entityDisplayConfig?.separator || " - ",
|
|
|
|
|
|
},
|
|
|
|
|
|
}));
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("엔티티 표시 컬럼 정보 로드 실패:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 🎯 엔티티 표시 컬럼 선택 토글
|
|
|
|
|
|
const toggleEntityDisplayColumn = (columnName: string, selectedColumn: string) => {
|
|
|
|
|
|
const configKey = `${columnName}`;
|
2025-09-23 17:43:24 +09:00
|
|
|
|
const localConfig = entityDisplayConfigs[configKey];
|
|
|
|
|
|
if (!localConfig) return;
|
2025-09-23 16:23:36 +09:00
|
|
|
|
|
2025-09-23 17:43:24 +09:00
|
|
|
|
const newSelectedColumns = localConfig.selectedColumns.includes(selectedColumn)
|
|
|
|
|
|
? localConfig.selectedColumns.filter((col) => col !== selectedColumn)
|
|
|
|
|
|
: [...localConfig.selectedColumns, selectedColumn];
|
2025-09-23 16:23:36 +09:00
|
|
|
|
|
2025-09-23 17:43:24 +09:00
|
|
|
|
// 로컬 상태 업데이트
|
2025-09-23 16:51:12 +09:00
|
|
|
|
setEntityDisplayConfigs((prev) => ({
|
2025-09-23 16:23:36 +09:00
|
|
|
|
...prev,
|
|
|
|
|
|
[configKey]: {
|
|
|
|
|
|
...prev[configKey],
|
|
|
|
|
|
selectedColumns: newSelectedColumns,
|
|
|
|
|
|
},
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
2025-09-23 17:43:24 +09:00
|
|
|
|
// 실제 컬럼 설정도 업데이트
|
|
|
|
|
|
const updatedColumns = config.columns?.map((col) => {
|
|
|
|
|
|
if (col.columnName === columnName && col.entityDisplayConfig) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
...col,
|
|
|
|
|
|
entityDisplayConfig: {
|
|
|
|
|
|
...col.entityDisplayConfig,
|
|
|
|
|
|
displayColumns: newSelectedColumns,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return col;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (updatedColumns) {
|
|
|
|
|
|
handleChange("columns", updatedColumns);
|
|
|
|
|
|
console.log("🎯 엔티티 표시 컬럼 설정 업데이트:", {
|
|
|
|
|
|
columnName,
|
|
|
|
|
|
selectedColumns: newSelectedColumns,
|
|
|
|
|
|
updatedColumn: updatedColumns.find((col) => col.columnName === columnName),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-09-23 16:23:36 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 🎯 엔티티 표시 구분자 업데이트
|
|
|
|
|
|
const updateEntityDisplaySeparator = (columnName: string, separator: string) => {
|
|
|
|
|
|
const configKey = `${columnName}`;
|
2025-09-23 17:43:24 +09:00
|
|
|
|
const localConfig = entityDisplayConfigs[configKey];
|
|
|
|
|
|
if (!localConfig) return;
|
2025-09-23 16:23:36 +09:00
|
|
|
|
|
2025-09-23 17:43:24 +09:00
|
|
|
|
// 로컬 상태 업데이트
|
2025-09-23 16:51:12 +09:00
|
|
|
|
setEntityDisplayConfigs((prev) => ({
|
2025-09-23 16:23:36 +09:00
|
|
|
|
...prev,
|
|
|
|
|
|
[configKey]: {
|
|
|
|
|
|
...prev[configKey],
|
|
|
|
|
|
separator,
|
|
|
|
|
|
},
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
2025-09-23 17:43:24 +09:00
|
|
|
|
// 실제 컬럼 설정도 업데이트
|
|
|
|
|
|
const updatedColumns = config.columns?.map((col) => {
|
|
|
|
|
|
if (col.columnName === columnName && col.entityDisplayConfig) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
...col,
|
|
|
|
|
|
entityDisplayConfig: {
|
|
|
|
|
|
...col.entityDisplayConfig,
|
|
|
|
|
|
separator,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return col;
|
2025-09-23 16:23:36 +09:00
|
|
|
|
});
|
2025-09-23 17:43:24 +09:00
|
|
|
|
|
|
|
|
|
|
if (updatedColumns) {
|
|
|
|
|
|
handleChange("columns", updatedColumns);
|
|
|
|
|
|
console.log("🎯 엔티티 표시 구분자 설정 업데이트:", {
|
|
|
|
|
|
columnName,
|
|
|
|
|
|
separator,
|
|
|
|
|
|
updatedColumn: updatedColumns.find((col) => col.columnName === columnName),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-09-23 16:23:36 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-15 11:43:59 +09:00
|
|
|
|
// 컬럼 순서 변경
|
|
|
|
|
|
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);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-23 14:26:18 +09:00
|
|
|
|
// 필터 추가
|
|
|
|
|
|
const addFilter = (columnName: string) => {
|
|
|
|
|
|
const existingFilter = config.filter?.filters?.find((f) => f.columnName === columnName);
|
|
|
|
|
|
if (existingFilter) return;
|
|
|
|
|
|
|
|
|
|
|
|
const column = availableColumns.find((col) => col.columnName === columnName);
|
|
|
|
|
|
if (!column) return;
|
|
|
|
|
|
|
|
|
|
|
|
// tableColumns에서 해당 컬럼의 메타정보 찾기
|
2025-09-23 16:59:12 +09:00
|
|
|
|
const tableColumn = tableColumns?.find((tc) => tc.columnName === columnName);
|
2025-09-23 14:26:18 +09:00
|
|
|
|
|
|
|
|
|
|
// 컬럼의 데이터 타입과 웹타입에 따라 위젯 타입 결정
|
|
|
|
|
|
const inferWidgetType = (dataType: string, webType?: string): string => {
|
|
|
|
|
|
// 웹타입이 있으면 우선 사용
|
|
|
|
|
|
if (webType) {
|
|
|
|
|
|
return webType;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 데이터 타입으로 추론
|
|
|
|
|
|
const type = dataType.toLowerCase();
|
|
|
|
|
|
if (type.includes("int") || type.includes("numeric") || type.includes("decimal")) return "number";
|
|
|
|
|
|
if (type.includes("date") || type.includes("timestamp")) return "date";
|
|
|
|
|
|
if (type.includes("bool")) return "boolean";
|
|
|
|
|
|
return "text";
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const widgetType = inferWidgetType(column.dataType, tableColumn?.webType || tableColumn?.web_type);
|
|
|
|
|
|
|
|
|
|
|
|
const newFilter = {
|
|
|
|
|
|
columnName,
|
|
|
|
|
|
widgetType,
|
|
|
|
|
|
label: column.label || column.columnName,
|
|
|
|
|
|
gridColumns: 3,
|
|
|
|
|
|
numberFilterMode: "range" as const,
|
|
|
|
|
|
// 코드 타입인 경우 코드 카테고리 추가
|
|
|
|
|
|
...(widgetType === "code" && {
|
|
|
|
|
|
codeCategory: tableColumn?.codeCategory || tableColumn?.code_category,
|
|
|
|
|
|
}),
|
|
|
|
|
|
// 엔티티 타입인 경우 참조 정보 추가
|
|
|
|
|
|
...(widgetType === "entity" && {
|
|
|
|
|
|
referenceTable: tableColumn?.referenceTable || tableColumn?.reference_table,
|
|
|
|
|
|
referenceColumn: tableColumn?.referenceColumn || tableColumn?.reference_column,
|
|
|
|
|
|
displayColumn: tableColumn?.displayColumn || tableColumn?.display_column,
|
|
|
|
|
|
}),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🔍 필터 추가:", newFilter);
|
|
|
|
|
|
|
|
|
|
|
|
const currentFilters = config.filter?.filters || [];
|
|
|
|
|
|
handleNestedChange("filter", "filters", [...currentFilters, newFilter]);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 필터 제거
|
|
|
|
|
|
const removeFilter = (index: number) => {
|
|
|
|
|
|
const currentFilters = config.filter?.filters || [];
|
|
|
|
|
|
const updatedFilters = currentFilters.filter((_, i) => i !== index);
|
|
|
|
|
|
handleNestedChange("filter", "filters", updatedFilters);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 필터 업데이트
|
|
|
|
|
|
const updateFilter = (index: number, key: string, value: any) => {
|
|
|
|
|
|
const currentFilters = config.filter?.filters || [];
|
|
|
|
|
|
const updatedFilters = currentFilters.map((filter, i) => (i === index ? { ...filter, [key]: value } : filter));
|
|
|
|
|
|
handleNestedChange("filter", "filters", updatedFilters);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-15 11:43:59 +09:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
<div className="text-sm font-medium">테이블 리스트 설정</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Tabs defaultValue="basic" className="w-full">
|
2025-09-16 18:02:19 +09:00
|
|
|
|
<TabsList className="grid w-full grid-cols-6">
|
2025-09-15 11:43:59 +09:00
|
|
|
|
<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>
|
2025-09-16 18:02:19 +09:00
|
|
|
|
<TabsTrigger value="join-columns" className="flex items-center gap-1">
|
|
|
|
|
|
<Plus className="h-3 w-3" />
|
|
|
|
|
|
조인
|
|
|
|
|
|
</TabsTrigger>
|
2025-09-15 11:43:59 +09:00
|
|
|
|
<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">
|
2025-09-18 15:14:14 +09:00
|
|
|
|
<ScrollArea className="h-[600px] pr-4">
|
2025-09-25 18:54:25 +09:00
|
|
|
|
{/* 표시 모드 설정 */}
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="text-base">표시 모드</CardTitle>
|
|
|
|
|
|
<CardDescription>데이터를 어떤 형태로 표시할지 선택하세요</CardDescription>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="space-y-4">
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<Label>표시 형태</Label>
|
|
|
|
|
|
<RadioGroup
|
|
|
|
|
|
value={config.displayMode || "table"}
|
|
|
|
|
|
onValueChange={(value: "table" | "card") => handleChange("displayMode", value)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<RadioGroupItem value="table" id="table-mode" />
|
|
|
|
|
|
<Label htmlFor="table-mode" className="cursor-pointer">
|
|
|
|
|
|
테이블 형태 (기본)
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<RadioGroupItem value="card" id="card-mode" />
|
|
|
|
|
|
<Label htmlFor="card-mode" className="cursor-pointer">
|
|
|
|
|
|
카드 형태
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</RadioGroup>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 카드 모드 설정 */}
|
|
|
|
|
|
{config.displayMode === "card" && (
|
|
|
|
|
|
<div className="space-y-4 border-t pt-4">
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<Label className="text-sm font-medium">카드 레이아웃 설정</Label>
|
2025-10-17 15:31:23 +09:00
|
|
|
|
|
2025-09-25 18:54:25 +09:00
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="cards-per-row">한 행당 카드 수</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={config.cardConfig?.cardsPerRow?.toString() || "3"}
|
2025-10-17 15:31:23 +09:00
|
|
|
|
onValueChange={(value) => handleNestedChange("cardConfig", "cardsPerRow", parseInt(value))}
|
2025-09-25 18:54:25 +09:00
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="1">1개</SelectItem>
|
|
|
|
|
|
<SelectItem value="2">2개</SelectItem>
|
|
|
|
|
|
<SelectItem value="3">3개</SelectItem>
|
|
|
|
|
|
<SelectItem value="4">4개</SelectItem>
|
|
|
|
|
|
<SelectItem value="5">5개</SelectItem>
|
|
|
|
|
|
<SelectItem value="6">6개</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="card-spacing">카드 간격 (px)</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="card-spacing"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
value={config.cardConfig?.cardSpacing || 16}
|
2025-10-17 15:31:23 +09:00
|
|
|
|
onChange={(e) => handleNestedChange("cardConfig", "cardSpacing", parseInt(e.target.value))}
|
2025-09-25 18:54:25 +09:00
|
|
|
|
min="0"
|
|
|
|
|
|
max="50"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<Label className="text-sm font-medium">카드 필드 매핑</Label>
|
2025-10-17 15:31:23 +09:00
|
|
|
|
|
2025-09-25 18:54:25 +09:00
|
|
|
|
<div className="grid grid-cols-1 gap-3">
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="id-column">ID 컬럼 (사번 등)</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={config.cardConfig?.idColumn || ""}
|
2025-10-17 15:31:23 +09:00
|
|
|
|
onValueChange={(value) => handleNestedChange("cardConfig", "idColumn", value)}
|
2025-09-25 18:54:25 +09:00
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
|
<SelectValue placeholder="ID 컬럼 선택" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{availableColumns.map((column) => (
|
|
|
|
|
|
<SelectItem key={column.columnName} value={column.columnName}>
|
|
|
|
|
|
{column.label || column.columnName}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="title-column">제목 컬럼 (이름 등)</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={config.cardConfig?.titleColumn || ""}
|
2025-10-17 15:31:23 +09:00
|
|
|
|
onValueChange={(value) => handleNestedChange("cardConfig", "titleColumn", value)}
|
2025-09-25 18:54:25 +09:00
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
|
<SelectValue placeholder="제목 컬럼 선택" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{availableColumns.map((column) => (
|
|
|
|
|
|
<SelectItem key={column.columnName} value={column.columnName}>
|
|
|
|
|
|
{column.label || column.columnName}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="subtitle-column">서브 제목 컬럼 (부서 등)</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={config.cardConfig?.subtitleColumn || ""}
|
2025-10-17 15:31:23 +09:00
|
|
|
|
onValueChange={(value) => handleNestedChange("cardConfig", "subtitleColumn", value)}
|
2025-09-25 18:54:25 +09:00
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
|
<SelectValue placeholder="서브 제목 컬럼 선택 (선택사항)" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="">선택 안함</SelectItem>
|
|
|
|
|
|
{availableColumns.map((column) => (
|
|
|
|
|
|
<SelectItem key={column.columnName} value={column.columnName}>
|
|
|
|
|
|
{column.label || column.columnName}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="description-column">설명 컬럼</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={config.cardConfig?.descriptionColumn || ""}
|
2025-10-17 15:31:23 +09:00
|
|
|
|
onValueChange={(value) => handleNestedChange("cardConfig", "descriptionColumn", value)}
|
2025-09-25 18:54:25 +09:00
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
|
<SelectValue placeholder="설명 컬럼 선택 (선택사항)" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="">선택 안함</SelectItem>
|
|
|
|
|
|
{availableColumns.map((column) => (
|
|
|
|
|
|
<SelectItem key={column.columnName} value={column.columnName}>
|
|
|
|
|
|
{column.label || column.columnName}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id="show-card-actions"
|
|
|
|
|
|
checked={config.cardConfig?.showActions !== false}
|
2025-10-17 15:31:23 +09:00
|
|
|
|
onCheckedChange={(checked) =>
|
2025-09-25 18:54:25 +09:00
|
|
|
|
handleNestedChange("cardConfig", "showActions", checked as boolean)
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor="show-card-actions" className="cursor-pointer">
|
|
|
|
|
|
카드에 액션 버튼 표시
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
2025-09-18 15:14:14 +09:00
|
|
|
|
<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>
|
2025-09-15 11:43:59 +09:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-18 15:14:14 +09:00
|
|
|
|
|
2025-09-15 11:43:59 +09:00
|
|
|
|
<div className="space-y-2">
|
2025-09-18 15:14:14 +09:00
|
|
|
|
<Label htmlFor="title">제목</Label>
|
2025-09-15 11:43:59 +09:00
|
|
|
|
<Input
|
2025-09-18 15:14:14 +09:00
|
|
|
|
id="title"
|
|
|
|
|
|
value={config.title || ""}
|
|
|
|
|
|
onChange={(e) => handleChange("title", e.target.value)}
|
|
|
|
|
|
placeholder="테이블 제목 (선택사항)"
|
2025-09-15 11:43:59 +09:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-09-18 15:14:14 +09:00
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
2025-09-15 11:43:59 +09:00
|
|
|
|
|
2025-09-18 15:14:14 +09:00
|
|
|
|
<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>
|
2025-09-15 11:43:59 +09:00
|
|
|
|
|
2025-09-18 15:14:14 +09:00
|
|
|
|
<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>
|
2025-09-15 11:43:59 +09:00
|
|
|
|
|
|
|
|
|
|
<Card>
|
2025-09-18 15:14:14 +09:00
|
|
|
|
<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>
|
2025-09-15 11:43:59 +09:00
|
|
|
|
</div>
|
2025-09-18 15:14:14 +09:00
|
|
|
|
|
|
|
|
|
|
{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>
|
|
|
|
|
|
)}
|
2025-09-15 11:43:59 +09:00
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
2025-09-18 15:14:14 +09:00
|
|
|
|
|
|
|
|
|
|
<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
|
2025-09-25 16:22:02 +09:00
|
|
|
|
key={`pageSize-${config.pagination?.pageSize}`}
|
2025-09-18 15:14:14 +09:00
|
|
|
|
value={config.pagination?.pageSize?.toString() || "20"}
|
2025-09-25 16:22:02 +09:00
|
|
|
|
onValueChange={(value) => {
|
|
|
|
|
|
// console.log("🎯 상세설정에서 페이지 크기 변경:", {
|
|
|
|
|
|
// from: config.pagination?.pageSize,
|
|
|
|
|
|
// to: parseInt(value),
|
|
|
|
|
|
// currentConfigPageSize: config.pagination?.pageSize
|
|
|
|
|
|
// });
|
|
|
|
|
|
handleNestedChange("pagination", "pageSize", parseInt(value));
|
|
|
|
|
|
}}
|
2025-09-18 15:14:14 +09:00
|
|
|
|
>
|
|
|
|
|
|
<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>
|
2025-09-15 11:43:59 +09:00
|
|
|
|
</div>
|
2025-09-18 15:14:14 +09:00
|
|
|
|
|
|
|
|
|
|
<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>
|
2025-09-15 11:43:59 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-18 15:14:14 +09:00
|
|
|
|
<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>
|
|
|
|
|
|
|
2025-09-15 11:43:59 +09:00
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
2025-09-18 15:14:14 +09:00
|
|
|
|
<CardTitle className="text-base">가로 스크롤 및 컬럼 고정</CardTitle>
|
|
|
|
|
|
<CardDescription>컬럼이 많을 때 가로 스크롤과 컬럼 고정 기능을 설정하세요</CardDescription>
|
2025-09-15 11:43:59 +09:00
|
|
|
|
</CardHeader>
|
2025-09-18 15:14:14 +09:00
|
|
|
|
<CardContent className="space-y-3">
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id="horizontalScrollEnabled"
|
|
|
|
|
|
checked={config.horizontalScroll?.enabled}
|
|
|
|
|
|
onCheckedChange={(checked) => handleNestedChange("horizontalScroll", "enabled", checked)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor="horizontalScrollEnabled">가로 스크롤 사용</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{config.horizontalScroll?.enabled && (
|
2025-09-15 11:43:59 +09:00
|
|
|
|
<div className="space-y-3">
|
2025-09-18 15:14:14 +09:00
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label htmlFor="maxVisibleColumns" className="text-sm">
|
|
|
|
|
|
최대 표시 컬럼 수
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="maxVisibleColumns"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
value={config.horizontalScroll?.maxVisibleColumns || 8}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
handleNestedChange("horizontalScroll", "maxVisibleColumns", parseInt(e.target.value) || 8)
|
|
|
|
|
|
}
|
|
|
|
|
|
min={3}
|
|
|
|
|
|
max={20}
|
|
|
|
|
|
placeholder="8"
|
|
|
|
|
|
className="h-8"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div className="text-xs text-gray-500">이 수를 넘는 컬럼이 있으면 가로 스크롤이 생성됩니다</div>
|
|
|
|
|
|
</div>
|
2025-09-15 11:43:59 +09:00
|
|
|
|
|
2025-09-18 15:14:14 +09:00
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label htmlFor="minColumnWidth" className="text-sm">
|
|
|
|
|
|
최소 너비 (px)
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="minColumnWidth"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
value={config.horizontalScroll?.minColumnWidth || 100}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
handleNestedChange("horizontalScroll", "minColumnWidth", parseInt(e.target.value) || 100)
|
|
|
|
|
|
}
|
|
|
|
|
|
min={50}
|
|
|
|
|
|
max={500}
|
|
|
|
|
|
placeholder="100"
|
|
|
|
|
|
className="h-8"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label htmlFor="maxColumnWidth" className="text-sm">
|
|
|
|
|
|
최대 너비 (px)
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="maxColumnWidth"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
value={config.horizontalScroll?.maxColumnWidth || 300}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
handleNestedChange("horizontalScroll", "maxColumnWidth", parseInt(e.target.value) || 300)
|
|
|
|
|
|
}
|
|
|
|
|
|
min={100}
|
|
|
|
|
|
max={800}
|
|
|
|
|
|
placeholder="300"
|
|
|
|
|
|
className="h-8"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-18 18:49:30 +09:00
|
|
|
|
)}
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="text-base">체크박스 설정</CardTitle>
|
|
|
|
|
|
<CardDescription>행 선택을 위한 체크박스 기능을 설정하세요</CardDescription>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="space-y-3">
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id="checkboxEnabled"
|
|
|
|
|
|
checked={config.checkbox?.enabled}
|
|
|
|
|
|
onCheckedChange={(checked) => handleNestedChange("checkbox", "enabled", checked)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor="checkboxEnabled">체크박스 사용</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{config.checkbox?.enabled && (
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id="checkboxMultiple"
|
|
|
|
|
|
checked={config.checkbox?.multiple}
|
|
|
|
|
|
onCheckedChange={(checked) => handleNestedChange("checkbox", "multiple", checked)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor="checkboxMultiple">다중 선택 (체크박스)</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label htmlFor="checkboxPosition" className="text-sm">
|
|
|
|
|
|
체크박스 위치
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={config.checkbox?.position || "left"}
|
|
|
|
|
|
onValueChange={(value) => handleNestedChange("checkbox", "position", value)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-8">
|
|
|
|
|
|
<SelectValue placeholder="위치 선택" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="left">왼쪽</SelectItem>
|
|
|
|
|
|
<SelectItem value="right">오른쪽</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id="checkboxSelectAll"
|
|
|
|
|
|
checked={config.checkbox?.selectAll}
|
|
|
|
|
|
onCheckedChange={(checked) => handleNestedChange("checkbox", "selectAll", checked)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor="checkboxSelectAll">전체 선택/해제 버튼 표시</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-18 15:14:14 +09:00
|
|
|
|
)}
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</ScrollArea>
|
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 컬럼 설정 탭 */}
|
|
|
|
|
|
<TabsContent value="columns" className="space-y-4">
|
|
|
|
|
|
<ScrollArea className="h-[600px] pr-4">
|
2025-09-23 16:51:12 +09:00
|
|
|
|
{/* 🎯 엔티티 컬럼 표시 설정 섹션 - 컬럼 설정 패널 바깥으로 분리 */}
|
|
|
|
|
|
{config.columns?.some((col) => col.isEntityJoin) && (
|
|
|
|
|
|
<Card className="border-l-4 border-l-orange-500">
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="flex items-center gap-2 text-base">🎯 엔티티 컬럼 표시 설정</CardTitle>
|
|
|
|
|
|
<CardDescription>엔티티 타입 컬럼의 표시할 컬럼들을 조합하여 설정하세요</CardDescription>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="space-y-4">
|
|
|
|
|
|
{config.columns
|
|
|
|
|
|
?.filter((col) => col.isEntityJoin && col.entityDisplayConfig)
|
|
|
|
|
|
.map((column) => (
|
|
|
|
|
|
<div key={column.columnName} className="space-y-3 rounded-lg border bg-orange-50 p-3">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<Badge variant="outline" className="border-orange-300 text-orange-600">
|
|
|
|
|
|
{column.columnName}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
<span className="text-sm font-medium">{column.displayName}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
size="sm"
|
2025-10-17 15:31:23 +09:00
|
|
|
|
onClick={() => {
|
|
|
|
|
|
// sourceTable 정보가 있는지 확인
|
|
|
|
|
|
const hasSourceTable =
|
|
|
|
|
|
column.entityDisplayConfig?.sourceTable ||
|
|
|
|
|
|
column.entityJoinInfo?.sourceTable ||
|
|
|
|
|
|
config.selectedTable ||
|
|
|
|
|
|
screenTableName;
|
|
|
|
|
|
|
|
|
|
|
|
if (!hasSourceTable) {
|
|
|
|
|
|
console.error("❌ sourceTable 정보를 찾을 수 없어서 컬럼 로드 불가:", {
|
|
|
|
|
|
columnName: column.columnName,
|
|
|
|
|
|
entityDisplayConfig: column.entityDisplayConfig,
|
|
|
|
|
|
entityJoinInfo: column.entityJoinInfo,
|
|
|
|
|
|
configSelectedTable: config.selectedTable,
|
|
|
|
|
|
screenTableName,
|
|
|
|
|
|
});
|
|
|
|
|
|
alert("컬럼 정보를 로드할 수 없습니다. 테이블 정보가 없습니다.");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
loadEntityDisplayConfig(column);
|
|
|
|
|
|
}}
|
|
|
|
|
|
disabled={
|
|
|
|
|
|
!column.entityDisplayConfig?.sourceTable &&
|
|
|
|
|
|
!column.entityJoinInfo?.sourceTable &&
|
|
|
|
|
|
!config.selectedTable &&
|
|
|
|
|
|
!screenTableName
|
|
|
|
|
|
}
|
2025-09-23 16:51:12 +09:00
|
|
|
|
className="h-6 text-xs"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Plus className="mr-1 h-3 w-3" />
|
|
|
|
|
|
컬럼 로드
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{entityDisplayConfigs[column.columnName] && (
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
{/* 구분자 설정 */}
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-xs">구분자</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
value={entityDisplayConfigs[column.columnName].separator}
|
|
|
|
|
|
onChange={(e) => updateEntityDisplaySeparator(column.columnName, e.target.value)}
|
|
|
|
|
|
className="h-7 text-xs"
|
|
|
|
|
|
placeholder=" - "
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 기본 테이블 컬럼 */}
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-xs text-blue-600">
|
|
|
|
|
|
기본 테이블: {column.entityDisplayConfig?.sourceTable}
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<div className="grid max-h-20 grid-cols-2 gap-1 overflow-y-auto">
|
|
|
|
|
|
{entityDisplayConfigs[column.columnName].sourceColumns.map((col) => (
|
|
|
|
|
|
<div key={col.columnName} className="flex items-center space-x-1">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id={`source-${column.columnName}-${col.columnName}`}
|
|
|
|
|
|
checked={entityDisplayConfigs[column.columnName].selectedColumns.includes(
|
|
|
|
|
|
col.columnName,
|
|
|
|
|
|
)}
|
|
|
|
|
|
onCheckedChange={() =>
|
|
|
|
|
|
toggleEntityDisplayColumn(column.columnName, col.columnName)
|
|
|
|
|
|
}
|
|
|
|
|
|
className="h-3 w-3"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label
|
|
|
|
|
|
htmlFor={`source-${column.columnName}-${col.columnName}`}
|
|
|
|
|
|
className="flex-1 cursor-pointer text-xs"
|
|
|
|
|
|
>
|
|
|
|
|
|
{col.displayName}
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 조인 테이블 컬럼 */}
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-xs text-green-600">
|
|
|
|
|
|
조인 테이블: {column.entityDisplayConfig?.joinTable}
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<div className="grid max-h-20 grid-cols-2 gap-1 overflow-y-auto">
|
|
|
|
|
|
{entityDisplayConfigs[column.columnName].joinColumns.map((col) => (
|
|
|
|
|
|
<div key={col.columnName} className="flex items-center space-x-1">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id={`join-${column.columnName}-${col.columnName}`}
|
|
|
|
|
|
checked={entityDisplayConfigs[column.columnName].selectedColumns.includes(
|
|
|
|
|
|
col.columnName,
|
|
|
|
|
|
)}
|
|
|
|
|
|
onCheckedChange={() =>
|
|
|
|
|
|
toggleEntityDisplayColumn(column.columnName, col.columnName)
|
|
|
|
|
|
}
|
|
|
|
|
|
className="h-3 w-3"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label
|
|
|
|
|
|
htmlFor={`join-${column.columnName}-${col.columnName}`}
|
|
|
|
|
|
className="flex-1 cursor-pointer text-xs"
|
|
|
|
|
|
>
|
|
|
|
|
|
{col.displayName}
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 선택된 컬럼 미리보기 */}
|
|
|
|
|
|
{entityDisplayConfigs[column.columnName].selectedColumns.length > 0 && (
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-xs">미리보기</Label>
|
|
|
|
|
|
<div className="flex flex-wrap gap-1 rounded bg-gray-50 p-2 text-xs">
|
|
|
|
|
|
{entityDisplayConfigs[column.columnName].selectedColumns.map((colName, idx) => (
|
|
|
|
|
|
<React.Fragment key={colName}>
|
|
|
|
|
|
<Badge variant="secondary" className="text-xs">
|
|
|
|
|
|
{colName}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
{idx < entityDisplayConfigs[column.columnName].selectedColumns.length - 1 && (
|
|
|
|
|
|
<span className="text-gray-400">
|
|
|
|
|
|
{entityDisplayConfigs[column.columnName].separator}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</React.Fragment>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-09-18 15:14:14 +09:00
|
|
|
|
{!screenTableName ? (
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardContent className="pt-6">
|
|
|
|
|
|
<div className="text-center text-gray-500">
|
|
|
|
|
|
<p>테이블이 연결되지 않았습니다.</p>
|
|
|
|
|
|
<p className="text-sm">화면에 테이블을 연결한 후 컬럼을 설정할 수 있습니다.</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
2025-09-23 14:26:18 +09:00
|
|
|
|
) : availableColumns.length === 0 ? (
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardContent className="pt-6">
|
|
|
|
|
|
<div className="text-center text-gray-500">
|
|
|
|
|
|
<p>컬럼을 추가하려면 먼저 컴포넌트에 테이블을 명시적으로 선택하거나</p>
|
|
|
|
|
|
<p className="text-sm">기본 설정 탭에서 테이블을 설정해주세요.</p>
|
|
|
|
|
|
<p className="mt-2 text-xs text-blue-600">현재 화면 테이블: {screenTableName}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
2025-09-18 15:14:14 +09:00
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<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) => (
|
2025-09-15 11:43:59 +09:00
|
|
|
|
<Button
|
2025-09-18 15:14:14 +09:00
|
|
|
|
key={column.columnName}
|
|
|
|
|
|
variant="outline"
|
2025-09-15 11:43:59 +09:00
|
|
|
|
size="sm"
|
2025-09-18 15:14:14 +09:00
|
|
|
|
onClick={() => addColumn(column.columnName)}
|
|
|
|
|
|
className="flex items-center gap-1"
|
2025-09-15 11:43:59 +09:00
|
|
|
|
>
|
2025-09-18 15:14:14 +09:00
|
|
|
|
<Plus className="h-3 w-3" />
|
|
|
|
|
|
{column.label || column.columnName}
|
|
|
|
|
|
<Badge variant="secondary" className="text-xs">
|
|
|
|
|
|
{column.dataType}
|
|
|
|
|
|
</Badge>
|
2025-09-15 11:43:59 +09:00
|
|
|
|
</Button>
|
2025-09-18 15:14:14 +09:00
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="py-4 text-center text-gray-500">
|
|
|
|
|
|
<p>컬럼 정보를 불러오는 중입니다...</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
2025-09-15 11:43:59 +09:00
|
|
|
|
|
2025-09-18 15:14:14 +09:00
|
|
|
|
{screenTableName && (
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="text-base">컬럼 설정</CardTitle>
|
|
|
|
|
|
<CardDescription>선택된 컬럼들의 표시 옵션을 설정하세요</CardDescription>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<ScrollArea className="h-96">
|
|
|
|
|
|
<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 })
|
2025-09-15 11:43:59 +09:00
|
|
|
|
}
|
|
|
|
|
|
/>
|
2025-09-18 15:14:14 +09:00
|
|
|
|
<span className="font-medium">
|
|
|
|
|
|
{availableColumns.find((col) => col.columnName === column.columnName)?.label ||
|
|
|
|
|
|
column.displayName ||
|
|
|
|
|
|
column.columnName}
|
|
|
|
|
|
</span>
|
2025-09-15 11:43:59 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-18 15:14:14 +09:00
|
|
|
|
<div className="flex items-center space-x-1">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() => moveColumn(column.columnName, "up")}
|
|
|
|
|
|
disabled={index === 0}
|
2025-09-15 11:43:59 +09:00
|
|
|
|
>
|
2025-09-18 15:14:14 +09:00
|
|
|
|
<ArrowUp className="h-3 w-3" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() => moveColumn(column.columnName, "down")}
|
|
|
|
|
|
disabled={index === (config.columns?.length || 0) - 1}
|
2025-09-15 11:43:59 +09:00
|
|
|
|
>
|
2025-09-18 15:14:14 +09:00
|
|
|
|
<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>
|
2025-09-15 11:43:59 +09:00
|
|
|
|
</div>
|
2025-09-18 15:14:14 +09:00
|
|
|
|
</div>
|
2025-09-15 11:43:59 +09:00
|
|
|
|
|
2025-09-18 15:14:14 +09:00
|
|
|
|
{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
|
2025-09-15 11:43:59 +09:00
|
|
|
|
}
|
2025-09-18 15:14:14 +09:00
|
|
|
|
onChange={(e) => updateColumn(column.columnName, { displayName: e.target.value })}
|
|
|
|
|
|
className="h-8"
|
2025-09-15 11:43:59 +09:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-09-18 15:14:14 +09:00
|
|
|
|
|
2025-09-23 16:51:12 +09:00
|
|
|
|
{/* 엔티티 타입 컬럼 표시 */}
|
|
|
|
|
|
{column.isEntityJoin && (
|
|
|
|
|
|
<div className="col-span-2">
|
|
|
|
|
|
<div className="flex items-center gap-2 rounded bg-orange-50 p-2">
|
|
|
|
|
|
<Badge variant="outline" className="border-orange-300 text-orange-600">
|
|
|
|
|
|
엔티티 타입
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
<span className="text-xs text-orange-600">
|
|
|
|
|
|
표시 컬럼 설정은 상단의 "🎯 엔티티 컬럼 표시 설정" 섹션에서 하세요
|
|
|
|
|
|
</span>
|
2025-09-23 16:23:36 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-09-18 15:14:14 +09:00
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-xs">정렬</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={column.align}
|
|
|
|
|
|
onValueChange={(value: "left" | "center" | "right") =>
|
|
|
|
|
|
updateColumn(column.columnName, { align: value })
|
2025-09-15 11:43:59 +09:00
|
|
|
|
}
|
2025-09-18 15:14:14 +09:00
|
|
|
|
>
|
|
|
|
|
|
<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"
|
2025-09-15 11:43:59 +09:00
|
|
|
|
/>
|
2025-09-18 15:14:14 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-xs">컬럼 고정</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={column.fixed === false ? "none" : column.fixed || "none"}
|
|
|
|
|
|
onValueChange={(value: string) => {
|
|
|
|
|
|
const fixedValue = value === "none" ? false : (value as "left" | "right");
|
|
|
|
|
|
updateColumn(column.columnName, {
|
|
|
|
|
|
fixed: fixedValue,
|
|
|
|
|
|
fixedOrder: fixedValue ? column.fixedOrder || 0 : undefined,
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-8">
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="none">고정 안함</SelectItem>
|
|
|
|
|
|
<SelectItem value="left">왼쪽 고정</SelectItem>
|
|
|
|
|
|
<SelectItem value="right">오른쪽 고정</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{(column.fixed === "left" || column.fixed === "right") && (
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-xs">고정 순서</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
value={column.fixedOrder || 0}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
updateColumn(column.columnName, {
|
|
|
|
|
|
fixedOrder: parseInt(e.target.value) || 0,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
placeholder="0"
|
|
|
|
|
|
className="h-8"
|
|
|
|
|
|
min="0"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</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>
|
2025-09-15 11:43:59 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-18 15:14:14 +09:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</ScrollArea>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</ScrollArea>
|
2025-09-15 11:43:59 +09:00
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
2025-09-16 18:02:19 +09:00
|
|
|
|
{/* Entity 조인 컬럼 추가 탭 */}
|
|
|
|
|
|
<TabsContent value="join-columns" className="space-y-4">
|
2025-09-18 15:14:14 +09:00
|
|
|
|
<ScrollArea className="h-[600px] pr-4">
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="text-base">Entity 조인 컬럼 추가</CardTitle>
|
|
|
|
|
|
<CardDescription>Entity 조인된 테이블의 다른 컬럼들을 추가로 표시할 수 있습니다.</CardDescription>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="space-y-4">
|
|
|
|
|
|
<ScrollArea className="max-h-96 pr-4">
|
|
|
|
|
|
{loadingEntityJoins ? (
|
|
|
|
|
|
<div className="text-muted-foreground py-4 text-center">조인 정보를 가져오는 중...</div>
|
|
|
|
|
|
) : entityJoinColumns.joinTables.length === 0 ? (
|
|
|
|
|
|
<div className="text-muted-foreground py-8 text-center">
|
|
|
|
|
|
<div className="text-sm">Entity 조인이 설정된 컬럼이 없습니다.</div>
|
|
|
|
|
|
<div className="mt-1 text-xs">
|
|
|
|
|
|
먼저 컬럼의 웹타입을 'entity'로 설정하고 참조 테이블을 지정해주세요.
|
|
|
|
|
|
</div>
|
2025-09-16 18:02:19 +09:00
|
|
|
|
</div>
|
2025-09-18 15:14:14 +09:00
|
|
|
|
) : (
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
{/* 조인 테이블별 그룹 */}
|
|
|
|
|
|
{entityJoinColumns.joinTables.map((joinTable, tableIndex) => (
|
|
|
|
|
|
<Card key={tableIndex} className="border-l-4 border-l-blue-500">
|
|
|
|
|
|
<CardHeader className="pb-3">
|
|
|
|
|
|
<CardTitle className="flex items-center gap-2 text-sm">
|
|
|
|
|
|
📊 {joinTable.tableName}
|
|
|
|
|
|
<Badge variant="outline" className="text-xs">
|
|
|
|
|
|
현재: {joinTable.currentDisplayColumn}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="pt-0">
|
|
|
|
|
|
{joinTable.availableColumns.length === 0 ? (
|
|
|
|
|
|
<div className="text-muted-foreground py-2 text-sm">추가할 수 있는 컬럼이 없습니다.</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="grid gap-2">
|
|
|
|
|
|
{joinTable.availableColumns.map((column, colIndex) => {
|
|
|
|
|
|
const matchingJoinColumn = entityJoinColumns.availableColumns.find(
|
|
|
|
|
|
(jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const isAlreadyAdded = config.columns?.some(
|
|
|
|
|
|
(col) => col.columnName === matchingJoinColumn?.joinAlias,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={colIndex}
|
|
|
|
|
|
className="flex items-center justify-between rounded border p-2"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
|
<div className="text-sm font-medium">{column.columnLabel}</div>
|
|
|
|
|
|
<div className="text-muted-foreground text-xs">
|
|
|
|
|
|
{column.columnName} ({column.dataType})
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{column.description && (
|
|
|
|
|
|
<div className="text-muted-foreground mt-1 text-xs">{column.description}</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
{isAlreadyAdded ? (
|
|
|
|
|
|
<Badge variant="secondary" className="text-xs">
|
|
|
|
|
|
추가됨
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
matchingJoinColumn && (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="outline"
|
2025-09-23 16:23:36 +09:00
|
|
|
|
onClick={() => addEntityColumn(matchingJoinColumn)}
|
2025-09-18 15:14:14 +09:00
|
|
|
|
className="text-xs"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Plus className="mr-1 h-3 w-3" />
|
|
|
|
|
|
추가
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 전체 사용 가능한 컬럼 요약 */}
|
|
|
|
|
|
{entityJoinColumns.availableColumns.length > 0 && (
|
|
|
|
|
|
<Card className="bg-muted/30">
|
|
|
|
|
|
<CardHeader className="pb-3">
|
|
|
|
|
|
<CardTitle className="text-sm">📋 추가 가능한 컬럼 요약</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="pt-0">
|
|
|
|
|
|
<div className="text-muted-foreground mb-2 text-sm">
|
|
|
|
|
|
총 {entityJoinColumns.availableColumns.length}개의 컬럼을 추가할 수 있습니다.
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex flex-wrap gap-1">
|
|
|
|
|
|
{entityJoinColumns.availableColumns.map((column, index) => {
|
2025-09-16 18:02:19 +09:00
|
|
|
|
const isAlreadyAdded = config.columns?.some(
|
2025-09-18 15:14:14 +09:00
|
|
|
|
(col) => col.columnName === column.joinAlias,
|
2025-09-16 18:02:19 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2025-09-18 15:14:14 +09:00
|
|
|
|
<Badge
|
|
|
|
|
|
key={index}
|
|
|
|
|
|
variant={isAlreadyAdded ? "secondary" : "outline"}
|
|
|
|
|
|
className="cursor-pointer text-xs"
|
2025-09-23 16:23:36 +09:00
|
|
|
|
onClick={() => !isAlreadyAdded && addEntityColumn(column)}
|
2025-09-18 15:14:14 +09:00
|
|
|
|
>
|
|
|
|
|
|
{column.columnLabel}
|
|
|
|
|
|
{!isAlreadyAdded && <Plus className="ml-1 h-2 w-2" />}
|
|
|
|
|
|
</Badge>
|
2025-09-16 18:02:19 +09:00
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
2025-09-18 15:14:14 +09:00
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</ScrollArea>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</ScrollArea>
|
2025-09-16 18:02:19 +09:00
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
2025-09-15 11:43:59 +09:00
|
|
|
|
{/* 필터 설정 탭 */}
|
|
|
|
|
|
<TabsContent value="filter" className="space-y-4">
|
2025-09-18 15:14:14 +09:00
|
|
|
|
<ScrollArea className="h-[600px] pr-4">
|
2025-09-23 14:26:18 +09:00
|
|
|
|
{/* 필터 기능 활성화 */}
|
2025-09-18 15:14:14 +09:00
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
2025-09-23 14:26:18 +09:00
|
|
|
|
<CardTitle className="text-base">필터 설정</CardTitle>
|
|
|
|
|
|
<CardDescription>테이블에서 사용할 검색 필터를 설정하세요</CardDescription>
|
2025-09-18 15:14:14 +09:00
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="space-y-4">
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id="filterEnabled"
|
2025-09-23 14:26:18 +09:00
|
|
|
|
checked={config.filter?.enabled || false}
|
2025-09-18 15:14:14 +09:00
|
|
|
|
onCheckedChange={(checked) => handleNestedChange("filter", "enabled", checked)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor="filterEnabled">필터 기능 사용</Label>
|
|
|
|
|
|
</div>
|
2025-09-23 14:26:18 +09:00
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
2025-09-15 11:43:59 +09:00
|
|
|
|
|
2025-09-23 14:26:18 +09:00
|
|
|
|
{/* 필터 목록 */}
|
|
|
|
|
|
{config.filter?.enabled && (
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="text-base">사용할 필터</CardTitle>
|
|
|
|
|
|
<CardDescription>검색에 사용할 컬럼 필터를 추가하고 설정하세요</CardDescription>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="space-y-4">
|
|
|
|
|
|
{/* 필터 추가 버튼 */}
|
|
|
|
|
|
{availableColumns.length > 0 && (
|
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
|
{availableColumns
|
|
|
|
|
|
.filter((col) => !config.filter?.filters?.find((f) => f.columnName === col.columnName))
|
|
|
|
|
|
.map((column) => (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
key={column.columnName}
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() => addFilter(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>
|
|
|
|
|
|
))}
|
2025-09-15 11:43:59 +09:00
|
|
|
|
</div>
|
2025-09-23 14:26:18 +09:00
|
|
|
|
)}
|
2025-09-15 11:43:59 +09:00
|
|
|
|
|
2025-09-23 14:26:18 +09:00
|
|
|
|
{/* 설정된 필터 목록 */}
|
|
|
|
|
|
{config.filter?.filters && config.filter.filters.length > 0 && (
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<h4 className="text-sm font-medium">설정된 필터</h4>
|
|
|
|
|
|
{config.filter.filters.map((filter, index) => (
|
|
|
|
|
|
<div key={filter.columnName} className="space-y-3 rounded-lg border p-4">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<Badge variant="outline">{filter.widgetType}</Badge>
|
|
|
|
|
|
<span className="font-medium">{filter.label}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() => removeFilter(index)}
|
|
|
|
|
|
className="text-red-600 hover:text-red-700"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Trash2 className="h-3 w-3" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
2025-09-18 15:14:14 +09:00
|
|
|
|
|
2025-09-23 14:26:18 +09:00
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-xs">표시명</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
value={filter.label}
|
|
|
|
|
|
onChange={(e) => updateFilter(index, "label", e.target.value)}
|
|
|
|
|
|
placeholder="필터 라벨"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-xs">그리드 컬럼</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={filter.gridColumns.toString()}
|
|
|
|
|
|
onValueChange={(value) => updateFilter(index, "gridColumns", parseInt(value))}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="2">2칸</SelectItem>
|
|
|
|
|
|
<SelectItem value="3">3칸</SelectItem>
|
|
|
|
|
|
<SelectItem value="4">4칸</SelectItem>
|
|
|
|
|
|
<SelectItem value="6">6칸</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 숫자 타입인 경우 검색 모드 선택 */}
|
|
|
|
|
|
{(filter.widgetType === "number" || filter.widgetType === "decimal") && (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-xs">검색 모드</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={filter.numberFilterMode || "range"}
|
|
|
|
|
|
onValueChange={(value) => updateFilter(index, "numberFilterMode", value)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="exact">정확한 값</SelectItem>
|
|
|
|
|
|
<SelectItem value="range">범위 검색</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 코드 타입인 경우 코드 카테고리 */}
|
|
|
|
|
|
{filter.widgetType === "code" && (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-xs">코드 카테고리</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
value={filter.codeCategory || ""}
|
|
|
|
|
|
onChange={(e) => updateFilter(index, "codeCategory", e.target.value)}
|
|
|
|
|
|
placeholder="코드 카테고리"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
2025-09-18 15:14:14 +09:00
|
|
|
|
</div>
|
2025-09-23 14:26:18 +09:00
|
|
|
|
)}
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
2025-09-18 15:14:14 +09:00
|
|
|
|
</ScrollArea>
|
2025-09-15 11:43:59 +09:00
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 액션 설정 탭 */}
|
|
|
|
|
|
<TabsContent value="actions" className="space-y-4">
|
2025-09-18 15:14:14 +09:00
|
|
|
|
<ScrollArea className="h-[600px] pr-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>
|
|
|
|
|
|
</ScrollArea>
|
2025-09-15 11:43:59 +09:00
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 스타일 설정 탭 */}
|
|
|
|
|
|
<TabsContent value="style" className="space-y-4">
|
2025-09-18 15:14:14 +09:00
|
|
|
|
<ScrollArea className="h-[600px] pr-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>
|
|
|
|
|
|
</ScrollArea>
|
2025-09-15 11:43:59 +09:00
|
|
|
|
</TabsContent>
|
|
|
|
|
|
</Tabs>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|