feat(WEB): 공정관리 품목 제거 기능 및 리팩토링

- ProcessDetail: 개별 품목 제거(removeProcessItem) 기능 추가
- ProcessDetail: onProcessUpdate 콜백으로 부모 컴포넌트 동기화
- ProcessDetail: 삭제 다이얼로그 제거, 품목 목록 flatMap 추출 방식 개선
- ProcessForm: 규칙 모달 관련 코드 추가
- RuleModal: UI 개선
- actions.ts: removeProcessItem API 함수 추가
This commit is contained in:
2026-02-09 21:31:00 +09:00
parent 2ad27d738f
commit 6d8116713f
5 changed files with 154 additions and 55 deletions

View File

@@ -20,15 +20,15 @@ import { PageHeader } from '@/components/organisms/PageHeader';
import { useMenuStore } from '@/store/menuStore';
import { usePermission } from '@/hooks/usePermission';
import { toast } from 'sonner';
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
import { getProcessSteps, reorderProcessSteps, deleteProcess } from './actions';
import { getProcessSteps, reorderProcessSteps, removeProcessItem } from './actions';
import type { Process, ProcessStep } from '@/types/process';
interface ProcessDetailProps {
process: Process;
onProcessUpdate?: (process: Process) => void;
}
export function ProcessDetail({ process }: ProcessDetailProps) {
export function ProcessDetail({ process, onProcessUpdate }: ProcessDetailProps) {
const router = useRouter();
const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed);
const { canUpdate } = usePermission();
@@ -37,19 +37,16 @@ export function ProcessDetail({ process }: ProcessDetailProps) {
const [steps, setSteps] = useState<ProcessStep[]>([]);
const [isStepsLoading, setIsStepsLoading] = useState(true);
// 삭제 다이얼로그 상태
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
// 드래그 상태
const [dragIndex, setDragIndex] = useState<number | null>(null);
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
const dragNodeRef = useRef<HTMLTableRowElement | null>(null);
// 품목 개수 계산 (기존 classificationRules에서 individual 품목)
const itemCount = process.classificationRules
// 개별 품목 목록 추출
const individualItems = process.classificationRules
.filter((r) => r.registrationType === 'individual')
.reduce((sum, r) => sum + (r.items?.length || 0), 0);
.flatMap((r) => r.items || []);
const itemCount = individualItems.length;
// 단계 목록 로드
useEffect(() => {
@@ -64,6 +61,21 @@ export function ProcessDetail({ process }: ProcessDetailProps) {
loadSteps();
}, [process.id]);
// 품목 삭제
const handleRemoveItem = async (itemId: string) => {
const remainingIds = individualItems
.filter((item) => item.id !== itemId)
.map((item) => parseInt(item.id, 10));
const result = await removeProcessItem(process.id, remainingIds);
if (result.success && result.data) {
toast.success('품목이 제거되었습니다.');
onProcessUpdate?.(result.data);
} else {
toast.error(result.error || '품목 제거에 실패했습니다.');
}
};
// 네비게이션
const handleEdit = () => {
router.push(`/ko/master-data/process-management/${process.id}?mode=edit`);
@@ -81,24 +93,6 @@ export function ProcessDetail({ process }: ProcessDetailProps) {
router.push(`/ko/master-data/process-management/${process.id}/steps/${stepId}`);
};
const handleDelete = async () => {
setIsDeleting(true);
try {
const result = await deleteProcess(process.id);
if (result.success) {
toast.success('공정이 삭제되었습니다.');
router.push('/ko/master-data/process-management');
} else {
toast.error(result.error || '삭제에 실패했습니다.');
}
} catch {
toast.error('삭제 중 오류가 발생했습니다.');
} finally {
setIsDeleting(false);
setDeleteDialogOpen(false);
}
};
// ===== 드래그&드롭 (HTML5 네이티브) =====
const handleDragStart = useCallback((e: React.DragEvent<HTMLTableRowElement>, index: number) => {
setDragIndex(index);
@@ -225,6 +219,28 @@ export function ProcessDetail({ process }: ProcessDetailProps) {
</div>
</div>
</CardHeader>
{individualItems.length > 0 && (
<CardContent className="pt-4">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2">
{individualItems.map((item) => (
<div
key={item.id}
className="flex items-center gap-2 rounded-md border bg-muted/30 px-3 py-1.5 text-sm"
>
<span className="font-mono text-muted-foreground shrink-0">{item.code}</span>
<span className="truncate flex-1">{item.name}</span>
<button
type="button"
onClick={() => handleRemoveItem(item.id)}
className="shrink-0 ml-auto rounded-sm text-muted-foreground/50 hover:text-destructive transition-colors"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
))}
</div>
</CardContent>
)}
</Card>
{/* 단계 테이블 */}
@@ -367,27 +383,12 @@ export function ProcessDetail({ process }: ProcessDetailProps) {
<span className="hidden md:inline"></span>
</Button>
{canUpdate && (
<div className="flex items-center gap-2">
<Button variant="destructive" onClick={() => setDeleteDialogOpen(true)} size="sm" className="md:size-default">
<Trash2 className="h-4 w-4 md:mr-2" />
<span className="hidden md:inline"></span>
</Button>
<Button onClick={handleEdit} size="sm" className="md:size-default">
<Edit className="h-4 w-4 md:mr-2" />
<span className="hidden md:inline"></span>
</Button>
</div>
<Button onClick={handleEdit} size="sm" className="md:size-default">
<Edit className="h-4 w-4 md:mr-2" />
<span className="hidden md:inline"></span>
</Button>
)}
</div>
{/* 삭제 확인 다이얼로그 */}
<DeleteConfirmDialog
open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
description="이 공정을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
loading={isDeleting}
onConfirm={handleDelete}
/>
</PageLayout>
);
}