2026-02-24 12:37:33 +09:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { useState, useCallback } from "react";
|
|
|
|
|
import { apiClient } from "@/lib/api/client";
|
|
|
|
|
import {
|
|
|
|
|
ProcessWorkStandardConfig,
|
|
|
|
|
ItemData,
|
|
|
|
|
RoutingVersion,
|
|
|
|
|
WorkItem,
|
|
|
|
|
WorkItemDetail,
|
|
|
|
|
SelectionState,
|
|
|
|
|
} from "../types";
|
|
|
|
|
|
2026-02-24 18:22:54 +09:00
|
|
|
const API_BASE = "/process-work-standard";
|
2026-02-24 12:37:33 +09:00
|
|
|
|
|
|
|
|
export function useProcessWorkStandard(config: ProcessWorkStandardConfig) {
|
|
|
|
|
const [items, setItems] = useState<ItemData[]>([]);
|
|
|
|
|
const [routings, setRoutings] = useState<RoutingVersion[]>([]);
|
|
|
|
|
const [workItems, setWorkItems] = useState<WorkItem[]>([]);
|
2026-02-26 20:49:25 +09:00
|
|
|
// 섹션(phase)별 독립적인 선택 상태 관리
|
|
|
|
|
const [selectedWorkItemIdByPhase, setSelectedWorkItemIdByPhase] = useState<Record<string, string | null>>({});
|
|
|
|
|
const [selectedDetailsByPhase, setSelectedDetailsByPhase] = useState<Record<string, WorkItemDetail[]>>({});
|
2026-02-24 12:37:33 +09:00
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
|
|
|
|
|
|
const [selection, setSelection] = useState<SelectionState>({
|
|
|
|
|
itemCode: null,
|
|
|
|
|
itemName: null,
|
|
|
|
|
routingVersionId: null,
|
|
|
|
|
routingVersionName: null,
|
|
|
|
|
routingDetailId: null,
|
|
|
|
|
processName: null,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 품목 목록 조회
|
|
|
|
|
const fetchItems = useCallback(
|
|
|
|
|
async (search?: string) => {
|
|
|
|
|
try {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
const ds = config.dataSource;
|
|
|
|
|
const params = new URLSearchParams({
|
|
|
|
|
tableName: ds.itemTable,
|
|
|
|
|
nameColumn: ds.itemNameColumn,
|
|
|
|
|
codeColumn: ds.itemCodeColumn,
|
|
|
|
|
routingTable: ds.routingVersionTable,
|
|
|
|
|
routingFkColumn: ds.routingFkColumn,
|
|
|
|
|
...(search ? { search } : {}),
|
|
|
|
|
});
|
|
|
|
|
const res = await apiClient.get(`${API_BASE}/items?${params}`);
|
|
|
|
|
if (res.data?.success) {
|
|
|
|
|
setItems(res.data.data);
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("품목 조회 실패", err);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[config.dataSource]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 라우팅 + 공정 조회
|
|
|
|
|
const fetchRoutings = useCallback(
|
|
|
|
|
async (itemCode: string) => {
|
|
|
|
|
try {
|
|
|
|
|
const ds = config.dataSource;
|
|
|
|
|
const params = new URLSearchParams({
|
|
|
|
|
routingVersionTable: ds.routingVersionTable,
|
|
|
|
|
routingDetailTable: ds.routingDetailTable,
|
|
|
|
|
routingFkColumn: ds.routingFkColumn,
|
|
|
|
|
processTable: ds.processTable,
|
|
|
|
|
processNameColumn: ds.processNameColumn,
|
|
|
|
|
processCodeColumn: ds.processCodeColumn,
|
|
|
|
|
});
|
|
|
|
|
const res = await apiClient.get(
|
|
|
|
|
`${API_BASE}/items/${encodeURIComponent(itemCode)}/routings?${params}`
|
|
|
|
|
);
|
|
|
|
|
if (res.data?.success) {
|
|
|
|
|
setRoutings(res.data.data);
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("라우팅 조회 실패", err);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[config.dataSource]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 작업 항목 조회
|
|
|
|
|
const fetchWorkItems = useCallback(async (routingDetailId: string) => {
|
|
|
|
|
try {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
const res = await apiClient.get(
|
|
|
|
|
`${API_BASE}/routing-detail/${routingDetailId}/work-items`
|
|
|
|
|
);
|
|
|
|
|
if (res.data?.success) {
|
|
|
|
|
setWorkItems(res.data.items || []);
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("작업 항목 조회 실패", err);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-02-26 20:49:25 +09:00
|
|
|
// 작업 항목 상세 조회 (phase별 독립 저장)
|
|
|
|
|
const fetchWorkItemDetails = useCallback(async (workItemId: string, phaseKey: string) => {
|
2026-02-24 12:37:33 +09:00
|
|
|
try {
|
|
|
|
|
const res = await apiClient.get(
|
|
|
|
|
`${API_BASE}/work-items/${workItemId}/details`
|
|
|
|
|
);
|
|
|
|
|
if (res.data?.success) {
|
2026-02-26 20:49:25 +09:00
|
|
|
setSelectedDetailsByPhase(prev => ({ ...prev, [phaseKey]: res.data.data }));
|
|
|
|
|
setSelectedWorkItemIdByPhase(prev => ({ ...prev, [phaseKey]: workItemId }));
|
2026-02-24 12:37:33 +09:00
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("상세 조회 실패", err);
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// 품목 선택
|
|
|
|
|
const selectItem = useCallback(
|
|
|
|
|
async (itemCode: string, itemName: string) => {
|
|
|
|
|
setSelection((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
itemCode,
|
|
|
|
|
itemName,
|
|
|
|
|
routingVersionId: null,
|
|
|
|
|
routingVersionName: null,
|
|
|
|
|
routingDetailId: null,
|
|
|
|
|
processName: null,
|
|
|
|
|
}));
|
|
|
|
|
setWorkItems([]);
|
2026-02-26 20:49:25 +09:00
|
|
|
setSelectedDetailsByPhase({});
|
|
|
|
|
setSelectedWorkItemIdByPhase({});
|
2026-02-24 12:37:33 +09:00
|
|
|
await fetchRoutings(itemCode);
|
|
|
|
|
},
|
|
|
|
|
[fetchRoutings]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 공정 선택
|
|
|
|
|
const selectProcess = useCallback(
|
|
|
|
|
async (
|
|
|
|
|
routingDetailId: string,
|
|
|
|
|
processName: string,
|
|
|
|
|
routingVersionId: string,
|
|
|
|
|
routingVersionName: string
|
|
|
|
|
) => {
|
|
|
|
|
setSelection((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
routingVersionId,
|
|
|
|
|
routingVersionName,
|
|
|
|
|
routingDetailId,
|
|
|
|
|
processName,
|
|
|
|
|
}));
|
2026-02-26 20:49:25 +09:00
|
|
|
setSelectedDetailsByPhase({});
|
|
|
|
|
setSelectedWorkItemIdByPhase({});
|
2026-02-24 12:37:33 +09:00
|
|
|
await fetchWorkItems(routingDetailId);
|
|
|
|
|
},
|
|
|
|
|
[fetchWorkItems]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 작업 항목 추가
|
|
|
|
|
const createWorkItem = useCallback(
|
|
|
|
|
async (data: {
|
|
|
|
|
work_phase: string;
|
|
|
|
|
title: string;
|
|
|
|
|
is_required: string;
|
|
|
|
|
description?: string;
|
|
|
|
|
details?: Array<{
|
|
|
|
|
detail_type?: string;
|
|
|
|
|
content: string;
|
|
|
|
|
is_required: string;
|
|
|
|
|
sort_order: number;
|
|
|
|
|
}>;
|
|
|
|
|
}) => {
|
|
|
|
|
if (!selection.routingDetailId) return null;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const nextOrder =
|
|
|
|
|
workItems.filter((wi) => wi.work_phase === data.work_phase).length + 1;
|
|
|
|
|
|
|
|
|
|
const res = await apiClient.post(`${API_BASE}/work-items`, {
|
|
|
|
|
routing_detail_id: selection.routingDetailId,
|
|
|
|
|
work_phase: data.work_phase,
|
|
|
|
|
title: data.title,
|
|
|
|
|
is_required: data.is_required,
|
|
|
|
|
sort_order: nextOrder,
|
|
|
|
|
description: data.description,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (res.data?.success && res.data.data) {
|
|
|
|
|
const newItem = res.data.data;
|
|
|
|
|
|
|
|
|
|
// 상세 항목도 함께 생성
|
|
|
|
|
if (data.details && data.details.length > 0) {
|
|
|
|
|
for (const detail of data.details) {
|
|
|
|
|
await apiClient.post(`${API_BASE}/work-item-details`, {
|
|
|
|
|
work_item_id: newItem.id,
|
|
|
|
|
...detail,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await fetchWorkItems(selection.routingDetailId);
|
|
|
|
|
return newItem;
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("작업 항목 생성 실패", err);
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
},
|
|
|
|
|
[selection.routingDetailId, workItems, fetchWorkItems]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 작업 항목 수정
|
|
|
|
|
const updateWorkItem = useCallback(
|
|
|
|
|
async (id: string, data: Partial<WorkItem>) => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await apiClient.put(`${API_BASE}/work-items/${id}`, data);
|
|
|
|
|
if (res.data?.success && selection.routingDetailId) {
|
|
|
|
|
await fetchWorkItems(selection.routingDetailId);
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("작업 항목 수정 실패", err);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[selection.routingDetailId, fetchWorkItems]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 작업 항목 삭제
|
|
|
|
|
const deleteWorkItem = useCallback(
|
|
|
|
|
async (id: string) => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await apiClient.delete(`${API_BASE}/work-items/${id}`);
|
|
|
|
|
if (res.data?.success && selection.routingDetailId) {
|
|
|
|
|
await fetchWorkItems(selection.routingDetailId);
|
2026-02-26 20:49:25 +09:00
|
|
|
// 삭제된 항목이 선택되어 있던 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;
|
|
|
|
|
});
|
2026-02-24 12:37:33 +09:00
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("작업 항목 삭제 실패", err);
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-02-26 20:49:25 +09:00
|
|
|
[selection.routingDetailId, selectedWorkItemIdByPhase, fetchWorkItems]
|
2026-02-24 12:37:33 +09:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 상세 추가
|
|
|
|
|
const createDetail = useCallback(
|
2026-02-26 20:49:25 +09:00
|
|
|
async (workItemId: string, data: Partial<WorkItemDetail>, phaseKey: string) => {
|
2026-02-24 12:37:33 +09:00
|
|
|
try {
|
|
|
|
|
const res = await apiClient.post(`${API_BASE}/work-item-details`, {
|
|
|
|
|
work_item_id: workItemId,
|
|
|
|
|
...data,
|
|
|
|
|
});
|
|
|
|
|
if (res.data?.success) {
|
2026-02-26 20:49:25 +09:00
|
|
|
await fetchWorkItemDetails(workItemId, phaseKey);
|
2026-02-24 12:37:33 +09:00
|
|
|
if (selection.routingDetailId) {
|
|
|
|
|
await fetchWorkItems(selection.routingDetailId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("상세 생성 실패", err);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[fetchWorkItemDetails, fetchWorkItems, selection.routingDetailId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 상세 수정
|
|
|
|
|
const updateDetail = useCallback(
|
2026-02-26 20:49:25 +09:00
|
|
|
async (id: string, data: Partial<WorkItemDetail>, phaseKey: string) => {
|
2026-02-24 12:37:33 +09:00
|
|
|
try {
|
|
|
|
|
const res = await apiClient.put(
|
|
|
|
|
`${API_BASE}/work-item-details/${id}`,
|
|
|
|
|
data
|
|
|
|
|
);
|
2026-02-26 20:49:25 +09:00
|
|
|
if (res.data?.success) {
|
|
|
|
|
const workItemId = selectedWorkItemIdByPhase[phaseKey];
|
|
|
|
|
if (workItemId) {
|
|
|
|
|
await fetchWorkItemDetails(workItemId, phaseKey);
|
|
|
|
|
}
|
2026-02-24 12:37:33 +09:00
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("상세 수정 실패", err);
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-02-26 20:49:25 +09:00
|
|
|
[selectedWorkItemIdByPhase, fetchWorkItemDetails]
|
2026-02-24 12:37:33 +09:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 상세 삭제
|
|
|
|
|
const deleteDetail = useCallback(
|
2026-02-26 20:49:25 +09:00
|
|
|
async (id: string, phaseKey: string) => {
|
2026-02-24 12:37:33 +09:00
|
|
|
try {
|
|
|
|
|
const res = await apiClient.delete(
|
|
|
|
|
`${API_BASE}/work-item-details/${id}`
|
|
|
|
|
);
|
|
|
|
|
if (res.data?.success) {
|
2026-02-26 20:49:25 +09:00
|
|
|
const workItemId = selectedWorkItemIdByPhase[phaseKey];
|
|
|
|
|
if (workItemId) {
|
|
|
|
|
await fetchWorkItemDetails(workItemId, phaseKey);
|
2026-02-24 12:37:33 +09:00
|
|
|
}
|
|
|
|
|
if (selection.routingDetailId) {
|
|
|
|
|
await fetchWorkItems(selection.routingDetailId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("상세 삭제 실패", err);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[
|
2026-02-26 20:49:25 +09:00
|
|
|
selectedWorkItemIdByPhase,
|
2026-02-24 12:37:33 +09:00
|
|
|
selection.routingDetailId,
|
|
|
|
|
fetchWorkItemDetails,
|
|
|
|
|
fetchWorkItems,
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
items,
|
|
|
|
|
routings,
|
|
|
|
|
workItems,
|
2026-02-26 20:49:25 +09:00
|
|
|
selectedWorkItemIdByPhase,
|
|
|
|
|
selectedDetailsByPhase,
|
2026-02-24 12:37:33 +09:00
|
|
|
selection,
|
|
|
|
|
loading,
|
|
|
|
|
saving,
|
|
|
|
|
fetchItems,
|
|
|
|
|
selectItem,
|
|
|
|
|
selectProcess,
|
|
|
|
|
fetchWorkItems,
|
|
|
|
|
fetchWorkItemDetails,
|
|
|
|
|
createWorkItem,
|
|
|
|
|
updateWorkItem,
|
|
|
|
|
deleteWorkItem,
|
|
|
|
|
createDetail,
|
|
|
|
|
updateDetail,
|
|
|
|
|
deleteDetail,
|
|
|
|
|
};
|
|
|
|
|
}
|