feat(split-panel-layout2): 그룹핑, 탭 필터링, 설정 모달 기능 추가
- types.ts: GroupingConfig, TabConfig, ColumnDisplayConfig 등 타입 확장 - Component: groupData, generateTabs, filterDataByTab 함수 추가 - ConfigPanel: SearchableColumnSelect, 설정 모달 상태 관리 추가 - 신규 모달: ActionButtonConfigModal, ColumnConfigModal, DataTransferConfigModal - UniversalFormModal: 연결필드 소스 테이블 Combobox로 변경
This commit is contained in:
@@ -0,0 +1,805 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type DragEndEvent,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { Plus, Settings2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { entityJoinApi } from "@/lib/api/entityJoin";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import type { ColumnConfig, SearchColumnConfig, GroupingConfig, ColumnDisplayConfig, EntityReferenceConfig } from "./types";
|
||||
import { SortableColumnItem } from "./components/SortableColumnItem";
|
||||
import { SearchableColumnSelect } from "./components/SearchableColumnSelect";
|
||||
|
||||
interface ColumnInfo {
|
||||
column_name: string;
|
||||
data_type: string;
|
||||
column_comment?: string;
|
||||
input_type?: string;
|
||||
web_type?: string;
|
||||
reference_table?: string;
|
||||
reference_column?: string;
|
||||
}
|
||||
|
||||
// 참조 테이블 컬럼 정보
|
||||
interface ReferenceColumnInfo {
|
||||
columnName: string;
|
||||
displayName: string;
|
||||
dataType: string;
|
||||
}
|
||||
|
||||
interface ColumnConfigModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
tableName: string;
|
||||
displayColumns: ColumnConfig[];
|
||||
searchColumns?: SearchColumnConfig[];
|
||||
grouping?: GroupingConfig;
|
||||
showSearch?: boolean;
|
||||
onSave: (config: {
|
||||
displayColumns: ColumnConfig[];
|
||||
searchColumns: SearchColumnConfig[];
|
||||
grouping: GroupingConfig;
|
||||
showSearch: boolean;
|
||||
}) => void;
|
||||
side: "left" | "right"; // 좌측/우측 패널 구분
|
||||
}
|
||||
|
||||
export const ColumnConfigModal: React.FC<ColumnConfigModalProps> = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
tableName,
|
||||
displayColumns: initialDisplayColumns,
|
||||
searchColumns: initialSearchColumns,
|
||||
grouping: initialGrouping,
|
||||
showSearch: initialShowSearch,
|
||||
onSave,
|
||||
side,
|
||||
}) => {
|
||||
// 로컬 상태 (모달 내에서만 사용, 저장 시 부모로 전달)
|
||||
const [displayColumns, setDisplayColumns] = useState<ColumnConfig[]>([]);
|
||||
const [searchColumns, setSearchColumns] = useState<SearchColumnConfig[]>([]);
|
||||
const [grouping, setGrouping] = useState<GroupingConfig>({ enabled: false, groupByColumn: "" });
|
||||
const [showSearch, setShowSearch] = useState(false);
|
||||
|
||||
// 컬럼 세부설정 모달
|
||||
const [detailModalOpen, setDetailModalOpen] = useState(false);
|
||||
const [editingColumnIndex, setEditingColumnIndex] = useState<number | null>(null);
|
||||
const [editingColumn, setEditingColumn] = useState<ColumnConfig | null>(null);
|
||||
|
||||
// 테이블 컬럼 목록
|
||||
const [columns, setColumns] = useState<ColumnInfo[]>([]);
|
||||
const [columnsLoading, setColumnsLoading] = useState(false);
|
||||
|
||||
// 엔티티 참조 관련 상태
|
||||
const [entityReferenceColumns, setEntityReferenceColumns] = useState<Map<string, ReferenceColumnInfo[]>>(new Map());
|
||||
const [loadingEntityColumns, setLoadingEntityColumns] = useState<Set<string>>(new Set());
|
||||
|
||||
// 드래그 센서
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 8,
|
||||
},
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
);
|
||||
|
||||
// 초기값 설정
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setDisplayColumns(initialDisplayColumns || []);
|
||||
setSearchColumns(initialSearchColumns || []);
|
||||
setGrouping(initialGrouping || { enabled: false, groupByColumn: "" });
|
||||
setShowSearch(initialShowSearch || false);
|
||||
}
|
||||
}, [open, initialDisplayColumns, initialSearchColumns, initialGrouping, initialShowSearch]);
|
||||
|
||||
// 테이블 컬럼 로드 (entity 타입 정보 포함)
|
||||
const loadColumns = useCallback(async () => {
|
||||
if (!tableName) {
|
||||
setColumns([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setColumnsLoading(true);
|
||||
try {
|
||||
const response = await apiClient.get(`/table-management/tables/${tableName}/columns?size=200`);
|
||||
|
||||
let columnList: any[] = [];
|
||||
if (response.data?.success && response.data?.data?.columns) {
|
||||
columnList = response.data.data.columns;
|
||||
} else if (Array.isArray(response.data?.data?.columns)) {
|
||||
columnList = response.data.data.columns;
|
||||
} else if (Array.isArray(response.data?.data)) {
|
||||
columnList = response.data.data;
|
||||
}
|
||||
|
||||
// entity 타입 정보를 포함하여 변환
|
||||
const transformedColumns = columnList.map((c: any) => ({
|
||||
column_name: c.columnName ?? c.column_name ?? c.name ?? "",
|
||||
data_type: c.dataType ?? c.data_type ?? c.type ?? "",
|
||||
column_comment: c.displayName ?? c.column_comment ?? c.label ?? "",
|
||||
input_type: c.inputType ?? c.input_type ?? "",
|
||||
web_type: c.webType ?? c.web_type ?? "",
|
||||
reference_table: c.referenceTable ?? c.reference_table ?? "",
|
||||
reference_column: c.referenceColumn ?? c.reference_column ?? "",
|
||||
}));
|
||||
|
||||
setColumns(transformedColumns);
|
||||
} catch (error) {
|
||||
console.error("컬럼 목록 로드 실패:", error);
|
||||
setColumns([]);
|
||||
} finally {
|
||||
setColumnsLoading(false);
|
||||
}
|
||||
}, [tableName]);
|
||||
|
||||
// 엔티티 참조 테이블의 컬럼 목록 로드
|
||||
const loadEntityReferenceColumns = useCallback(async (columnName: string, referenceTable: string) => {
|
||||
if (!referenceTable || entityReferenceColumns.has(columnName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingEntityColumns(prev => new Set(prev).add(columnName));
|
||||
try {
|
||||
const result = await entityJoinApi.getReferenceTableColumns(referenceTable);
|
||||
if (result?.columns) {
|
||||
setEntityReferenceColumns(prev => {
|
||||
const newMap = new Map(prev);
|
||||
newMap.set(columnName, result.columns);
|
||||
return newMap;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`엔티티 참조 컬럼 로드 실패 (${referenceTable}):`, error);
|
||||
} finally {
|
||||
setLoadingEntityColumns(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(columnName);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
}, [entityReferenceColumns]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && tableName) {
|
||||
loadColumns();
|
||||
}
|
||||
}, [open, tableName, loadColumns]);
|
||||
|
||||
// 드래그 종료 핸들러
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (over && active.id !== over.id) {
|
||||
const oldIndex = displayColumns.findIndex((col, idx) => `col-${idx}` === active.id);
|
||||
const newIndex = displayColumns.findIndex((col, idx) => `col-${idx}` === over.id);
|
||||
|
||||
if (oldIndex !== -1 && newIndex !== -1) {
|
||||
setDisplayColumns(arrayMove(displayColumns, oldIndex, newIndex));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 컬럼 추가
|
||||
const handleAddColumn = () => {
|
||||
setDisplayColumns([
|
||||
...displayColumns,
|
||||
{
|
||||
name: "",
|
||||
label: "",
|
||||
displayRow: side === "left" ? "name" : "info",
|
||||
sourceTable: tableName,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
// 컬럼 삭제
|
||||
const handleRemoveColumn = (index: number) => {
|
||||
setDisplayColumns(displayColumns.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
// 컬럼 업데이트 (entity 타입이면 참조 테이블 컬럼도 로드)
|
||||
const handleUpdateColumn = (index: number, updates: Partial<ColumnConfig>) => {
|
||||
const newColumns = [...displayColumns];
|
||||
newColumns[index] = { ...newColumns[index], ...updates };
|
||||
setDisplayColumns(newColumns);
|
||||
|
||||
// 컬럼명이 변경된 경우 entity 타입인지 확인하고 참조 테이블 컬럼 로드
|
||||
if (updates.name) {
|
||||
const columnInfo = columns.find(c => c.column_name === updates.name);
|
||||
if (columnInfo && (columnInfo.input_type === 'entity' || columnInfo.web_type === 'entity')) {
|
||||
if (columnInfo.reference_table) {
|
||||
loadEntityReferenceColumns(updates.name, columnInfo.reference_table);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 컬럼 세부설정 열기 (entity 타입이면 참조 테이블 컬럼도 로드)
|
||||
const handleOpenDetailSettings = (index: number) => {
|
||||
const column = displayColumns[index];
|
||||
setEditingColumnIndex(index);
|
||||
setEditingColumn({ ...column });
|
||||
setDetailModalOpen(true);
|
||||
|
||||
// entity 타입인지 확인하고 참조 테이블 컬럼 로드
|
||||
if (column.name) {
|
||||
const columnInfo = columns.find(c => c.column_name === column.name);
|
||||
if (columnInfo && (columnInfo.input_type === 'entity' || columnInfo.web_type === 'entity')) {
|
||||
if (columnInfo.reference_table) {
|
||||
loadEntityReferenceColumns(column.name, columnInfo.reference_table);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 컬럼 세부설정 저장
|
||||
const handleSaveDetailSettings = () => {
|
||||
if (editingColumnIndex !== null && editingColumn) {
|
||||
handleUpdateColumn(editingColumnIndex, editingColumn);
|
||||
}
|
||||
setDetailModalOpen(false);
|
||||
setEditingColumnIndex(null);
|
||||
setEditingColumn(null);
|
||||
};
|
||||
|
||||
// 검색 컬럼 추가
|
||||
const handleAddSearchColumn = () => {
|
||||
setSearchColumns([...searchColumns, { columnName: "", label: "" }]);
|
||||
};
|
||||
|
||||
// 검색 컬럼 삭제
|
||||
const handleRemoveSearchColumn = (index: number) => {
|
||||
setSearchColumns(searchColumns.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
// 검색 컬럼 업데이트
|
||||
const handleUpdateSearchColumn = (index: number, columnName: string) => {
|
||||
const newColumns = [...searchColumns];
|
||||
newColumns[index] = { ...newColumns[index], columnName };
|
||||
setSearchColumns(newColumns);
|
||||
};
|
||||
|
||||
// 저장
|
||||
const handleSave = () => {
|
||||
onSave({
|
||||
displayColumns,
|
||||
searchColumns,
|
||||
grouping,
|
||||
showSearch,
|
||||
});
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
// 엔티티 표시 컬럼 토글
|
||||
const toggleEntityDisplayColumn = (selectedColumn: string) => {
|
||||
if (!editingColumn) return;
|
||||
|
||||
const currentDisplayColumns = editingColumn.entityReference?.displayColumns || [];
|
||||
const newDisplayColumns = currentDisplayColumns.includes(selectedColumn)
|
||||
? currentDisplayColumns.filter(col => col !== selectedColumn)
|
||||
: [...currentDisplayColumns, selectedColumn];
|
||||
|
||||
setEditingColumn({
|
||||
...editingColumn,
|
||||
entityReference: {
|
||||
...editingColumn.entityReference,
|
||||
displayColumns: newDisplayColumns,
|
||||
} as EntityReferenceConfig,
|
||||
});
|
||||
};
|
||||
|
||||
// 현재 편집 중인 컬럼이 entity 타입인지 확인
|
||||
const getEditingColumnEntityInfo = useCallback(() => {
|
||||
if (!editingColumn?.name) return null;
|
||||
const columnInfo = columns.find(c => c.column_name === editingColumn.name);
|
||||
if (!columnInfo) return null;
|
||||
if (columnInfo.input_type !== 'entity' && columnInfo.web_type !== 'entity') return null;
|
||||
return {
|
||||
referenceTable: columnInfo.reference_table || '',
|
||||
referenceColumns: entityReferenceColumns.get(editingColumn.name) || [],
|
||||
isLoading: loadingEntityColumns.has(editingColumn.name),
|
||||
};
|
||||
}, [editingColumn, columns, entityReferenceColumns, loadingEntityColumns]);
|
||||
|
||||
// 이미 선택된 컬럼명 목록 (중복 선택 방지용)
|
||||
const selectedColumnNames = displayColumns.map((col) => col.name).filter(Boolean);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="flex h-[80vh] max-w-[95vw] flex-col overflow-hidden sm:max-w-[700px]">
|
||||
<DialogHeader className="shrink-0">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Settings2 className="h-5 w-5" />
|
||||
{side === "left" ? "좌측" : "우측"} 패널 컬럼 설정
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
표시할 컬럼을 추가하고 순서를 드래그로 변경할 수 있습니다.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs defaultValue="columns" className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<TabsList className="grid w-full shrink-0 grid-cols-3">
|
||||
<TabsTrigger value="columns">표시 컬럼</TabsTrigger>
|
||||
<TabsTrigger value="grouping" disabled={side === "right"}>
|
||||
그룹핑
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="search">검색</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 표시 컬럼 탭 */}
|
||||
<TabsContent value="columns" className="mt-4 flex min-h-0 flex-1 flex-col overflow-hidden data-[state=inactive]:hidden">
|
||||
<div className="flex shrink-0 items-center justify-between mb-3">
|
||||
<Label className="text-sm font-medium">
|
||||
표시할 컬럼 ({displayColumns.length}개)
|
||||
</Label>
|
||||
<Button size="sm" variant="outline" onClick={handleAddColumn}>
|
||||
<Plus className="mr-1 h-4 w-4" />
|
||||
컬럼 추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="min-h-0 flex-1">
|
||||
<div className="pr-4">
|
||||
{displayColumns.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<p className="text-muted-foreground text-sm mb-2">
|
||||
표시할 컬럼이 없습니다
|
||||
</p>
|
||||
<Button size="sm" variant="outline" onClick={handleAddColumn}>
|
||||
<Plus className="mr-1 h-4 w-4" />
|
||||
첫 번째 컬럼 추가
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={displayColumns.map((_, idx) => `col-${idx}`)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{displayColumns.map((col, index) => (
|
||||
<div key={`col-${index}`} className="space-y-2">
|
||||
<SortableColumnItem
|
||||
id={`col-${index}`}
|
||||
column={col}
|
||||
index={index}
|
||||
onSettingsClick={() => handleOpenDetailSettings(index)}
|
||||
onRemove={() => handleRemoveColumn(index)}
|
||||
showGroupingSettings={grouping.enabled}
|
||||
/>
|
||||
{/* 컬럼 빠른 선택 (인라인) */}
|
||||
{!col.name && (
|
||||
<div className="ml-6 pl-2 border-l-2 border-muted">
|
||||
<SearchableColumnSelect
|
||||
tableName={tableName}
|
||||
value={col.name}
|
||||
onValueChange={(value) => {
|
||||
const colInfo = columns.find((c) => c.column_name === value);
|
||||
handleUpdateColumn(index, {
|
||||
name: value,
|
||||
label: colInfo?.column_comment || "",
|
||||
});
|
||||
}}
|
||||
excludeColumns={selectedColumnNames}
|
||||
placeholder="컬럼을 선택하세요"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
{/* 그룹핑 탭 (좌측 패널만) */}
|
||||
<TabsContent value="grouping" className="mt-4 flex min-h-0 flex-1 flex-col overflow-hidden data-[state=inactive]:hidden">
|
||||
<ScrollArea className="min-h-0 flex-1">
|
||||
<div className="space-y-4 pr-4">
|
||||
<div className="flex items-center justify-between rounded-lg border p-4">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">그룹핑 사용</Label>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
동일한 값을 가진 행들을 하나로 그룹화합니다
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={grouping.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
setGrouping({ ...grouping, enabled: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{grouping.enabled && (
|
||||
<div className="space-y-3 p-4 border rounded-lg bg-muted/30">
|
||||
<div>
|
||||
<Label className="text-sm">그룹 기준 컬럼</Label>
|
||||
<SearchableColumnSelect
|
||||
tableName={tableName}
|
||||
value={grouping.groupByColumn}
|
||||
onValueChange={(value) =>
|
||||
setGrouping({ ...grouping, groupByColumn: value })
|
||||
}
|
||||
placeholder="그룹 기준 컬럼 선택"
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
예: item_id로 그룹핑하면 같은 품목의 데이터를 하나로 표시합니다
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
{/* 검색 탭 */}
|
||||
<TabsContent value="search" className="mt-4 flex min-h-0 flex-1 flex-col overflow-hidden data-[state=inactive]:hidden">
|
||||
<ScrollArea className="min-h-0 flex-1">
|
||||
<div className="space-y-4 pr-4">
|
||||
<div className="flex items-center justify-between rounded-lg border p-4">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">검색 표시</Label>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
검색 입력창을 표시합니다
|
||||
</p>
|
||||
</div>
|
||||
<Switch checked={showSearch} onCheckedChange={setShowSearch} />
|
||||
</div>
|
||||
|
||||
{showSearch && (
|
||||
<div className="space-y-3 p-4 border rounded-lg bg-muted/30">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm">검색 대상 컬럼</Label>
|
||||
<Button size="sm" variant="ghost" onClick={handleAddSearchColumn}>
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{searchColumns.length === 0 ? (
|
||||
<div className="text-center py-4 text-muted-foreground text-sm border rounded-md">
|
||||
검색할 컬럼을 추가하세요
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{searchColumns.map((searchCol, index) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<SearchableColumnSelect
|
||||
tableName={tableName}
|
||||
value={searchCol.columnName}
|
||||
onValueChange={(value) => handleUpdateSearchColumn(index, value)}
|
||||
placeholder="검색 컬럼 선택"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-9 w-9 p-0 text-destructive"
|
||||
onClick={() => handleRemoveSearchColumn(index)}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<DialogFooter className="mt-4 shrink-0">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSave}>저장</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 컬럼 세부설정 모달 */}
|
||||
<Dialog open={detailModalOpen} onOpenChange={setDetailModalOpen}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>컬럼 세부설정</DialogTitle>
|
||||
<DialogDescription>
|
||||
{editingColumn?.label || editingColumn?.name || "컬럼"}의 표시 설정을 변경합니다.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{editingColumn && (
|
||||
<div className="space-y-4">
|
||||
{/* 기본 설정 */}
|
||||
<div className="space-y-3 p-3 border rounded-lg">
|
||||
<h4 className="text-sm font-medium">기본 설정</h4>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs">컬럼 선택</Label>
|
||||
<SearchableColumnSelect
|
||||
tableName={tableName}
|
||||
value={editingColumn.name}
|
||||
onValueChange={(value) => {
|
||||
const colInfo = columns.find((c) => c.column_name === value);
|
||||
setEditingColumn({
|
||||
...editingColumn,
|
||||
name: value,
|
||||
label: colInfo?.column_comment || editingColumn.label,
|
||||
});
|
||||
}}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs">표시 라벨</Label>
|
||||
<Input
|
||||
value={editingColumn.label || ""}
|
||||
onChange={(e) =>
|
||||
setEditingColumn({ ...editingColumn, label: e.target.value })
|
||||
}
|
||||
placeholder="라벨명 (미입력 시 컬럼명 사용)"
|
||||
className="mt-1 h-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs">표시 위치</Label>
|
||||
<Select
|
||||
value={editingColumn.displayRow || "name"}
|
||||
onValueChange={(value: "name" | "info") =>
|
||||
setEditingColumn({ ...editingColumn, displayRow: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="mt-1 h-9">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="name">이름 행 (Name Row)</SelectItem>
|
||||
<SelectItem value="info">정보 행 (Info Row)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs">컬럼 너비 (px)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={editingColumn.width || ""}
|
||||
onChange={(e) =>
|
||||
setEditingColumn({
|
||||
...editingColumn,
|
||||
width: e.target.value ? parseInt(e.target.value) : undefined,
|
||||
})
|
||||
}
|
||||
placeholder="자동"
|
||||
className="mt-1 h-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 그룹핑/집계 설정 (그룹핑 활성화 시만) */}
|
||||
{grouping.enabled && (
|
||||
<div className="space-y-3 p-3 border rounded-lg">
|
||||
<h4 className="text-sm font-medium">그룹핑/집계 설정</h4>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs">표시 방식</Label>
|
||||
<Select
|
||||
value={editingColumn.displayConfig?.displayType || "text"}
|
||||
onValueChange={(value: "text" | "badge") =>
|
||||
setEditingColumn({
|
||||
...editingColumn,
|
||||
displayConfig: {
|
||||
...editingColumn.displayConfig,
|
||||
displayType: value,
|
||||
} as ColumnDisplayConfig,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="mt-1 h-9">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="text">텍스트 (기본)</SelectItem>
|
||||
<SelectItem value="badge">배지 (태그 형태)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
배지는 여러 값을 태그 형태로 나란히 표시합니다
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-xs">집계 사용</Label>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
그룹핑 시 값을 집계합니다
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={editingColumn.displayConfig?.aggregate?.enabled || false}
|
||||
onCheckedChange={(checked) =>
|
||||
setEditingColumn({
|
||||
...editingColumn,
|
||||
displayConfig: {
|
||||
displayType: editingColumn.displayConfig?.displayType || "text",
|
||||
aggregate: {
|
||||
enabled: checked,
|
||||
function: editingColumn.displayConfig?.aggregate?.function || "DISTINCT",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{editingColumn.displayConfig?.aggregate?.enabled && (
|
||||
<div>
|
||||
<Label className="text-xs">집계 방식</Label>
|
||||
<Select
|
||||
value={editingColumn.displayConfig?.aggregate?.function || "DISTINCT"}
|
||||
onValueChange={(value: "DISTINCT" | "COUNT") =>
|
||||
setEditingColumn({
|
||||
...editingColumn,
|
||||
displayConfig: {
|
||||
displayType: editingColumn.displayConfig?.displayType || "text",
|
||||
aggregate: {
|
||||
enabled: true,
|
||||
function: value,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="mt-1 h-9">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="DISTINCT">중복제거 (고유값만)</SelectItem>
|
||||
<SelectItem value="COUNT">개수</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 엔티티 참조 설정 (entity 타입 컬럼일 때만 표시) */}
|
||||
{(() => {
|
||||
const entityInfo = getEditingColumnEntityInfo();
|
||||
if (!entityInfo) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-3 p-3 border rounded-lg">
|
||||
<h4 className="text-sm font-medium">엔티티 표시 컬럼</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
참조 테이블: <span className="font-medium">{entityInfo.referenceTable}</span>
|
||||
</p>
|
||||
|
||||
{entityInfo.isLoading ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<span className="text-sm text-muted-foreground">컬럼 정보 로딩 중...</span>
|
||||
</div>
|
||||
) : entityInfo.referenceColumns.length === 0 ? (
|
||||
<div className="text-center py-4 text-muted-foreground text-sm border rounded-md">
|
||||
참조 테이블의 컬럼 정보를 불러올 수 없습니다
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="max-h-40">
|
||||
<div className="space-y-2 pr-4">
|
||||
{entityInfo.referenceColumns.map((col) => {
|
||||
const isSelected = (editingColumn.entityReference?.displayColumns || []).includes(col.columnName);
|
||||
return (
|
||||
<div
|
||||
key={col.columnName}
|
||||
className={cn(
|
||||
"flex items-center gap-2 p-2 rounded-md cursor-pointer hover:bg-muted/50 transition-colors",
|
||||
isSelected && "bg-muted"
|
||||
)}
|
||||
onClick={() => toggleEntityDisplayColumn(col.columnName)}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={() => toggleEntityDisplayColumn(col.columnName)}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-sm font-medium truncate block">
|
||||
{col.displayName || col.columnName}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground truncate block">
|
||||
{col.columnName} ({col.dataType})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
|
||||
{(editingColumn.entityReference?.displayColumns || []).length > 0 && (
|
||||
<div className="mt-2 pt-2 border-t">
|
||||
<p className="text-xs text-muted-foreground mb-1">
|
||||
선택됨: {(editingColumn.entityReference?.displayColumns || []).length}개
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{(editingColumn.entityReference?.displayColumns || []).map((colName) => {
|
||||
const colInfo = entityInfo.referenceColumns.find(c => c.columnName === colName);
|
||||
return (
|
||||
<span
|
||||
key={colName}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded-full text-xs bg-primary/10 text-primary"
|
||||
>
|
||||
{colInfo?.displayName || colName}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDetailModalOpen(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSaveDetailSettings}>적용</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColumnConfigModal;
|
||||
|
||||
Reference in New Issue
Block a user