807 lines
32 KiB
TypeScript
807 lines
32 KiB
TypeScript
"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;
|
||
|
||
|