),
[
+ parentId,
+ rootProcessOptions,
processName,
processType,
processCategory,
diff --git a/src/components/process-management/ProcessListClient.tsx b/src/components/process-management/ProcessListClient.tsx
index 1778aa63..65fc9b60 100644
--- a/src/components/process-management/ProcessListClient.tsx
+++ b/src/components/process-management/ProcessListClient.tsx
@@ -12,7 +12,7 @@
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
import { useRouter } from 'next/navigation';
import { useDateRange } from '@/hooks';
-import { Wrench, Plus, GripVertical } from 'lucide-react';
+import { Wrench, Plus, GripVertical, ChevronRight, ChevronDown } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { TableCell, TableRow, TableHead } from '@/components/ui/table';
import { Checkbox } from '@/components/ui/checkbox';
@@ -51,6 +51,55 @@ export default function ProcessListClient({ initialData = [], initialStats }: Pr
// 검색어 상태
const [searchQuery, setSearchQuery] = useState('');
+ // 트리 접기/펼치기 상태 (부모 공정 ID → expanded 여부)
+ const [expandedParents, setExpandedParents] = useState
>(new Set());
+
+ const toggleExpand = useCallback((parentId: string) => {
+ setExpandedParents(prev => {
+ const next = new Set(prev);
+ if (next.has(parentId)) next.delete(parentId);
+ else next.add(parentId);
+ return next;
+ });
+ }, []);
+
+ // 트리 구조 정렬: 부모 바로 뒤에 자식을 배치
+ const treeOrderedProcesses = useMemo(() => {
+ const childrenMap = new Map();
+ const roots: Process[] = [];
+
+ for (const p of allProcesses) {
+ if (p.parentId) {
+ const siblings = childrenMap.get(p.parentId) || [];
+ siblings.push(p);
+ childrenMap.set(p.parentId, siblings);
+ } else {
+ roots.push(p);
+ }
+ }
+
+ const result: Process[] = [];
+ for (const root of roots) {
+ result.push(root);
+ const children = childrenMap.get(root.id) || [];
+ if (children.length > 0 && expandedParents.has(root.id)) {
+ result.push(...children);
+ }
+ }
+ return result;
+ }, [allProcesses, expandedParents]);
+
+ // 초기 로드 시 자식이 있는 부모는 자동 펼침
+ useEffect(() => {
+ const parentsWithChildren = new Set();
+ for (const p of allProcesses) {
+ if (p.parentId) parentsWithChildren.add(p.parentId);
+ }
+ if (parentsWithChildren.size > 0) {
+ setExpandedParents(parentsWithChildren);
+ }
+ }, [allProcesses]);
+
// 드래그&드롭 순서 변경 상태
const [isOrderChanged, setIsOrderChanged] = useState(false);
const dragProcessIdRef = useRef(null);
@@ -425,23 +474,26 @@ export default function ProcessListClient({ initialData = [], initialStats }: Pr
const itemCount = process.classificationRules
.filter(r => r.registrationType === 'individual')
.reduce((sum, r) => sum + (r.items?.length || 0), 0);
+ const isChild = !!process.parentId;
+ const hasChildren = allProcesses.some(p => p.parentId === process.id);
+ const isExpanded = expandedParents.has(process.id);
return (
handleDragStart(e, process.id)}
- onDragOver={handleDragOver}
- onDragLeave={handleDragLeave}
- onDragEnd={handleDragEnd}
- onDrop={(e) => handleProcessDrop(e, process.id)}
- className={`cursor-pointer hover:bg-muted/50 ${handlers.isSelected ? 'bg-blue-50' : ''}`}
+ draggable={!isChild}
+ onDragStart={!isChild ? (e) => handleDragStart(e, process.id) : undefined}
+ onDragOver={!isChild ? handleDragOver : undefined}
+ onDragLeave={!isChild ? handleDragLeave : undefined}
+ onDragEnd={!isChild ? handleDragEnd : undefined}
+ onDrop={!isChild ? (e) => handleProcessDrop(e, process.id) : undefined}
+ className={`cursor-pointer hover:bg-muted/50 ${handlers.isSelected ? 'bg-blue-50' : ''} ${isChild ? 'bg-muted/20' : ''}`}
onClick={() => handleRowClick(process)}
>
e.stopPropagation()}
>
-
+ {!isChild && }
e.stopPropagation()}>
{globalIndex}
- {process.processCode}
- {process.processName}
+
+ {isChild && └}
+ {process.processCode}
+
+
+
+ {hasChildren && (
+
+ )}
+ {process.processName}
+ {hasChildren && (
+
+ {allProcesses.filter(p => p.parentId === process.id).length}
+
+ )}
+
+
{process.department}
{itemCount > 0 ? itemCount : '-'}
@@ -487,50 +562,53 @@ export default function ProcessListClient({ initialData = [], initialStats }: Pr
const itemCount = process.classificationRules
.filter(r => r.registrationType === 'individual')
.reduce((sum, r) => sum + (r.items?.length || 0), 0);
+ const isChild = !!process.parentId;
return (
- handleRowClick(process)}
- headerBadges={
- <>
- {globalIndex}
- {process.processCode}
- >
- }
- title={process.processName}
- statusBadge={
- {
- e.stopPropagation();
- handleToggleStatus(process.id);
- }}
- >
- {process.status === '사용중' ? '사용' : '미사용'}
-
- }
- infoGrid={
-
-
- 0 ? `${itemCount}개` : '-'} />
-
-
-
- }
- />
+
+
handleRowClick(process)}
+ headerBadges={
+ <>
+ {globalIndex}
+ {process.processCode}
+ {isChild && 하위}
+ >
+ }
+ title={process.processName}
+ statusBadge={
+ {
+ e.stopPropagation();
+ handleToggleStatus(process.id);
+ }}
+ >
+ {process.status === '사용중' ? '사용' : '미사용'}
+
+ }
+ infoGrid={
+
+
+ 0 ? `${itemCount}개` : '-'} />
+
+
+
+ }
+ />
+
);
},
}),
- [handleCreate, handleRowClick, handleToggleStatus, handleBulkDelete, startDate, endDate, searchQuery, isOrderChanged, handleSaveOrder, handleDragStart, handleDragOver, handleDragLeave, handleDragEnd, handleProcessDrop]
+ [handleCreate, handleRowClick, handleToggleStatus, handleBulkDelete, startDate, endDate, searchQuery, isOrderChanged, handleSaveOrder, handleDragStart, handleDragOver, handleDragLeave, handleDragEnd, handleProcessDrop, allProcesses, expandedParents, toggleExpand]
);
return (
<>
-
+
{/* 삭제 확인 다이얼로그 */}
);
return {
+ parent_id: data.parentId || null,
process_name: data.processName,
process_type: data.processType,
department: data.department || null,
@@ -426,6 +427,23 @@ export async function getProcessOptions(): Promise<{
};
}
+/**
+ * 루트 공정 목록 조회 (부모 공정 선택 드롭다운용)
+ * parent_id가 null인 공정만 반환
+ */
+export async function getRootProcessOptions(): Promise<{
+ success: boolean;
+ data?: Array<{ id: string; processCode: string; processName: string }>;
+ error?: string;
+}> {
+ const result = await getProcessList({ size: 1000 });
+ if (!result.success || !result.data) return { success: false, error: result.error };
+ const roots = result.data.items
+ .filter(p => !p.parentId)
+ .map(p => ({ id: p.id, processCode: p.processCode, processName: p.processName }));
+ return { success: true, data: roots };
+}
+
/**
* 공정 통계
*/