From 38ade7562ebe41c6320bf9156b26e4c593b91593 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 26 Feb 2026 20:49:25 +0900 Subject: [PATCH] refactor: Update ProcessWorkStandard component to manage work item selection by phase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removed the "정보조회" option from the default configuration. - Refactored the ProcessWorkStandardComponent to handle work item selection independently for each phase. - Updated the WorkPhaseSection to pass phase-specific parameters for work item selection and detail management. - Enhanced the useProcessWorkStandard hook to maintain separate states for selected work items and details by phase, improving data handling and user experience. --- .../ProcessWorkStandardComponent.tsx | 12 +-- .../components/WorkItemAddModal.tsx | 21 ++++- .../components/WorkPhaseSection.tsx | 23 +++--- .../v2-process-work-standard/config.ts | 1 - .../hooks/useProcessWorkStandard.ts | 75 +++++++++++------- .../SplitPanelLayoutComponent.tsx | 76 ++++++++++++++++++- 6 files changed, 154 insertions(+), 54 deletions(-) diff --git a/frontend/lib/registry/components/v2-process-work-standard/ProcessWorkStandardComponent.tsx b/frontend/lib/registry/components/v2-process-work-standard/ProcessWorkStandardComponent.tsx index c859f108..cf7b306f 100644 --- a/frontend/lib/registry/components/v2-process-work-standard/ProcessWorkStandardComponent.tsx +++ b/frontend/lib/registry/components/v2-process-work-standard/ProcessWorkStandardComponent.tsx @@ -42,8 +42,8 @@ export function ProcessWorkStandardComponent({ items, routings, workItems, - selectedWorkItemDetails, - selectedWorkItemId, + selectedWorkItemIdByPhase, + selectedDetailsByPhase, selection, loading, fetchItems, @@ -105,8 +105,8 @@ export function ProcessWorkStandardComponent({ ); const handleSelectWorkItem = useCallback( - (workItemId: string) => { - fetchWorkItemDetails(workItemId); + (workItemId: string, phaseKey: string) => { + fetchWorkItemDetails(workItemId, phaseKey); }, [fetchWorkItemDetails] ); @@ -191,8 +191,8 @@ export function ProcessWorkStandardComponent({ key={phase.key} phase={phase} items={workItemsByPhase[phase.key] || []} - selectedWorkItemId={selectedWorkItemId} - selectedWorkItemDetails={selectedWorkItemDetails} + selectedWorkItemId={selectedWorkItemIdByPhase[phase.key] || null} + selectedWorkItemDetails={selectedDetailsByPhase[phase.key] || []} detailTypes={config.detailTypes} readonly={config.readonly} onSelectWorkItem={handleSelectWorkItem} diff --git a/frontend/lib/registry/components/v2-process-work-standard/components/WorkItemAddModal.tsx b/frontend/lib/registry/components/v2-process-work-standard/components/WorkItemAddModal.tsx index 6a907f58..e9f97c02 100644 --- a/frontend/lib/registry/components/v2-process-work-standard/components/WorkItemAddModal.tsx +++ b/frontend/lib/registry/components/v2-process-work-standard/components/WorkItemAddModal.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { Plus, Trash2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -61,11 +61,24 @@ export function WorkItemAddModal({ detailTypes, editItem, }: WorkItemAddModalProps) { - const [title, setTitle] = useState(editItem?.title || ""); - const [isRequired, setIsRequired] = useState(editItem?.is_required || "Y"); - const [description, setDescription] = useState(editItem?.description || ""); + const [title, setTitle] = useState(""); + const [isRequired, setIsRequired] = useState("Y"); + const [description, setDescription] = useState(""); const [details, setDetails] = useState([]); + useEffect(() => { + if (open && editItem) { + setTitle(editItem.title || ""); + setIsRequired(editItem.is_required || "Y"); + setDescription(editItem.description || ""); + } else if (open && !editItem) { + setTitle(""); + setIsRequired("Y"); + setDescription(""); + setDetails([]); + } + }, [open, editItem]); + const resetForm = () => { setTitle(""); setIsRequired("Y"); diff --git a/frontend/lib/registry/components/v2-process-work-standard/components/WorkPhaseSection.tsx b/frontend/lib/registry/components/v2-process-work-standard/components/WorkPhaseSection.tsx index c8acb04d..1f2e7e4a 100644 --- a/frontend/lib/registry/components/v2-process-work-standard/components/WorkPhaseSection.tsx +++ b/frontend/lib/registry/components/v2-process-work-standard/components/WorkPhaseSection.tsx @@ -20,13 +20,13 @@ interface WorkPhaseSectionProps { selectedWorkItemDetails: WorkItemDetail[]; detailTypes: DetailTypeDefinition[]; readonly?: boolean; - onSelectWorkItem: (workItemId: string) => void; + onSelectWorkItem: (workItemId: string, phaseKey: string) => void; onAddWorkItem: (phase: string) => void; onEditWorkItem: (item: WorkItem) => void; onDeleteWorkItem: (id: string) => void; - onCreateDetail: (workItemId: string, data: Partial) => void; - onUpdateDetail: (id: string, data: Partial) => void; - onDeleteDetail: (id: string) => void; + onCreateDetail: (workItemId: string, data: Partial, phaseKey: string) => void; + onUpdateDetail: (id: string, data: Partial, phaseKey: string) => void; + onDeleteDetail: (id: string, phaseKey: string) => void; } export function WorkPhaseSection({ @@ -45,9 +45,6 @@ export function WorkPhaseSection({ onDeleteDetail, }: WorkPhaseSectionProps) { const selectedItem = items.find((i) => i.id === selectedWorkItemId) || null; - const isThisSectionSelected = items.some( - (i) => i.id === selectedWorkItemId - ); return (
@@ -94,7 +91,7 @@ export function WorkPhaseSection({ item={item} isSelected={selectedWorkItemId === item.id} readonly={readonly} - onClick={() => onSelectWorkItem(item.id)} + onClick={() => onSelectWorkItem(item.id, phase.key)} onEdit={() => onEditWorkItem(item)} onDelete={() => onDeleteWorkItem(item.id)} /> @@ -106,15 +103,15 @@ export function WorkPhaseSection({ {/* 우측: 상세 리스트 */}
- selectedWorkItemId && onCreateDetail(selectedWorkItemId, data) + selectedWorkItemId && onCreateDetail(selectedWorkItemId, data, phase.key) } - onUpdateDetail={onUpdateDetail} - onDeleteDetail={onDeleteDetail} + onUpdateDetail={(id, data) => onUpdateDetail(id, data, phase.key)} + onDeleteDetail={(id) => onDeleteDetail(id, phase.key)} />
diff --git a/frontend/lib/registry/components/v2-process-work-standard/config.ts b/frontend/lib/registry/components/v2-process-work-standard/config.ts index 8c73ffd7..43ad60cd 100644 --- a/frontend/lib/registry/components/v2-process-work-standard/config.ts +++ b/frontend/lib/registry/components/v2-process-work-standard/config.ts @@ -27,7 +27,6 @@ export const defaultConfig: ProcessWorkStandardConfig = { { value: "inspect", label: "검사항목" }, { value: "procedure", label: "작업절차" }, { value: "input", label: "직접입력" }, - { value: "info", label: "정보조회" }, ], splitRatio: 30, leftPanelTitle: "품목 및 공정 선택", diff --git a/frontend/lib/registry/components/v2-process-work-standard/hooks/useProcessWorkStandard.ts b/frontend/lib/registry/components/v2-process-work-standard/hooks/useProcessWorkStandard.ts index 759eb9c7..e909d291 100644 --- a/frontend/lib/registry/components/v2-process-work-standard/hooks/useProcessWorkStandard.ts +++ b/frontend/lib/registry/components/v2-process-work-standard/hooks/useProcessWorkStandard.ts @@ -17,8 +17,9 @@ export function useProcessWorkStandard(config: ProcessWorkStandardConfig) { const [items, setItems] = useState([]); const [routings, setRoutings] = useState([]); const [workItems, setWorkItems] = useState([]); - const [selectedWorkItemDetails, setSelectedWorkItemDetails] = useState([]); - const [selectedWorkItemId, setSelectedWorkItemId] = useState(null); + // 섹션(phase)별 독립적인 선택 상태 관리 + const [selectedWorkItemIdByPhase, setSelectedWorkItemIdByPhase] = useState>({}); + const [selectedDetailsByPhase, setSelectedDetailsByPhase] = useState>({}); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); @@ -101,15 +102,15 @@ export function useProcessWorkStandard(config: ProcessWorkStandardConfig) { } }, []); - // 작업 항목 상세 조회 - const fetchWorkItemDetails = useCallback(async (workItemId: string) => { + // 작업 항목 상세 조회 (phase별 독립 저장) + const fetchWorkItemDetails = useCallback(async (workItemId: string, phaseKey: string) => { try { const res = await apiClient.get( `${API_BASE}/work-items/${workItemId}/details` ); if (res.data?.success) { - setSelectedWorkItemDetails(res.data.data); - setSelectedWorkItemId(workItemId); + setSelectedDetailsByPhase(prev => ({ ...prev, [phaseKey]: res.data.data })); + setSelectedWorkItemIdByPhase(prev => ({ ...prev, [phaseKey]: workItemId })); } } catch (err) { console.error("상세 조회 실패", err); @@ -129,8 +130,8 @@ export function useProcessWorkStandard(config: ProcessWorkStandardConfig) { processName: null, })); setWorkItems([]); - setSelectedWorkItemDetails([]); - setSelectedWorkItemId(null); + setSelectedDetailsByPhase({}); + setSelectedWorkItemIdByPhase({}); await fetchRoutings(itemCode); }, [fetchRoutings] @@ -151,8 +152,8 @@ export function useProcessWorkStandard(config: ProcessWorkStandardConfig) { routingDetailId, processName, })); - setSelectedWorkItemDetails([]); - setSelectedWorkItemId(null); + setSelectedDetailsByPhase({}); + setSelectedWorkItemIdByPhase({}); await fetchWorkItems(routingDetailId); }, [fetchWorkItems] @@ -233,28 +234,43 @@ export function useProcessWorkStandard(config: ProcessWorkStandardConfig) { const res = await apiClient.delete(`${API_BASE}/work-items/${id}`); if (res.data?.success && selection.routingDetailId) { await fetchWorkItems(selection.routingDetailId); - if (selectedWorkItemId === id) { - setSelectedWorkItemDetails([]); - setSelectedWorkItemId(null); - } + // 삭제된 항목이 선택되어 있던 phase의 선택 상태 초기화 + setSelectedWorkItemIdByPhase(prev => { + const next = { ...prev }; + for (const phaseKey of Object.keys(next)) { + if (next[phaseKey] === id) { + next[phaseKey] = null; + } + } + return next; + }); + setSelectedDetailsByPhase(prev => { + const next = { ...prev }; + for (const phaseKey of Object.keys(next)) { + if (selectedWorkItemIdByPhase[phaseKey] === id) { + next[phaseKey] = []; + } + } + return next; + }); } } catch (err) { console.error("작업 항목 삭제 실패", err); } }, - [selection.routingDetailId, selectedWorkItemId, fetchWorkItems] + [selection.routingDetailId, selectedWorkItemIdByPhase, fetchWorkItems] ); // 상세 추가 const createDetail = useCallback( - async (workItemId: string, data: Partial) => { + async (workItemId: string, data: Partial, phaseKey: string) => { try { const res = await apiClient.post(`${API_BASE}/work-item-details`, { work_item_id: workItemId, ...data, }); if (res.data?.success) { - await fetchWorkItemDetails(workItemId); + await fetchWorkItemDetails(workItemId, phaseKey); if (selection.routingDetailId) { await fetchWorkItems(selection.routingDetailId); } @@ -268,32 +284,36 @@ export function useProcessWorkStandard(config: ProcessWorkStandardConfig) { // 상세 수정 const updateDetail = useCallback( - async (id: string, data: Partial) => { + async (id: string, data: Partial, phaseKey: string) => { try { const res = await apiClient.put( `${API_BASE}/work-item-details/${id}`, data ); - if (res.data?.success && selectedWorkItemId) { - await fetchWorkItemDetails(selectedWorkItemId); + if (res.data?.success) { + const workItemId = selectedWorkItemIdByPhase[phaseKey]; + if (workItemId) { + await fetchWorkItemDetails(workItemId, phaseKey); + } } } catch (err) { console.error("상세 수정 실패", err); } }, - [selectedWorkItemId, fetchWorkItemDetails] + [selectedWorkItemIdByPhase, fetchWorkItemDetails] ); // 상세 삭제 const deleteDetail = useCallback( - async (id: string) => { + async (id: string, phaseKey: string) => { try { const res = await apiClient.delete( `${API_BASE}/work-item-details/${id}` ); if (res.data?.success) { - if (selectedWorkItemId) { - await fetchWorkItemDetails(selectedWorkItemId); + const workItemId = selectedWorkItemIdByPhase[phaseKey]; + if (workItemId) { + await fetchWorkItemDetails(workItemId, phaseKey); } if (selection.routingDetailId) { await fetchWorkItems(selection.routingDetailId); @@ -304,7 +324,7 @@ export function useProcessWorkStandard(config: ProcessWorkStandardConfig) { } }, [ - selectedWorkItemId, + selectedWorkItemIdByPhase, selection.routingDetailId, fetchWorkItemDetails, fetchWorkItems, @@ -315,8 +335,8 @@ export function useProcessWorkStandard(config: ProcessWorkStandardConfig) { items, routings, workItems, - selectedWorkItemDetails, - selectedWorkItemId, + selectedWorkItemIdByPhase, + selectedDetailsByPhase, selection, loading, saving, @@ -325,7 +345,6 @@ export function useProcessWorkStandard(config: ProcessWorkStandardConfig) { selectProcess, fetchWorkItems, fetchWorkItemDetails, - setSelectedWorkItemId, createWorkItem, updateWorkItem, deleteWorkItem, diff --git a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx index 5e8bee69..427f2da5 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -3361,6 +3361,10 @@ export const SplitPanelLayoutComponent: React.FC })); // 🔧 그룹화된 데이터 렌더링 + const hasGroupedLeftActions = !isDesignMode && ( + (componentConfig.leftPanel?.showEdit !== false) || + (componentConfig.leftPanel?.showDelete !== false) + ); if (groupedLeftData.length > 0) { return (
@@ -3385,6 +3389,10 @@ export const SplitPanelLayoutComponent: React.FC {col.label} ))} + {hasGroupedLeftActions && ( + + + )} @@ -3399,7 +3407,7 @@ export const SplitPanelLayoutComponent: React.FC handleLeftItemSelect(item)} - className={`hover:bg-accent cursor-pointer transition-colors ${ + className={`group hover:bg-accent cursor-pointer transition-colors ${ isSelected ? "bg-primary/10" : "" }`} > @@ -3417,6 +3425,34 @@ export const SplitPanelLayoutComponent: React.FC )} ))} + {hasGroupedLeftActions && ( + +
+ {(componentConfig.leftPanel?.showEdit !== false) && ( + + )} + {(componentConfig.leftPanel?.showDelete !== false) && ( + + )} +
+ + )} ); })} @@ -3429,6 +3465,10 @@ export const SplitPanelLayoutComponent: React.FC } // 🔧 일반 테이블 렌더링 (그룹화 없음) + const hasLeftTableActions = !isDesignMode && ( + (componentConfig.leftPanel?.showEdit !== false) || + (componentConfig.leftPanel?.showDelete !== false) + ); return (
@@ -3447,6 +3487,10 @@ export const SplitPanelLayoutComponent: React.FC {col.label} ))} + {hasLeftTableActions && ( + + )} @@ -3461,7 +3505,7 @@ export const SplitPanelLayoutComponent: React.FC handleLeftItemSelect(item)} - className={`hover:bg-accent cursor-pointer transition-colors ${ + className={`group hover:bg-accent cursor-pointer transition-colors ${ isSelected ? "bg-primary/10" : "" }`} > @@ -3479,6 +3523,34 @@ export const SplitPanelLayoutComponent: React.FC )} ))} + {hasLeftTableActions && ( + + )} ); })}
+
+
+ {(componentConfig.leftPanel?.showEdit !== false) && ( + + )} + {(componentConfig.leftPanel?.showDelete !== false) && ( + + )} +
+