feat: 다중 선택 및 일괄 삭제 기능 추가
- 카테고리 값 관리 컴포넌트에 체크박스를 통한 다중 선택 기능을 추가하였습니다. - 선택된 카테고리를 일괄 삭제할 수 있는 다이얼로그를 구현하였습니다. - 테이블 관리 서비스에서 다중 선택 처리 로직을 추가하여, 파이프(|)로 구분된 값을 처리하도록 개선하였습니다. - 관련된 로그 메시지를 추가하여 다중 선택 및 삭제 과정에서의 정보를 기록하도록 하였습니다.
This commit is contained in:
parent
64cc5c6772
commit
042488d51b
|
|
@ -1465,6 +1465,31 @@ export class TableManagementService {
|
|||
|
||||
const webType = columnInfo.webType;
|
||||
|
||||
// 🔧 다중선택 처리: actualValue가 파이프(|)를 포함하고 날짜 타입이 아닌 경우
|
||||
if (
|
||||
typeof actualValue === "string" &&
|
||||
actualValue.includes("|") &&
|
||||
webType !== "date" &&
|
||||
webType !== "datetime"
|
||||
) {
|
||||
const multiValues = actualValue
|
||||
.split("|")
|
||||
.filter((v: string) => v.trim() !== "");
|
||||
if (multiValues.length > 0) {
|
||||
const placeholders = multiValues
|
||||
.map((_: string, idx: number) => `$${paramIndex + idx}`)
|
||||
.join(", ");
|
||||
logger.info(
|
||||
`🔍 다중선택 필터 적용 (객체): ${columnName} IN (${multiValues.join(", ")})`
|
||||
);
|
||||
return {
|
||||
whereClause: `${columnName}::text IN (${placeholders})`,
|
||||
values: multiValues,
|
||||
paramCount: multiValues.length,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 웹타입별 검색 조건 구성
|
||||
switch (webType) {
|
||||
case "date":
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export function RegistryProvider({ children }: RegistryProviderProps) {
|
|||
|
||||
// V2 Core 초기화 (느슨한 결합 아키텍처)
|
||||
initV2Core({
|
||||
debug: process.env.NODE_ENV === "development",
|
||||
debug: false,
|
||||
legacyBridge: {
|
||||
legacyToV2: true,
|
||||
v2ToLegacy: true,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,668 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useTableOptions } from "@/contexts/TableOptionsContext";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { GripVertical, Eye, EyeOff, Lock, ArrowRight, X, Settings, Filter, Layers } from "lucide-react";
|
||||
import { ColumnVisibility, TableFilter, GroupSumConfig } from "@/types/table-options";
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onFiltersApplied?: (filters: TableFilter[]) => void;
|
||||
screenId?: number;
|
||||
}
|
||||
|
||||
// 컬럼 필터 설정 인터페이스
|
||||
interface ColumnFilterConfig {
|
||||
columnName: string;
|
||||
columnLabel: string;
|
||||
inputType: string;
|
||||
enabled: boolean;
|
||||
filterType: "text" | "number" | "date" | "select";
|
||||
width?: number;
|
||||
selectOptions?: Array<{ label: string; value: string }>;
|
||||
}
|
||||
|
||||
export const TableSettingsModal: React.FC<Props> = ({ isOpen, onClose, onFiltersApplied, screenId }) => {
|
||||
const { getTable, selectedTableId } = useTableOptions();
|
||||
const table = selectedTableId ? getTable(selectedTableId) : undefined;
|
||||
|
||||
const [activeTab, setActiveTab] = useState("columns");
|
||||
|
||||
// 컬럼 가시성 상태
|
||||
const [localColumns, setLocalColumns] = useState<ColumnVisibility[]>([]);
|
||||
const [draggedColumnIndex, setDraggedColumnIndex] = useState<number | null>(null);
|
||||
const [frozenColumnCount, setFrozenColumnCount] = useState<number>(0);
|
||||
|
||||
// 필터 상태
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFilterConfig[]>([]);
|
||||
const [selectAllFilters, setSelectAllFilters] = useState(false);
|
||||
const [groupSumEnabled, setGroupSumEnabled] = useState(false);
|
||||
const [groupByColumn, setGroupByColumn] = useState<string>("");
|
||||
|
||||
// 그룹화 상태
|
||||
const [selectedGroupColumns, setSelectedGroupColumns] = useState<string[]>([]);
|
||||
const [draggedGroupIndex, setDraggedGroupIndex] = useState<number | null>(null);
|
||||
|
||||
// 테이블 정보 로드 - 컬럼 가시성
|
||||
useEffect(() => {
|
||||
if (table) {
|
||||
setLocalColumns(
|
||||
table.columns.map((col) => ({
|
||||
columnName: col.columnName,
|
||||
visible: col.visible,
|
||||
width: col.width,
|
||||
order: 0,
|
||||
}))
|
||||
);
|
||||
setFrozenColumnCount(table.frozenColumnCount ?? 0);
|
||||
}
|
||||
}, [table]);
|
||||
|
||||
// 테이블 정보 로드 - 필터
|
||||
useEffect(() => {
|
||||
if (table?.columns && table?.tableName) {
|
||||
const storageKey = screenId
|
||||
? `table_filters_${table.tableName}_screen_${screenId}`
|
||||
: `table_filters_${table.tableName}`;
|
||||
const savedFilters = localStorage.getItem(storageKey);
|
||||
|
||||
const groupSumKey = screenId
|
||||
? `table_groupsum_${table.tableName}_screen_${screenId}`
|
||||
: `table_groupsum_${table.tableName}`;
|
||||
const savedGroupSum = localStorage.getItem(groupSumKey);
|
||||
|
||||
if (savedGroupSum) {
|
||||
try {
|
||||
const parsed = JSON.parse(savedGroupSum) as GroupSumConfig;
|
||||
setGroupSumEnabled(parsed.enabled);
|
||||
setGroupByColumn(parsed.groupByColumn || "");
|
||||
} catch {
|
||||
setGroupSumEnabled(false);
|
||||
setGroupByColumn("");
|
||||
}
|
||||
}
|
||||
|
||||
if (savedFilters) {
|
||||
try {
|
||||
const parsed = JSON.parse(savedFilters);
|
||||
setColumnFilters(parsed);
|
||||
setSelectAllFilters(parsed.every((f: ColumnFilterConfig) => f.enabled));
|
||||
} catch {
|
||||
initializeFilters();
|
||||
}
|
||||
} else {
|
||||
initializeFilters();
|
||||
}
|
||||
}
|
||||
}, [table?.columns, table?.tableName, screenId]);
|
||||
|
||||
const initializeFilters = () => {
|
||||
if (!table?.columns) return;
|
||||
|
||||
const filters: ColumnFilterConfig[] = table.columns
|
||||
.filter((col) => col.columnName !== "__checkbox__")
|
||||
.map((col) => {
|
||||
let filterType: "text" | "number" | "date" | "select" = "text";
|
||||
const inputType = col.inputType || "";
|
||||
|
||||
if (["number", "decimal", "currency", "integer"].includes(inputType)) {
|
||||
filterType = "number";
|
||||
} else if (["date", "datetime", "time"].includes(inputType)) {
|
||||
filterType = "date";
|
||||
} else if (["select", "dropdown", "code", "category", "entity"].includes(inputType)) {
|
||||
filterType = "select";
|
||||
}
|
||||
|
||||
return {
|
||||
columnName: col.columnName,
|
||||
columnLabel: col.columnLabel,
|
||||
inputType,
|
||||
enabled: false,
|
||||
filterType,
|
||||
width: 200,
|
||||
};
|
||||
});
|
||||
|
||||
setColumnFilters(filters);
|
||||
setSelectAllFilters(false);
|
||||
};
|
||||
|
||||
// 컬럼 가시성 핸들러
|
||||
const handleVisibilityChange = (columnName: string, visible: boolean) => {
|
||||
setLocalColumns((prev) =>
|
||||
prev.map((col) => (col.columnName === columnName ? { ...col, visible } : col))
|
||||
);
|
||||
};
|
||||
|
||||
const handleWidthChange = (columnName: string, width: number) => {
|
||||
setLocalColumns((prev) =>
|
||||
prev.map((col) => (col.columnName === columnName ? { ...col, width } : col))
|
||||
);
|
||||
};
|
||||
|
||||
const moveColumn = (fromIndex: number, toIndex: number) => {
|
||||
const newColumns = [...localColumns];
|
||||
const [movedItem] = newColumns.splice(fromIndex, 1);
|
||||
newColumns.splice(toIndex, 0, movedItem);
|
||||
setLocalColumns(newColumns);
|
||||
};
|
||||
|
||||
// 필터 핸들러
|
||||
const handleFilterEnabledChange = (columnName: string, enabled: boolean) => {
|
||||
setColumnFilters((prev) =>
|
||||
prev.map((f) => (f.columnName === columnName ? { ...f, enabled } : f))
|
||||
);
|
||||
};
|
||||
|
||||
const handleFilterTypeChange = (columnName: string, filterType: "text" | "number" | "date" | "select") => {
|
||||
setColumnFilters((prev) =>
|
||||
prev.map((f) => (f.columnName === columnName ? { ...f, filterType } : f))
|
||||
);
|
||||
};
|
||||
|
||||
const handleFilterWidthChange = (columnName: string, width: number) => {
|
||||
setColumnFilters((prev) =>
|
||||
prev.map((f) => (f.columnName === columnName ? { ...f, width } : f))
|
||||
);
|
||||
};
|
||||
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
setSelectAllFilters(checked);
|
||||
setColumnFilters((prev) => prev.map((f) => ({ ...f, enabled: checked })));
|
||||
};
|
||||
|
||||
// 그룹화 핸들러
|
||||
const toggleGroupColumn = (columnName: string) => {
|
||||
if (selectedGroupColumns.includes(columnName)) {
|
||||
setSelectedGroupColumns(selectedGroupColumns.filter((c) => c !== columnName));
|
||||
} else {
|
||||
setSelectedGroupColumns([...selectedGroupColumns, columnName]);
|
||||
}
|
||||
};
|
||||
|
||||
const removeGroupColumn = (columnName: string) => {
|
||||
setSelectedGroupColumns(selectedGroupColumns.filter((c) => c !== columnName));
|
||||
};
|
||||
|
||||
const moveGroupColumn = (fromIndex: number, toIndex: number) => {
|
||||
const newColumns = [...selectedGroupColumns];
|
||||
const [movedItem] = newColumns.splice(fromIndex, 1);
|
||||
newColumns.splice(toIndex, 0, movedItem);
|
||||
setSelectedGroupColumns(newColumns);
|
||||
};
|
||||
|
||||
const clearGrouping = () => {
|
||||
setSelectedGroupColumns([]);
|
||||
table?.onGroupChange([]);
|
||||
};
|
||||
|
||||
// 틀고정 컬럼 수 변경 핸들러
|
||||
const handleFrozenColumnCountChange = (value: string) => {
|
||||
const count = parseInt(value) || 0;
|
||||
// 최대값은 표시 가능한 컬럼 수
|
||||
const maxCount = localColumns.filter((col) => col.visible).length;
|
||||
setFrozenColumnCount(Math.min(Math.max(0, count), maxCount));
|
||||
};
|
||||
|
||||
const visibleCount = localColumns.filter((col) => col.visible).length;
|
||||
|
||||
// 저장
|
||||
const handleSave = () => {
|
||||
if (!table) return;
|
||||
|
||||
// 1. 컬럼 가시성 저장
|
||||
table.onColumnVisibilityChange(localColumns);
|
||||
|
||||
// 2. 컬럼 순서 변경 콜백 호출
|
||||
if (table.onColumnOrderChange) {
|
||||
const newOrder = localColumns
|
||||
.map((col) => col.columnName)
|
||||
.filter((name) => name !== "__checkbox__");
|
||||
table.onColumnOrderChange(newOrder);
|
||||
}
|
||||
|
||||
// 3. 틀고정 컬럼 수 변경 콜백 호출 (현재 컬럼 상태도 함께 전달)
|
||||
if (table.onFrozenColumnCountChange) {
|
||||
const updatedColumns = localColumns.map((col) => ({
|
||||
columnName: col.columnName,
|
||||
visible: col.visible,
|
||||
}));
|
||||
table.onFrozenColumnCountChange(frozenColumnCount, updatedColumns);
|
||||
}
|
||||
|
||||
// 2. 필터 설정 저장
|
||||
const storageKey = screenId
|
||||
? `table_filters_${table.tableName}_screen_${screenId}`
|
||||
: `table_filters_${table.tableName}`;
|
||||
localStorage.setItem(storageKey, JSON.stringify(columnFilters));
|
||||
|
||||
// 그룹별 합산 설정 저장
|
||||
const groupSumKey = screenId
|
||||
? `table_groupsum_${table.tableName}_screen_${screenId}`
|
||||
: `table_groupsum_${table.tableName}`;
|
||||
const groupSumConfig: GroupSumConfig = {
|
||||
enabled: groupSumEnabled,
|
||||
groupByColumn: groupByColumn || undefined,
|
||||
};
|
||||
localStorage.setItem(groupSumKey, JSON.stringify(groupSumConfig));
|
||||
|
||||
// 활성화된 필터만 콜백
|
||||
const activeFilters: TableFilter[] = columnFilters
|
||||
.filter((f) => f.enabled)
|
||||
.map((f) => ({
|
||||
columnName: f.columnName,
|
||||
operator: "contains",
|
||||
value: "",
|
||||
filterType: f.filterType,
|
||||
width: f.width || 200,
|
||||
}));
|
||||
onFiltersApplied?.(activeFilters);
|
||||
|
||||
// 3. 그룹화 저장
|
||||
table.onGroupChange(selectedGroupColumns);
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base sm:text-lg">테이블 설정</DialogTitle>
|
||||
<DialogDescription className="text-xs sm:text-sm">
|
||||
테이블의 컬럼, 필터, 그룹화를 설정합니다
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="columns" className="gap-1.5 text-xs sm:text-sm">
|
||||
<Settings className="h-3.5 w-3.5" />
|
||||
컬럼 설정
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="filters" className="gap-1.5 text-xs sm:text-sm">
|
||||
<Filter className="h-3.5 w-3.5" />
|
||||
필터 설정
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="grouping" className="gap-1.5 text-xs sm:text-sm">
|
||||
<Layers className="h-3.5 w-3.5" />
|
||||
그룹 설정
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 컬럼 설정 탭 */}
|
||||
<TabsContent value="columns" className="mt-4">
|
||||
<div className="space-y-4">
|
||||
{/* 상태 표시 및 틀고정 설정 */}
|
||||
<div className="flex flex-col gap-3 rounded-lg border bg-muted/50 p-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-muted-foreground text-xs sm:text-sm">
|
||||
{visibleCount}/{localColumns.length}개 컬럼 표시 중
|
||||
</div>
|
||||
|
||||
{/* 틀고정 설정 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Lock className="text-muted-foreground h-4 w-4" />
|
||||
<Label className="text-muted-foreground whitespace-nowrap text-xs">
|
||||
틀고정:
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={frozenColumnCount}
|
||||
onChange={(e) => handleFrozenColumnCountChange(e.target.value)}
|
||||
className="h-7 w-16 text-xs sm:h-8 sm:w-20 sm:text-sm"
|
||||
min={0}
|
||||
max={visibleCount}
|
||||
placeholder="0"
|
||||
/>
|
||||
<span className="text-muted-foreground whitespace-nowrap text-xs">
|
||||
개 컬럼
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (table) {
|
||||
setLocalColumns(
|
||||
table.columns.map((col) => ({
|
||||
columnName: col.columnName,
|
||||
visible: true,
|
||||
width: 150,
|
||||
order: 0,
|
||||
}))
|
||||
);
|
||||
setFrozenColumnCount(0);
|
||||
}
|
||||
}}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 컬럼 목록 */}
|
||||
<ScrollArea className="h-[300px]">
|
||||
<div className="space-y-2 pr-4">
|
||||
{localColumns.map((col, index) => {
|
||||
const originalCol = table?.columns.find((c) => c.columnName === col.columnName);
|
||||
if (!originalCol) return null;
|
||||
|
||||
// 표시 가능한 컬럼 중 몇 번째인지 계산 (틀고정 표시용)
|
||||
const visibleIndex = localColumns
|
||||
.slice(0, index + 1)
|
||||
.filter((c) => c.visible).length;
|
||||
const isFrozen = col.visible && visibleIndex <= frozenColumnCount;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={col.columnName}
|
||||
draggable
|
||||
onDragStart={() => setDraggedColumnIndex(index)}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
if (draggedColumnIndex !== null && draggedColumnIndex !== index) {
|
||||
moveColumn(draggedColumnIndex, index);
|
||||
setDraggedColumnIndex(index);
|
||||
}
|
||||
}}
|
||||
onDragEnd={() => setDraggedColumnIndex(null)}
|
||||
className={`flex cursor-move items-center gap-3 rounded-lg border p-3 transition-colors ${
|
||||
isFrozen
|
||||
? "border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950/30"
|
||||
: "bg-background hover:bg-muted/50"
|
||||
}`}
|
||||
>
|
||||
{/* 드래그 핸들 */}
|
||||
<GripVertical className="text-muted-foreground h-4 w-4 shrink-0" />
|
||||
|
||||
{/* 체크박스 */}
|
||||
<Checkbox
|
||||
checked={col.visible}
|
||||
onCheckedChange={(checked) =>
|
||||
handleVisibilityChange(col.columnName, checked as boolean)
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 가시성/틀고정 아이콘 */}
|
||||
{isFrozen ? (
|
||||
<Lock className="h-4 w-4 shrink-0 text-blue-500" />
|
||||
) : col.visible ? (
|
||||
<Eye className="text-primary h-4 w-4 shrink-0" />
|
||||
) : (
|
||||
<EyeOff className="text-muted-foreground h-4 w-4 shrink-0" />
|
||||
)}
|
||||
|
||||
{/* 컬럼명 */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate text-xs font-medium sm:text-sm">
|
||||
{originalCol.columnLabel}
|
||||
</span>
|
||||
{isFrozen && (
|
||||
<span className="text-[10px] font-medium text-blue-600 dark:text-blue-400">
|
||||
(고정)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-muted-foreground truncate text-[10px] sm:text-xs">
|
||||
{col.columnName}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 너비 설정 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-muted-foreground text-xs">너비:</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={col.width || 150}
|
||||
onChange={(e) => handleWidthChange(col.columnName, parseInt(e.target.value) || 150)}
|
||||
className="h-7 w-16 text-xs sm:h-8 sm:w-20 sm:text-sm"
|
||||
min={50}
|
||||
max={500}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 필터 설정 탭 */}
|
||||
<TabsContent value="filters" className="mt-4">
|
||||
<div className="space-y-4">
|
||||
{/* 전체 선택 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={selectAllFilters}
|
||||
onCheckedChange={(checked) => handleSelectAll(checked as boolean)}
|
||||
/>
|
||||
<Label className="text-xs sm:text-sm">전체 선택</Label>
|
||||
</div>
|
||||
|
||||
{/* 필터 목록 */}
|
||||
<ScrollArea className="h-[300px]">
|
||||
<div className="space-y-2 pr-4">
|
||||
{columnFilters.map((filter) => (
|
||||
<div
|
||||
key={filter.columnName}
|
||||
className="flex items-center gap-2 rounded-lg border bg-background p-2"
|
||||
>
|
||||
<Checkbox
|
||||
checked={filter.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
handleFilterEnabledChange(filter.columnName, checked as boolean)
|
||||
}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-xs font-medium sm:text-sm">
|
||||
{filter.columnLabel}
|
||||
</div>
|
||||
</div>
|
||||
<Select
|
||||
value={filter.filterType}
|
||||
onValueChange={(v) =>
|
||||
handleFilterTypeChange(filter.columnName, v as "text" | "number" | "date" | "select")
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-7 w-20 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="text">텍스트</SelectItem>
|
||||
<SelectItem value="number">숫자</SelectItem>
|
||||
<SelectItem value="date">날짜</SelectItem>
|
||||
<SelectItem value="select">선택</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
type="number"
|
||||
min={100}
|
||||
max={400}
|
||||
value={filter.width || 200}
|
||||
onChange={(e) =>
|
||||
handleFilterWidthChange(filter.columnName, parseInt(e.target.value) || 200)
|
||||
}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span className="text-muted-foreground text-xs">px</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* 그룹별 합산 설정 */}
|
||||
<div className="rounded-lg border bg-muted/30 p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-xs font-medium sm:text-sm">그룹별 합산</div>
|
||||
<div className="text-muted-foreground text-[10px] sm:text-xs">
|
||||
같은 값끼리 그룹핑하여 합산
|
||||
</div>
|
||||
</div>
|
||||
<Switch checked={groupSumEnabled} onCheckedChange={setGroupSumEnabled} />
|
||||
</div>
|
||||
{groupSumEnabled && (
|
||||
<div className="mt-3">
|
||||
<Select value={groupByColumn} onValueChange={setGroupByColumn}>
|
||||
<SelectTrigger className="h-8 text-xs sm:text-sm">
|
||||
<SelectValue placeholder="그룹화 기준 컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{columnFilters.map((f) => (
|
||||
<SelectItem key={f.columnName} value={f.columnName}>
|
||||
{f.columnLabel}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 그룹 설정 탭 */}
|
||||
<TabsContent value="grouping" className="mt-4">
|
||||
<div className="space-y-4">
|
||||
{/* 선택된 그룹화 컬럼 */}
|
||||
{selectedGroupColumns.length > 0 && (
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="text-xs font-medium sm:text-sm">
|
||||
그룹화 순서 ({selectedGroupColumns.length}개)
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={clearGrouping} className="h-7 text-xs">
|
||||
전체 해제
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{selectedGroupColumns.map((colName, index) => {
|
||||
const col = table?.columns.find((c) => c.columnName === colName);
|
||||
if (!col) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={colName}
|
||||
draggable
|
||||
onDragStart={() => setDraggedGroupIndex(index)}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
if (draggedGroupIndex !== null && draggedGroupIndex !== index) {
|
||||
moveGroupColumn(draggedGroupIndex, index);
|
||||
setDraggedGroupIndex(index);
|
||||
}
|
||||
}}
|
||||
onDragEnd={() => setDraggedGroupIndex(null)}
|
||||
className="hover:bg-primary/10 bg-primary/5 flex cursor-move items-center gap-2 rounded-lg border p-2 transition-colors"
|
||||
>
|
||||
<GripVertical className="text-muted-foreground h-4 w-4 flex-shrink-0" />
|
||||
<div className="bg-primary text-primary-foreground flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full text-xs">
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-xs font-medium sm:text-sm">{col.columnLabel}</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeGroupColumn(colName)}
|
||||
className="h-6 w-6 flex-shrink-0 p-0"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 그룹화 순서 미리보기 */}
|
||||
<div className="bg-muted/30 mt-2 rounded-lg border p-2">
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs">
|
||||
{selectedGroupColumns.map((colName, index) => {
|
||||
const col = table?.columns.find((c) => c.columnName === colName);
|
||||
return (
|
||||
<React.Fragment key={colName}>
|
||||
<span className="font-medium">{col?.columnLabel}</span>
|
||||
{index < selectedGroupColumns.length - 1 && (
|
||||
<ArrowRight className="text-muted-foreground h-3 w-3" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 사용 가능한 컬럼 */}
|
||||
<div>
|
||||
<div className="mb-2 text-xs font-medium sm:text-sm">사용 가능한 컬럼</div>
|
||||
<ScrollArea className={selectedGroupColumns.length > 0 ? "h-[200px]" : "h-[320px]"}>
|
||||
<div className="space-y-2 pr-4">
|
||||
{table?.columns
|
||||
.filter((col) => !selectedGroupColumns.includes(col.columnName))
|
||||
.map((col) => (
|
||||
<div
|
||||
key={col.columnName}
|
||||
className="hover:bg-muted/50 flex cursor-pointer items-center gap-3 rounded-lg border bg-background p-2 transition-colors"
|
||||
onClick={() => toggleGroupColumn(col.columnName)}
|
||||
>
|
||||
<Checkbox
|
||||
checked={false}
|
||||
onCheckedChange={() => toggleGroupColumn(col.columnName)}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-xs font-medium sm:text-sm">{col.columnLabel}</div>
|
||||
<div className="text-muted-foreground truncate text-[10px] sm:text-xs">
|
||||
{col.columnName}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSave} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
|
||||
저장
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -3,9 +3,10 @@
|
|||
/**
|
||||
* 카테고리 값 관리 - 트리 구조 버전
|
||||
* - 3단계 트리 구조 지원 (대분류/중분류/소분류)
|
||||
* - 체크박스를 통한 다중 선택 및 일괄 삭제 지원
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import {
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
|
|
@ -20,6 +21,7 @@ import {
|
|||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
CategoryValue,
|
||||
|
|
@ -65,11 +67,13 @@ interface TreeNodeProps {
|
|||
expandedNodes: Set<number>;
|
||||
selectedValueId?: number;
|
||||
searchQuery: string;
|
||||
checkedIds: Set<number>;
|
||||
onToggle: (valueId: number) => void;
|
||||
onSelect: (value: CategoryValue) => void;
|
||||
onAdd: (parentValue: CategoryValue | null) => void;
|
||||
onEdit: (value: CategoryValue) => void;
|
||||
onDelete: (value: CategoryValue) => void;
|
||||
onCheck: (valueId: number, checked: boolean) => void;
|
||||
}
|
||||
|
||||
// 검색어가 노드 또는 하위에 매칭되는지 확인
|
||||
|
|
@ -90,15 +94,18 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
|||
expandedNodes,
|
||||
selectedValueId,
|
||||
searchQuery,
|
||||
checkedIds,
|
||||
onToggle,
|
||||
onSelect,
|
||||
onAdd,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onCheck,
|
||||
}) => {
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
const isExpanded = expandedNodes.has(node.valueId);
|
||||
const isSelected = selectedValueId === node.valueId;
|
||||
const isChecked = checkedIds.has(node.valueId);
|
||||
const canAddChild = node.depth < 3;
|
||||
|
||||
// 검색 필터링
|
||||
|
|
@ -138,11 +145,22 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
|||
className={cn(
|
||||
"group flex items-center gap-1 rounded-md px-2 py-2 transition-colors",
|
||||
isSelected ? "border-primary bg-primary/10 border-l-2" : "hover:bg-muted/50",
|
||||
isChecked && "bg-primary/5",
|
||||
"cursor-pointer",
|
||||
)}
|
||||
style={{ paddingLeft: `${level * 20 + 8}px` }}
|
||||
onClick={() => onSelect(node)}
|
||||
>
|
||||
{/* 체크박스 */}
|
||||
<Checkbox
|
||||
checked={isChecked}
|
||||
onCheckedChange={(checked) => {
|
||||
onCheck(node.valueId, checked as boolean);
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="mr-1"
|
||||
/>
|
||||
|
||||
{/* 확장 토글 */}
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -233,11 +251,13 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
|||
expandedNodes={expandedNodes}
|
||||
selectedValueId={selectedValueId}
|
||||
searchQuery={searchQuery}
|
||||
checkedIds={checkedIds}
|
||||
onToggle={onToggle}
|
||||
onSelect={onSelect}
|
||||
onAdd={onAdd}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
onCheck={onCheck}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -259,11 +279,13 @@ export const CategoryValueManagerTree: React.FC<CategoryValueManagerTreeProps> =
|
|||
const [selectedValue, setSelectedValue] = useState<CategoryValue | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [showInactive, setShowInactive] = useState(false);
|
||||
const [checkedIds, setCheckedIds] = useState<Set<number>>(new Set());
|
||||
|
||||
// 모달 상태
|
||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
||||
const [parentValue, setParentValue] = useState<CategoryValue | null>(null);
|
||||
const [editingValue, setEditingValue] = useState<CategoryValue | null>(null);
|
||||
const [deletingValue, setDeletingValue] = useState<CategoryValue | null>(null);
|
||||
|
|
@ -289,12 +311,52 @@ export const CategoryValueManagerTree: React.FC<CategoryValueManagerTreeProps> =
|
|||
}, []);
|
||||
|
||||
// 하위 항목 개수만 계산 (자기 자신 제외)
|
||||
const countAllDescendants = useCallback((node: CategoryValue): number => {
|
||||
if (!node.children || node.children.length === 0) {
|
||||
return 0;
|
||||
const countAllDescendants = useCallback(
|
||||
(node: CategoryValue): number => {
|
||||
if (!node.children || node.children.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
return countAllValues(node.children);
|
||||
},
|
||||
[countAllValues],
|
||||
);
|
||||
|
||||
// 노드와 모든 하위 항목의 ID 수집
|
||||
const collectNodeAndDescendantIds = useCallback((node: CategoryValue): number[] => {
|
||||
const ids: number[] = [node.valueId];
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
ids.push(...collectNodeAndDescendantIds(child));
|
||||
}
|
||||
}
|
||||
return countAllValues(node.children);
|
||||
}, [countAllValues]);
|
||||
return ids;
|
||||
}, []);
|
||||
|
||||
// 트리에서 valueId로 노드 찾기
|
||||
const findNodeById = useCallback((nodes: CategoryValue[], valueId: number): CategoryValue | null => {
|
||||
for (const node of nodes) {
|
||||
if (node.valueId === valueId) {
|
||||
return node;
|
||||
}
|
||||
if (node.children) {
|
||||
const found = findNodeById(node.children, valueId);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
// 체크된 항목들의 총 삭제 대상 수 계산 (하위 포함)
|
||||
const totalDeleteCount = useMemo(() => {
|
||||
const allIds = new Set<number>();
|
||||
checkedIds.forEach((id) => {
|
||||
const node = findNodeById(tree, id);
|
||||
if (node) {
|
||||
collectNodeAndDescendantIds(node).forEach((descendantId) => allIds.add(descendantId));
|
||||
}
|
||||
});
|
||||
return allIds.size;
|
||||
}, [checkedIds, tree, findNodeById, collectNodeAndDescendantIds]);
|
||||
|
||||
// 활성 노드만 필터링
|
||||
const filterActiveNodes = useCallback((nodes: CategoryValue[]): CategoryValue[] => {
|
||||
|
|
@ -306,37 +368,41 @@ export const CategoryValueManagerTree: React.FC<CategoryValueManagerTreeProps> =
|
|||
}));
|
||||
}, []);
|
||||
|
||||
// 데이터 로드
|
||||
const loadTree = useCallback(async () => {
|
||||
if (!tableName || !columnName) return;
|
||||
// 데이터 로드 (keepExpanded: true면 기존 펼침 상태 유지)
|
||||
const loadTree = useCallback(
|
||||
async (keepExpanded = false) => {
|
||||
if (!tableName || !columnName) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getCategoryTree(tableName, columnName);
|
||||
if (response.success && response.data) {
|
||||
let filteredTree = response.data;
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getCategoryTree(tableName, columnName);
|
||||
if (response.success && response.data) {
|
||||
let filteredTree = response.data;
|
||||
|
||||
// 비활성 필터링
|
||||
if (!showInactive) {
|
||||
filteredTree = filterActiveNodes(response.data);
|
||||
// 비활성 필터링
|
||||
if (!showInactive) {
|
||||
filteredTree = filterActiveNodes(response.data);
|
||||
}
|
||||
|
||||
setTree(filteredTree);
|
||||
|
||||
// 기존 펼침 상태 유지하지 않으면 모두 접기 (대분류만 표시)
|
||||
if (!keepExpanded) {
|
||||
setExpandedNodes(new Set());
|
||||
}
|
||||
|
||||
// 전체 개수 업데이트
|
||||
const totalCount = countAllValues(response.data);
|
||||
onValueCountChange?.(totalCount);
|
||||
}
|
||||
|
||||
setTree(filteredTree);
|
||||
|
||||
// 1단계 노드는 기본 펼침
|
||||
const rootIds = new Set(filteredTree.map((n) => n.valueId));
|
||||
setExpandedNodes(rootIds);
|
||||
|
||||
// 전체 개수 업데이트
|
||||
const totalCount = countAllValues(response.data);
|
||||
onValueCountChange?.(totalCount);
|
||||
} catch (error) {
|
||||
console.error("카테고리 트리 로드 오류:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("카테고리 트리 로드 오류:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [tableName, columnName, showInactive, countAllValues, filterActiveNodes, onValueCountChange]);
|
||||
},
|
||||
[tableName, columnName, showInactive, countAllValues, filterActiveNodes, onValueCountChange],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadTree();
|
||||
|
|
@ -375,6 +441,43 @@ export const CategoryValueManagerTree: React.FC<CategoryValueManagerTreeProps> =
|
|||
});
|
||||
};
|
||||
|
||||
// 체크박스 핸들러
|
||||
const handleCheck = useCallback((valueId: number, checked: boolean) => {
|
||||
setCheckedIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (checked) {
|
||||
next.add(valueId);
|
||||
} else {
|
||||
next.delete(valueId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 전체 선택/해제
|
||||
const handleSelectAll = useCallback(() => {
|
||||
if (checkedIds.size === countAllValues(tree)) {
|
||||
setCheckedIds(new Set());
|
||||
} else {
|
||||
const allIds = new Set<number>();
|
||||
const collectAllIds = (nodes: CategoryValue[]) => {
|
||||
for (const node of nodes) {
|
||||
allIds.add(node.valueId);
|
||||
if (node.children) {
|
||||
collectAllIds(node.children);
|
||||
}
|
||||
}
|
||||
};
|
||||
collectAllIds(tree);
|
||||
setCheckedIds(allIds);
|
||||
}
|
||||
}, [checkedIds.size, tree, countAllValues]);
|
||||
|
||||
// 선택 해제
|
||||
const handleClearSelection = useCallback(() => {
|
||||
setCheckedIds(new Set());
|
||||
}, []);
|
||||
|
||||
// 추가 모달 열기
|
||||
const handleOpenAddModal = (parent: CategoryValue | null) => {
|
||||
setParentValue(parent);
|
||||
|
|
@ -440,7 +543,9 @@ export const CategoryValueManagerTree: React.FC<CategoryValueManagerTreeProps> =
|
|||
if (response.success) {
|
||||
toast.success("카테고리가 추가되었습니다");
|
||||
setIsAddModalOpen(false);
|
||||
loadTree();
|
||||
// 기존 펼침 상태 유지하면서 데이터 새로고침
|
||||
await loadTree(true);
|
||||
// 부모 노드만 펼치기 (하위 추가 시)
|
||||
if (parentValue) {
|
||||
setExpandedNodes((prev) => new Set([...prev, parentValue.valueId]));
|
||||
}
|
||||
|
|
@ -469,7 +574,7 @@ export const CategoryValueManagerTree: React.FC<CategoryValueManagerTreeProps> =
|
|||
if (response.success) {
|
||||
toast.success("카테고리가 수정되었습니다");
|
||||
setIsEditModalOpen(false);
|
||||
loadTree();
|
||||
loadTree(true); // 기존 펼침 상태 유지
|
||||
} else {
|
||||
toast.error(response.error || "수정 실패");
|
||||
}
|
||||
|
|
@ -489,7 +594,12 @@ export const CategoryValueManagerTree: React.FC<CategoryValueManagerTreeProps> =
|
|||
toast.success("카테고리가 삭제되었습니다");
|
||||
setIsDeleteDialogOpen(false);
|
||||
setSelectedValue(null);
|
||||
loadTree();
|
||||
setCheckedIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(deletingValue.valueId);
|
||||
return next;
|
||||
});
|
||||
loadTree(true); // 기존 펼침 상태 유지
|
||||
} else {
|
||||
toast.error(response.error || "삭제 실패");
|
||||
}
|
||||
|
|
@ -499,22 +609,85 @@ export const CategoryValueManagerTree: React.FC<CategoryValueManagerTreeProps> =
|
|||
}
|
||||
};
|
||||
|
||||
// 다중 삭제 처리
|
||||
const handleBulkDelete = async () => {
|
||||
if (checkedIds.size === 0) return;
|
||||
|
||||
try {
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
// 체크된 항목들을 순차적으로 삭제 (하위는 백엔드에서 자동 삭제)
|
||||
for (const valueId of Array.from(checkedIds)) {
|
||||
try {
|
||||
const response = await deleteCategoryValue(valueId);
|
||||
if (response.success) {
|
||||
successCount++;
|
||||
} else {
|
||||
failCount++;
|
||||
}
|
||||
} catch {
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
setIsBulkDeleteDialogOpen(false);
|
||||
setCheckedIds(new Set());
|
||||
setSelectedValue(null);
|
||||
loadTree(true); // 기존 펼침 상태 유지
|
||||
|
||||
if (failCount === 0) {
|
||||
toast.success(`${successCount}개 카테고리가 삭제되었습니다 (하위 항목 포함)`);
|
||||
} else {
|
||||
toast.warning(`${successCount}개 삭제 성공, ${failCount}개 삭제 실패`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("카테고리 일괄 삭제 오류:", error);
|
||||
toast.error("카테고리 일괄 삭제 중 오류가 발생했습니다");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* 헤더 */}
|
||||
<div className="mb-3 flex items-center justify-between border-b pb-3">
|
||||
<h3 className="text-base font-semibold">{columnLabel} 카테고리</h3>
|
||||
<Button variant="default" size="sm" className="h-8 gap-1.5 text-xs" onClick={() => handleOpenAddModal(null)}>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
대분류 추가
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-base font-semibold">{columnLabel} 카테고리</h3>
|
||||
{checkedIds.size > 0 && (
|
||||
<span className="bg-primary/10 text-primary rounded-full px-2 py-0.5 text-xs">
|
||||
{checkedIds.size}개 선택
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{checkedIds.size > 0 && (
|
||||
<>
|
||||
<Button variant="outline" size="sm" className="h-8 text-xs" onClick={handleClearSelection}>
|
||||
선택 해제
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="h-8 gap-1.5 text-xs"
|
||||
onClick={() => setIsBulkDeleteDialogOpen(true)}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
선택 삭제
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button variant="default" size="sm" className="h-8 gap-1.5 text-xs" onClick={() => handleOpenAddModal(null)}>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
대분류 추가
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 툴바 */}
|
||||
<div className="mb-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
{/* 검색 */}
|
||||
<div className="relative max-w-xs flex-1">
|
||||
<Search className="text-muted-foreground absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2" />
|
||||
<Search className="text-muted-foreground absolute top-1/2 left-2.5 h-4 w-4 -translate-y-1/2" />
|
||||
<Input
|
||||
placeholder="검색..."
|
||||
value={searchQuery}
|
||||
|
|
@ -533,13 +706,16 @@ export const CategoryValueManagerTree: React.FC<CategoryValueManagerTreeProps> =
|
|||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={handleSelectAll}>
|
||||
{checkedIds.size === countAllValues(tree) && tree.length > 0 ? "전체 해제" : "전체 선택"}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={expandAll}>
|
||||
전체 펼침
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={collapseAll}>
|
||||
전체 접기
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={loadTree} title="새로고침">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => loadTree()} title="새로고침">
|
||||
<RefreshCw className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -568,18 +744,19 @@ export const CategoryValueManagerTree: React.FC<CategoryValueManagerTreeProps> =
|
|||
expandedNodes={expandedNodes}
|
||||
selectedValueId={selectedValue?.valueId}
|
||||
searchQuery={searchQuery}
|
||||
checkedIds={checkedIds}
|
||||
onToggle={handleToggle}
|
||||
onSelect={setSelectedValue}
|
||||
onAdd={handleOpenAddModal}
|
||||
onEdit={handleOpenEditModal}
|
||||
onDelete={handleOpenDeleteDialog}
|
||||
onCheck={handleCheck}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* 추가 모달 */}
|
||||
<Dialog open={isAddModalOpen} onOpenChange={setIsAddModalOpen}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[450px]">
|
||||
|
|
@ -588,7 +765,9 @@ export const CategoryValueManagerTree: React.FC<CategoryValueManagerTreeProps> =
|
|||
{parentValue ? `"${parentValue.valueLabel}" 하위 추가` : "대분류 추가"}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-xs sm:text-sm">
|
||||
{parentValue ? `${parentValue.depth + 1}단계 카테고리를 추가합니다` : "1단계 대분류 카테고리를 추가합니다"}
|
||||
{parentValue
|
||||
? `${parentValue.depth + 1}단계 카테고리를 추가합니다`
|
||||
: "1단계 대분류 카테고리를 추가합니다"}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
|
@ -631,7 +810,11 @@ export const CategoryValueManagerTree: React.FC<CategoryValueManagerTreeProps> =
|
|||
</div>
|
||||
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
<Button variant="outline" onClick={() => setIsAddModalOpen(false)} className="h-9 flex-1 text-sm sm:flex-none">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsAddModalOpen(false)}
|
||||
className="h-9 flex-1 text-sm sm:flex-none"
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleAdd} className="h-9 flex-1 text-sm sm:flex-none">
|
||||
|
|
@ -685,7 +868,11 @@ export const CategoryValueManagerTree: React.FC<CategoryValueManagerTreeProps> =
|
|||
</div>
|
||||
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
<Button variant="outline" onClick={() => setIsEditModalOpen(false)} className="h-9 flex-1 text-sm sm:flex-none">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsEditModalOpen(false)}
|
||||
className="h-9 flex-1 text-sm sm:flex-none"
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleEdit} className="h-9 flex-1 text-sm sm:flex-none">
|
||||
|
|
@ -714,15 +901,46 @@ export const CategoryValueManagerTree: React.FC<CategoryValueManagerTreeProps> =
|
|||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDelete} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
||||
<AlertDialogAction
|
||||
onClick={handleDelete}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* 다중 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={isBulkDeleteDialogOpen} onOpenChange={setIsBulkDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>카테고리 일괄 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
선택한 <strong>{checkedIds.size}개</strong> 카테고리를 삭제하시겠습니까?
|
||||
{totalDeleteCount > checkedIds.size && (
|
||||
<>
|
||||
<br />
|
||||
<span className="text-destructive">하위 카테고리 포함 총 {totalDeleteCount}개가 삭제됩니다.</span>
|
||||
</>
|
||||
)}
|
||||
<br />
|
||||
<span className="text-muted-foreground text-xs">삭제된 카테고리는 복구할 수 없습니다.</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleBulkDelete}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{totalDeleteCount}개 삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryValueManagerTree;
|
||||
|
||||
|
|
|
|||
|
|
@ -261,9 +261,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
|
||||
// 객체인 경우 tableName 속성 추출 시도
|
||||
if (typeof finalSelectedTable === "object" && finalSelectedTable !== null) {
|
||||
console.warn("⚠️ selectedTable이 객체입니다:", finalSelectedTable);
|
||||
finalSelectedTable = (finalSelectedTable as any).tableName || (finalSelectedTable as any).name || tableName;
|
||||
console.log("✅ 객체에서 추출한 테이블명:", finalSelectedTable);
|
||||
}
|
||||
|
||||
tableConfig.selectedTable = finalSelectedTable;
|
||||
|
|
@ -741,7 +739,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
});
|
||||
|
||||
if (hasChanges) {
|
||||
console.log("🔗 [TableList] 연결된 필터 값 변경:", newFilterValues);
|
||||
setLinkedFilterValues(newFilterValues);
|
||||
|
||||
// searchValues에 연결된 필터 값 병합
|
||||
|
|
@ -797,13 +794,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
componentType: "table",
|
||||
|
||||
receiveData: async (receivedData: any[], config: DataReceiverConfig) => {
|
||||
console.log("📥 TableList 데이터 수신:", {
|
||||
componentId: component.id,
|
||||
receivedDataCount: receivedData.length,
|
||||
mode: config.mode,
|
||||
currentDataCount: data.length,
|
||||
});
|
||||
|
||||
try {
|
||||
let newData: any[] = [];
|
||||
|
||||
|
|
@ -811,13 +801,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
case "append":
|
||||
// 기존 데이터에 추가
|
||||
newData = [...data, ...receivedData];
|
||||
console.log("✅ Append 모드: 기존 데이터에 추가", { newDataCount: newData.length });
|
||||
break;
|
||||
|
||||
case "replace":
|
||||
// 기존 데이터를 완전히 교체
|
||||
newData = receivedData;
|
||||
console.log("✅ Replace 모드: 데이터 교체", { newDataCount: newData.length });
|
||||
break;
|
||||
|
||||
case "merge":
|
||||
|
|
@ -833,7 +821,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
}
|
||||
});
|
||||
newData = Array.from(existingMap.values());
|
||||
console.log("✅ Merge 모드: 데이터 병합", { newDataCount: newData.length });
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -842,10 +829,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
|
||||
// 총 아이템 수 업데이트
|
||||
setTotalItems(newData.length);
|
||||
|
||||
console.log("✅ 데이터 수신 완료:", { finalDataCount: newData.length });
|
||||
} catch (error) {
|
||||
console.error("❌ 데이터 수신 실패:", error);
|
||||
console.error("데이터 수신 실패:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
|
@ -879,12 +864,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
componentId: component.id,
|
||||
componentType: "table-list",
|
||||
receiveData: async (incomingData: any[], mode: "append" | "replace" | "merge") => {
|
||||
console.log("📥 [TableListComponent] 분할 패널에서 데이터 수신:", {
|
||||
count: incomingData.length,
|
||||
mode,
|
||||
position: currentSplitPosition,
|
||||
});
|
||||
|
||||
await dataReceiver.receiveData(incomingData, {
|
||||
targetComponentId: component.id,
|
||||
targetComponentType: "table-list",
|
||||
|
|
@ -917,24 +896,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
|
||||
// 컬럼의 고유 값 조회 함수
|
||||
const getColumnUniqueValues = async (columnName: string) => {
|
||||
console.log("🔍 [getColumnUniqueValues] 호출됨:", {
|
||||
columnName,
|
||||
dataLength: data.length,
|
||||
columnMeta: columnMeta[columnName],
|
||||
sampleData: data[0],
|
||||
});
|
||||
|
||||
const meta = columnMeta[columnName];
|
||||
const inputType = meta?.inputType || "text";
|
||||
|
||||
// 카테고리 타입인 경우 전체 정의된 값 조회 (백엔드 API)
|
||||
if (inputType === "category") {
|
||||
try {
|
||||
console.log("🔍 [getColumnUniqueValues] 카테고리 전체 값 조회:", {
|
||||
tableName: tableConfig.selectedTable,
|
||||
columnName,
|
||||
});
|
||||
|
||||
// API 클라이언트 사용 (쿠키 인증 자동 처리)
|
||||
const { apiClient } = await import("@/lib/api/client");
|
||||
const response = await apiClient.get(`/table-categories/${tableConfig.selectedTable}/${columnName}/values`);
|
||||
|
|
@ -945,24 +912,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
label: item.valueLabel, // 카멜케이스
|
||||
}));
|
||||
|
||||
console.log("✅ [getColumnUniqueValues] 카테고리 전체 값:", {
|
||||
columnName,
|
||||
count: categoryOptions.length,
|
||||
options: categoryOptions,
|
||||
});
|
||||
|
||||
return categoryOptions;
|
||||
} else {
|
||||
console.warn("⚠️ [getColumnUniqueValues] 응답 형식 오류:", response.data);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("❌ [getColumnUniqueValues] 카테고리 조회 실패:", {
|
||||
error: error.message,
|
||||
response: error.response?.data,
|
||||
status: error.response?.status,
|
||||
columnName,
|
||||
tableName: tableConfig.selectedTable,
|
||||
});
|
||||
// 에러 시 현재 데이터 기반으로 fallback
|
||||
}
|
||||
}
|
||||
|
|
@ -971,15 +923,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
const isLabelType = ["category", "entity", "code"].includes(inputType);
|
||||
const labelField = isLabelType ? `${columnName}_name` : columnName;
|
||||
|
||||
console.log("🔍 [getColumnUniqueValues] 데이터 기반 조회:", {
|
||||
columnName,
|
||||
inputType,
|
||||
isLabelType,
|
||||
labelField,
|
||||
hasLabelField: data[0] && labelField in data[0],
|
||||
sampleLabelValue: data[0] ? data[0][labelField] : undefined,
|
||||
});
|
||||
|
||||
// 현재 로드된 데이터에서 고유 값 추출
|
||||
const uniqueValuesMap = new Map<string, string>(); // value -> label
|
||||
|
||||
|
|
@ -1000,15 +943,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
console.log("✅ [getColumnUniqueValues] 데이터 기반 결과:", {
|
||||
columnName,
|
||||
inputType,
|
||||
isLabelType,
|
||||
labelField,
|
||||
uniqueCount: result.length,
|
||||
values: result,
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
|
@ -1085,10 +1019,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
setSortColumn(column);
|
||||
setSortDirection(direction);
|
||||
hasInitializedSort.current = true;
|
||||
console.log("📂 localStorage에서 정렬 상태 복원:", { column, direction });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 정렬 상태 복원 실패:", error);
|
||||
// 정렬 상태 복원 실패
|
||||
}
|
||||
}
|
||||
}, [tableConfig.selectedTable, userId]);
|
||||
|
|
@ -1104,12 +1037,10 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
if (savedOrder) {
|
||||
try {
|
||||
const parsedOrder = JSON.parse(savedOrder);
|
||||
console.log("📂 localStorage에서 컬럼 순서 불러오기:", { storageKey, columnOrder: parsedOrder });
|
||||
setColumnOrder(parsedOrder);
|
||||
|
||||
// 부모 컴포넌트에 초기 컬럼 순서 전달
|
||||
if (onSelectedRowsChange && parsedOrder.length > 0) {
|
||||
console.log("✅ 초기 컬럼 순서 전달:", parsedOrder);
|
||||
|
||||
// 초기 데이터도 함께 전달 (컬럼 순서대로 재정렬)
|
||||
const initialData = data.map((row: any) => {
|
||||
|
|
@ -1598,48 +1529,36 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
// 자동 컬럼 매칭도 equals 연산자 사용
|
||||
linkedFilterValues[colName] = { value: colValue, operator: "equals" };
|
||||
hasLinkedFiltersConfigured = true;
|
||||
console.log(`🔗 [TableList] 자동 컬럼 매칭: ${colName} = ${colValue}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(linkedFilterValues).length > 0) {
|
||||
console.log("🔗 [TableList] 자동 컬럼 매칭 필터 적용:", linkedFilterValues);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(linkedFilterValues).length > 0) {
|
||||
console.log("🔗 [TableList] 연결 필터 적용:", linkedFilterValues);
|
||||
}
|
||||
}
|
||||
|
||||
// 🆕 연결 필터가 설정되어 있지만 좌측에서 데이터가 선택되지 않은 경우
|
||||
// 연결 필터가 설정되어 있지만 좌측에서 데이터가 선택되지 않은 경우
|
||||
// → 빈 데이터 표시 (모든 데이터를 보여주지 않음)
|
||||
if (hasLinkedFiltersConfigured && !hasSelectedLeftData) {
|
||||
console.log("⚠️ [TableList] 연결 필터 설정됨 but 좌측 데이터 미선택 → 빈 데이터 표시");
|
||||
setData([]);
|
||||
setTotalItems(0);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 🆕 RelatedDataButtons 대상이지만 아직 버튼이 선택되지 않은 경우
|
||||
// RelatedDataButtons 대상이지만 아직 버튼이 선택되지 않은 경우
|
||||
// → 빈 데이터 표시 (모든 데이터를 보여주지 않음)
|
||||
if (isRelatedButtonTarget && !relatedButtonFilter) {
|
||||
console.log("⚠️ [TableList] RelatedDataButtons 대상이지만 버튼 미선택 → 빈 데이터 표시");
|
||||
setData([]);
|
||||
setTotalItems(0);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 🆕 RelatedDataButtons 필터 값 준비
|
||||
// RelatedDataButtons 필터 값 준비
|
||||
const relatedButtonFilterValues: Record<string, any> = {};
|
||||
if (relatedButtonFilter) {
|
||||
relatedButtonFilterValues[relatedButtonFilter.filterColumn] = {
|
||||
value: relatedButtonFilter.filterValue,
|
||||
operator: "equals",
|
||||
};
|
||||
console.log("🔗 [TableList] RelatedDataButtons 필터 적용:", relatedButtonFilterValues);
|
||||
}
|
||||
|
||||
// 검색 필터, 연결 필터, RelatedDataButtons 필터 병합
|
||||
|
|
@ -1662,8 +1581,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
const connectionId = connectionIdMatch ? parseInt(connectionIdMatch[1]) : null;
|
||||
|
||||
if (connectionId) {
|
||||
console.log("🌐 [TableList] REST API 데이터 소스 호출", { connectionId });
|
||||
|
||||
// REST API 연결 정보 가져오기 및 데이터 조회
|
||||
const { ExternalRestApiConnectionAPI } = await import("@/lib/api/externalRestApiConnection");
|
||||
const restApiData = await ExternalRestApiConnectionAPI.fetchData(
|
||||
|
|
@ -1677,11 +1594,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
total: restApiData.total || restApiData.rows?.length || 0,
|
||||
totalPages: Math.ceil((restApiData.total || restApiData.rows?.length || 0) / pageSize),
|
||||
};
|
||||
|
||||
console.log("✅ [TableList] REST API 응답:", {
|
||||
dataLength: response.data.length,
|
||||
total: response.total,
|
||||
});
|
||||
} else {
|
||||
throw new Error("REST API 연결 ID를 찾을 수 없습니다.");
|
||||
}
|
||||
|
|
@ -1722,31 +1634,15 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
// 1순위: props로 전달받은 formData에서 값 가져오기 (모달에서 사용)
|
||||
if (propFormData && propFormData[fieldName]) {
|
||||
filterValue = propFormData[fieldName];
|
||||
console.log("🔗 [TableList] formData에서 excludeFilter 값 가져오기:", {
|
||||
field: fieldName,
|
||||
value: filterValue,
|
||||
});
|
||||
}
|
||||
// 2순위: URL 파라미터에서 값 가져오기
|
||||
else if (typeof window !== "undefined") {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
filterValue = urlParams.get(fieldName);
|
||||
if (filterValue) {
|
||||
console.log("🔗 [TableList] URL에서 excludeFilter 값 가져오기:", {
|
||||
field: fieldName,
|
||||
value: filterValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
// 3순위: 분할 패널 부모 데이터에서 값 가져오기
|
||||
if (!filterValue && splitPanelContext?.selectedLeftData) {
|
||||
filterValue = splitPanelContext.selectedLeftData[fieldName];
|
||||
if (filterValue) {
|
||||
console.log("🔗 [TableList] 분할패널에서 excludeFilter 값 가져오기:", {
|
||||
field: fieldName,
|
||||
value: filterValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1759,7 +1655,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
filterColumn: excludeConfig.filterColumn,
|
||||
filterValue: filterValue,
|
||||
};
|
||||
console.log("🚫 [TableList] 제외 필터 적용:", excludeFilterParam);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1874,8 +1769,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
};
|
||||
|
||||
const handleSort = (column: string) => {
|
||||
console.log("🔄 정렬 클릭:", { column, currentSortColumn: sortColumn, currentSortDirection: sortDirection });
|
||||
|
||||
let newSortColumn = column;
|
||||
let newSortDirection: "asc" | "desc" = "asc";
|
||||
|
||||
|
|
@ -1889,7 +1782,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
newSortDirection = "asc";
|
||||
}
|
||||
|
||||
// 🎯 정렬 상태를 localStorage에 저장 (사용자별)
|
||||
// 정렬 상태를 localStorage에 저장 (사용자별)
|
||||
if (tableConfig.selectedTable && userId) {
|
||||
const storageKey = `table_sort_state_${tableConfig.selectedTable}_${userId}`;
|
||||
try {
|
||||
|
|
@ -1900,15 +1793,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
direction: newSortDirection,
|
||||
}),
|
||||
);
|
||||
console.log("💾 정렬 상태 저장:", { column: newSortColumn, direction: newSortDirection });
|
||||
} catch (error) {
|
||||
console.error("❌ 정렬 상태 저장 실패:", error);
|
||||
// 정렬 상태 저장 실패
|
||||
}
|
||||
}
|
||||
|
||||
console.log("📊 새로운 정렬 정보:", { newSortColumn, newSortDirection });
|
||||
console.log("🔍 onSelectedRowsChange 존재 여부:", !!onSelectedRowsChange);
|
||||
|
||||
// 정렬 변경 시 선택 정보와 함께 정렬 정보도 전달
|
||||
if (onSelectedRowsChange) {
|
||||
const selectedRowsData = data.filter((row, index) => selectedRows.has(getRowKey(row, index)));
|
||||
|
|
@ -1960,16 +1849,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
return reordered;
|
||||
});
|
||||
|
||||
console.log("✅ 정렬 정보 전달:", {
|
||||
selectedRowsCount: selectedRows.size,
|
||||
selectedRowsDataCount: selectedRowsData.length,
|
||||
sortBy: newSortColumn,
|
||||
sortOrder: newSortDirection,
|
||||
columnOrder: columnOrder.length > 0 ? columnOrder : undefined,
|
||||
tableDisplayDataCount: reorderedData.length,
|
||||
firstRowAfterSort: reorderedData[0]?.[newSortColumn],
|
||||
lastRowAfterSort: reorderedData[reorderedData.length - 1]?.[newSortColumn],
|
||||
});
|
||||
onSelectedRowsChange(
|
||||
Array.from(selectedRows),
|
||||
selectedRowsData,
|
||||
|
|
@ -2023,8 +1902,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
};
|
||||
|
||||
const handleClearAdvancedFilters = useCallback(() => {
|
||||
console.log("🔄 필터 초기화 시작", { 이전searchValues: searchValues });
|
||||
|
||||
// 상태를 초기화하고 useEffect로 데이터 새로고침
|
||||
setSearchValues({});
|
||||
setCurrentPage(1);
|
||||
|
|
@ -2173,30 +2050,15 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
// currentSplitPosition을 사용하여 정확한 위치 확인 (splitPanelPosition이 없을 수 있음)
|
||||
const effectiveSplitPosition = splitPanelPosition || currentSplitPosition;
|
||||
|
||||
console.log("🔗 [TableList] 행 클릭 - 분할 패널 위치 확인:", {
|
||||
splitPanelPosition,
|
||||
currentSplitPosition,
|
||||
effectiveSplitPosition,
|
||||
hasSplitPanelContext: !!splitPanelContext,
|
||||
disableAutoDataTransfer: splitPanelContext?.disableAutoDataTransfer,
|
||||
});
|
||||
|
||||
if (splitPanelContext && effectiveSplitPosition === "left" && !splitPanelContext.disableAutoDataTransfer) {
|
||||
if (!isCurrentlySelected) {
|
||||
// 선택된 경우: 데이터 저장
|
||||
splitPanelContext.setSelectedLeftData(row);
|
||||
console.log("🔗 [TableList] 분할 패널 좌측 데이터 저장:", {
|
||||
row,
|
||||
parentDataMapping: splitPanelContext.parentDataMapping,
|
||||
});
|
||||
} else {
|
||||
// 선택 해제된 경우: 데이터 초기화
|
||||
splitPanelContext.setSelectedLeftData(null);
|
||||
console.log("🔗 [TableList] 분할 패널 좌측 데이터 초기화");
|
||||
}
|
||||
}
|
||||
|
||||
console.log("행 클릭:", { row, index, isSelected: !isCurrentlySelected });
|
||||
};
|
||||
|
||||
// 🆕 셀 클릭 핸들러 (포커스 설정 + 행 선택)
|
||||
|
|
@ -2457,12 +2319,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
},
|
||||
}));
|
||||
|
||||
console.log("📝 배치 편집 추가:", { columnName, newValue, pendingCount: pendingChanges.size + 1 });
|
||||
cancelEditing();
|
||||
return;
|
||||
}
|
||||
|
||||
// 🆕 즉시 모드: 바로 저장
|
||||
// 즉시 모드: 바로 저장
|
||||
try {
|
||||
const { apiClient } = await import("@/lib/api/client");
|
||||
|
||||
|
|
@ -2476,10 +2337,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
|
||||
// 데이터 새로고침 트리거
|
||||
setRefreshTrigger((prev) => prev + 1);
|
||||
|
||||
console.log("✅ 셀 편집 저장 완료:", { columnName, newValue });
|
||||
} catch (error) {
|
||||
console.error("❌ 셀 편집 저장 실패:", error);
|
||||
// 셀 편집 저장 실패
|
||||
}
|
||||
|
||||
cancelEditing();
|
||||
|
|
@ -2524,21 +2383,18 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
setRefreshTrigger((prev) => prev + 1);
|
||||
|
||||
toast.success(`${pendingChanges.size}개의 변경사항이 저장되었습니다.`);
|
||||
console.log("✅ 배치 저장 완료:", pendingChanges.size, "개");
|
||||
} catch (error) {
|
||||
console.error("❌ 배치 저장 실패:", error);
|
||||
toast.error("저장 중 오류가 발생했습니다.");
|
||||
}
|
||||
}, [pendingChanges, tableConfig.selectedTable, tableConfig.primaryKey]);
|
||||
|
||||
// 🆕 배치 취소: 모든 변경사항 롤백
|
||||
// 배치 취소: 모든 변경사항 롤백
|
||||
const cancelBatchChanges = useCallback(() => {
|
||||
if (pendingChanges.size === 0) return;
|
||||
|
||||
setPendingChanges(new Map());
|
||||
setLocalEditedData({});
|
||||
toast.info("변경사항이 취소되었습니다.");
|
||||
console.log("🔄 배치 편집 취소");
|
||||
}, [pendingChanges.size]);
|
||||
|
||||
// 🆕 특정 셀이 수정되었는지 확인
|
||||
|
|
@ -2715,9 +2571,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
XLSX.writeFile(wb, fileName);
|
||||
|
||||
toast.success(`${exportData.length}개 행이 Excel로 내보내기 되었습니다.`);
|
||||
console.log("✅ Excel 내보내기 완료:", fileName);
|
||||
} catch (error) {
|
||||
console.error("❌ Excel 내보내기 실패:", error);
|
||||
toast.error("Excel 내보내기 중 오류가 발생했습니다.");
|
||||
}
|
||||
},
|
||||
|
|
@ -2783,10 +2637,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
...prev,
|
||||
[rowKey]: details,
|
||||
}));
|
||||
|
||||
console.log("✅ 상세 데이터 로딩 완료:", { rowKey, count: details.length });
|
||||
} catch (error) {
|
||||
console.error("❌ 상세 데이터 로딩 실패:", error);
|
||||
setDetailData((prev) => ({
|
||||
...prev,
|
||||
[rowKey]: [],
|
||||
|
|
@ -2883,10 +2734,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
...prev,
|
||||
[cacheKey]: options,
|
||||
}));
|
||||
|
||||
console.log("✅ Cascading options 로딩 완료:", { columnName, parentValue, count: options.length });
|
||||
} catch (error) {
|
||||
console.error("❌ Cascading options 로딩 실패:", error);
|
||||
setCascadingOptions((prev) => ({
|
||||
...prev,
|
||||
[cacheKey]: [],
|
||||
|
|
@ -3040,13 +2888,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
|
||||
wsRef.current.onopen = () => {
|
||||
setWsConnectionStatus("connected");
|
||||
console.log("✅ WebSocket 연결됨:", tableConfig.selectedTable);
|
||||
};
|
||||
|
||||
wsRef.current.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
console.log("📨 WebSocket 메시지 수신:", message);
|
||||
|
||||
switch (message.type) {
|
||||
case "insert":
|
||||
|
|
@ -3069,32 +2915,29 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
setRefreshTrigger((prev) => prev + 1);
|
||||
break;
|
||||
default:
|
||||
console.log("알 수 없는 메시지 타입:", message.type);
|
||||
// 알 수 없는 메시지 타입
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("WebSocket 메시지 파싱 오류:", error);
|
||||
// WebSocket 메시지 파싱 오류
|
||||
}
|
||||
};
|
||||
|
||||
wsRef.current.onclose = () => {
|
||||
setWsConnectionStatus("disconnected");
|
||||
console.log("🔌 WebSocket 연결 종료");
|
||||
|
||||
// 자동 재연결 (5초 후)
|
||||
if (isRealTimeEnabled) {
|
||||
reconnectTimeoutRef.current = setTimeout(() => {
|
||||
console.log("🔄 WebSocket 재연결 시도...");
|
||||
connectWebSocket();
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
wsRef.current.onerror = (error) => {
|
||||
console.error("❌ WebSocket 오류:", error);
|
||||
wsRef.current.onerror = () => {
|
||||
setWsConnectionStatus("disconnected");
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("WebSocket 연결 실패:", error);
|
||||
setWsConnectionStatus("disconnected");
|
||||
}
|
||||
}, [isRealTimeEnabled, tableConfig.selectedTable]);
|
||||
|
|
@ -3179,9 +3022,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
await navigator.clipboard.writeText(tsvContent);
|
||||
|
||||
toast.success(`${copyData.length}행 복사됨`);
|
||||
console.log("✅ 클립보드 복사:", copyData.length, "행");
|
||||
} catch (error) {
|
||||
console.error("❌ 클립보드 복사 실패:", error);
|
||||
toast.error("복사 실패");
|
||||
}
|
||||
}, [selectedRows, filteredData, focusedCell, visibleColumns, columnLabels, getRowKey]);
|
||||
|
|
@ -3532,7 +3373,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
|
||||
setColumnOrder(newOrder);
|
||||
toast.info("컬럼 순서가 변경되었습니다.");
|
||||
console.log("✅ 컬럼 순서 변경:", { from: draggedColumnIndex, to: targetIndex });
|
||||
|
||||
handleColumnDragEnd();
|
||||
},
|
||||
|
|
@ -3623,10 +3463,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
// 로컬에서만 순서 변경 (저장 안함)
|
||||
toast.info("순서가 변경되었습니다. (로컬만)");
|
||||
}
|
||||
|
||||
console.log("✅ 행 순서 변경:", { from: draggedRowIndex, to: targetIndex });
|
||||
} catch (error) {
|
||||
console.error("❌ 행 순서 변경 실패:", error);
|
||||
toast.error("순서 변경 중 오류가 발생했습니다.");
|
||||
}
|
||||
|
||||
|
|
@ -4712,8 +4549,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
return filteredData;
|
||||
}
|
||||
|
||||
console.log("🔍 [테이블리스트] 그룹합산 적용:", groupSumConfig);
|
||||
|
||||
const groupByColumn = groupSumConfig.groupByColumn;
|
||||
const groupMap = new Map<string, any>();
|
||||
|
||||
|
|
@ -4766,11 +4601,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
});
|
||||
|
||||
const result = Array.from(groupMap.values());
|
||||
console.log("🔗 [테이블리스트] 그룹별 합산 결과:", {
|
||||
원본개수: filteredData.length,
|
||||
그룹개수: result.length,
|
||||
그룹기준: groupByColumn,
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [filteredData, groupSumConfig]);
|
||||
|
|
@ -4878,7 +4708,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
useEffect(() => {
|
||||
const handleRefreshTable = () => {
|
||||
if (tableConfig.selectedTable && !isDesignMode) {
|
||||
console.log("🔄 [TableList] refreshTable 이벤트 수신 - 데이터 새로고침");
|
||||
setRefreshTrigger((prev) => prev + 1);
|
||||
}
|
||||
};
|
||||
|
|
@ -4904,23 +4733,21 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
};
|
||||
}, [tableConfig.selectedTable, isDesignMode, component.id]);
|
||||
|
||||
// 🆕 테이블명 변경 시 전역 레지스트리에서 확인
|
||||
// 테이블명 변경 시 전역 레지스트리에서 확인
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined" && window.__relatedButtonsTargetTables && tableConfig.selectedTable) {
|
||||
const isTarget = window.__relatedButtonsTargetTables.has(tableConfig.selectedTable);
|
||||
if (isTarget) {
|
||||
console.log("📝 [TableList] 전역 레지스트리에서 RelatedDataButtons 대상 확인:", tableConfig.selectedTable);
|
||||
setIsRelatedButtonTarget(true);
|
||||
}
|
||||
}
|
||||
}, [tableConfig.selectedTable]);
|
||||
|
||||
// 🆕 RelatedDataButtons 등록/해제 이벤트 리스너
|
||||
// RelatedDataButtons 등록/해제 이벤트 리스너
|
||||
useEffect(() => {
|
||||
const handleRelatedButtonRegister = (event: CustomEvent) => {
|
||||
const { targetTable } = event.detail || {};
|
||||
if (targetTable === tableConfig.selectedTable) {
|
||||
console.log("📝 [TableList] RelatedDataButtons 대상으로 등록됨:", tableConfig.selectedTable);
|
||||
setIsRelatedButtonTarget(true);
|
||||
}
|
||||
};
|
||||
|
|
@ -4928,7 +4755,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
const handleRelatedButtonUnregister = (event: CustomEvent) => {
|
||||
const { targetTable } = event.detail || {};
|
||||
if (targetTable === tableConfig.selectedTable) {
|
||||
console.log("📝 [TableList] RelatedDataButtons 대상에서 해제됨:", tableConfig.selectedTable);
|
||||
setIsRelatedButtonTarget(false);
|
||||
setRelatedButtonFilter(null);
|
||||
}
|
||||
|
|
@ -4939,7 +4765,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
V2_EVENTS.RELATED_BUTTON_REGISTER,
|
||||
(payload) => {
|
||||
if (payload.targetTables.includes(tableConfig.selectedTable || "")) {
|
||||
console.log("📝 [TableList] RelatedDataButtons 대상으로 등록됨:", tableConfig.selectedTable);
|
||||
setIsRelatedButtonTarget(true);
|
||||
}
|
||||
},
|
||||
|
|
@ -4950,7 +4775,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
V2_EVENTS.RELATED_BUTTON_UNREGISTER,
|
||||
(payload) => {
|
||||
if (payload.buttonId) {
|
||||
console.log("📝 [TableList] RelatedDataButtons 대상에서 해제됨:", tableConfig.selectedTable);
|
||||
setIsRelatedButtonTarget(false);
|
||||
setRelatedButtonFilter(null);
|
||||
}
|
||||
|
|
@ -4970,7 +4794,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
};
|
||||
}, [tableConfig.selectedTable, component.id]);
|
||||
|
||||
// 🆕 RelatedDataButtons 선택 이벤트 리스너 (버튼 선택 시 테이블 필터링)
|
||||
// RelatedDataButtons 선택 이벤트 리스너 (버튼 선택 시 테이블 필터링)
|
||||
useEffect(() => {
|
||||
const handleRelatedButtonSelect = (event: CustomEvent) => {
|
||||
const { targetTable, filterColumn, filterValue } = event.detail || {};
|
||||
|
|
@ -4979,15 +4803,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
if (targetTable === tableConfig.selectedTable) {
|
||||
// filterValue가 null이면 선택 해제 (빈 상태)
|
||||
if (filterValue === null || filterValue === undefined) {
|
||||
console.log("📌 [TableList] RelatedDataButtons 선택 해제 (빈 상태):", tableConfig.selectedTable);
|
||||
setRelatedButtonFilter(null);
|
||||
setIsRelatedButtonTarget(true); // 대상으로 등록은 유지
|
||||
} else {
|
||||
console.log("📌 [TableList] RelatedDataButtons 필터 적용:", {
|
||||
tableName: tableConfig.selectedTable,
|
||||
filterColumn,
|
||||
filterValue,
|
||||
});
|
||||
setRelatedButtonFilter({ filterColumn, filterValue });
|
||||
setIsRelatedButtonTarget(true);
|
||||
}
|
||||
|
|
@ -5000,14 +4818,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
(payload) => {
|
||||
if (payload.tableName === tableConfig.selectedTable) {
|
||||
if (!payload.selectedData || payload.selectedData.length === 0) {
|
||||
console.log("📌 [TableList] RelatedDataButtons 선택 해제 (빈 상태):", tableConfig.selectedTable);
|
||||
setRelatedButtonFilter(null);
|
||||
setIsRelatedButtonTarget(true);
|
||||
} else {
|
||||
console.log("📌 [TableList] RelatedDataButtons 필터 적용:", {
|
||||
tableName: tableConfig.selectedTable,
|
||||
selectedData: payload.selectedData,
|
||||
});
|
||||
// 첫 번째 선택된 데이터의 ID를 필터로 사용
|
||||
const firstItem = payload.selectedData[0];
|
||||
if (firstItem?.id) {
|
||||
|
|
|
|||
|
|
@ -3,13 +3,11 @@
|
|||
import React, { useState, useEffect, useRef, useMemo } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Settings, Filter, Layers, X, Check, ChevronsUpDown } from "lucide-react";
|
||||
import { Settings, X, ChevronsUpDown } from "lucide-react";
|
||||
import { useTableOptions } from "@/contexts/TableOptionsContext";
|
||||
import { useTableSearchWidgetHeight } from "@/contexts/TableSearchWidgetHeightContext";
|
||||
import { useActiveTab } from "@/contexts/ActiveTabContext";
|
||||
import { ColumnVisibilityPanel } from "@/components/screen/table-options/ColumnVisibilityPanel";
|
||||
import { FilterPanel } from "@/components/screen/table-options/FilterPanel";
|
||||
import { GroupingPanel } from "@/components/screen/table-options/GroupingPanel";
|
||||
import { TableSettingsModal } from "@/components/screen/table-options/TableSettingsModal";
|
||||
import { TableFilter } from "@/types/table-options";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { ModernDatePicker } from "@/components/screen/filters/ModernDatePicker";
|
||||
|
|
@ -50,24 +48,8 @@ interface TableSearchWidgetProps {
|
|||
}
|
||||
|
||||
export function TableSearchWidget({ component, screenId, onHeightChange }: TableSearchWidgetProps) {
|
||||
console.log("🎯🎯🎯 [TableSearchWidget] 함수 시작!", { componentId: component?.id, screenId });
|
||||
|
||||
// 🔧 직접 useTableOptions 호출 (에러 발생 시 catch하지 않고 그대로 throw)
|
||||
const tableOptionsContext = useTableOptions();
|
||||
console.log("✅ [TableSearchWidget] useTableOptions 성공", { hasContext: !!tableOptionsContext });
|
||||
|
||||
const { registeredTables, selectedTableId, setSelectedTableId, getTable, getActiveTabTables } = tableOptionsContext;
|
||||
|
||||
// 등록된 테이블 확인 로그
|
||||
console.log("🔍 [TableSearchWidget] 등록된 테이블:", {
|
||||
count: registeredTables.size,
|
||||
tables: Array.from(registeredTables.entries()).map(([id, t]) => ({
|
||||
id,
|
||||
tableName: t.tableName,
|
||||
hasOnFilterChange: typeof t.onFilterChange === "function",
|
||||
})),
|
||||
selectedTableId,
|
||||
});
|
||||
const { isPreviewMode } = useScreenPreview(); // 미리보기 모드 확인
|
||||
const { getAllActiveTabIds, activeTabs } = useActiveTab(); // 활성 탭 정보
|
||||
|
||||
|
|
@ -86,9 +68,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
// 탭별 필터 값 저장 (탭 ID -> 필터 값)
|
||||
const [tabFilterValues, setTabFilterValues] = useState<Record<string, Record<string, any>>>({});
|
||||
|
||||
const [columnVisibilityOpen, setColumnVisibilityOpen] = useState(false);
|
||||
const [filterOpen, setFilterOpen] = useState(false);
|
||||
const [groupingOpen, setGroupingOpen] = useState(false);
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
|
||||
// 활성화된 필터 목록
|
||||
const [activeFilters, setActiveFilters] = useState<TableFilter[]>([]);
|
||||
|
|
@ -153,24 +133,16 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
|
||||
// currentTable은 tableList(필터링된 목록)에서 가져와야 함
|
||||
const currentTable = useMemo(() => {
|
||||
console.log("🔍 [TableSearchWidget] currentTable 계산:", {
|
||||
selectedTableId,
|
||||
tableListLength: tableList.length,
|
||||
tableList: tableList.map((t) => ({ id: t.tableId, name: t.tableName, parentTabId: t.parentTabId })),
|
||||
});
|
||||
|
||||
if (!selectedTableId) return undefined;
|
||||
|
||||
// 먼저 tableList(필터링된 목록)에서 찾기
|
||||
const tableFromList = tableList.find((t) => t.tableId === selectedTableId);
|
||||
if (tableFromList) {
|
||||
console.log("✅ [TableSearchWidget] 테이블 찾음 (tableList):", tableFromList.tableName);
|
||||
return tableFromList;
|
||||
}
|
||||
|
||||
// tableList에 없으면 전체에서 찾기 (폴백)
|
||||
const tableFromAll = getTable(selectedTableId);
|
||||
console.log("🔄 [TableSearchWidget] 테이블 찾음 (전체):", tableFromAll?.tableName);
|
||||
return tableFromAll;
|
||||
}, [selectedTableId, tableList, getTable]);
|
||||
|
||||
|
|
@ -186,28 +158,16 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
return;
|
||||
}
|
||||
|
||||
// 🆕 탭 전환 감지: 활성 탭이 변경되었는지 확인
|
||||
// 탭 전환 감지: 활성 탭이 변경되었는지 확인
|
||||
const tabChanged = prevActiveTabIdsRef.current !== activeTabIdsStr;
|
||||
if (tabChanged) {
|
||||
console.log("🔄 [TableSearchWidget] 탭 전환 감지:", {
|
||||
이전탭: prevActiveTabIdsRef.current,
|
||||
현재탭: activeTabIdsStr,
|
||||
가용테이블: tableList.map((t) => ({ id: t.tableId, tableName: t.tableName, parentTabId: t.parentTabId })),
|
||||
현재선택테이블: selectedTableId,
|
||||
});
|
||||
prevActiveTabIdsRef.current = activeTabIdsStr;
|
||||
|
||||
// 🆕 탭 전환 시: 해당 탭에 속한 테이블 중 첫 번째 강제 선택
|
||||
// 탭 전환 시: 해당 탭에 속한 테이블 중 첫 번째 강제 선택
|
||||
const activeTabTable = tableList.find((t) => t.parentTabId && activeTabIds.includes(t.parentTabId));
|
||||
const targetTable = activeTabTable || tableList[0];
|
||||
|
||||
if (targetTable) {
|
||||
console.log("✅ [TableSearchWidget] 탭 전환으로 테이블 강제 선택:", {
|
||||
테이블ID: targetTable.tableId,
|
||||
테이블명: targetTable.tableName,
|
||||
탭ID: targetTable.parentTabId,
|
||||
이전테이블: selectedTableId,
|
||||
});
|
||||
setSelectedTableId(targetTable.tableId);
|
||||
}
|
||||
return; // 탭 전환 시에는 여기서 종료
|
||||
|
|
@ -222,11 +182,6 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
const targetTable = activeTabTable || tableList[0];
|
||||
|
||||
if (targetTable && targetTable.tableId !== selectedTableId) {
|
||||
console.log("✅ [TableSearchWidget] 테이블 자동 선택 (초기):", {
|
||||
테이블ID: targetTable.tableId,
|
||||
테이블명: targetTable.tableName,
|
||||
탭ID: targetTable.parentTabId,
|
||||
});
|
||||
setSelectedTableId(targetTable.tableId);
|
||||
}
|
||||
}
|
||||
|
|
@ -270,13 +225,6 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
|
||||
// 현재 테이블의 저장된 필터 불러오기 (동적 모드) 또는 고정 필터 적용 (고정 모드)
|
||||
useEffect(() => {
|
||||
console.log("📋 [TableSearchWidget] 필터 설정 useEffect 실행:", {
|
||||
currentTable: currentTable?.tableName,
|
||||
currentTableTabId,
|
||||
filterMode,
|
||||
selectedTableId,
|
||||
컬럼수: currentTable?.columns?.length,
|
||||
});
|
||||
if (!currentTable?.tableName) return;
|
||||
|
||||
// 고정 모드: presetFilters를 activeFilters로 설정
|
||||
|
|
@ -317,13 +265,6 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
: `table_filters_${currentTable.tableName}`;
|
||||
const savedFilters = localStorage.getItem(filterConfigKey);
|
||||
|
||||
console.log("🔑 [TableSearchWidget] 필터 설정 키 확인:", {
|
||||
filterConfigKey,
|
||||
savedFilters: savedFilters ? `${savedFilters.substring(0, 100)}...` : null,
|
||||
screenId,
|
||||
tableName: currentTable.tableName,
|
||||
});
|
||||
|
||||
if (savedFilters) {
|
||||
try {
|
||||
const parsed = JSON.parse(savedFilters) as Array<{
|
||||
|
|
@ -346,13 +287,6 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
width: f.width || 200,
|
||||
}));
|
||||
|
||||
console.log("📌 [TableSearchWidget] 필터 설정 로드:", {
|
||||
filterConfigKey,
|
||||
총필터수: parsed.length,
|
||||
활성화필터수: activeFiltersList.length,
|
||||
활성화필터: activeFiltersList.map((f) => f.columnName),
|
||||
});
|
||||
|
||||
setActiveFilters(activeFiltersList);
|
||||
|
||||
// 탭별 저장된 필터 값 복원
|
||||
|
|
@ -382,10 +316,6 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
}
|
||||
} else {
|
||||
// 필터 설정이 없으면 activeFilters와 filterValues 모두 초기화
|
||||
console.log("⚠️ [TableSearchWidget] 저장된 필터 설정 없음 - 필터 초기화:", {
|
||||
tableName: currentTable.tableName,
|
||||
filterConfigKey,
|
||||
});
|
||||
setActiveFilters([]);
|
||||
setFilterValues({});
|
||||
setSelectOptions({});
|
||||
|
|
@ -540,7 +470,8 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
}
|
||||
|
||||
// 다중선택 배열을 처리 (파이프로 연결된 문자열로 변환)
|
||||
if (filter.filterType === "select" && Array.isArray(filterValue)) {
|
||||
// filterType에 관계없이 배열이면 파이프로 연결
|
||||
if (Array.isArray(filterValue)) {
|
||||
filterValue = filterValue.join("|");
|
||||
}
|
||||
|
||||
|
|
@ -553,26 +484,11 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
// 빈 값 체크
|
||||
if (!f.value) return false;
|
||||
if (typeof f.value === "string" && f.value === "") return false;
|
||||
if (Array.isArray(f.value) && f.value.length === 0) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
console.log("🔍 [TableSearchWidget] applyFilters 호출:", {
|
||||
currentTableId: currentTable?.tableId,
|
||||
currentTableName: currentTable?.tableName,
|
||||
hasOnFilterChange: !!currentTable?.onFilterChange,
|
||||
filtersCount: filtersWithValues.length,
|
||||
filters: filtersWithValues.map((f) => ({
|
||||
col: f.columnName,
|
||||
op: f.operator,
|
||||
val: f.value,
|
||||
})),
|
||||
});
|
||||
|
||||
if (currentTable?.onFilterChange) {
|
||||
currentTable.onFilterChange(filtersWithValues);
|
||||
} else {
|
||||
console.warn("⚠️ [TableSearchWidget] onFilterChange가 없음!", { currentTable });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -771,54 +687,28 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* 동적 모드일 때만 설정 버튼들 표시 (미리보기에서는 비활성화) */}
|
||||
{/* 동적 모드일 때만 설정 버튼 표시 (미리보기에서는 비활성화) */}
|
||||
{filterMode === "dynamic" && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => !isPreviewMode && setColumnVisibilityOpen(true)}
|
||||
disabled={!selectedTableId || isPreviewMode}
|
||||
className="h-8 text-xs sm:h-9 sm:text-sm"
|
||||
>
|
||||
<Settings className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
|
||||
테이블 옵션
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => !isPreviewMode && setFilterOpen(true)}
|
||||
disabled={!selectedTableId || isPreviewMode}
|
||||
className="h-8 text-xs sm:h-9 sm:text-sm"
|
||||
>
|
||||
<Filter className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
|
||||
필터 설정
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => !isPreviewMode && setGroupingOpen(true)}
|
||||
disabled={!selectedTableId || isPreviewMode}
|
||||
className="h-8 text-xs sm:h-9 sm:text-sm"
|
||||
>
|
||||
<Layers className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
|
||||
그룹 설정
|
||||
</Button>
|
||||
</>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => !isPreviewMode && setSettingsOpen(true)}
|
||||
disabled={!selectedTableId || isPreviewMode}
|
||||
className="h-8 text-xs sm:h-9 sm:text-sm"
|
||||
>
|
||||
<Settings className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
|
||||
테이블 설정
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 패널들 */}
|
||||
<ColumnVisibilityPanel isOpen={columnVisibilityOpen} onClose={() => setColumnVisibilityOpen(false)} />
|
||||
<FilterPanel
|
||||
isOpen={filterOpen}
|
||||
onClose={() => setFilterOpen(false)}
|
||||
{/* 통합 설정 모달 */}
|
||||
<TableSettingsModal
|
||||
isOpen={settingsOpen}
|
||||
onClose={() => setSettingsOpen(false)}
|
||||
onFiltersApplied={(filters) => setActiveFilters(filters)}
|
||||
screenId={screenId}
|
||||
/>
|
||||
<GroupingPanel isOpen={groupingOpen} onClose={() => setGroupingOpen(false)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -529,8 +529,6 @@ export class ButtonActionExecutor {
|
|||
// 약간의 대기 시간을 주어 이벤트 핸들러가 formData를 업데이트할 수 있도록 함
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
console.log("📦 [handleSave] beforeFormSave 이벤트 후 formData keys:", Object.keys(context.formData || {}));
|
||||
|
||||
// 검증 실패 시 저장 중단
|
||||
if (beforeSaveEventDetail.validationFailed) {
|
||||
console.log("❌ [handleSave] 검증 실패로 저장 중단:", beforeSaveEventDetail.validationErrors);
|
||||
|
|
@ -549,13 +547,13 @@ export class ButtonActionExecutor {
|
|||
);
|
||||
|
||||
if (hasTableSectionData) {
|
||||
console.log("📋 [handleSave] _tableSection_ 데이터 감지 - onSave 콜백 건너뛰고 테이블 섹션 저장 로직 사용");
|
||||
|
||||
}
|
||||
|
||||
// 🆕 EditModal 등에서 전달된 onSave 콜백이 있으면 우선 사용
|
||||
// 단, _tableSection_ 데이터가 있으면 건너뛰기 (handleUniversalFormModalTableSectionSave가 처리)
|
||||
if (onSave && !hasTableSectionData) {
|
||||
console.log("✅ [handleSave] onSave 콜백 발견 - 콜백 실행 (테이블 섹션 데이터 없음)");
|
||||
|
||||
try {
|
||||
await onSave();
|
||||
return true;
|
||||
|
|
@ -2214,14 +2212,6 @@ export class ButtonActionExecutor {
|
|||
// 섹션별 원본 데이터가 있으면 사용, 없으면 전역 originalGroupedData 사용
|
||||
const originalDataForDelete = sectionOriginalData.length > 0 ? sectionOriginalData : originalGroupedData;
|
||||
|
||||
console.log(`🔍 [DELETE 비교] 섹션 ${sectionId}:`, {
|
||||
sectionOriginalKey,
|
||||
sectionOriginalCount: sectionOriginalData.length,
|
||||
globalOriginalCount: originalGroupedData.length,
|
||||
usingData: sectionOriginalData.length > 0 ? "섹션별 원본" : "전역 원본",
|
||||
currentCount: currentItems.length,
|
||||
});
|
||||
|
||||
// ⚠️ id 타입 통일: 문자열로 변환하여 비교 (숫자 vs 문자열 불일치 방지)
|
||||
const currentIds = new Set(currentItems.map((item) => String(item.id)).filter(Boolean));
|
||||
const deletedItems = originalDataForDelete.filter((orig) => orig.id && !currentIds.has(String(orig.id)));
|
||||
|
|
|
|||
|
|
@ -319,8 +319,6 @@ class LegacyEventAdapter {
|
|||
this.config = { ...this.config, ...options };
|
||||
}
|
||||
|
||||
console.log("[LegacyEventAdapter] 초기화 시작", this.config);
|
||||
|
||||
EVENT_MAPPINGS.forEach((mapping) => {
|
||||
// 레거시 → V2 브릿지
|
||||
if (this.config.legacyToV2) {
|
||||
|
|
@ -334,9 +332,6 @@ class LegacyEventAdapter {
|
|||
});
|
||||
|
||||
this.isActive = true;
|
||||
console.log(
|
||||
`[LegacyEventAdapter] 초기화 완료 (${EVENT_MAPPINGS.length}개 매핑)`
|
||||
);
|
||||
}
|
||||
|
||||
private setupLegacyToV2Bridge(mapping: EventMapping): void {
|
||||
|
|
@ -411,8 +406,6 @@ class LegacyEventAdapter {
|
|||
|
||||
this.bridgedEvents.clear();
|
||||
this.isActive = false;
|
||||
|
||||
console.log("[LegacyEventAdapter] 정리 완료");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -55,8 +55,6 @@ export function initV2Core(options?: V2CoreOptions): void {
|
|||
legacyBridge = { legacyToV2: true, v2ToLegacy: true },
|
||||
} = options ?? {};
|
||||
|
||||
console.log("[V2Core] 초기화 시작...");
|
||||
|
||||
// 디버그 모드 설정
|
||||
v2EventBus.debug = debug;
|
||||
|
||||
|
|
@ -64,11 +62,6 @@ export function initV2Core(options?: V2CoreOptions): void {
|
|||
legacyEventAdapter.init(legacyBridge);
|
||||
|
||||
isInitialized = true;
|
||||
|
||||
console.log("[V2Core] 초기화 완료", {
|
||||
debug,
|
||||
legacyBridge: legacyEventAdapter.active,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue