# 테이블 검색 필터 컴포넌트 분리 및 통합 계획서

## 📋 목차

1. [현황 분석](#1-현황-분석)
2. [목표 및 요구사항](#2-목표-및-요구사항)
3. [아키텍처 설계](#3-아키텍처-설계)
4. [구현 계획](#4-구현-계획)
5. [파일 구조](#5-파일-구조)
6. [통합 시나리오](#6-통합-시나리오)
7. [주요 기능 및 개선 사항](#7-주요-기능-및-개선-사항)
8. [예상 장점](#8-예상-장점)
9. [구현 우선순위](#9-구현-우선순위)
10. [체크리스트](#10-체크리스트)

---

## 1. 현황 분석

### 1.1 현재 구조

- **테이블 리스트 컴포넌트**에 테이블 옵션이 내장되어 있음
- 각 테이블 컴포넌트마다 개별적으로 옵션 기능 구현
- 코드 중복 및 유지보수 어려움

### 1.2 현재 제공 기능

#### 테이블 옵션

- 컬럼 표시/숨김 설정
- 컬럼 순서 변경 (드래그앤드롭)
- 컬럼 너비 조정
- 고정 컬럼 설정

#### 필터 설정

- 컬럼별 검색 필터 적용
- 다중 필터 조건 지원
- 연산자 선택 (같음, 포함, 시작, 끝)

#### 그룹 설정

- 컬럼별 데이터 그룹화
- 다중 그룹 레벨 지원
- 그룹별 집계 표시

### 1.3 적용 대상 컴포넌트

1. **TableList**: 기본 테이블 리스트 컴포넌트
2. **SplitPanel**: 좌/우 분할 테이블 (마스터-디테일 관계)
3. **FlowWidget**: 플로우 스텝별 데이터 테이블

---

## 2. 목표 및 요구사항

### 2.1 핵심 목표

1. 테이블 옵션 기능을 **재사용 가능한 공통 컴포넌트**로 분리
2. 화면에 있는 테이블 컴포넌트를 **자동 감지**하여 검색 가능
3. 각 컴포넌트의 테이블 데이터와 **독립적으로 연동**
4. 기존 기능을 유지하면서 확장 가능한 구조 구축

### 2.2 기능 요구사항

#### 자동 감지

- 화면 로드 시 테이블 컴포넌트 자동 식별
- 컴포넌트 추가/제거 시 동적 반영
- 테이블 ID 기반 고유 식별

#### 다중 테이블 지원

- 한 화면에 여러 테이블이 있을 경우 선택 가능
- 테이블 간 독립적인 설정 관리
- 선택된 테이블에만 옵션 적용

#### 실시간 적용

- 필터/그룹 설정 시 즉시 테이블 업데이트
- 불필요한 전체 화면 리렌더링 방지
- 최적화된 데이터 조회

#### 상태 독립성

- 각 테이블의 설정이 독립적으로 유지
- 한 테이블의 설정이 다른 테이블에 영향 없음
- 화면 전환 시 설정 보존 (선택사항)

### 2.3 비기능 요구사항

- **성능**: 100개 이상의 컬럼도 부드럽게 처리
- **접근성**: 키보드 네비게이션 지원
- **반응형**: 모바일/태블릿 대응
- **확장성**: 새로운 테이블 타입 추가 용이

---

## 3. 아키텍처 설계

### 3.1 컴포넌트 구조

```
TableOptionsToolbar (신규 - 메인 툴바)
├── TableSelector (다중 테이블 선택 드롭다운)
├── ColumnVisibilityButton (테이블 옵션 버튼)
├── FilterButton (필터 설정 버튼)
└── GroupingButton (그룹 설정 버튼)

패널 컴포넌트들 (Dialog 형태)
├── ColumnVisibilityPanel (컬럼 표시/숨김 설정)
├── FilterPanel (검색 필터 설정)
└── GroupingPanel (그룹화 설정)

Context & Provider
├── TableOptionsContext (테이블 등록 및 관리)
└── TableOptionsProvider (전역 상태 관리)

화면 컴포넌트들 (기존 수정)
├── TableList → TableOptionsContext 연동
├── SplitPanel → 좌/우 각각 등록
└── FlowWidget → 스텝별 등록
```

### 3.2 데이터 흐름

```mermaid
graph TD
    A[화면 컴포넌트] --> B[registerTable 호출]
    B --> C[TableOptionsContext에 등록]
    C --> D[TableOptionsToolbar에서 목록 조회]
    D --> E[사용자가 테이블 선택]
    E --> F[옵션 버튼 클릭]
    F --> G[패널 열림]
    G --> H[설정 변경]
    H --> I[선택된 테이블의 콜백 호출]
    I --> J[테이블 컴포넌트 업데이트]
    J --> K[데이터 재조회/재렌더링]
```

### 3.3 상태 관리 구조

```typescript
// Context에서 관리하는 전역 상태
{
  registeredTables: Map<tableId, TableRegistration> {
    "table-list-123": {
      tableId: "table-list-123",
      label: "품목 관리",
      tableName: "item_info",
      columns: [...],
      onFilterChange: (filters) => {},
      onGroupChange: (groups) => {},
      onColumnVisibilityChange: (columns) => {}
    },
    "split-panel-left-456": {
      tableId: "split-panel-left-456",
      label: "분할 패널 (좌측)",
      tableName: "category_values",
      columns: [...],
      ...
    }
  }
}

// 각 테이블 컴포넌트가 관리하는 로컬 상태
{
  filters: [
    { columnName: "item_name", operator: "contains", value: "나사" }
  ],
  grouping: ["category_id", "material"],
  columnVisibility: [
    { columnName: "item_name", visible: true, width: 200, order: 1 },
    { columnName: "status", visible: false, width: 100, order: 2 }
  ]
}
```

---

## 4. 구현 계획

### Phase 1: Context 및 Provider 구현

#### 4.1.1 타입 정의

**파일**: `types/table-options.ts`

```typescript
/**
 * 테이블 필터 조건
 */
export interface TableFilter {
  columnName: string;
  operator:
    | "equals"
    | "contains"
    | "startsWith"
    | "endsWith"
    | "gt"
    | "lt"
    | "gte"
    | "lte"
    | "notEquals";
  value: string | number | boolean;
}

/**
 * 컬럼 표시 설정
 */
export interface ColumnVisibility {
  columnName: string;
  visible: boolean;
  width?: number;
  order?: number;
  fixed?: boolean; // 좌측 고정 여부
}

/**
 * 테이블 컬럼 정보
 */
export interface TableColumn {
  columnName: string;
  columnLabel: string;
  inputType: string;
  visible: boolean;
  width: number;
  sortable?: boolean;
  filterable?: boolean;
}

/**
 * 테이블 등록 정보
 */
export interface TableRegistration {
  tableId: string; // 고유 ID (예: "table-list-123")
  label: string; // 사용자에게 보이는 이름 (예: "품목 관리")
  tableName: string; // 실제 DB 테이블명 (예: "item_info")
  columns: TableColumn[];

  // 콜백 함수들
  onFilterChange: (filters: TableFilter[]) => void;
  onGroupChange: (groups: string[]) => void;
  onColumnVisibilityChange: (columns: ColumnVisibility[]) => void;
}

/**
 * Context 값 타입
 */
export interface TableOptionsContextValue {
  registeredTables: Map<string, TableRegistration>;
  registerTable: (registration: TableRegistration) => void;
  unregisterTable: (tableId: string) => void;
  getTable: (tableId: string) => TableRegistration | undefined;
  selectedTableId: string | null;
  setSelectedTableId: (tableId: string | null) => void;
}
```

#### 4.1.2 Context 생성

**파일**: `contexts/TableOptionsContext.tsx`

```typescript
import React, {
  createContext,
  useContext,
  useState,
  useCallback,
  ReactNode,
} from "react";
import {
  TableRegistration,
  TableOptionsContextValue,
} from "@/types/table-options";

const TableOptionsContext = createContext<TableOptionsContextValue | undefined>(
  undefined
);

export const TableOptionsProvider: React.FC<{ children: ReactNode }> = ({
  children,
}) => {
  const [registeredTables, setRegisteredTables] = useState<
    Map<string, TableRegistration>
  >(new Map());
  const [selectedTableId, setSelectedTableId] = useState<string | null>(null);

  /**
   * 테이블 등록
   */
  const registerTable = useCallback((registration: TableRegistration) => {
    setRegisteredTables((prev) => {
      const newMap = new Map(prev);
      newMap.set(registration.tableId, registration);

      // 첫 번째 테이블이면 자동 선택
      if (newMap.size === 1) {
        setSelectedTableId(registration.tableId);
      }

      return newMap;
    });

    console.log(
      `[TableOptions] 테이블 등록: ${registration.label} (${registration.tableId})`
    );
  }, []);

  /**
   * 테이블 등록 해제
   */
  const unregisterTable = useCallback(
    (tableId: string) => {
      setRegisteredTables((prev) => {
        const newMap = new Map(prev);
        const removed = newMap.delete(tableId);

        if (removed) {
          console.log(`[TableOptions] 테이블 해제: ${tableId}`);

          // 선택된 테이블이 제거되면 첫 번째 테이블 선택
          if (selectedTableId === tableId) {
            const firstTableId = newMap.keys().next().value;
            setSelectedTableId(firstTableId || null);
          }
        }

        return newMap;
      });
    },
    [selectedTableId]
  );

  /**
   * 특정 테이블 조회
   */
  const getTable = useCallback(
    (tableId: string) => {
      return registeredTables.get(tableId);
    },
    [registeredTables]
  );

  return (
    <TableOptionsContext.Provider
      value={{
        registeredTables,
        registerTable,
        unregisterTable,
        getTable,
        selectedTableId,
        setSelectedTableId,
      }}
    >
      {children}
    </TableOptionsContext.Provider>
  );
};

/**
 * Context Hook
 */
export const useTableOptions = () => {
  const context = useContext(TableOptionsContext);
  if (!context) {
    throw new Error("useTableOptions must be used within TableOptionsProvider");
  }
  return context;
};
```

---

### Phase 2: TableOptionsToolbar 컴포넌트 구현

**파일**: `components/screen/table-options/TableOptionsToolbar.tsx`

```typescript
import React, { useState } from "react";
import { useTableOptions } from "@/contexts/TableOptionsContext";
import { Button } from "@/components/ui/button";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { Settings, Filter, Layers } from "lucide-react";
import { ColumnVisibilityPanel } from "./ColumnVisibilityPanel";
import { FilterPanel } from "./FilterPanel";
import { GroupingPanel } from "./GroupingPanel";

export const TableOptionsToolbar: React.FC = () => {
  const { registeredTables, selectedTableId, setSelectedTableId } =
    useTableOptions();

  const [columnPanelOpen, setColumnPanelOpen] = useState(false);
  const [filterPanelOpen, setFilterPanelOpen] = useState(false);
  const [groupPanelOpen, setGroupPanelOpen] = useState(false);

  const tableList = Array.from(registeredTables.values());
  const selectedTable = selectedTableId
    ? registeredTables.get(selectedTableId)
    : null;

  // 테이블이 없으면 표시하지 않음
  if (tableList.length === 0) {
    return null;
  }

  return (
    <div className="flex items-center gap-2 border-b bg-background p-2">
      {/* 테이블 선택 (2개 이상일 때만 표시) */}
      {tableList.length > 1 && (
        <Select
          value={selectedTableId || ""}
          onValueChange={setSelectedTableId}
        >
          <SelectTrigger className="h-8 w-48 text-xs sm:h-9 sm:w-64 sm:text-sm">
            <SelectValue placeholder="테이블 선택" />
          </SelectTrigger>
          <SelectContent>
            {tableList.map((table) => (
              <SelectItem key={table.tableId} value={table.tableId}>
                {table.label}
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
      )}

      {/* 테이블이 1개일 때는 이름만 표시 */}
      {tableList.length === 1 && (
        <div className="text-xs font-medium sm:text-sm">
          {tableList[0].label}
        </div>
      )}

      {/* 컬럼 수 표시 */}
      <div className="text-xs text-muted-foreground sm:text-sm">
        전체 {selectedTable?.columns.length || 0}개
      </div>

      <div className="flex-1" />

      {/* 옵션 버튼들 */}
      <Button
        variant="outline"
        size="sm"
        onClick={() => setColumnPanelOpen(true)}
        className="h-8 text-xs sm:h-9 sm:text-sm"
        disabled={!selectedTableId}
      >
        <Settings className="mr-2 h-4 w-4" />
        테이블 옵션
      </Button>

      <Button
        variant="outline"
        size="sm"
        onClick={() => setFilterPanelOpen(true)}
        className="h-8 text-xs sm:h-9 sm:text-sm"
        disabled={!selectedTableId}
      >
        <Filter className="mr-2 h-4 w-4" />
        필터 설정
      </Button>

      <Button
        variant="outline"
        size="sm"
        onClick={() => setGroupPanelOpen(true)}
        className="h-8 text-xs sm:h-9 sm:text-sm"
        disabled={!selectedTableId}
      >
        <Layers className="mr-2 h-4 w-4" />
        그룹 설정
      </Button>

      {/* 패널들 */}
      {selectedTableId && (
        <>
          <ColumnVisibilityPanel
            tableId={selectedTableId}
            open={columnPanelOpen}
            onOpenChange={setColumnPanelOpen}
          />
          <FilterPanel
            tableId={selectedTableId}
            open={filterPanelOpen}
            onOpenChange={setFilterPanelOpen}
          />
          <GroupingPanel
            tableId={selectedTableId}
            open={groupPanelOpen}
            onOpenChange={setGroupPanelOpen}
          />
        </>
      )}
    </div>
  );
};
```

---

### Phase 3: 패널 컴포넌트 구현

#### 4.3.1 ColumnVisibilityPanel

**파일**: `components/screen/table-options/ColumnVisibilityPanel.tsx`

```typescript
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 { GripVertical, Eye, EyeOff } from "lucide-react";
import { ColumnVisibility } from "@/types/table-options";

interface Props {
  tableId: string;
  open: boolean;
  onOpenChange: (open: boolean) => void;
}

export const ColumnVisibilityPanel: React.FC<Props> = ({
  tableId,
  open,
  onOpenChange,
}) => {
  const { getTable } = useTableOptions();
  const table = getTable(tableId);

  const [localColumns, setLocalColumns] = useState<ColumnVisibility[]>([]);

  // 테이블 정보 로드
  useEffect(() => {
    if (table) {
      setLocalColumns(
        table.columns.map((col) => ({
          columnName: col.columnName,
          visible: col.visible,
          width: col.width,
          order: 0,
        }))
      );
    }
  }, [table]);

  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 handleApply = () => {
    table?.onColumnVisibilityChange(localColumns);
    onOpenChange(false);
  };

  const handleReset = () => {
    if (table) {
      setLocalColumns(
        table.columns.map((col) => ({
          columnName: col.columnName,
          visible: true,
          width: 150,
          order: 0,
        }))
      );
    }
  };

  const visibleCount = localColumns.filter((col) => col.visible).length;

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <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>

        <div className="space-y-3 sm:space-y-4">
          {/* 상태 표시 */}
          <div className="flex items-center justify-between rounded-lg border bg-muted/50 p-3">
            <div className="text-xs text-muted-foreground sm:text-sm">
              {visibleCount}/{localColumns.length}개 컬럼 표시 중
            </div>
            <Button
              variant="ghost"
              size="sm"
              onClick={handleReset}
              className="h-7 text-xs"
            >
              초기화
            </Button>
          </div>

          {/* 컬럼 리스트 */}
          <ScrollArea className="h-[300px] sm:h-[400px]">
            <div className="space-y-2 pr-4">
              {localColumns.map((col, index) => {
                const columnMeta = table?.columns.find(
                  (c) => c.columnName === col.columnName
                );
                return (
                  <div
                    key={col.columnName}
                    className="flex items-center gap-3 rounded-lg border bg-background p-3 transition-colors hover:bg-muted/50"
                  >
                    {/* 드래그 핸들 */}
                    <GripVertical className="h-4 w-4 shrink-0 text-muted-foreground" />

                    {/* 체크박스 */}
                    <Checkbox
                      checked={col.visible}
                      onCheckedChange={(checked) =>
                        handleVisibilityChange(
                          col.columnName,
                          checked as boolean
                        )
                      }
                    />

                    {/* 가시성 아이콘 */}
                    {col.visible ? (
                      <Eye className="h-4 w-4 shrink-0 text-primary" />
                    ) : (
                      <EyeOff className="h-4 w-4 shrink-0 text-muted-foreground" />
                    )}

                    {/* 컬럼명 */}
                    <div className="flex-1">
                      <div className="text-xs font-medium sm:text-sm">
                        {columnMeta?.columnLabel}
                      </div>
                      <div className="text-[10px] text-muted-foreground sm:text-xs">
                        {col.columnName}
                      </div>
                    </div>

                    {/* 너비 설정 */}
                    <div className="flex items-center gap-2">
                      <Label className="text-xs text-muted-foreground">
                        너비:
                      </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>

        <DialogFooter className="gap-2 sm:gap-0">
          <Button
            variant="outline"
            onClick={() => onOpenChange(false)}
            className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
          >
            취소
          </Button>
          <Button
            onClick={handleApply}
            className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
          >
            저장
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
};
```

#### 4.3.2 FilterPanel

**파일**: `components/screen/table-options/FilterPanel.tsx`

```typescript
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 { Input } from "@/components/ui/input";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Plus, X } from "lucide-react";
import { TableFilter } from "@/types/table-options";

interface Props {
  tableId: string;
  open: boolean;
  onOpenChange: (open: boolean) => void;
}

export const FilterPanel: React.FC<Props> = ({
  tableId,
  open,
  onOpenChange,
}) => {
  const { getTable } = useTableOptions();
  const table = getTable(tableId);

  const [activeFilters, setActiveFilters] = useState<TableFilter[]>([]);

  const addFilter = () => {
    setActiveFilters([
      ...activeFilters,
      { columnName: "", operator: "contains", value: "" },
    ]);
  };

  const removeFilter = (index: number) => {
    setActiveFilters(activeFilters.filter((_, i) => i !== index));
  };

  const updateFilter = (
    index: number,
    field: keyof TableFilter,
    value: any
  ) => {
    setActiveFilters(
      activeFilters.map((filter, i) =>
        i === index ? { ...filter, [field]: value } : filter
      )
    );
  };

  const applyFilters = () => {
    // 빈 필터 제거
    const validFilters = activeFilters.filter(
      (f) => f.columnName && f.value !== ""
    );
    table?.onFilterChange(validFilters);
    onOpenChange(false);
  };

  const clearFilters = () => {
    setActiveFilters([]);
    table?.onFilterChange([]);
  };

  const operatorLabels: Record<string, string> = {
    equals: "같음",
    contains: "포함",
    startsWith: "시작",
    endsWith: "끝",
    gt: "보다 큼",
    lt: "보다 작음",
    gte: "이상",
    lte: "이하",
    notEquals: "같지 않음",
  };

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <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>

        <div className="space-y-3 sm:space-y-4">
          {/* 전체 선택/해제 */}
          <div className="flex items-center justify-between">
            <div className="text-xs text-muted-foreground sm:text-sm">
              총 {activeFilters.length}개의 검색 필터가 표시됩니다
            </div>
            <Button
              variant="ghost"
              size="sm"
              onClick={clearFilters}
              className="h-7 text-xs"
            >
              초기화
            </Button>
          </div>

          {/* 필터 리스트 */}
          <ScrollArea className="h-[300px] sm:h-[400px]">
            <div className="space-y-3 pr-4">
              {activeFilters.map((filter, index) => (
                <div
                  key={index}
                  className="flex flex-col gap-2 rounded-lg border bg-background p-3 sm:flex-row sm:items-center"
                >
                  {/* 컬럼 선택 */}
                  <Select
                    value={filter.columnName}
                    onValueChange={(val) =>
                      updateFilter(index, "columnName", val)
                    }
                  >
                    <SelectTrigger className="h-8 text-xs sm:h-9 sm:w-40 sm:text-sm">
                      <SelectValue placeholder="컬럼 선택" />
                    </SelectTrigger>
                    <SelectContent>
                      {table?.columns
                        .filter((col) => col.filterable !== false)
                        .map((col) => (
                          <SelectItem
                            key={col.columnName}
                            value={col.columnName}
                          >
                            {col.columnLabel}
                          </SelectItem>
                        ))}
                    </SelectContent>
                  </Select>

                  {/* 연산자 선택 */}
                  <Select
                    value={filter.operator}
                    onValueChange={(val) =>
                      updateFilter(index, "operator", val)
                    }
                  >
                    <SelectTrigger className="h-8 text-xs sm:h-9 sm:w-32 sm:text-sm">
                      <SelectValue />
                    </SelectTrigger>
                    <SelectContent>
                      {Object.entries(operatorLabels).map(([value, label]) => (
                        <SelectItem key={value} value={value}>
                          {label}
                        </SelectItem>
                      ))}
                    </SelectContent>
                  </Select>

                  {/* 값 입력 */}
                  <Input
                    value={filter.value as string}
                    onChange={(e) =>
                      updateFilter(index, "value", e.target.value)
                    }
                    placeholder="값 입력"
                    className="h-8 flex-1 text-xs sm:h-9 sm:text-sm"
                  />

                  {/* 삭제 버튼 */}
                  <Button
                    variant="ghost"
                    size="icon"
                    onClick={() => removeFilter(index)}
                    className="h-8 w-8 shrink-0 sm:h-9 sm:w-9"
                  >
                    <X className="h-4 w-4" />
                  </Button>
                </div>
              ))}
            </div>
          </ScrollArea>

          {/* 필터 추가 버튼 */}
          <Button
            variant="outline"
            onClick={addFilter}
            className="h-8 w-full text-xs sm:h-9 sm:text-sm"
          >
            <Plus className="mr-2 h-4 w-4" />
            필터 추가
          </Button>
        </div>

        <DialogFooter className="gap-2 sm:gap-0">
          <Button
            variant="outline"
            onClick={() => onOpenChange(false)}
            className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
          >
            취소
          </Button>
          <Button
            onClick={applyFilters}
            className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
          >
            저장
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
};
```

#### 4.3.3 GroupingPanel

**파일**: `components/screen/table-options/GroupingPanel.tsx`

```typescript
import React, { useState } 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 { ScrollArea } from "@/components/ui/scroll-area";
import { ArrowRight } from "lucide-react";

interface Props {
  tableId: string;
  open: boolean;
  onOpenChange: (open: boolean) => void;
}

export const GroupingPanel: React.FC<Props> = ({
  tableId,
  open,
  onOpenChange,
}) => {
  const { getTable } = useTableOptions();
  const table = getTable(tableId);

  const [selectedColumns, setSelectedColumns] = useState<string[]>([]);

  const toggleColumn = (columnName: string) => {
    if (selectedColumns.includes(columnName)) {
      setSelectedColumns(selectedColumns.filter((c) => c !== columnName));
    } else {
      setSelectedColumns([...selectedColumns, columnName]);
    }
  };

  const applyGrouping = () => {
    table?.onGroupChange(selectedColumns);
    onOpenChange(false);
  };

  const clearGrouping = () => {
    setSelectedColumns([]);
    table?.onGroupChange([]);
  };

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="max-w-[95vw] sm:max-w-xl">
        <DialogHeader>
          <DialogTitle className="text-base sm:text-lg">그룹 설정</DialogTitle>
          <DialogDescription className="text-xs sm:text-sm">
            데이터를 그룹화할 컬럼을 선택하세요
          </DialogDescription>
        </DialogHeader>

        <div className="space-y-3 sm:space-y-4">
          {/* 상태 표시 */}
          <div className="flex items-center justify-between rounded-lg border bg-muted/50 p-3">
            <div className="text-xs text-muted-foreground sm:text-sm">
              {selectedColumns.length}개 컬럼으로 그룹화
            </div>
            <Button
              variant="ghost"
              size="sm"
              onClick={clearGrouping}
              className="h-7 text-xs"
            >
              초기화
            </Button>
          </div>

          {/* 컬럼 리스트 */}
          <ScrollArea className="h-[250px] sm:h-[300px]">
            <div className="space-y-2 pr-4">
              {table?.columns.map((col, index) => {
                const isSelected = selectedColumns.includes(col.columnName);
                const order = selectedColumns.indexOf(col.columnName) + 1;

                return (
                  <div
                    key={col.columnName}
                    className="flex items-center gap-3 rounded-lg border bg-background p-3 transition-colors hover:bg-muted/50"
                  >
                    <Checkbox
                      checked={isSelected}
                      onCheckedChange={() => toggleColumn(col.columnName)}
                    />

                    <div className="flex-1">
                      <div className="text-xs font-medium sm:text-sm">
                        {col.columnLabel}
                      </div>
                      <div className="text-[10px] text-muted-foreground sm:text-xs">
                        {col.columnName}
                      </div>
                    </div>

                    {isSelected && (
                      <div className="flex items-center gap-1 rounded-full bg-primary px-2 py-0.5 text-xs text-primary-foreground">
                        {order}번째
                      </div>
                    )}
                  </div>
                );
              })}
            </div>
          </ScrollArea>

          {/* 그룹 순서 미리보기 */}
          {selectedColumns.length > 0 && (
            <div className="rounded-lg border bg-muted/30 p-3">
              <div className="mb-2 text-xs font-medium sm:text-sm">
                그룹화 순서
              </div>
              <div className="flex flex-wrap items-center gap-2 text-xs sm:text-sm">
                {selectedColumns.map((colName, index) => {
                  const col = table?.columns.find(
                    (c) => c.columnName === colName
                  );
                  return (
                    <React.Fragment key={colName}>
                      <div className="rounded bg-primary/10 px-2 py-1 font-medium">
                        {col?.columnLabel}
                      </div>
                      {index < selectedColumns.length - 1 && (
                        <ArrowRight className="h-3 w-3 text-muted-foreground" />
                      )}
                    </React.Fragment>
                  );
                })}
              </div>
            </div>
          )}
        </div>

        <DialogFooter className="gap-2 sm:gap-0">
          <Button
            variant="outline"
            onClick={() => onOpenChange(false)}
            className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
          >
            취소
          </Button>
          <Button
            onClick={applyGrouping}
            className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
          >
            저장
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
};
```

---

### Phase 4: 기존 테이블 컴포넌트 통합

#### 4.4.1 TableList 컴포넌트 수정

**파일**: `components/screen/interactive/TableList.tsx`

```typescript
import { useEffect, useState, useCallback } from "react";
import { useTableOptions } from "@/contexts/TableOptionsContext";
import { TableFilter, ColumnVisibility } from "@/types/table-options";

export const TableList: React.FC<Props> = ({ component }) => {
  const { registerTable, unregisterTable } = useTableOptions();

  // 로컬 상태
  const [filters, setFilters] = useState<TableFilter[]>([]);
  const [grouping, setGrouping] = useState<string[]>([]);
  const [columnVisibility, setColumnVisibility] = useState<ColumnVisibility[]>(
    []
  );
  const [data, setData] = useState<any[]>([]);

  const tableId = `table-list-${component.id}`;

  // 테이블 등록
  useEffect(() => {
    registerTable({
      tableId,
      label: component.title || "테이블",
      tableName: component.tableName,
      columns: component.columns.map((col) => ({
        columnName: col.field,
        columnLabel: col.label,
        inputType: col.inputType,
        visible: col.visible ?? true,
        width: col.width || 150,
        sortable: col.sortable,
        filterable: col.filterable,
      })),
      onFilterChange: setFilters,
      onGroupChange: setGrouping,
      onColumnVisibilityChange: setColumnVisibility,
    });

    return () => unregisterTable(tableId);
  }, [component.id, component.tableName, component.columns]);

  // 데이터 조회
  const fetchData = useCallback(async () => {
    try {
      const params = {
        tableName: component.tableName,
        filters: JSON.stringify(filters),
        groupBy: grouping.join(","),
      };

      const response = await apiClient.get("/api/table/data", { params });

      if (response.data.success) {
        setData(response.data.data);
      }
    } catch (error) {
      console.error("데이터 조회 실패:", error);
    }
  }, [component.tableName, filters, grouping]);

  // 필터/그룹 변경 시 데이터 재조회
  useEffect(() => {
    fetchData();
  }, [fetchData]);

  // 표시할 컬럼 필터링
  const visibleColumns = component.columns.filter((col) => {
    const visibility = columnVisibility.find((v) => v.columnName === col.field);
    return visibility ? visibility.visible : col.visible !== false;
  });

  return (
    <div className="flex h-full flex-col">
      {/* 기존 테이블 UI */}
      <div className="flex-1 overflow-auto">
        <table className="w-full">
          <thead>
            <tr>
              {visibleColumns.map((col) => {
                const visibility = columnVisibility.find(
                  (v) => v.columnName === col.field
                );
                const width = visibility?.width || col.width || 150;

                return (
                  <th key={col.field} style={{ width: `${width}px` }}>
                    {col.label}
                  </th>
                );
              })}
            </tr>
          </thead>
          <tbody>
            {data.map((row, rowIndex) => (
              <tr key={rowIndex}>
                {visibleColumns.map((col) => (
                  <td key={col.field}>{row[col.field]}</td>
                ))}
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
};
```

#### 4.4.2 SplitPanel 컴포넌트 수정

**파일**: `components/screen/interactive/SplitPanel.tsx`

```typescript
export const SplitPanel: React.FC<Props> = ({ component }) => {
  const { registerTable, unregisterTable } = useTableOptions();

  // 좌측 테이블 상태
  const [leftFilters, setLeftFilters] = useState<TableFilter[]>([]);
  const [leftGrouping, setLeftGrouping] = useState<string[]>([]);
  const [leftColumnVisibility, setLeftColumnVisibility] = useState<
    ColumnVisibility[]
  >([]);

  // 우측 테이블 상태
  const [rightFilters, setRightFilters] = useState<TableFilter[]>([]);
  const [rightGrouping, setRightGrouping] = useState<string[]>([]);
  const [rightColumnVisibility, setRightColumnVisibility] = useState<
    ColumnVisibility[]
  >([]);

  const leftTableId = `split-panel-left-${component.id}`;
  const rightTableId = `split-panel-right-${component.id}`;

  // 좌측 테이블 등록
  useEffect(() => {
    registerTable({
      tableId: leftTableId,
      label: `${component.title || "분할 패널"} (좌측)`,
      tableName: component.leftPanel.tableName,
      columns: component.leftPanel.columns.map((col) => ({
        columnName: col.field,
        columnLabel: col.label,
        inputType: col.inputType,
        visible: col.visible ?? true,
        width: col.width || 150,
      })),
      onFilterChange: setLeftFilters,
      onGroupChange: setLeftGrouping,
      onColumnVisibilityChange: setLeftColumnVisibility,
    });

    return () => unregisterTable(leftTableId);
  }, [component.leftPanel]);

  // 우측 테이블 등록
  useEffect(() => {
    registerTable({
      tableId: rightTableId,
      label: `${component.title || "분할 패널"} (우측)`,
      tableName: component.rightPanel.tableName,
      columns: component.rightPanel.columns.map((col) => ({
        columnName: col.field,
        columnLabel: col.label,
        inputType: col.inputType,
        visible: col.visible ?? true,
        width: col.width || 150,
      })),
      onFilterChange: setRightFilters,
      onGroupChange: setRightGrouping,
      onColumnVisibilityChange: setRightColumnVisibility,
    });

    return () => unregisterTable(rightTableId);
  }, [component.rightPanel]);

  return (
    <div className="flex h-full gap-4">
      {/* 좌측 테이블 */}
      <div className="flex-1">
        <TableList
          component={component.leftPanel}
          filters={leftFilters}
          grouping={leftGrouping}
          columnVisibility={leftColumnVisibility}
        />
      </div>

      {/* 우측 테이블 */}
      <div className="flex-1">
        <TableList
          component={component.rightPanel}
          filters={rightFilters}
          grouping={rightGrouping}
          columnVisibility={rightColumnVisibility}
        />
      </div>
    </div>
  );
};
```

#### 4.4.3 FlowWidget 컴포넌트 수정

**파일**: `components/screen/interactive/FlowWidget.tsx`

```typescript
export const FlowWidget: React.FC<Props> = ({ component }) => {
  const { registerTable, unregisterTable } = useTableOptions();

  const [selectedStep, setSelectedStep] = useState<any | null>(null);
  const [filters, setFilters] = useState<TableFilter[]>([]);
  const [grouping, setGrouping] = useState<string[]>([]);
  const [columnVisibility, setColumnVisibility] = useState<ColumnVisibility[]>(
    []
  );

  const tableId = selectedStep
    ? `flow-widget-${component.id}-step-${selectedStep.id}`
    : null;

  // 선택된 스텝의 테이블 등록
  useEffect(() => {
    if (!selectedStep || !tableId) return;

    registerTable({
      tableId,
      label: `${selectedStep.name} 데이터`,
      tableName: component.tableName,
      columns: component.displayColumns.map((col) => ({
        columnName: col.field,
        columnLabel: col.label,
        inputType: col.inputType,
        visible: col.visible ?? true,
        width: col.width || 150,
      })),
      onFilterChange: setFilters,
      onGroupChange: setGrouping,
      onColumnVisibilityChange: setColumnVisibility,
    });

    return () => unregisterTable(tableId);
  }, [selectedStep, component.displayColumns]);

  return (
    <div className="flex h-full flex-col">
      {/* 플로우 스텝 선택 UI */}
      <div className="border-b p-2">{/* 스텝 선택 드롭다운 */}</div>

      {/* 테이블 */}
      <div className="flex-1">
        {selectedStep && (
          <TableList
            component={component}
            filters={filters}
            grouping={grouping}
            columnVisibility={columnVisibility}
          />
        )}
      </div>
    </div>
  );
};
```

---

### Phase 5: InteractiveScreenViewer 통합

**파일**: `components/screen/InteractiveScreenViewer.tsx`

```typescript
import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
import { TableOptionsToolbar } from "@/components/screen/table-options/TableOptionsToolbar";

export const InteractiveScreenViewer: React.FC<Props> = ({ screenData }) => {
  return (
    <TableOptionsProvider>
      <div className="flex h-full flex-col">
        {/* 테이블 옵션 툴바 */}
        <TableOptionsToolbar />

        {/* 화면 컨텐츠 */}
        <div className="flex-1 overflow-auto">
          {screenData.components.map((component) => (
            <ComponentRenderer key={component.id} component={component} />
          ))}
        </div>
      </div>
    </TableOptionsProvider>
  );
};
```

---

### Phase 6: 백엔드 API 개선

**파일**: `backend-node/src/controllers/tableController.ts`

```typescript
/**
 * 테이블 데이터 조회 (필터/그룹 지원)
 */
export async function getTableData(req: Request, res: Response) {
  const companyCode = req.user!.companyCode;
  const { tableName, filters, groupBy, page = 1, pageSize = 50 } = req.query;

  try {
    // 필터 파싱
    const parsedFilters: TableFilter[] = filters
      ? JSON.parse(filters as string)
      : [];

    // WHERE 절 생성
    const whereConditions: string[] = [`company_code = $1`];
    const params: any[] = [companyCode];

    parsedFilters.forEach((filter, index) => {
      const paramIndex = index + 2;

      switch (filter.operator) {
        case "equals":
          whereConditions.push(`${filter.columnName} = $${paramIndex}`);
          params.push(filter.value);
          break;
        case "contains":
          whereConditions.push(`${filter.columnName} ILIKE $${paramIndex}`);
          params.push(`%${filter.value}%`);
          break;
        case "startsWith":
          whereConditions.push(`${filter.columnName} ILIKE $${paramIndex}`);
          params.push(`${filter.value}%`);
          break;
        case "endsWith":
          whereConditions.push(`${filter.columnName} ILIKE $${paramIndex}`);
          params.push(`%${filter.value}`);
          break;
        case "gt":
          whereConditions.push(`${filter.columnName} > $${paramIndex}`);
          params.push(filter.value);
          break;
        case "lt":
          whereConditions.push(`${filter.columnName} < $${paramIndex}`);
          params.push(filter.value);
          break;
        case "gte":
          whereConditions.push(`${filter.columnName} >= $${paramIndex}`);
          params.push(filter.value);
          break;
        case "lte":
          whereConditions.push(`${filter.columnName} <= $${paramIndex}`);
          params.push(filter.value);
          break;
        case "notEquals":
          whereConditions.push(`${filter.columnName} != $${paramIndex}`);
          params.push(filter.value);
          break;
      }
    });

    const whereSql = `WHERE ${whereConditions.join(" AND ")}`;
    const groupBySql = groupBy ? `GROUP BY ${groupBy}` : "";

    // 페이징
    const offset =
      (parseInt(page as string) - 1) * parseInt(pageSize as string);
    const limitSql = `LIMIT ${pageSize} OFFSET ${offset}`;

    // 카운트 쿼리
    const countQuery = `SELECT COUNT(*) as total FROM ${tableName} ${whereSql}`;
    const countResult = await pool.query(countQuery, params);
    const total = parseInt(countResult.rows[0].total);

    // 데이터 쿼리
    const dataQuery = `
      SELECT * FROM ${tableName}
      ${whereSql}
      ${groupBySql}
      ORDER BY id DESC
      ${limitSql}
    `;
    const dataResult = await pool.query(dataQuery, params);

    return res.json({
      success: true,
      data: dataResult.rows,
      pagination: {
        page: parseInt(page as string),
        pageSize: parseInt(pageSize as string),
        total,
        totalPages: Math.ceil(total / parseInt(pageSize as string)),
      },
    });
  } catch (error: any) {
    logger.error("테이블 데이터 조회 실패", {
      error: error.message,
      tableName,
    });
    return res.status(500).json({
      success: false,
      error: "데이터 조회 중 오류가 발생했습니다",
    });
  }
}
```

---

## 5. 파일 구조

```
frontend/
├── types/
│   └── table-options.ts                     # 타입 정의
│
├── contexts/
│   └── TableOptionsContext.tsx              # Context 및 Provider
│
├── components/
│   └── screen/
│       ├── table-options/
│       │   ├── TableOptionsToolbar.tsx      # 메인 툴바
│       │   ├── ColumnVisibilityPanel.tsx    # 테이블 옵션 패널
│       │   ├── FilterPanel.tsx              # 필터 설정 패널
│       │   └── GroupingPanel.tsx            # 그룹 설정 패널
│       │
│       ├── interactive/
│       │   ├── TableList.tsx                # 수정: Context 연동
│       │   ├── SplitPanel.tsx               # 수정: Context 연동
│       │   └── FlowWidget.tsx               # 수정: Context 연동
│       │
│       └── InteractiveScreenViewer.tsx      # 수정: Provider 래핑
│
backend-node/
└── src/
    └── controllers/
        └── tableController.ts               # 수정: 필터/그룹 지원
```

---

## 6. 통합 시나리오

### 6.1 단일 테이블 화면

```tsx
<InteractiveScreenViewer>
  <TableOptionsProvider>
    <TableOptionsToolbar /> {/* 자동으로 1개 테이블 선택 */}
    <TableList /> {/* 자동 등록 */}
  </TableOptionsProvider>
</InteractiveScreenViewer>
```

**동작 흐름**:

1. TableList 마운트 → Context에 테이블 등록
2. TableOptionsToolbar에서 자동으로 해당 테이블 선택
3. 사용자가 필터 설정 → onFilterChange 콜백 호출
4. TableList에서 filters 상태 업데이트 → 데이터 재조회

### 6.2 다중 테이블 화면 (SplitPanel)

```tsx
<InteractiveScreenViewer>
  <TableOptionsProvider>
    <TableOptionsToolbar /> {/* 좌/우 테이블 선택 가능 */}
    <SplitPanel>
      {" "}
      {/* 좌/우 각각 등록 */}
      <TableList /> {/* 좌측 */}
      <TableList /> {/* 우측 */}
    </SplitPanel>
  </TableOptionsProvider>
</InteractiveScreenViewer>
```

**동작 흐름**:

1. SplitPanel 마운트 → 좌/우 테이블 각각 등록
2. TableOptionsToolbar에서 드롭다운으로 테이블 선택
3. 선택된 테이블에 대해서만 옵션 적용
4. 각 테이블의 상태는 독립적으로 관리

### 6.3 플로우 위젯 화면

```tsx
<InteractiveScreenViewer>
  <TableOptionsProvider>
    <TableOptionsToolbar /> {/* 현재 스텝 테이블 자동 선택 */}
    <FlowWidget /> {/* 스텝 변경 시 자동 재등록 */}
  </TableOptionsProvider>
</InteractiveScreenViewer>
```

**동작 흐름**:

1. FlowWidget 마운트 → 초기 스텝 테이블 등록
2. 사용자가 다른 스텝 선택 → 기존 테이블 해제 + 새 테이블 등록
3. TableOptionsToolbar에서 자동으로 새 테이블 선택
4. 스텝별로 독립적인 필터/그룹 설정 유지

---

## 7. 주요 기능 및 개선 사항

### 7.1 자동 감지 메커니즘

**구현 방법**:

- 각 테이블 컴포넌트가 마운트될 때 `registerTable()` 호출
- 언마운트 시 `unregisterTable()` 호출
- Context가 등록된 테이블 목록을 Map으로 관리

**장점**:

- 개발자가 수동으로 테이블 목록을 관리할 필요 없음
- 동적으로 컴포넌트가 추가/제거되어도 자동 반영
- 컴포넌트 간 느슨한 결합 유지

### 7.2 독립적 상태 관리

**구현 방법**:

- 각 테이블 컴포넌트가 자체 상태(filters, grouping, columnVisibility) 관리
- Context는 상태를 직접 저장하지 않고 콜백 함수만 저장
- 콜백을 통해 각 테이블에 설정 전달

**장점**:

- 한 테이블의 설정이 다른 테이블에 영향 없음
- 메모리 효율적 (Context에 모든 상태 저장 불필요)
- 각 테이블이 독립적으로 최적화 가능

### 7.3 실시간 반영

**구현 방법**:

- 옵션 변경 시 즉시 해당 테이블의 콜백 호출
- 테이블 컴포넌트는 상태 변경을 감지하여 자동 리렌더링
- useCallback과 useMemo로 불필요한 리렌더링 방지

**장점**:

- 사용자 경험 향상 (즉각적인 피드백)
- 성능 최적화 (변경된 테이블만 업데이트)

### 7.4 확장성

**새로운 테이블 컴포넌트 추가 방법**:

```typescript
export const MyCustomTable: React.FC = () => {
  const { registerTable, unregisterTable } = useTableOptions();
  const [filters, setFilters] = useState<TableFilter[]>([]);

  useEffect(() => {
    registerTable({
      tableId: "my-custom-table-123",
      label: "커스텀 테이블",
      tableName: "custom_table",
      columns: [...],
      onFilterChange: setFilters,
      onGroupChange: setGrouping,
      onColumnVisibilityChange: setColumnVisibility,
    });

    return () => unregisterTable("my-custom-table-123");
  }, []);

  // 나머지 구현...
};
```

---

## 8. 예상 장점

### 8.1 개발자 측면

1. **코드 재사용성**: 공통 로직을 한 곳에서 관리
2. **유지보수 용이**: 버그 수정 시 한 곳만 수정
3. **일관된 UX**: 모든 테이블에서 동일한 사용자 경험
4. **빠른 개발**: 새 테이블 추가 시 Context만 연동

### 8.2 사용자 측면

1. **직관적인 UI**: 통일된 인터페이스로 학습 비용 감소
2. **유연한 검색**: 다양한 필터 조합으로 원하는 데이터 빠르게 찾기
3. **맞춤 설정**: 각 테이블별로 컬럼 표시/숨김 설정 가능
4. **효율적인 작업**: 그룹화로 대량 데이터를 구조적으로 확인

### 8.3 성능 측면

1. **최적화된 렌더링**: 변경된 테이블만 리렌더링
2. **효율적인 상태 관리**: Context에 최소한의 정보만 저장
3. **지연 로딩**: 패널은 열릴 때만 렌더링
4. **백엔드 부하 감소**: 필터링된 데이터만 조회

---

## 9. 구현 우선순위

### Phase 1: 기반 구조 (1-2일)

- [ ] 타입 정의 작성
- [ ] Context 및 Provider 구현
- [ ] 테스트용 간단한 TableOptionsToolbar 작성

### Phase 2: 툴바 및 패널 (2-3일)

- [ ] TableOptionsToolbar 완성
- [ ] ColumnVisibilityPanel 구현
- [ ] FilterPanel 구현
- [ ] GroupingPanel 구현

### Phase 3: 기존 컴포넌트 통합 (2-3일)

- [ ] TableList Context 연동
- [ ] SplitPanel Context 연동 (좌/우 분리)
- [ ] FlowWidget Context 연동
- [ ] InteractiveScreenViewer Provider 래핑

### Phase 4: 백엔드 API (1-2일)

- [ ] 필터 처리 로직 구현
- [ ] 그룹화 처리 로직 구현
- [ ] 페이징 최적화
- [ ] 성능 테스트

### Phase 5: 테스트 및 최적화 (1-2일)

- [ ] 단위 테스트 작성
- [ ] 통합 테스트
- [ ] 성능 프로파일링
- [ ] 버그 수정 및 최적화

**총 예상 기간**: 약 7-12일

---

## 10. 체크리스트

### 개발 전 확인사항

- [ ] 현재 테이블 옵션 기능 목록 정리
- [ ] 기존 코드의 중복 로직 파악
- [ ] 백엔드 API 현황 파악
- [ ] 성능 요구사항 정의

### 개발 중 확인사항

- [ ] 타입 정의 완료
- [ ] Context 및 Provider 동작 테스트
- [ ] 각 패널 UI/UX 검토
- [ ] 기존 컴포넌트와의 호환성 확인
- [ ] 백엔드 API 연동 테스트

### 개발 후 확인사항

- [ ] 모든 테이블 컴포넌트에서 정상 작동
- [ ] 다중 테이블 화면에서 독립성 확인
- [ ] 성능 요구사항 충족 확인
- [ ] 사용자 테스트 및 피드백 반영
- [ ] 문서화 완료

### 배포 전 확인사항

- [ ] 기존 화면에 영향 없는지 확인
- [ ] 롤백 계획 수립
- [ ] 사용자 가이드 작성
- [ ] 팀 공유 및 교육

---

## 11. 주의사항

### 11.1 멀티테넌시 준수

모든 데이터 조회 시 `company_code` 필터링 필수:

```typescript
// ✅ 올바른 방법
const whereConditions: string[] = [`company_code = $1`];
const params: any[] = [companyCode];

// ❌ 잘못된 방법
const whereConditions: string[] = []; // company_code 필터링 누락
```

### 11.2 SQL 인젝션 방지

필터 값은 반드시 파라미터 바인딩 사용:

```typescript
// ✅ 올바른 방법
whereConditions.push(`${filter.columnName} = $${paramIndex}`);
params.push(filter.value);

// ❌ 잘못된 방법
whereConditions.push(`${filter.columnName} = '${filter.value}'`); // SQL 인젝션 위험
```

### 11.3 성능 고려사항

- 컬럼이 많은 테이블(100개 이상)의 경우 가상 스크롤 적용
- 필터 변경 시 디바운싱으로 API 호출 최소화
- 그룹화는 데이터량에 따라 프론트엔드/백엔드 선택적 처리

### 11.4 접근성

- 키보드 네비게이션 지원 (Tab, Enter, Esc)
- 스크린 리더 호환성 확인
- 색상 대비 4.5:1 이상 유지

---

## 12. 추가 고려사항

### 12.1 설정 저장 기능

사용자별로 테이블 설정을 저장하여 화면 재방문 시 복원:

```typescript
// 로컬 스토리지에 저장
localStorage.setItem(
  `table-settings-${tableId}`,
  JSON.stringify({ columnVisibility, filters, grouping })
);

// 불러오기
const savedSettings = localStorage.getItem(`table-settings-${tableId}`);
if (savedSettings) {
  const { columnVisibility, filters, grouping } = JSON.parse(savedSettings);
  setColumnVisibility(columnVisibility);
  setFilters(filters);
  setGrouping(grouping);
}
```

### 12.2 내보내기 기능

현재 필터/그룹 설정으로 Excel 내보내기:

```typescript
const exportToExcel = () => {
  const params = {
    tableName: component.tableName,
    filters: JSON.stringify(filters),
    groupBy: grouping.join(","),
    columns: visibleColumns.map((c) => c.field),
  };

  window.location.href = `/api/table/export?${new URLSearchParams(params)}`;
};
```

### 12.3 필터 프리셋

자주 사용하는 필터 조합을 프리셋으로 저장:

```typescript
interface FilterPreset {
  id: string;
  name: string;
  filters: TableFilter[];
  grouping: string[];
}

const presets: FilterPreset[] = [
  { id: "active-items", name: "활성 품목만", filters: [...], grouping: [] },
  { id: "by-category", name: "카테고리별 그룹", filters: [], grouping: ["category_id"] },
];
```

---

## 13. 참고 자료

- [Tanstack Table 문서](https://tanstack.com/table/v8)
- [shadcn/ui Dialog 컴포넌트](https://ui.shadcn.com/docs/components/dialog)
- [React Context 최적화 가이드](https://react.dev/learn/passing-data-deeply-with-context)
- [PostgreSQL 필터링 최적화](https://www.postgresql.org/docs/current/indexes.html)

---

## 14. 브라우저 테스트 결과

### 테스트 환경

- **날짜**: 2025-01-13
- **브라우저**: Chrome
- **테스트 URL**: http://localhost:9771/screens/106
- **화면**: DTG 수명주기 관리 - 스텝 (FlowWidget)

### 테스트 항목 및 결과

#### ✅ 1. 테이블 옵션 (ColumnVisibilityPanel)

- **상태**: 정상 동작
- **테스트 내용**:
  - 툴바의 "테이블 옵션" 버튼 클릭 시 다이얼로그 정상 표시
  - 7개 컬럼 모두 정상 표시 (장치 코드, 시리얼넘버, manufacturer, 모델명, 품번, 차량 타입, 차량 번호)
  - 각 컬럼마다 체크박스, 드래그 핸들, 미리보기 아이콘, 너비 설정 표시
  - "초기화" 버튼 표시
- **스크린샷**: `column-visibility-panel.png`

#### ✅ 2. 필터 설정 (FilterPanel)

- **상태**: 정상 동작
- **테스트 내용**:
  - 툴바의 "필터 설정" 버튼 클릭 시 다이얼로그 정상 표시
  - "총 0개의 검색 필터가 표시됩니다" 메시지 표시
  - "필터 추가" 버튼 정상 표시
  - "초기화" 버튼 표시
- **스크린샷**: `filter-panel-empty.png`

#### ✅ 3. 그룹 설정 (GroupingPanel)

- **상태**: 정상 동작
- **테스트 내용**:
  - 툴바의 "그룹 설정" 버튼 클릭 시 다이얼로그 정상 표시
  - "0개 컬럼으로 그룹화" 메시지 표시
  - 7개 컬럼 모두 체크박스로 표시
  - 각 컬럼의 라벨 및 필드명 정상 표시
  - "초기화" 버튼 표시
- **스크린샷**: `grouping-panel.png`

#### ✅ 4. Context 통합

- **상태**: 정상 동작
- **테스트 내용**:
  - `TableOptionsProvider`가 `/screens/[screenId]/page.tsx`에 정상 통합
  - `FlowWidget` 컴포넌트가 `TableOptionsContext`에 정상 등록
  - 에러 없이 페이지 로드 및 렌더링 완료

### 검증 완료 사항

1. ✅ 타입 정의 및 Context 구현 완료
2. ✅ 패널 컴포넌트 3개 구현 완료 (ColumnVisibility, Filter, Grouping)
3. ✅ TableOptionsToolbar 메인 컴포넌트 구현 완료
4. ✅ TableOptionsProvider 통합 완료
5. ✅ FlowWidget에 Context 연동 완료
6. ✅ 브라우저 테스트 완료 (모든 기능 정상 동작)

### 향후 개선 사항

1. **백엔드 API 통합**: 현재는 프론트엔드 상태 관리만 구현됨. 백엔드 API에 필터/그룹/컬럼 설정 파라미터 전달 필요
2. **필터 적용 로직**: 필터 추가 후 실제 데이터 필터링 구현
3. **그룹화 적용 로직**: 그룹 선택 후 실제 데이터 그룹화 구현
4. **컬럼 순서/너비 적용**: 드래그앤드롭으로 변경한 순서 및 너비를 실제 테이블에 반영

---

## 15. 변경 이력

| 날짜       | 버전 | 변경 내용                                    | 작성자 |
| ---------- | ---- | -------------------------------------------- | ------ |
| 2025-01-13 | 1.0  | 초안 작성                                    | AI     |
| 2025-01-13 | 1.1  | 프론트엔드 구현 완료 및 브라우저 테스트 완료 | AI     |

---

## 16. 구현 완료 요약

### 생성된 파일

1. `frontend/types/table-options.ts` - 타입 정의
2. `frontend/contexts/TableOptionsContext.tsx` - Context 구현
3. `frontend/components/screen/table-options/ColumnVisibilityPanel.tsx` - 컬럼 가시성 패널
4. `frontend/components/screen/table-options/FilterPanel.tsx` - 필터 패널
5. `frontend/components/screen/table-options/GroupingPanel.tsx` - 그룹핑 패널
6. `frontend/components/screen/table-options/TableOptionsToolbar.tsx` - 메인 툴바

### 수정된 파일

1. `frontend/app/(main)/screens/[screenId]/page.tsx` - Provider 통합 (화면 뷰어)
2. `frontend/components/screen/ScreenDesigner.tsx` - Provider 통합 (화면 디자이너)
3. `frontend/components/screen/InteractiveDataTable.tsx` - Context 연동
4. `frontend/components/screen/widgets/FlowWidget.tsx` - Context 연동
5. `frontend/lib/registry/components/table-list/TableListComponent.tsx` - Context 연동
6. `frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx` - Context 연동

### 구현 완료 기능

- ✅ Context API 기반 테이블 자동 감지 시스템
- ✅ 컬럼 표시/숨기기, 순서 변경, 너비 설정
- ✅ 필터 추가 UI (백엔드 연동 대기)
- ✅ 그룹화 컬럼 선택 UI (백엔드 연동 대기)
- ✅ 여러 테이블 컴포넌트 지원 (FlowWidget, TableList, SplitPanel, InteractiveDataTable)
- ✅ shadcn/ui 기반 일관된 디자인 시스템
- ✅ 브라우저 테스트 완료

---

이 계획서를 검토하신 후 수정사항이나 추가 요구사항을 알려주세요!
