계층 구조 트리 뷰
This commit is contained in:
parent
efaa267d78
commit
672aba8404
|
|
@ -6,7 +6,7 @@ import { SplitPanelLayoutConfig } from "./types";
|
|||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Plus, Search, GripVertical, Loader2, ChevronDown, ChevronUp, Save } from "lucide-react";
|
||||
import { Plus, Search, GripVertical, Loader2, ChevronDown, ChevronUp, Save, ChevronRight } from "lucide-react";
|
||||
import { dataApi } from "@/lib/api/data";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { tableTypeApi } from "@/lib/api/screen";
|
||||
|
|
@ -47,11 +47,12 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
const [isLoadingLeft, setIsLoadingLeft] = useState(false);
|
||||
const [isLoadingRight, setIsLoadingRight] = useState(false);
|
||||
const [rightTableColumns, setRightTableColumns] = useState<any[]>([]); // 우측 테이블 컬럼 정보
|
||||
const [expandedItems, setExpandedItems] = useState<Set<any>>(new Set()); // 펼쳐진 항목들
|
||||
const { toast } = useToast();
|
||||
|
||||
// 추가 모달 상태
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
const [addModalPanel, setAddModalPanel] = useState<"left" | "right" | null>(null);
|
||||
const [addModalPanel, setAddModalPanel] = useState<"left" | "right" | "left-item" | null>(null);
|
||||
const [addModalFormData, setAddModalFormData] = useState<Record<string, any>>({});
|
||||
|
||||
// 리사이저 드래그 상태
|
||||
|
|
@ -88,6 +89,53 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
border: isSelected ? "2px solid #3b82f6" : "1px solid #e5e7eb",
|
||||
};
|
||||
|
||||
// 계층 구조 빌드 함수 (트리 구조 유지)
|
||||
const buildHierarchy = useCallback((items: any[]): any[] => {
|
||||
if (!items || items.length === 0) return [];
|
||||
|
||||
const itemAddConfig = componentConfig.leftPanel?.itemAddConfig;
|
||||
if (!itemAddConfig) return items.map(item => ({ ...item, children: [] })); // 계층 설정이 없으면 평면 목록
|
||||
|
||||
const { sourceColumn, parentColumn } = itemAddConfig;
|
||||
if (!sourceColumn || !parentColumn) return items.map(item => ({ ...item, children: [] }));
|
||||
|
||||
// ID를 키로 하는 맵 생성
|
||||
const itemMap = new Map<any, any>();
|
||||
const rootItems: any[] = [];
|
||||
|
||||
// 모든 항목을 맵에 추가하고 children 배열 초기화
|
||||
items.forEach(item => {
|
||||
const id = item[sourceColumn];
|
||||
itemMap.set(id, { ...item, children: [], level: 0 });
|
||||
});
|
||||
|
||||
// 부모-자식 관계 설정
|
||||
items.forEach(item => {
|
||||
const id = item[sourceColumn];
|
||||
const parentId = item[parentColumn];
|
||||
const currentItem = itemMap.get(id);
|
||||
|
||||
if (!currentItem) return;
|
||||
|
||||
if (!parentId || parentId === null || parentId === '') {
|
||||
// 최상위 항목
|
||||
rootItems.push(currentItem);
|
||||
} else {
|
||||
// 부모가 있는 항목
|
||||
const parentItem = itemMap.get(parentId);
|
||||
if (parentItem) {
|
||||
currentItem.level = parentItem.level + 1;
|
||||
parentItem.children.push(currentItem);
|
||||
} else {
|
||||
// 부모를 찾을 수 없으면 최상위로 처리
|
||||
rootItems.push(currentItem);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return rootItems;
|
||||
}, [componentConfig.leftPanel?.itemAddConfig]);
|
||||
|
||||
// 좌측 데이터 로드
|
||||
const loadLeftData = useCallback(async () => {
|
||||
const leftTableName = componentConfig.leftPanel?.tableName;
|
||||
|
|
@ -100,7 +148,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
size: 100,
|
||||
// searchTerm 제거 - 클라이언트 사이드에서 필터링
|
||||
});
|
||||
setLeftData(result.data);
|
||||
|
||||
// 계층 구조 빌드
|
||||
const hierarchicalData = buildHierarchy(result.data);
|
||||
setLeftData(hierarchicalData);
|
||||
} catch (error) {
|
||||
console.error("좌측 데이터 로드 실패:", error);
|
||||
toast({
|
||||
|
|
@ -111,7 +162,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
} finally {
|
||||
setIsLoadingLeft(false);
|
||||
}
|
||||
}, [componentConfig.leftPanel?.tableName, isDesignMode, toast]);
|
||||
}, [componentConfig.leftPanel?.tableName, isDesignMode, toast, buildHierarchy]);
|
||||
|
||||
// 우측 데이터 로드
|
||||
const loadRightData = useCallback(
|
||||
|
|
@ -215,6 +266,19 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
loadRightTableColumns();
|
||||
}, [componentConfig.rightPanel?.tableName, isDesignMode]);
|
||||
|
||||
// 항목 펼치기/접기 토글
|
||||
const toggleExpand = useCallback((itemId: any) => {
|
||||
setExpandedItems(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(itemId)) {
|
||||
newSet.delete(itemId);
|
||||
} else {
|
||||
newSet.add(itemId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 추가 버튼 핸들러
|
||||
const handleAddClick = useCallback((panel: "left" | "right") => {
|
||||
setAddModalPanel(panel);
|
||||
|
|
@ -222,15 +286,65 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
setShowAddModal(true);
|
||||
}, []);
|
||||
|
||||
// 항목별 추가 버튼 핸들러 (좌측 항목의 + 버튼 - 하위 항목 추가)
|
||||
const handleItemAddClick = useCallback((item: any) => {
|
||||
const itemAddConfig = componentConfig.leftPanel?.itemAddConfig;
|
||||
|
||||
if (!itemAddConfig) {
|
||||
toast({
|
||||
title: "설정 오류",
|
||||
description: "하위 항목 추가 설정이 없습니다.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { sourceColumn, parentColumn } = itemAddConfig;
|
||||
|
||||
if (!sourceColumn || !parentColumn) {
|
||||
toast({
|
||||
title: "설정 오류",
|
||||
description: "현재 항목 ID 컬럼과 상위 항목 저장 컬럼을 설정해주세요.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 선택된 항목의 sourceColumn 값을 가져와서 parentColumn에 매핑
|
||||
const sourceValue = item[sourceColumn];
|
||||
|
||||
if (!sourceValue) {
|
||||
toast({
|
||||
title: "데이터 오류",
|
||||
description: `선택한 항목의 ${sourceColumn} 값이 없습니다.`,
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 좌측 패널 추가 모달 열기 (parentColumn 값 미리 채우기)
|
||||
setAddModalPanel("left-item");
|
||||
setAddModalFormData({ [parentColumn]: sourceValue });
|
||||
setShowAddModal(true);
|
||||
}, [componentConfig, toast]);
|
||||
|
||||
// 추가 모달 저장
|
||||
const handleAddModalSave = useCallback(async () => {
|
||||
const tableName = addModalPanel === "left"
|
||||
? componentConfig.leftPanel?.tableName
|
||||
: componentConfig.rightPanel?.tableName;
|
||||
// 테이블명과 모달 컬럼 결정
|
||||
let tableName: string | undefined;
|
||||
let modalColumns: Array<{ name: string; label: string; required?: boolean }> | undefined;
|
||||
|
||||
const modalColumns = addModalPanel === "left"
|
||||
? componentConfig.leftPanel?.addModalColumns
|
||||
: componentConfig.rightPanel?.addModalColumns;
|
||||
if (addModalPanel === "left") {
|
||||
tableName = componentConfig.leftPanel?.tableName;
|
||||
modalColumns = componentConfig.leftPanel?.addModalColumns;
|
||||
} else if (addModalPanel === "right") {
|
||||
tableName = componentConfig.rightPanel?.tableName;
|
||||
modalColumns = componentConfig.rightPanel?.addModalColumns;
|
||||
} else if (addModalPanel === "left-item") {
|
||||
// 하위 항목 추가 (좌측 테이블에 추가)
|
||||
tableName = componentConfig.leftPanel?.tableName;
|
||||
modalColumns = componentConfig.leftPanel?.itemAddConfig?.addModalColumns;
|
||||
}
|
||||
|
||||
if (!tableName) {
|
||||
toast({
|
||||
|
|
@ -270,9 +384,11 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
setAddModalFormData({});
|
||||
|
||||
// 데이터 새로고침
|
||||
if (addModalPanel === "left") {
|
||||
if (addModalPanel === "left" || addModalPanel === "left-item") {
|
||||
// 좌측 패널 데이터 새로고침 (일반 추가 또는 하위 항목 추가)
|
||||
loadLeftData();
|
||||
} else if (selectedLeftItem) {
|
||||
} else if (addModalPanel === "right" && selectedLeftItem) {
|
||||
// 우측 패널 데이터 새로고침
|
||||
loadRightData(selectedLeftItem);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -487,16 +603,18 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
})
|
||||
: leftData;
|
||||
|
||||
return filteredLeftData.length > 0 ? (
|
||||
// 실제 데이터 표시
|
||||
filteredLeftData.map((item, index) => {
|
||||
const itemId = item.id || item.ID || item[Object.keys(item)[0]] || index;
|
||||
const isSelected =
|
||||
selectedLeftItem && (selectedLeftItem.id === itemId || selectedLeftItem === item);
|
||||
// 재귀 렌더링 함수
|
||||
const renderTreeItem = (item: any, index: number): React.ReactNode => {
|
||||
const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || 'id';
|
||||
const itemId = item[sourceColumn] || item.id || item.ID || index;
|
||||
const isSelected = selectedLeftItem && (selectedLeftItem[sourceColumn] === itemId || selectedLeftItem === item);
|
||||
const hasChildren = item.children && item.children.length > 0;
|
||||
const isExpanded = expandedItems.has(itemId);
|
||||
const level = item.level || 0;
|
||||
|
||||
// 조인에 사용하는 leftColumn을 필수로 표시
|
||||
const leftColumn = componentConfig.rightPanel?.relation?.leftColumn;
|
||||
let displayFields: { label: string; value: any }[] = [];
|
||||
// 조인에 사용하는 leftColumn을 필수로 표시
|
||||
const leftColumn = componentConfig.rightPanel?.relation?.leftColumn;
|
||||
let displayFields: { label: string; value: any }[] = [];
|
||||
|
||||
// 디버그 로그
|
||||
if (index === 0) {
|
||||
|
|
@ -541,22 +659,71 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
}
|
||||
}
|
||||
|
||||
const displayTitle = displayFields[0]?.value || item.name || item.title || `항목 ${index + 1}`;
|
||||
const displaySubtitle = displayFields[1]?.value || null;
|
||||
const displayTitle = displayFields[0]?.value || item.name || item.title || `항목 ${index + 1}`;
|
||||
const displaySubtitle = displayFields[1]?.value || null;
|
||||
|
||||
return (
|
||||
return (
|
||||
<React.Fragment key={itemId}>
|
||||
{/* 현재 항목 */}
|
||||
<div
|
||||
key={itemId}
|
||||
onClick={() => handleLeftItemSelect(item)}
|
||||
className={`cursor-pointer rounded-md p-3 transition-colors hover:bg-muted ${
|
||||
className={`group relative cursor-pointer rounded-md p-3 transition-colors hover:bg-muted ${
|
||||
isSelected ? "bg-primary/10 text-primary" : "text-foreground"
|
||||
}`}
|
||||
style={{ paddingLeft: `${12 + level * 24}px` }}
|
||||
>
|
||||
<div className="truncate font-medium">{displayTitle}</div>
|
||||
{displaySubtitle && <div className="truncate text-xs text-muted-foreground">{displaySubtitle}</div>}
|
||||
<div
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
handleLeftItemSelect(item);
|
||||
if (hasChildren) {
|
||||
toggleExpand(itemId);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* 펼치기/접기 아이콘 */}
|
||||
{hasChildren ? (
|
||||
<div className="flex-shrink-0">
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4 text-gray-500" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 text-gray-500" />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-5" />
|
||||
)}
|
||||
|
||||
{/* 항목 내용 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="truncate font-medium">{displayTitle}</div>
|
||||
{displaySubtitle && <div className="truncate text-xs text-muted-foreground">{displaySubtitle}</div>}
|
||||
</div>
|
||||
|
||||
{/* 항목별 추가 버튼 */}
|
||||
{componentConfig.leftPanel?.showItemAddButton && !isDesignMode && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleItemAddClick(item);
|
||||
}}
|
||||
className="flex-shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
title="하위 항목 추가"
|
||||
>
|
||||
<Plus className="h-5 w-5 rounded-md bg-primary p-1 text-primary-foreground hover:bg-primary/90" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
|
||||
{/* 자식 항목들 (접혀있으면 표시 안함) */}
|
||||
{hasChildren && isExpanded && item.children.map((child: any, childIndex: number) => renderTreeItem(child, childIndex))}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return filteredLeftData.length > 0 ? (
|
||||
// 실제 데이터 표시
|
||||
filteredLeftData.map((item, index) => renderTreeItem(item, index))
|
||||
) : (
|
||||
// 검색 결과 없음
|
||||
<div className="py-8 text-center text-sm text-muted-foreground">
|
||||
|
|
@ -842,37 +1009,62 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base sm:text-lg">
|
||||
{addModalPanel === "left" ? componentConfig.leftPanel?.title : componentConfig.rightPanel?.title} 추가
|
||||
{addModalPanel === "left"
|
||||
? `${componentConfig.leftPanel?.title} 추가`
|
||||
: addModalPanel === "right"
|
||||
? `${componentConfig.rightPanel?.title} 추가`
|
||||
: `하위 ${componentConfig.leftPanel?.title} 추가`}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-xs sm:text-sm">
|
||||
새로운 데이터를 추가합니다. 필수 항목을 입력해주세요.
|
||||
{addModalPanel === "left-item"
|
||||
? "선택한 항목의 하위 항목을 추가합니다. 필수 항목을 입력해주세요."
|
||||
: "새로운 데이터를 추가합니다. 필수 항목을 입력해주세요."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
{(addModalPanel === "left"
|
||||
? componentConfig.leftPanel?.addModalColumns
|
||||
: componentConfig.rightPanel?.addModalColumns
|
||||
)?.map((col, index) => (
|
||||
<div key={index}>
|
||||
<Label htmlFor={col.name} className="text-xs sm:text-sm">
|
||||
{col.label} {col.required && <span className="text-destructive">*</span>}
|
||||
</Label>
|
||||
<Input
|
||||
id={col.name}
|
||||
value={addModalFormData[col.name] || ""}
|
||||
onChange={(e) => {
|
||||
setAddModalFormData(prev => ({
|
||||
...prev,
|
||||
[col.name]: e.target.value
|
||||
}));
|
||||
}}
|
||||
placeholder={`${col.label} 입력`}
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
required={col.required}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{(() => {
|
||||
// 어떤 컬럼들을 표시할지 결정
|
||||
let modalColumns: Array<{ name: string; label: string; required?: boolean }> | undefined;
|
||||
|
||||
if (addModalPanel === "left") {
|
||||
modalColumns = componentConfig.leftPanel?.addModalColumns;
|
||||
} else if (addModalPanel === "right") {
|
||||
modalColumns = componentConfig.rightPanel?.addModalColumns;
|
||||
} else if (addModalPanel === "left-item") {
|
||||
modalColumns = componentConfig.leftPanel?.itemAddConfig?.addModalColumns;
|
||||
}
|
||||
|
||||
return modalColumns?.map((col, index) => {
|
||||
// 항목별 추가 버튼으로 열렸을 때, parentColumn은 미리 채워져 있고 수정 불가
|
||||
const isPreFilled = addModalPanel === "left-item"
|
||||
&& componentConfig.leftPanel?.itemAddConfig?.parentColumn === col.name
|
||||
&& addModalFormData[col.name];
|
||||
|
||||
return (
|
||||
<div key={index}>
|
||||
<Label htmlFor={col.name} className="text-xs sm:text-sm">
|
||||
{col.label} {col.required && <span className="text-destructive">*</span>}
|
||||
{isPreFilled && <span className="ml-2 text-[10px] text-blue-600">(자동 설정됨)</span>}
|
||||
</Label>
|
||||
<Input
|
||||
id={col.name}
|
||||
value={addModalFormData[col.name] || ""}
|
||||
onChange={(e) => {
|
||||
setAddModalFormData(prev => ({
|
||||
...prev,
|
||||
[col.name]: e.target.value
|
||||
}));
|
||||
}}
|
||||
placeholder={`${col.label} 입력`}
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
required={col.required}
|
||||
disabled={isPreFilled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
|
|
|
|||
|
|
@ -90,6 +90,29 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [config.leftPanel?.tableName, screenTableName, loadedTableColumns, config.leftPanel?.showAdd]);
|
||||
|
||||
// 좌측 패널 하위 항목 추가 모달 PK 자동 추가
|
||||
useEffect(() => {
|
||||
const leftTableName = config.leftPanel?.tableName || screenTableName;
|
||||
if (leftTableName && loadedTableColumns[leftTableName] && config.leftPanel?.showItemAddButton) {
|
||||
const currentAddModalColumns = config.leftPanel?.itemAddConfig?.addModalColumns || [];
|
||||
const updatedColumns = ensurePrimaryKeysInAddModal(leftTableName, currentAddModalColumns);
|
||||
|
||||
// PK가 추가되었으면 업데이트
|
||||
if (updatedColumns.length !== currentAddModalColumns.length) {
|
||||
console.log(`🔄 좌측 패널 하위 항목 추가: PK 컬럼 자동 추가 (${leftTableName})`);
|
||||
updateLeftPanel({
|
||||
itemAddConfig: {
|
||||
...config.leftPanel?.itemAddConfig,
|
||||
addModalColumns: updatedColumns,
|
||||
parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "",
|
||||
sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "",
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [config.leftPanel?.tableName, screenTableName, loadedTableColumns, config.leftPanel?.showItemAddButton]);
|
||||
|
||||
// 우측 패널 테이블 컬럼 로드 완료 시 PK 자동 추가
|
||||
useEffect(() => {
|
||||
const rightTableName = config.rightPanel?.tableName;
|
||||
|
|
@ -340,6 +363,307 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>각 항목에 + 버튼</Label>
|
||||
<Switch
|
||||
checked={config.leftPanel?.showItemAddButton ?? false}
|
||||
onCheckedChange={(checked) => updateLeftPanel({ showItemAddButton: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 항목별 + 버튼 설정 (하위 항목 추가) */}
|
||||
{config.leftPanel?.showItemAddButton && (
|
||||
<div className="space-y-3 rounded-lg border border-blue-200 bg-blue-50 p-3">
|
||||
<Label className="text-sm font-semibold">하위 항목 추가 설정</Label>
|
||||
<p className="text-xs text-gray-600">
|
||||
+ 버튼 클릭 시 선택된 항목의 하위 항목을 추가합니다 (예: 부서 → 하위 부서)
|
||||
</p>
|
||||
|
||||
{/* 현재 항목의 값을 가져올 컬럼 (sourceColumn) */}
|
||||
<div>
|
||||
<Label className="text-xs">현재 항목 ID 컬럼</Label>
|
||||
<p className="mb-2 text-[10px] text-gray-500">
|
||||
선택된 항목의 어떤 컬럼 값을 사용할지 (예: dept_code)
|
||||
</p>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="h-8 w-full justify-between text-xs"
|
||||
>
|
||||
{config.leftPanel?.itemAddConfig?.sourceColumn || "컬럼 선택"}
|
||||
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
|
||||
<CommandEmpty className="text-xs">컬럼을 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup className="max-h-[200px] overflow-auto">
|
||||
{leftTableColumns
|
||||
.filter((column) => !['company_code', 'company_name'].includes(column.columnName))
|
||||
.map((column) => (
|
||||
<CommandItem
|
||||
key={column.columnName}
|
||||
value={column.columnName}
|
||||
onSelect={(value) => {
|
||||
updateLeftPanel({
|
||||
itemAddConfig: {
|
||||
...config.leftPanel?.itemAddConfig,
|
||||
sourceColumn: value,
|
||||
parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "",
|
||||
}
|
||||
});
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
config.leftPanel?.itemAddConfig?.sourceColumn === column.columnName ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{column.columnLabel || column.columnName}
|
||||
<span className="ml-2 text-[10px] text-gray-500">
|
||||
({column.columnName})
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{/* 상위 항목 ID를 저장할 컬럼 (parentColumn) */}
|
||||
<div>
|
||||
<Label className="text-xs">상위 항목 저장 컬럼</Label>
|
||||
<p className="mb-2 text-[10px] text-gray-500">
|
||||
하위 항목에서 상위 항목 ID를 저장할 컬럼 (예: parent_dept_code)
|
||||
</p>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="h-8 w-full justify-between text-xs"
|
||||
>
|
||||
{config.leftPanel?.itemAddConfig?.parentColumn || "컬럼 선택"}
|
||||
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
|
||||
<CommandEmpty className="text-xs">컬럼을 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup className="max-h-[200px] overflow-auto">
|
||||
{leftTableColumns
|
||||
.filter((column) => !['company_code', 'company_name'].includes(column.columnName))
|
||||
.map((column) => (
|
||||
<CommandItem
|
||||
key={column.columnName}
|
||||
value={column.columnName}
|
||||
onSelect={(value) => {
|
||||
updateLeftPanel({
|
||||
itemAddConfig: {
|
||||
...config.leftPanel?.itemAddConfig,
|
||||
parentColumn: value,
|
||||
sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "",
|
||||
}
|
||||
});
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
config.leftPanel?.itemAddConfig?.parentColumn === column.columnName ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{column.columnLabel || column.columnName}
|
||||
<span className="ml-2 text-[10px] text-gray-500">
|
||||
({column.columnName})
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{/* 하위 항목 추가 모달 컬럼 설정 */}
|
||||
<div className="space-y-2 rounded border border-blue-300 bg-white p-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs font-semibold">추가 모달 입력 컬럼</Label>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const currentColumns = config.leftPanel?.itemAddConfig?.addModalColumns || [];
|
||||
const newColumns = [
|
||||
...currentColumns,
|
||||
{ name: "", label: "", required: false },
|
||||
];
|
||||
updateLeftPanel({
|
||||
itemAddConfig: {
|
||||
...config.leftPanel?.itemAddConfig,
|
||||
addModalColumns: newColumns,
|
||||
parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "",
|
||||
sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "",
|
||||
}
|
||||
});
|
||||
}}
|
||||
className="h-6 text-[10px]"
|
||||
>
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
컬럼 추가
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-600">
|
||||
하위 항목 추가 시 입력받을 필드를 선택하세요
|
||||
</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
{(config.leftPanel?.itemAddConfig?.addModalColumns || []).length === 0 ? (
|
||||
<div className="rounded-md border border-dashed border-gray-300 bg-gray-50 p-2 text-center">
|
||||
<p className="text-[10px] text-gray-500">설정된 컬럼이 없습니다</p>
|
||||
</div>
|
||||
) : (
|
||||
(config.leftPanel?.itemAddConfig?.addModalColumns || []).map((col, index) => {
|
||||
const column = leftTableColumns.find(c => c.columnName === col.name);
|
||||
const isPK = column?.isPrimaryKey || false;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-md border p-2",
|
||||
isPK ? "border-yellow-300 bg-yellow-50" : "bg-white"
|
||||
)}
|
||||
>
|
||||
{isPK && (
|
||||
<span className="rounded bg-yellow-200 px-1.5 py-0.5 text-[10px] font-semibold text-yellow-700">
|
||||
PK
|
||||
</span>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
disabled={isPK}
|
||||
className="h-7 w-full justify-between text-[10px]"
|
||||
>
|
||||
{col.name || "컬럼 선택"}
|
||||
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
|
||||
<CommandEmpty className="text-xs">컬럼을 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup className="max-h-[200px] overflow-auto">
|
||||
{leftTableColumns
|
||||
.filter((column) => !['company_code', 'company_name'].includes(column.columnName))
|
||||
.map((column) => (
|
||||
<CommandItem
|
||||
key={column.columnName}
|
||||
value={column.columnName}
|
||||
onSelect={(value) => {
|
||||
const newColumns = [...(config.leftPanel?.itemAddConfig?.addModalColumns || [])];
|
||||
newColumns[index] = {
|
||||
...newColumns[index],
|
||||
name: value,
|
||||
label: column.columnLabel || value,
|
||||
};
|
||||
updateLeftPanel({
|
||||
itemAddConfig: {
|
||||
...config.leftPanel?.itemAddConfig,
|
||||
addModalColumns: newColumns,
|
||||
parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "",
|
||||
sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "",
|
||||
}
|
||||
});
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
col.name === column.columnName ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{column.columnLabel || column.columnName}
|
||||
<span className="ml-2 text-[10px] text-gray-500">
|
||||
({column.columnName})
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<label className="flex cursor-pointer items-center gap-1 text-[10px] text-gray-600">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={col.required ?? false}
|
||||
disabled={isPK}
|
||||
onChange={(e) => {
|
||||
const newColumns = [...(config.leftPanel?.itemAddConfig?.addModalColumns || [])];
|
||||
newColumns[index] = {
|
||||
...newColumns[index],
|
||||
required: e.target.checked,
|
||||
};
|
||||
updateLeftPanel({
|
||||
itemAddConfig: {
|
||||
...config.leftPanel?.itemAddConfig,
|
||||
addModalColumns: newColumns,
|
||||
parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "",
|
||||
sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "",
|
||||
}
|
||||
});
|
||||
}}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
필수
|
||||
</label>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
disabled={isPK}
|
||||
onClick={() => {
|
||||
const newColumns = (config.leftPanel?.itemAddConfig?.addModalColumns || []).filter(
|
||||
(_, i) => i !== index
|
||||
);
|
||||
updateLeftPanel({
|
||||
itemAddConfig: {
|
||||
...config.leftPanel?.itemAddConfig,
|
||||
addModalColumns: newColumns,
|
||||
parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "",
|
||||
sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "",
|
||||
}
|
||||
});
|
||||
}}
|
||||
className="h-7 w-7 p-0"
|
||||
title={isPK ? "PK 컬럼은 삭제할 수 없습니다" : ""}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 좌측 패널 추가 모달 컬럼 설정 */}
|
||||
{config.leftPanel?.showAdd && (
|
||||
<div className="space-y-3 rounded-lg border border-purple-200 bg-purple-50 p-3">
|
||||
|
|
|
|||
|
|
@ -21,6 +21,21 @@ export interface SplitPanelLayoutConfig {
|
|||
label: string;
|
||||
required?: boolean;
|
||||
}>;
|
||||
// 각 항목에 + 버튼 표시 (하위 항목 추가)
|
||||
showItemAddButton?: boolean;
|
||||
// + 버튼 클릭 시 하위 항목 추가를 위한 설정
|
||||
itemAddConfig?: {
|
||||
// 하위 항목 추가 모달에서 입력받을 컬럼
|
||||
addModalColumns?: Array<{
|
||||
name: string;
|
||||
label: string;
|
||||
required?: boolean;
|
||||
}>;
|
||||
// 상위 항목의 ID를 저장할 컬럼 (예: parent_dept_code)
|
||||
parentColumn: string;
|
||||
// 현재 항목의 어떤 컬럼 값을 parentColumn에 넣을지 (예: dept_code)
|
||||
sourceColumn: string;
|
||||
};
|
||||
};
|
||||
|
||||
// 우측 패널 설정
|
||||
|
|
|
|||
Loading…
Reference in New Issue