분할패널 테이블 리스트 구현
This commit is contained in:
@@ -48,6 +48,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
const [isLoadingRight, setIsLoadingRight] = useState(false);
|
||||
const [rightTableColumns, setRightTableColumns] = useState<any[]>([]); // 우측 테이블 컬럼 정보
|
||||
const [expandedItems, setExpandedItems] = useState<Set<any>>(new Set()); // 펼쳐진 항목들
|
||||
const [leftColumnLabels, setLeftColumnLabels] = useState<Record<string, string>>({}); // 좌측 컬럼 라벨
|
||||
const [rightColumnLabels, setRightColumnLabels] = useState<Record<string, string>>({}); // 우측 컬럼 라벨
|
||||
const { toast } = useToast();
|
||||
|
||||
// 추가 모달 상태
|
||||
@@ -270,6 +272,32 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
[rightTableColumns],
|
||||
);
|
||||
|
||||
// 좌측 테이블 컬럼 라벨 로드
|
||||
useEffect(() => {
|
||||
const loadLeftColumnLabels = async () => {
|
||||
const leftTableName = componentConfig.leftPanel?.tableName;
|
||||
if (!leftTableName || isDesignMode) return;
|
||||
|
||||
try {
|
||||
const columnsResponse = await tableTypeApi.getColumns(leftTableName);
|
||||
const labels: Record<string, string> = {};
|
||||
columnsResponse.forEach((col: any) => {
|
||||
const columnName = col.columnName || col.column_name;
|
||||
const label = col.columnLabel || col.column_label || col.displayName || columnName;
|
||||
if (columnName) {
|
||||
labels[columnName] = label;
|
||||
}
|
||||
});
|
||||
setLeftColumnLabels(labels);
|
||||
console.log("✅ 좌측 컬럼 라벨 로드:", labels);
|
||||
} catch (error) {
|
||||
console.error("좌측 테이블 컬럼 라벨 로드 실패:", error);
|
||||
}
|
||||
};
|
||||
|
||||
loadLeftColumnLabels();
|
||||
}, [componentConfig.leftPanel?.tableName, isDesignMode]);
|
||||
|
||||
// 우측 테이블 컬럼 정보 로드
|
||||
useEffect(() => {
|
||||
const loadRightTableColumns = async () => {
|
||||
@@ -279,6 +307,18 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
try {
|
||||
const columnsResponse = await tableTypeApi.getColumns(rightTableName);
|
||||
setRightTableColumns(columnsResponse || []);
|
||||
|
||||
// 우측 컬럼 라벨도 함께 로드
|
||||
const labels: Record<string, string> = {};
|
||||
columnsResponse.forEach((col: any) => {
|
||||
const columnName = col.columnName || col.column_name;
|
||||
const label = col.columnLabel || col.column_label || col.displayName || columnName;
|
||||
if (columnName) {
|
||||
labels[columnName] = label;
|
||||
}
|
||||
});
|
||||
setRightColumnLabels(labels);
|
||||
console.log("✅ 우측 컬럼 라벨 로드:", labels);
|
||||
} catch (error) {
|
||||
console.error("우측 테이블 컬럼 정보 로드 실패:", error);
|
||||
}
|
||||
@@ -784,46 +824,157 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 overflow-auto p-4">
|
||||
{/* 좌측 데이터 목록 */}
|
||||
<div className="space-y-1">
|
||||
{isDesignMode ? (
|
||||
// 디자인 모드: 샘플 데이터
|
||||
<>
|
||||
<div
|
||||
onClick={() => handleLeftItemSelect({ id: 1, name: "항목 1" })}
|
||||
className={`hover:bg-accent cursor-pointer rounded-md p-3 transition-colors ${
|
||||
selectedLeftItem?.id === 1 ? "bg-primary/10 text-primary" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium">항목 1</div>
|
||||
<div className="text-muted-foreground text-xs">설명 텍스트</div>
|
||||
{/* 좌측 데이터 목록/테이블 */}
|
||||
{componentConfig.leftPanel?.displayMode === "table" ? (
|
||||
// 테이블 모드
|
||||
<div className="w-full">
|
||||
{isDesignMode ? (
|
||||
// 디자인 모드: 샘플 테이블
|
||||
<div className="overflow-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">컬럼 1</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">컬럼 2</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">컬럼 3</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 bg-white">
|
||||
<tr className="hover:bg-gray-50 cursor-pointer">
|
||||
<td className="whitespace-nowrap px-3 py-2 text-sm">데이터 1-1</td>
|
||||
<td className="whitespace-nowrap px-3 py-2 text-sm">데이터 1-2</td>
|
||||
<td className="whitespace-nowrap px-3 py-2 text-sm">데이터 1-3</td>
|
||||
</tr>
|
||||
<tr className="hover:bg-gray-50 cursor-pointer">
|
||||
<td className="whitespace-nowrap px-3 py-2 text-sm">데이터 2-1</td>
|
||||
<td className="whitespace-nowrap px-3 py-2 text-sm">데이터 2-2</td>
|
||||
<td className="whitespace-nowrap px-3 py-2 text-sm">데이터 2-3</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => handleLeftItemSelect({ id: 2, name: "항목 2" })}
|
||||
className={`hover:bg-accent cursor-pointer rounded-md p-3 transition-colors ${
|
||||
selectedLeftItem?.id === 2 ? "bg-primary/10 text-primary" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium">항목 2</div>
|
||||
<div className="text-muted-foreground text-xs">설명 텍스트</div>
|
||||
) : isLoadingLeft ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin" />
|
||||
<span className="text-muted-foreground ml-2 text-sm">데이터를 불러오는 중...</span>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => handleLeftItemSelect({ id: 3, name: "항목 3" })}
|
||||
className={`hover:bg-accent cursor-pointer rounded-md p-3 transition-colors ${
|
||||
selectedLeftItem?.id === 3 ? "bg-primary/10 text-primary" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium">항목 3</div>
|
||||
<div className="text-muted-foreground text-xs">설명 텍스트</div>
|
||||
) : (
|
||||
(() => {
|
||||
const filteredData = leftSearchQuery
|
||||
? leftData.filter((item) => {
|
||||
const searchLower = leftSearchQuery.toLowerCase();
|
||||
return Object.entries(item).some(([key, value]) => {
|
||||
if (value === null || value === undefined) return false;
|
||||
return String(value).toLowerCase().includes(searchLower);
|
||||
});
|
||||
})
|
||||
: leftData;
|
||||
|
||||
const displayColumns = componentConfig.leftPanel?.columns || [];
|
||||
const columnsToShow = displayColumns.length > 0
|
||||
? displayColumns.map(col => ({
|
||||
...col,
|
||||
label: leftColumnLabels[col.name] || col.label || col.name
|
||||
}))
|
||||
: Object.keys(filteredData[0] || {}).filter(key => key !== 'children' && key !== 'level').slice(0, 5).map(key => ({
|
||||
name: key,
|
||||
label: leftColumnLabels[key] || key,
|
||||
width: 150,
|
||||
align: "left" as const
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="overflow-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="sticky top-0 bg-gray-50 z-10">
|
||||
<tr>
|
||||
{columnsToShow.map((col, idx) => (
|
||||
<th
|
||||
key={idx}
|
||||
className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
style={{ width: col.width ? `${col.width}px` : 'auto', textAlign: col.align || "left" }}
|
||||
>
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 bg-white">
|
||||
{filteredData.map((item, idx) => {
|
||||
const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || 'id';
|
||||
const itemId = item[sourceColumn] || item.id || item.ID || idx;
|
||||
const isSelected = selectedLeftItem && (selectedLeftItem[sourceColumn] === itemId || selectedLeftItem === item);
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={itemId}
|
||||
onClick={() => handleLeftItemSelect(item)}
|
||||
className={`hover:bg-accent cursor-pointer transition-colors ${
|
||||
isSelected ? "bg-primary/10" : ""
|
||||
}`}
|
||||
>
|
||||
{columnsToShow.map((col, colIdx) => (
|
||||
<td
|
||||
key={colIdx}
|
||||
className="whitespace-nowrap px-3 py-2 text-sm text-gray-900"
|
||||
style={{ textAlign: col.align || "left" }}
|
||||
>
|
||||
{item[col.name] !== null && item[col.name] !== undefined
|
||||
? String(item[col.name])
|
||||
: "-"}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// 목록 모드 (기존)
|
||||
<div className="space-y-1">
|
||||
{isDesignMode ? (
|
||||
// 디자인 모드: 샘플 데이터
|
||||
<>
|
||||
<div
|
||||
onClick={() => handleLeftItemSelect({ id: 1, name: "항목 1" })}
|
||||
className={`hover:bg-accent cursor-pointer rounded-md p-3 transition-colors ${
|
||||
selectedLeftItem?.id === 1 ? "bg-primary/10 text-primary" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium">항목 1</div>
|
||||
<div className="text-muted-foreground text-xs">설명 텍스트</div>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => handleLeftItemSelect({ id: 2, name: "항목 2" })}
|
||||
className={`hover:bg-accent cursor-pointer rounded-md p-3 transition-colors ${
|
||||
selectedLeftItem?.id === 2 ? "bg-primary/10 text-primary" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium">항목 2</div>
|
||||
<div className="text-muted-foreground text-xs">설명 텍스트</div>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => handleLeftItemSelect({ id: 3, name: "항목 3" })}
|
||||
className={`hover:bg-accent cursor-pointer rounded-md p-3 transition-colors ${
|
||||
selectedLeftItem?.id === 3 ? "bg-primary/10 text-primary" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium">항목 3</div>
|
||||
<div className="text-muted-foreground text-xs">설명 텍스트</div>
|
||||
</div>
|
||||
</>
|
||||
) : isLoadingLeft ? (
|
||||
// 로딩 중
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin" />
|
||||
<span className="text-muted-foreground ml-2 text-sm">데이터를 불러오는 중...</span>
|
||||
</div>
|
||||
</>
|
||||
) : isLoadingLeft ? (
|
||||
// 로딩 중
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin" />
|
||||
<span className="text-muted-foreground ml-2 text-sm">데이터를 불러오는 중...</span>
|
||||
</div>
|
||||
) : (
|
||||
) : (
|
||||
(() => {
|
||||
// 검색 필터링 (클라이언트 사이드)
|
||||
const filteredLeftData = leftSearchQuery
|
||||
@@ -1001,7 +1152,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -1081,6 +1233,107 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
})
|
||||
: rightData;
|
||||
|
||||
// 테이블 모드 체크
|
||||
const isTableMode = componentConfig.rightPanel?.displayMode === "table";
|
||||
|
||||
if (isTableMode) {
|
||||
// 테이블 모드 렌더링
|
||||
const displayColumns = componentConfig.rightPanel?.columns || [];
|
||||
const columnsToShow = displayColumns.length > 0
|
||||
? displayColumns.map(col => ({
|
||||
...col,
|
||||
label: rightColumnLabels[col.name] || col.label || col.name
|
||||
}))
|
||||
: Object.keys(filteredData[0] || {}).filter(key => !key.toLowerCase().includes("password")).slice(0, 5).map(key => ({
|
||||
name: key,
|
||||
label: rightColumnLabels[key] || key,
|
||||
width: 150,
|
||||
align: "left" as const
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-2 text-xs text-muted-foreground">
|
||||
{filteredData.length}개의 관련 데이터
|
||||
{rightSearchQuery && filteredData.length !== rightData.length && (
|
||||
<span className="ml-1 text-primary">(전체 {rightData.length}개 중)</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="overflow-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="sticky top-0 bg-gray-50 z-10">
|
||||
<tr>
|
||||
{columnsToShow.map((col, idx) => (
|
||||
<th
|
||||
key={idx}
|
||||
className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
style={{ width: col.width ? `${col.width}px` : 'auto', textAlign: col.align || "left" }}
|
||||
>
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
{!isDesignMode && (
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase">작업</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 bg-white">
|
||||
{filteredData.map((item, idx) => {
|
||||
const itemId = item.id || item.ID || idx;
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={itemId}
|
||||
className="hover:bg-accent transition-colors"
|
||||
>
|
||||
{columnsToShow.map((col, colIdx) => (
|
||||
<td
|
||||
key={colIdx}
|
||||
className="whitespace-nowrap px-3 py-2 text-sm text-gray-900"
|
||||
style={{ textAlign: col.align || "left" }}
|
||||
>
|
||||
{item[col.name] !== null && item[col.name] !== undefined
|
||||
? String(item[col.name])
|
||||
: "-"}
|
||||
</td>
|
||||
))}
|
||||
{!isDesignMode && (
|
||||
<td className="whitespace-nowrap px-3 py-2 text-right text-sm">
|
||||
<div className="flex justify-end gap-1">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEditClick("right", item);
|
||||
}}
|
||||
className="rounded p-1 hover:bg-gray-200 transition-colors"
|
||||
title="수정"
|
||||
>
|
||||
<Pencil className="h-4 w-4 text-gray-600" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteClick("right", item);
|
||||
}}
|
||||
className="rounded p-1 hover:bg-red-100 transition-colors"
|
||||
title="삭제"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-red-600" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 목록 모드 (기존)
|
||||
return filteredData.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
<div className="mb-2 text-xs text-muted-foreground">
|
||||
|
||||
Reference in New Issue
Block a user