ERP-node/테이블_그룹핑_기능_구현_계획서.md

366 lines
13 KiB
Markdown
Raw Permalink Normal View History

# 테이블 그룹핑 기능 구현 계획서
## 📋 개요
테이블 리스트 컴포넌트와 플로우 위젯에 그룹핑 기능을 추가하여, 사용자가 선택한 컬럼(들)을 기준으로 데이터를 그룹화하여 표시합니다.
## 🎯 핵심 요구사항
### 1. 기능 요구사항
- ✅ 그룹핑할 컬럼을 다중 선택 가능
- ✅ 선택한 컬럼 순서대로 계층적 그룹화
- ✅ 그룹 헤더에 그룹 정보와 데이터 개수 표시
- ✅ 그룹 펼치기/접기 기능
- ✅ localStorage에 그룹 설정 저장/복원
- ✅ 그룹 해제 기능
### 2. 적용 대상
- TableListComponent (`frontend/lib/registry/components/table-list/TableListComponent.tsx`)
- FlowWidget (`frontend/components/screen/widgets/FlowWidget.tsx`)
## 🎨 UI 디자인
### 그룹 설정 다이얼로그
```tsx
┌─────────────────────────────────────┐
│ 📊 그룹 설정 │
│ 데이터를 그룹화할 컬럼을 선택하세요 │
├─────────────────────────────────────┤
│ │
│ [x] 통화 │
│ [ ] 단위 │
│ [ ] 품목코드 │
│ [ ] 품목명 │
│ [ ] 규격 │
│ │
│ 💡 선택된 그룹: 통화 │
│ │
├─────────────────────────────────────┤
│ [취소] [적용] │
└─────────────────────────────────────┘
```
### 그룹화된 테이블 표시
```tsx
┌─────────────────────────────────────────────────────┐
│ 📦 판매품목 목록 총 3개 [🎨 그룹: 통화 ×] │
├─────────────────────────────────────────────────────┤
│ │
│ ▼ 통화: KRW > 단위: EA (2건) │
│ ┌─────────────────────────────────────────────┐ │
│ │ 품목코드 │ 품목명 │ 규격 │ 단위 │ │
│ ├─────────────────────────────────────────────┤ │
│ │ SALE-001 │ 볼트 M8x20 │ M8x20 │ EA │ │
│ │ SALE-004 │ 스프링 와셔 │ M10 │ EA │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ▼ 통화: USD > 단위: EA (1건) │
│ ┌─────────────────────────────────────────────┐ │
│ │ 품목코드 │ 품목명 │ 규격 │ 단위 │ │
│ ├─────────────────────────────────────────────┤ │
│ │ SALE-002 │ 너트 M8 │ M8 │ EA │ │
│ └─────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────┘
```
## 🔧 기술 구현
### 1. 상태 관리
```typescript
// 그룹 설정 관련 상태
const [groupByColumns, setGroupByColumns] = useState<string[]>([]); // 그룹화할 컬럼 목록
const [isGroupSettingOpen, setIsGroupSettingOpen] = useState(false); // 그룹 설정 다이얼로그
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(new Set()); // 접힌 그룹
```
### 2. 데이터 그룹화 로직
```typescript
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 저장/로드
```typescript
// 저장 키
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. 그룹 헤더 렌더링
```tsx
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. 그룹 설정 다이얼로그
```tsx
<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. 그룹 해제 버튼
```tsx
{/* 헤더 영역 */}
<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
**상태**: 구현 예정