feat(WEB): 공정관리 드래그 순서변경, 수주서/출고증 리디자인, 체크리스트 관리 추가
- 공정관리: 드래그&드롭 순서 변경 기능 추가 (reorderProcesses API) - 수주서(SalesOrderDocument): 기획서 D1.8 기준 리디자인, 출고증과 동일 자재 섹션 구조 - 출고증(ShipmentOrderDocument): 레이아웃 개선 - 체크리스트 관리 페이지 신규 추가 (master-data/checklist-management) - QMS 품질감사: 타입 및 목데이터 수정 - menuRefresh 유틸 정리 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,10 +9,11 @@
|
||||
* - 삭제 다이얼로그
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Wrench, Plus } from 'lucide-react';
|
||||
import { TableCell, TableRow } from '@/components/ui/table';
|
||||
import { Wrench, Plus, GripVertical } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TableCell, TableRow, TableHead } from '@/components/ui/table';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
@@ -26,7 +27,7 @@ import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard';
|
||||
import { toast } from 'sonner';
|
||||
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
import type { Process } from '@/types/process';
|
||||
import { getProcessList, deleteProcess, deleteProcesses, toggleProcessActive, getProcessStats } from './actions';
|
||||
import { getProcessList, deleteProcess, deleteProcesses, toggleProcessActive, getProcessStats, reorderProcesses } from './actions';
|
||||
|
||||
interface ProcessListClientProps {
|
||||
initialData?: Process[];
|
||||
@@ -50,6 +51,13 @@ export default function ProcessListClient({ initialData = [], initialStats }: Pr
|
||||
// 검색어 상태
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// 드래그&드롭 순서 변경 상태
|
||||
const [isOrderChanged, setIsOrderChanged] = useState(false);
|
||||
const dragProcessIdRef = useRef<string | null>(null);
|
||||
const dragNodeRef = useRef<HTMLTableRowElement | null>(null);
|
||||
const allProcessesRef = useRef(allProcesses);
|
||||
allProcessesRef.current = allProcesses;
|
||||
|
||||
// ===== 데이터 로드 =====
|
||||
const loadData = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
@@ -177,6 +185,78 @@ export default function ProcessListClient({ initialData = [], initialStats }: Pr
|
||||
}
|
||||
}, [allProcesses]);
|
||||
|
||||
// ===== 드래그&드롭 순서 변경 =====
|
||||
const handleDragStart = useCallback((e: React.DragEvent<HTMLTableRowElement>, processId: string) => {
|
||||
dragProcessIdRef.current = processId;
|
||||
dragNodeRef.current = e.currentTarget;
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
requestAnimationFrame(() => {
|
||||
if (dragNodeRef.current) {
|
||||
dragNodeRef.current.style.opacity = '0.4';
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleDragOver = useCallback((e: React.DragEvent<HTMLTableRowElement>) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
e.currentTarget.classList.add('border-t-2', 'border-t-primary');
|
||||
}, []);
|
||||
|
||||
const handleDragLeave = useCallback((e: React.DragEvent<HTMLTableRowElement>) => {
|
||||
const related = e.relatedTarget as Node;
|
||||
if (!e.currentTarget.contains(related)) {
|
||||
e.currentTarget.classList.remove('border-t-2', 'border-t-primary');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDragEnd = useCallback(() => {
|
||||
if (dragNodeRef.current) {
|
||||
dragNodeRef.current.style.opacity = '1';
|
||||
}
|
||||
dragProcessIdRef.current = null;
|
||||
dragNodeRef.current = null;
|
||||
}, []);
|
||||
|
||||
const handleProcessDrop = useCallback((e: React.DragEvent<HTMLTableRowElement>, dropProcessId: string) => {
|
||||
e.preventDefault();
|
||||
e.currentTarget.classList.remove('border-t-2', 'border-t-primary');
|
||||
const dragId = dragProcessIdRef.current;
|
||||
if (!dragId || dragId === dropProcessId) {
|
||||
handleDragEnd();
|
||||
return;
|
||||
}
|
||||
setAllProcesses((prev) => {
|
||||
const updated = [...prev];
|
||||
const dragIdx = updated.findIndex(p => p.id === dragId);
|
||||
const dropIdx = updated.findIndex(p => p.id === dropProcessId);
|
||||
if (dragIdx === -1 || dropIdx === -1) return prev;
|
||||
const [moved] = updated.splice(dragIdx, 1);
|
||||
updated.splice(dropIdx, 0, moved);
|
||||
return updated;
|
||||
});
|
||||
setIsOrderChanged(true);
|
||||
handleDragEnd();
|
||||
}, [handleDragEnd]);
|
||||
|
||||
const handleSaveOrder = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const orderData = allProcessesRef.current.map((p, idx) => ({ id: p.id, order: idx + 1 }));
|
||||
const result = await reorderProcesses(orderData);
|
||||
if (result.success) {
|
||||
toast.success('순서가 저장되었습니다.');
|
||||
setIsOrderChanged(false);
|
||||
} else {
|
||||
toast.error(result.error || '순서 저장에 실패했습니다.');
|
||||
}
|
||||
} catch {
|
||||
toast.error('순서 저장 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// ===== UniversalListPage Config =====
|
||||
const config: UniversalListConfig<Process> = useMemo(
|
||||
() => ({
|
||||
@@ -230,8 +310,11 @@ export default function ProcessListClient({ initialData = [], initialStats }: Pr
|
||||
},
|
||||
},
|
||||
|
||||
// 테이블 컬럼
|
||||
// 테이블 컬럼 (showCheckbox: false + renderCustomTableHeader로 수동 관리)
|
||||
showCheckbox: false,
|
||||
columns: [
|
||||
{ key: 'drag', label: '', className: 'w-[40px]' },
|
||||
{ key: 'checkbox', label: '', className: 'w-[50px]' },
|
||||
{ key: 'no', label: '번호', className: 'w-[60px] text-center' },
|
||||
{ key: 'processCode', label: '공정번호', className: 'w-[120px]' },
|
||||
{ key: 'processName', label: '공정명', className: 'min-w-[200px]' },
|
||||
@@ -240,6 +323,25 @@ export default function ProcessListClient({ initialData = [], initialStats }: Pr
|
||||
{ key: 'status', label: '상태', className: 'w-[80px] text-center' },
|
||||
],
|
||||
|
||||
// 커스텀 테이블 헤더 (드래그 → 전체선택 체크박스 → 번호 → 데이터 순)
|
||||
renderCustomTableHeader: ({ displayData, selectedItems, onToggleSelectAll }) => (
|
||||
<>
|
||||
<TableHead className="w-[40px]" />
|
||||
<TableHead className="w-[50px] text-center">
|
||||
<Checkbox
|
||||
checked={displayData.length > 0 && selectedItems.size === displayData.length}
|
||||
onCheckedChange={onToggleSelectAll}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="w-[60px] text-center">번호</TableHead>
|
||||
<TableHead className="w-[120px]">공정번호</TableHead>
|
||||
<TableHead className="min-w-[200px]">공정명</TableHead>
|
||||
<TableHead className="w-[120px]">담당부서</TableHead>
|
||||
<TableHead className="w-[80px] text-center">품목</TableHead>
|
||||
<TableHead className="w-[80px] text-center">상태</TableHead>
|
||||
</>
|
||||
),
|
||||
|
||||
// 클라이언트 사이드 필터링
|
||||
clientSideFiltering: true,
|
||||
itemsPerPage: 20,
|
||||
@@ -292,6 +394,13 @@ export default function ProcessListClient({ initialData = [], initialStats }: Pr
|
||||
);
|
||||
},
|
||||
|
||||
// 순서 변경 저장 버튼
|
||||
headerActions: () => (
|
||||
<Button variant="outline" onClick={handleSaveOrder} disabled={!isOrderChanged}>
|
||||
순서 변경 저장
|
||||
</Button>
|
||||
),
|
||||
|
||||
// 등록 버튼 (공통 컴포넌트에서 오른쪽에 렌더링)
|
||||
createButton: {
|
||||
label: '공정 등록',
|
||||
@@ -315,9 +424,21 @@ export default function ProcessListClient({ initialData = [], initialStats }: Pr
|
||||
return (
|
||||
<TableRow
|
||||
key={process.id}
|
||||
draggable
|
||||
onDragStart={(e) => 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' : ''}`}
|
||||
onClick={() => handleRowClick(process)}
|
||||
>
|
||||
<TableCell
|
||||
className="text-center cursor-grab active:cursor-grabbing"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<GripVertical className="h-4 w-4 text-muted-foreground mx-auto" />
|
||||
</TableCell>
|
||||
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
|
||||
<Checkbox
|
||||
checked={handlers.isSelected}
|
||||
@@ -388,7 +509,7 @@ export default function ProcessListClient({ initialData = [], initialStats }: Pr
|
||||
);
|
||||
},
|
||||
}),
|
||||
[handleCreate, handleRowClick, handleToggleStatus, handleBulkDelete, startDate, endDate, searchQuery]
|
||||
[handleCreate, handleRowClick, handleToggleStatus, handleBulkDelete, startDate, endDate, searchQuery, isOrderChanged, handleSaveOrder, handleDragStart, handleDragOver, handleDragLeave, handleDragEnd, handleProcessDrop]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user