Files
vexplor/테이블_그룹핑_기능_구현_계획서.md
kjs b607ef0aa0 feat: TableListComponent에 그룹핑 기능 구현
- 다중 컬럼 선택으로 계층적 그룹화 지원
- 그룹 설정 다이얼로그 추가
- 그룹별 데이터 펼치기/접기 기능
- 그룹 헤더에 항목 개수 표시
- localStorage에 그룹 설정 저장/복원
- 그룹 해제 버튼 추가
- 그룹 표시 배지 UI

주요 기능:
- 사용자가 원하는 컬럼(들)을 선택하여 그룹화
- 그룹 키: '통화:KRW > 단위:EA' 형식으로 표시
- 그룹 헤더 클릭으로 펼치기/접기
- 그룹 없을 때는 기존 렌더링 방식 유지
2025-11-03 14:08:26 +09:00

13 KiB
Raw Permalink Blame History

테이블 그룹핑 기능 구현 계획서

📋 개요

테이블 리스트 컴포넌트와 플로우 위젯에 그룹핑 기능을 추가하여, 사용자가 선택한 컬럼(들)을 기준으로 데이터를 그룹화하여 표시합니다.

🎯 핵심 요구사항

1. 기능 요구사항

  • 그룹핑할 컬럼을 다중 선택 가능
  • 선택한 컬럼 순서대로 계층적 그룹화
  • 그룹 헤더에 그룹 정보와 데이터 개수 표시
  • 그룹 펼치기/접기 기능
  • localStorage에 그룹 설정 저장/복원
  • 그룹 해제 기능

2. 적용 대상

  • TableListComponent (frontend/lib/registry/components/table-list/TableListComponent.tsx)
  • FlowWidget (frontend/components/screen/widgets/FlowWidget.tsx)

🎨 UI 디자인

그룹 설정 다이얼로그

┌─────────────────────────────────────┐
 📊 그룹 설정                         
 데이터를 그룹화할 컬럼을 선택하세요  
├─────────────────────────────────────┤
                                     
 [x] 통화                             
 [ ] 단위                             
 [ ] 품목코드                         
 [ ] 품목명                           
 [ ] 규격                             
                                     
 💡 선택된 그룹: 통화                 
                                     
├─────────────────────────────────────┤
           [취소]  [적용]             
└─────────────────────────────────────┘

그룹화된 테이블 표시

┌─────────────────────────────────────────────────────┐
 📦 판매품목 목록         3    [🎨 그룹: 통화 ×] 
├─────────────────────────────────────────────────────┤
                                                     
  통화: KRW > 단위: EA  (2)                       
   ┌─────────────────────────────────────────────┐   
    품목코드   품목명       규격       단위    
   ├─────────────────────────────────────────────┤   
    SALE-001  볼트 M8x20   M8x20     EA     
    SALE-004  스프링 와셔   M10       EA     
   └─────────────────────────────────────────────┘   
                                                     
  통화: USD > 단위: EA  (1)                       
   ┌─────────────────────────────────────────────┐   
    품목코드   품목명       규격       단위    
   ├─────────────────────────────────────────────┤   
    SALE-002  너트 M8      M8        EA     
   └─────────────────────────────────────────────┘   
                                                     
└─────────────────────────────────────────────────────┘

🔧 기술 구현

1. 상태 관리

// 그룹 설정 관련 상태
const [groupByColumns, setGroupByColumns] = useState<string[]>([]); // 그룹화할 컬럼 목록
const [isGroupSettingOpen, setIsGroupSettingOpen] = useState(false); // 그룹 설정 다이얼로그
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(new Set()); // 접힌 그룹

2. 데이터 그룹화 로직

interface GroupedData {
  groupKey: string; // "통화:KRW > 단위:EA"
  groupValues: Record<string, any>; // { 통화: "KRW", 단위: "EA" }
  items: any[]; // 그룹에 속한 데이터
  count: number; // 항목 개수
}

const groupDataByColumns = (
  data: any[], 
  groupColumns: string[]
): GroupedData[] => {
  if (groupColumns.length === 0) return [];
  
  const grouped = new Map<string, any[]>();
  
  data.forEach(item => {
    // 그룹 키 생성: "통화:KRW > 단위:EA"
    const keyParts = groupColumns.map(col => `${col}:${item[col] || '-'}`);
    const groupKey = keyParts.join(' > ');
    
    if (!grouped.has(groupKey)) {
      grouped.set(groupKey, []);
    }
    grouped.get(groupKey)!.push(item);
  });
  
  return Array.from(grouped.entries()).map(([groupKey, items]) => {
    const groupValues: Record<string, any> = {};
    groupColumns.forEach(col => {
      groupValues[col] = items[0]?.[col];
    });
    
    return {
      groupKey,
      groupValues,
      items,
      count: items.length,
    };
  });
};

3. localStorage 저장/로드

// 저장 키
const groupSettingKey = useMemo(() => {
  if (!tableConfig.selectedTable) return null;
  return `table-list-group-${tableConfig.selectedTable}`;
}, [tableConfig.selectedTable]);

// 그룹 설정 저장
const saveGroupSettings = useCallback(() => {
  if (!groupSettingKey) return;
  
  try {
    localStorage.setItem(groupSettingKey, JSON.stringify(groupByColumns));
    setIsGroupSettingOpen(false);
    toast.success("그룹 설정이 저장되었습니다");
  } catch (error) {
    console.error("그룹 설정 저장 실패:", error);
    toast.error("설정 저장에 실패했습니다");
  }
}, [groupSettingKey, groupByColumns]);

// 그룹 설정 로드
useEffect(() => {
  if (!groupSettingKey || visibleColumns.length === 0) return;
  
  try {
    const saved = localStorage.getItem(groupSettingKey);
    if (saved) {
      const savedGroups = JSON.parse(saved);
      setGroupByColumns(savedGroups);
    }
  } catch (error) {
    console.error("그룹 설정 불러오기 실패:", error);
  }
}, [groupSettingKey, visibleColumns]);

4. 그룹 헤더 렌더링

const renderGroupHeader = (group: GroupedData) => {
  const isCollapsed = collapsedGroups.has(group.groupKey);
  
  return (
    <div 
      className="bg-muted/50 flex items-center gap-3 border-b p-3 cursor-pointer hover:bg-muted"
      onClick={() => toggleGroupCollapse(group.groupKey)}
    >
      {/* 펼치기/접기 아이콘 */}
      {isCollapsed ? (
        <ChevronRight className="h-4 w-4" />
      ) : (
        <ChevronDown className="h-4 w-4" />
      )}
      
      {/* 그룹 정보 */}
      <span className="font-medium text-sm">
        {groupByColumns.map((col, idx) => (
          <span key={col}>
            {idx > 0 && <span className="text-muted-foreground"> &gt; </span>}
            <span className="text-muted-foreground">{columnLabels[col] || col}:</span>
            {" "}
            <span className="text-foreground">{group.groupValues[col]}</span>
          </span>
        ))}
      </span>
      
      {/* 항목 개수 */}
      <span className="text-muted-foreground text-xs ml-auto">
        ({group.count})
      </span>
    </div>
  );
};

5. 그룹 설정 다이얼로그

<Dialog open={isGroupSettingOpen} onOpenChange={setIsGroupSettingOpen}>
  <DialogContent className="max-w-[95vw] sm:max-w-[500px]">
    <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="max-h-[50vh] space-y-2 overflow-y-auto rounded border p-2">
        {visibleColumns
          .filter((col) => col.columnName !== "__checkbox__")
          .map((col) => (
            <div key={col.columnName} className="hover:bg-muted/50 flex items-center gap-3 rounded p-2">
              <Checkbox
                id={`group-${col.columnName}`}
                checked={groupByColumns.includes(col.columnName)}
                onCheckedChange={() => toggleGroupColumn(col.columnName)}
              />
              <Label
                htmlFor={`group-${col.columnName}`}
                className="flex-1 cursor-pointer text-xs font-normal sm:text-sm"
              >
                {columnLabels[col.columnName] || col.displayName || col.columnName}
              </Label>
            </div>
          ))}
      </div>

      {/* 선택된 그룹 안내 */}
      <div className="text-muted-foreground bg-muted/30 rounded p-3 text-xs">
        {groupByColumns.length === 0 ? (
          <span>그룹화할 컬럼을 선택하세요</span>
        ) : (
          <span>
            선택된 그룹: <span className="text-primary font-semibold">
              {groupByColumns.map((col) => columnLabels[col] || col).join(" → ")}
            </span>
          </span>
        )}
      </div>
    </div>

    <DialogFooter className="gap-2 sm:gap-0">
      <Button
        variant="outline"
        onClick={() => setIsGroupSettingOpen(false)}
        className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
      >
        취소
      </Button>
      <Button onClick={saveGroupSettings} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
        적용
      </Button>
    </DialogFooter>
  </DialogContent>
</Dialog>

6. 그룹 해제 버튼

{/* 헤더 영역 */}
<div className="flex items-center justify-between">
  <h2>{tableLabel}</h2>
  <div className="flex items-center gap-2">
    {/* 그룹 표시 배지 */}
    {groupByColumns.length > 0 && (
      <div className="bg-primary/10 text-primary flex items-center gap-2 rounded px-3 py-1 text-xs">
        <span>그룹: {groupByColumns.map(col => columnLabels[col] || col).join(", ")}</span>
        <button
          onClick={() => {
            setGroupByColumns([]);
            localStorage.removeItem(groupSettingKey || "");
            toast.success("그룹이 해제되었습니다");
          }}
          className="hover:bg-primary/20 rounded p-0.5"
        >
          <X className="h-3 w-3" />
        </button>
      </div>
    )}
    
    {/* 그룹 설정 버튼 */}
    <Button
      variant="outline"
      size="sm"
      onClick={() => setIsGroupSettingOpen(true)}
    >
      <Layers className="mr-2 h-4 w-4" />
      그룹 설정
    </Button>
  </div>
</div>

📝 구현 순서

Phase 1: TableListComponent 구현

  1. 상태 관리 추가 (groupByColumns, isGroupSettingOpen, collapsedGroups)
  2. 그룹화 로직 구현 (groupDataByColumns 함수)
  3. localStorage 저장/로드 로직
  4. 그룹 설정 다이얼로그 UI
  5. 그룹 헤더 렌더링
  6. 그룹별 데이터 렌더링
  7. 그룹 해제 기능

Phase 2: FlowWidget 구현

  1. TableListComponent와 동일한 로직 적용
  2. 스텝 데이터에 그룹화 적용
  3. UI 통일성 유지

Phase 3: 테스트 및 최적화

  1. 다중 그룹 계층 테스트
  2. 대량 데이터 성능 테스트
  3. localStorage 저장/복원 테스트
  4. 그룹 펼치기/접기 테스트

🎯 예상 효과

사용자 경험 개선

  • 데이터를 논리적으로 그룹화하여 가독성 향상
  • 대량 데이터를 효율적으로 탐색 가능
  • 사용자 정의 뷰 제공

데이터 분석 지원

  • 카테고리별 데이터 분석 용이
  • 통계 정보 제공 (그룹별 개수)
  • 계층적 데이터 구조 시각화

⚠️ 주의사항

성능 고려사항

  • 그룹화는 클라이언트 측에서 수행
  • 대량 데이터의 경우 성능 저하 가능
  • 필요시 서버 측 그룹화로 전환 검토

사용성

  • 그룹화 해제가 쉽게 가능해야 함
  • 그룹 설정이 직관적이어야 함
  • 모바일에서도 사용 가능한 UI

📊 구현 상태

  • Phase 1: TableListComponent 구현
    • 상태 관리 추가
    • 그룹화 로직 구현
    • localStorage 연동
    • UI 구현
  • Phase 2: FlowWidget 구현
  • Phase 3: 테스트 및 최적화

작성일: 2025-11-03 버전: 1.0 상태: 구현 예정