ERP-node/frontend/components/unified/config-panels/UnifiedListConfigPanel.tsx

504 lines
19 KiB
TypeScript
Raw Normal View History

2025-12-19 15:44:38 +09:00
"use client";
/**
* UnifiedList
* .
2025-12-23 13:53:22 +09:00
* -
* - +
2025-12-19 15:44:38 +09:00
*/
2025-12-23 13:53:22 +09:00
import React, { useState, useEffect, useMemo } from "react";
2025-12-19 15:44:38 +09:00
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button";
2025-12-19 16:40:40 +09:00
import { Input } from "@/components/ui/input";
2025-12-23 13:53:22 +09:00
import { Database, Link2, GripVertical, ChevronDown, ChevronRight } from "lucide-react";
2025-12-19 16:40:40 +09:00
import { tableTypeApi } from "@/lib/api/screen";
2025-12-23 13:53:22 +09:00
import { cn } from "@/lib/utils";
2025-12-19 15:44:38 +09:00
interface UnifiedListConfigPanelProps {
config: Record<string, any>;
onChange: (config: Record<string, any>) => void;
2025-12-23 13:53:22 +09:00
/** 현재 화면의 테이블명 */
currentTableName?: string;
2025-12-19 16:40:40 +09:00
}
interface ColumnOption {
columnName: string;
displayName: string;
2025-12-23 13:53:22 +09:00
isJoinColumn?: boolean;
sourceTable?: string;
inputType?: string;
2025-12-19 16:40:40 +09:00
}
2025-12-19 15:44:38 +09:00
export const UnifiedListConfigPanel: React.FC<UnifiedListConfigPanelProps> = ({
config,
onChange,
2025-12-23 13:53:22 +09:00
currentTableName,
2025-12-19 15:44:38 +09:00
}) => {
2025-12-23 13:53:22 +09:00
// 컬럼 목록 (테이블 컬럼 + 엔티티 조인 컬럼)
2025-12-19 16:40:40 +09:00
const [columns, setColumns] = useState<ColumnOption[]>([]);
const [loadingColumns, setLoadingColumns] = useState(false);
2025-12-23 13:53:22 +09:00
const [expandedJoinSections, setExpandedJoinSections] = useState<Set<string>>(new Set());
2025-12-19 16:40:40 +09:00
2025-12-19 15:44:38 +09:00
// 설정 업데이트 핸들러
const updateConfig = (field: string, value: any) => {
2025-12-23 13:53:22 +09:00
const newConfig = { ...config, [field]: value };
console.log("⚙️ UnifiedListConfigPanel updateConfig:", { field, value, newConfig });
onChange(newConfig);
2025-12-19 15:44:38 +09:00
};
2025-12-23 13:53:22 +09:00
// 테이블명 (현재 화면의 테이블 사용)
const tableName = currentTableName || config.tableName;
2025-12-19 16:40:40 +09:00
2025-12-24 13:54:24 +09:00
// 화면의 테이블명을 config에 자동 저장
useEffect(() => {
if (currentTableName && config.tableName !== currentTableName) {
onChange({ ...config, tableName: currentTableName });
}
}, [currentTableName]);
2025-12-23 13:53:22 +09:00
// 테이블 컬럼 및 엔티티 조인 컬럼 로드
2025-12-19 16:40:40 +09:00
useEffect(() => {
const loadColumns = async () => {
2025-12-23 13:53:22 +09:00
if (!tableName) {
2025-12-19 16:40:40 +09:00
setColumns([]);
return;
}
2025-12-23 13:53:22 +09:00
2025-12-19 16:40:40 +09:00
setLoadingColumns(true);
try {
2025-12-23 13:53:22 +09:00
// 1. 테이블 컬럼 로드
const columnData = await tableTypeApi.getColumns(tableName);
const baseColumns: ColumnOption[] = columnData.map((c: any) => ({
2025-12-19 16:40:40 +09:00
columnName: c.columnName || c.column_name,
2025-12-23 13:53:22 +09:00
displayName: c.displayName || c.columnLabel || c.columnName || c.column_name,
isJoinColumn: false,
inputType: c.inputType || c.input_type || c.webType || c.web_type,
}));
// 2. 엔티티 타입 컬럼 찾기 및 조인 컬럼 정보 로드
const entityColumns = columnData.filter((c: any) => (c.inputType || c.input_type) === "entity");
const joinColumnOptions: ColumnOption[] = [];
for (const entityCol of entityColumns) {
const colName = entityCol.columnName || entityCol.column_name;
// referenceTable 우선순위:
// 1. 컬럼의 reference_table 필드
// 2. detailSettings.referenceTable
let referenceTable = entityCol.referenceTable || entityCol.reference_table;
if (!referenceTable) {
let detailSettings = entityCol.detailSettings || entityCol.detail_settings;
if (typeof detailSettings === "string") {
try {
detailSettings = JSON.parse(detailSettings);
} catch {
continue;
}
}
referenceTable = detailSettings?.referenceTable;
}
if (referenceTable) {
try {
const refColumnData = await tableTypeApi.getColumns(referenceTable);
refColumnData.forEach((refCol: any) => {
const refColName = refCol.columnName || refCol.column_name;
const refDisplayName = refCol.displayName || refCol.columnLabel || refColName;
joinColumnOptions.push({
columnName: `${colName}.${refColName}`,
displayName: refDisplayName,
isJoinColumn: true,
sourceTable: referenceTable,
});
});
} catch (error) {
console.error(`참조 테이블 ${referenceTable} 컬럼 로드 실패:`, error);
}
}
}
setColumns([...baseColumns, ...joinColumnOptions]);
2025-12-19 16:40:40 +09:00
} catch (error) {
console.error("컬럼 목록 로드 실패:", error);
2025-12-23 13:53:22 +09:00
setColumns([]);
2025-12-19 16:40:40 +09:00
} finally {
setLoadingColumns(false);
}
};
2025-12-23 13:53:22 +09:00
2025-12-19 16:40:40 +09:00
loadColumns();
2025-12-23 13:53:22 +09:00
}, [tableName]);
2025-12-19 16:40:40 +09:00
2025-12-23 13:53:22 +09:00
// 컬럼 설정
2026-01-05 15:35:19 +09:00
const configColumns: Array<{
key: string;
title: string;
width?: string;
isJoinColumn?: boolean;
inputType?: string;
thousandSeparator?: boolean;
}> = config.columns || [];
2025-12-19 15:44:38 +09:00
2025-12-23 13:53:22 +09:00
// 컬럼이 추가되었는지 확인
const isColumnAdded = (columnName: string) => {
return configColumns.some((col) => col.key === columnName);
2025-12-19 15:44:38 +09:00
};
2025-12-23 13:53:22 +09:00
// 컬럼 토글 (추가/제거)
const toggleColumn = (column: ColumnOption) => {
if (isColumnAdded(column.columnName)) {
// 제거
const newColumns = configColumns.filter((col) => col.key !== column.columnName);
updateConfig("columns", newColumns);
} else {
// 추가
2026-01-05 15:35:19 +09:00
const isNumberType = ["number", "decimal", "integer", "float", "double", "numeric", "currency"].includes(
column.inputType || "",
);
2025-12-23 13:53:22 +09:00
const newColumn = {
key: column.columnName,
title: column.displayName,
width: "",
isJoinColumn: column.isJoinColumn || false,
2026-01-05 15:35:19 +09:00
inputType: column.inputType,
thousandSeparator: isNumberType ? true : undefined, // 숫자 타입은 기본적으로 천단위 구분자 사용
2025-12-23 13:53:22 +09:00
};
updateConfig("columns", [...configColumns, newColumn]);
}
};
2026-01-05 15:35:19 +09:00
// 컬럼 천단위 구분자 토글
const toggleThousandSeparator = (columnKey: string, checked: boolean) => {
const newColumns = configColumns.map((col) => (col.key === columnKey ? { ...col, thousandSeparator: checked } : col));
updateConfig("columns", newColumns);
};
// 숫자 타입 컬럼인지 확인
const isNumberColumn = (columnKey: string) => {
const colInfo = columns.find((c) => c.columnName === columnKey);
const configCol = configColumns.find((c) => c.key === columnKey);
const inputType = configCol?.inputType || colInfo?.inputType || "";
return ["number", "decimal", "integer", "float", "double", "numeric", "currency"].includes(inputType);
};
2025-12-23 13:53:22 +09:00
// 컬럼 제목 수정
const updateColumnTitle = (columnKey: string, title: string) => {
const newColumns = configColumns.map((col) => (col.key === columnKey ? { ...col, title } : col));
2025-12-19 15:44:38 +09:00
updateConfig("columns", newColumns);
};
2025-12-23 13:53:22 +09:00
// 그룹별 컬럼 분리
const baseColumns = useMemo(() => columns.filter((col) => !col.isJoinColumn), [columns]);
// 조인 컬럼을 소스 테이블별로 그룹화
const joinColumnsByTable = useMemo(() => {
const grouped: Record<string, ColumnOption[]> = {};
columns
.filter((col) => col.isJoinColumn)
.forEach((col) => {
const table = col.sourceTable || "unknown";
if (!grouped[table]) {
grouped[table] = [];
}
grouped[table].push(col);
});
return grouped;
}, [columns]);
// 조인 섹션 토글
const toggleJoinSection = (tableName: string) => {
setExpandedJoinSections((prev) => {
const newSet = new Set(prev);
if (newSet.has(tableName)) {
newSet.delete(tableName);
} else {
newSet.add(tableName);
}
return newSet;
});
};
2025-12-19 15:44:38 +09:00
return (
<div className="space-y-4">
{/* 뷰 모드 */}
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
2025-12-23 13:53:22 +09:00
<Select value={config.viewMode || "table"} onValueChange={(value) => updateConfig("viewMode", value)}>
2025-12-19 15:44:38 +09:00
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="방식 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="table"></SelectItem>
<SelectItem value="card"></SelectItem>
</SelectContent>
</Select>
</div>
2025-12-23 13:53:22 +09:00
{/* 카드 모드 설정 */}
{config.viewMode === "card" && (
<>
<Separator />
<div className="space-y-3">
<Label className="text-xs font-medium"> </Label>
2025-12-19 15:44:38 +09:00
2025-12-23 13:53:22 +09:00
{/* 제목 컬럼 */}
<div className="space-y-1">
<Label className="text-muted-foreground text-[10px]"> </Label>
<Select
value={config.cardConfig?.titleColumn || ""}
onValueChange={(value) => updateConfig("cardConfig", { ...config.cardConfig, titleColumn: value })}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{configColumns.map((col: any) => (
<SelectItem key={col.key} value={col.key}>
{col.title}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
2025-12-19 15:44:38 +09:00
2025-12-23 13:53:22 +09:00
{/* 부제목 컬럼 */}
<div className="space-y-1">
<Label className="text-muted-foreground text-[10px]"> </Label>
2025-12-19 16:40:40 +09:00
<Select
2025-12-23 13:53:22 +09:00
value={config.cardConfig?.subtitleColumn || "_none_"}
onValueChange={(value) =>
updateConfig("cardConfig", { ...config.cardConfig, subtitleColumn: value === "_none_" ? "" : value })
}
2025-12-19 16:40:40 +09:00
>
2025-12-23 13:53:22 +09:00
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="선택 (선택사항)" />
2025-12-19 16:40:40 +09:00
</SelectTrigger>
<SelectContent>
2025-12-23 13:53:22 +09:00
<SelectItem value="_none_"></SelectItem>
{configColumns.map((col: any) => (
<SelectItem key={col.key} value={col.key}>
{col.title}
2025-12-19 16:40:40 +09:00
</SelectItem>
))}
</SelectContent>
</Select>
2025-12-23 13:53:22 +09:00
</div>
{/* 행당 카드 수 */}
<div className="space-y-1">
<Label className="text-muted-foreground text-[10px]"> </Label>
<Select
value={String(config.cardConfig?.cardsPerRow || 3)}
onValueChange={(value) =>
updateConfig("cardConfig", { ...config.cardConfig, cardsPerRow: parseInt(value) })
}
2025-12-19 15:44:38 +09:00
>
2025-12-23 13:53:22 +09:00
<SelectTrigger className="h-7 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1</SelectItem>
<SelectItem value="2">2</SelectItem>
<SelectItem value="3">3</SelectItem>
<SelectItem value="4">4</SelectItem>
</SelectContent>
</Select>
2025-12-19 15:44:38 +09:00
</div>
2025-12-23 13:53:22 +09:00
</div>
</>
)}
<Separator />
{/* 컬럼 선택 */}
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
{loadingColumns ? (
<p className="text-muted-foreground py-2 text-xs"> ...</p>
) : !tableName ? (
<p className="text-muted-foreground py-2 text-xs"> </p>
) : (
<div className="max-h-60 space-y-2 overflow-y-auto rounded-md border p-2">
{/* 테이블 컬럼 */}
<div className="space-y-0.5">
{baseColumns.map((column) => (
<div
key={column.columnName}
className={cn(
"hover:bg-muted/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1",
isColumnAdded(column.columnName) && "bg-primary/10",
)}
onClick={() => toggleColumn(column)}
>
<Checkbox
checked={isColumnAdded(column.columnName)}
onCheckedChange={() => toggleColumn(column)}
className="pointer-events-none h-3.5 w-3.5 flex-shrink-0"
/>
<Database className="text-muted-foreground h-3 w-3 flex-shrink-0" />
<span className="truncate text-xs">{column.displayName}</span>
</div>
))}
</div>
{/* 조인 컬럼 (테이블별 그룹) */}
{Object.keys(joinColumnsByTable).length > 0 && (
<div className="mt-2 border-t pt-2">
<div className="text-muted-foreground mb-1 flex items-center gap-1 text-[10px] font-medium">
<Link2 className="h-3 w-3 text-blue-500" />
</div>
{Object.entries(joinColumnsByTable).map(([refTable, refColumns]) => (
<div key={refTable} className="mb-1">
<div
className="hover:bg-muted/30 flex cursor-pointer items-center gap-1 rounded px-1 py-0.5"
onClick={() => toggleJoinSection(refTable)}
>
{expandedJoinSections.has(refTable) ? (
<ChevronDown className="text-muted-foreground h-3 w-3 flex-shrink-0" />
) : (
<ChevronRight className="text-muted-foreground h-3 w-3 flex-shrink-0" />
)}
<span className="truncate text-[10px] font-medium text-blue-600">{refTable}</span>
<span className="text-muted-foreground text-[10px]">({refColumns.length})</span>
</div>
{expandedJoinSections.has(refTable) && (
<div className="ml-3 space-y-0.5">
{refColumns.map((column) => (
<div
key={column.columnName}
className={cn(
"hover:bg-muted/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1",
isColumnAdded(column.columnName) && "bg-blue-50",
)}
onClick={() => toggleColumn(column)}
>
<Checkbox
checked={isColumnAdded(column.columnName)}
onCheckedChange={() => toggleColumn(column)}
className="pointer-events-none h-3.5 w-3.5 flex-shrink-0"
/>
<span className="min-w-0 flex-1 truncate text-xs">{column.displayName}</span>
</div>
))}
</div>
)}
</div>
))}
</div>
)}
</div>
)}
2025-12-19 15:44:38 +09:00
</div>
2025-12-23 13:53:22 +09:00
{/* 선택된 컬럼 상세 설정 */}
{configColumns.length > 0 && (
<>
<Separator />
<div className="space-y-2">
<Label className="text-xs font-medium"> ({configColumns.length})</Label>
<div className="max-h-40 space-y-1 overflow-y-auto">
{configColumns.map((column, index) => {
const colInfo = columns.find((c) => c.columnName === column.key);
2026-01-05 15:35:19 +09:00
const showThousandSeparator = isNumberColumn(column.key);
2025-12-23 13:53:22 +09:00
return (
2026-01-05 15:35:19 +09:00
<div key={column.key} className="bg-muted/30 space-y-1.5 rounded-md border p-2">
<div className="flex items-center gap-2">
<GripVertical className="text-muted-foreground h-3 w-3 cursor-grab" />
{column.isJoinColumn ? (
<Link2 className="h-3 w-3 flex-shrink-0 text-blue-500" />
) : (
<Database className="text-muted-foreground h-3 w-3 flex-shrink-0" />
)}
<Input
value={column.title}
onChange={(e) => updateColumnTitle(column.key, e.target.value)}
placeholder="제목"
className="h-6 flex-1 text-xs"
/>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => toggleColumn(colInfo || { columnName: column.key, displayName: column.title })}
className="text-destructive h-6 w-6 p-0"
>
×
</Button>
</div>
{/* 숫자 컬럼인 경우 천단위 구분자 옵션 표시 */}
{showThousandSeparator && (
<div className="ml-5 flex items-center gap-2">
<Checkbox
id={`thousand-${column.key}`}
checked={column.thousandSeparator !== false}
onCheckedChange={(checked) => toggleThousandSeparator(column.key, !!checked)}
className="h-3 w-3"
/>
<label htmlFor={`thousand-${column.key}`} className="text-muted-foreground text-[10px]">
</label>
</div>
2025-12-23 13:53:22 +09:00
)}
</div>
);
})}
</div>
</div>
</>
)}
2025-12-19 15:44:38 +09:00
<Separator />
2026-01-05 15:30:57 +09:00
{/* 페이지네이션 설정 */}
2025-12-19 15:44:38 +09:00
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="pagination"
checked={config.pagination !== false}
onCheckedChange={(checked) => updateConfig("pagination", checked)}
/>
2026-01-05 15:30:57 +09:00
<label htmlFor="pagination" className="text-xs font-medium">
2025-12-23 13:53:22 +09:00
</label>
2025-12-19 15:44:38 +09:00
</div>
2026-01-05 15:30:57 +09:00
{config.pagination !== false && (
2025-12-23 13:53:22 +09:00
<div className="space-y-2">
2026-01-05 15:30:57 +09:00
<Label className="text-xs"> </Label>
2025-12-23 13:53:22 +09:00
<Select
value={String(config.pageSize || 10)}
onValueChange={(value) => updateConfig("pageSize", Number(value))}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="5">5</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="20">20</SelectItem>
<SelectItem value="50">50</SelectItem>
<SelectItem value="100">100</SelectItem>
</SelectContent>
</Select>
</div>
2026-01-05 15:30:57 +09:00
)}
</div>
2025-12-19 15:44:38 +09:00
</div>
);
};
UnifiedListConfigPanel.displayName = "UnifiedListConfigPanel";
export default UnifiedListConfigPanel;