refactor: Update ProcessWorkStandard component to manage work item selection by phase
- 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.
This commit is contained in:
parent
2335a413cb
commit
38ade7562e
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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<ModalDetail[]>([]);
|
||||
|
||||
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");
|
||||
|
|
|
|||
|
|
@ -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<WorkItemDetail>) => void;
|
||||
onUpdateDetail: (id: string, data: Partial<WorkItemDetail>) => void;
|
||||
onDeleteDetail: (id: string) => void;
|
||||
onCreateDetail: (workItemId: string, data: Partial<WorkItemDetail>, phaseKey: string) => void;
|
||||
onUpdateDetail: (id: string, data: Partial<WorkItemDetail>, 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 (
|
||||
<div className="rounded-lg border bg-card">
|
||||
|
|
@ -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({
|
|||
{/* 우측: 상세 리스트 */}
|
||||
<div className="flex-1">
|
||||
<WorkItemDetailList
|
||||
workItem={isThisSectionSelected ? selectedItem : null}
|
||||
details={isThisSectionSelected ? selectedWorkItemDetails : []}
|
||||
workItem={selectedItem}
|
||||
details={selectedWorkItemDetails}
|
||||
detailTypes={detailTypes}
|
||||
readonly={readonly}
|
||||
onCreateDetail={(data) =>
|
||||
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)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ export const defaultConfig: ProcessWorkStandardConfig = {
|
|||
{ value: "inspect", label: "검사항목" },
|
||||
{ value: "procedure", label: "작업절차" },
|
||||
{ value: "input", label: "직접입력" },
|
||||
{ value: "info", label: "정보조회" },
|
||||
],
|
||||
splitRatio: 30,
|
||||
leftPanelTitle: "품목 및 공정 선택",
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ export function useProcessWorkStandard(config: ProcessWorkStandardConfig) {
|
|||
const [items, setItems] = useState<ItemData[]>([]);
|
||||
const [routings, setRoutings] = useState<RoutingVersion[]>([]);
|
||||
const [workItems, setWorkItems] = useState<WorkItem[]>([]);
|
||||
const [selectedWorkItemDetails, setSelectedWorkItemDetails] = useState<WorkItemDetail[]>([]);
|
||||
const [selectedWorkItemId, setSelectedWorkItemId] = useState<string | null>(null);
|
||||
// 섹션(phase)별 독립적인 선택 상태 관리
|
||||
const [selectedWorkItemIdByPhase, setSelectedWorkItemIdByPhase] = useState<Record<string, string | null>>({});
|
||||
const [selectedDetailsByPhase, setSelectedDetailsByPhase] = useState<Record<string, WorkItemDetail[]>>({});
|
||||
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<WorkItemDetail>) => {
|
||||
async (workItemId: string, data: Partial<WorkItemDetail>, 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<WorkItemDetail>) => {
|
||||
async (id: string, data: Partial<WorkItemDetail>, 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,
|
||||
|
|
|
|||
|
|
@ -3361,6 +3361,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
}));
|
||||
|
||||
// 🔧 그룹화된 데이터 렌더링
|
||||
const hasGroupedLeftActions = !isDesignMode && (
|
||||
(componentConfig.leftPanel?.showEdit !== false) ||
|
||||
(componentConfig.leftPanel?.showDelete !== false)
|
||||
);
|
||||
if (groupedLeftData.length > 0) {
|
||||
return (
|
||||
<div className="overflow-auto">
|
||||
|
|
@ -3385,6 +3389,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
{hasGroupedLeftActions && (
|
||||
<th className="px-3 py-2 text-right text-xs font-medium tracking-wider text-gray-500 uppercase whitespace-nowrap" style={{ width: "80px" }}>
|
||||
</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 bg-white">
|
||||
|
|
@ -3399,7 +3407,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
<tr
|
||||
key={itemId}
|
||||
onClick={() => 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<SplitPanelLayoutComponentProps>
|
|||
)}
|
||||
</td>
|
||||
))}
|
||||
{hasGroupedLeftActions && (
|
||||
<td className="px-3 py-2 text-right">
|
||||
<div className="flex items-center justify-end gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
{(componentConfig.leftPanel?.showEdit !== false) && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEditClick("left", item);
|
||||
}}
|
||||
className="rounded p-1 transition-colors hover:bg-gray-200"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5 text-gray-500" />
|
||||
</button>
|
||||
)}
|
||||
{(componentConfig.leftPanel?.showDelete !== false) && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteClick("left", item);
|
||||
}}
|
||||
className="rounded p-1 transition-colors hover:bg-red-100"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5 text-red-500" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
|
|
@ -3429,6 +3465,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
}
|
||||
|
||||
// 🔧 일반 테이블 렌더링 (그룹화 없음)
|
||||
const hasLeftTableActions = !isDesignMode && (
|
||||
(componentConfig.leftPanel?.showEdit !== false) ||
|
||||
(componentConfig.leftPanel?.showDelete !== false)
|
||||
);
|
||||
return (
|
||||
<div className="overflow-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
|
|
@ -3447,6 +3487,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
{hasLeftTableActions && (
|
||||
<th className="px-3 py-2 text-right text-xs font-medium tracking-wider text-gray-500 uppercase whitespace-nowrap" style={{ width: "80px" }}>
|
||||
</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 bg-white">
|
||||
|
|
@ -3461,7 +3505,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
<tr
|
||||
key={itemId}
|
||||
onClick={() => 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<SplitPanelLayoutComponentProps>
|
|||
)}
|
||||
</td>
|
||||
))}
|
||||
{hasLeftTableActions && (
|
||||
<td className="px-3 py-2 text-right">
|
||||
<div className="flex items-center justify-end gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
{(componentConfig.leftPanel?.showEdit !== false) && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEditClick("left", item);
|
||||
}}
|
||||
className="rounded p-1 transition-colors hover:bg-gray-200"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5 text-gray-500" />
|
||||
</button>
|
||||
)}
|
||||
{(componentConfig.leftPanel?.showDelete !== false) && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteClick("left", item);
|
||||
}}
|
||||
className="rounded p-1 transition-colors hover:bg-red-100"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5 text-red-500" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
Loading…
Reference in New Issue