From 7bd1269aadf5d397ebc39bbd45772b36a383fa0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Mon, 9 Feb 2026 10:33:02 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[WEB]=20=EC=A4=91=EA=B0=84=EA=B2=80?= =?UTF-8?q?=EC=82=AC=20=ED=94=84=EB=A1=A0=ED=8A=B8=EC=97=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=97=B0=EB=8F=99=20(Phase=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WorkerScreen/actions.ts에 saveItemInspection, getWorkOrderInspectionData 서버 액션 추가 - handleInspectionComplete에서 POST /items/{itemId}/inspection API 호출 연동 - 작업지시 선택 시 GET /inspection-data로 기존 검사 데이터 자동 로드 - InspectionInputModal에 initialData prop 추가 (재클릭 시 저장된 값 표시) - WorkItemData에 apiItemId, workOrderId 필드 추가 (실제 DB ID 보존) - 기존 saveInspectionData deprecated 처리 --- .../production/WorkOrders/actions.ts | 3 +- .../WorkerScreen/InspectionInputModal.tsx | 19 ++++- .../production/WorkerScreen/actions.ts | 83 +++++++++++++++++++ .../production/WorkerScreen/index.tsx | 78 ++++++++++++++--- .../production/WorkerScreen/types.ts | 2 + 5 files changed, 173 insertions(+), 12 deletions(-) diff --git a/src/components/production/WorkOrders/actions.ts b/src/components/production/WorkOrders/actions.ts index 1b2aa96d..450f14f1 100644 --- a/src/components/production/WorkOrders/actions.ts +++ b/src/components/production/WorkOrders/actions.ts @@ -625,7 +625,8 @@ export async function updateWorkOrderItemStatus( } } -// ===== 중간검사 데이터 저장 ===== +// ===== 중간검사 데이터 저장 (deprecated: WorkerScreen/actions.ts의 saveItemInspection 사용) ===== +/** @deprecated WorkerScreen/actions.ts의 saveItemInspection을 사용하세요 */ export async function saveInspectionData( workOrderId: string, processType: string, diff --git a/src/components/production/WorkerScreen/InspectionInputModal.tsx b/src/components/production/WorkerScreen/InspectionInputModal.tsx index 8a45ca38..baebf59f 100644 --- a/src/components/production/WorkerScreen/InspectionInputModal.tsx +++ b/src/components/production/WorkerScreen/InspectionInputModal.tsx @@ -61,6 +61,7 @@ interface InspectionInputModalProps { processType: InspectionProcessType; productName?: string; specification?: string; + initialData?: InspectionData; onComplete: (data: InspectionData) => void; } @@ -192,6 +193,7 @@ export function InspectionInputModal({ processType, productName = '', specification = '', + initialData, onComplete, }: InspectionInputModalProps) { const [formData, setFormData] = useState({ @@ -208,6 +210,21 @@ export function InspectionInputModal({ useEffect(() => { if (open) { + // initialData가 있으면 기존 저장 데이터로 복원 + if (initialData) { + setFormData({ + ...initialData, + productName: initialData.productName || productName, + specification: initialData.specification || specification, + }); + if (initialData.gapPoints) { + setGapPoints(initialData.gapPoints); + } else { + setGapPoints(Array(5).fill(null).map(() => ({ left: null, right: null }))); + } + return; + } + // 공정별 기본값 설정 - 모두 양호/OK/적합 상태로 초기화 const baseData: InspectionData = { productName, @@ -259,7 +276,7 @@ export function InspectionInputModal({ setGapPoints(Array(5).fill(null).map(() => ({ left: null, right: null }))); } - }, [open, productName, specification, processType]); + }, [open, productName, specification, processType, initialData]); const handleComplete = () => { const data: InspectionData = { diff --git a/src/components/production/WorkerScreen/actions.ts b/src/components/production/WorkerScreen/actions.ts index 55395ad0..13c102f6 100644 --- a/src/components/production/WorkerScreen/actions.ts +++ b/src/components/production/WorkerScreen/actions.ts @@ -827,4 +827,87 @@ export async function getWorkOrderDetail( console.error('[WorkerScreenActions] getWorkOrderDetail error:', error); return { success: false, data: [], error: '서버 오류' }; } +} + +// ===== 개소별 중간검사 데이터 저장 ===== +export async function saveItemInspection( + workOrderId: string, + itemId: number, + processType: string, + inspectionData: Record +): Promise<{ success: boolean; data?: Record; error?: string }> { + try { + console.log('[WorkerScreenActions] POST item inspection:', { workOrderId, itemId, processType }); + + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/items/${itemId}/inspection`, + { + method: 'POST', + body: JSON.stringify({ + process_type: processType, + inspection_data: inspectionData, + }), + } + ); + + if (error || !response) { + return { success: false, error: error?.message || 'API 요청 실패' }; + } + + const result = await response.json(); + console.log('[WorkerScreenActions] POST item inspection response:', result); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '검사 데이터 저장에 실패했습니다.' }; + } + + return { success: true, data: result.data }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[WorkerScreenActions] saveItemInspection error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } +} + +// ===== 작업지시 전체 검사 데이터 조회 ===== +export interface InspectionDataItem { + item_id: number; + item_name: string; + specification: string | null; + quantity: number; + sort_order: number; + options: Record | null; + inspection_data: Record; +} + +export async function getWorkOrderInspectionData( + workOrderId: string +): Promise<{ + success: boolean; + data?: { work_order_id: number; items: InspectionDataItem[]; total: number }; + error?: string; +}> { + try { + const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/inspection-data`; + + console.log('[WorkerScreenActions] GET inspection data:', url); + + const { response, error } = await serverFetch(url, { method: 'GET' }); + + if (error || !response) { + return { success: false, error: error?.message || 'API 요청 실패' }; + } + + const result = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '검사 데이터 조회에 실패했습니다.' }; + } + + return { success: true, data: result.data }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[WorkerScreenActions] getWorkOrderInspectionData error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } } \ No newline at end of file diff --git a/src/components/production/WorkerScreen/index.tsx b/src/components/production/WorkerScreen/index.tsx index 32712df5..12254ba6 100644 --- a/src/components/production/WorkerScreen/index.tsx +++ b/src/components/production/WorkerScreen/index.tsx @@ -33,7 +33,7 @@ import { Button } from '@/components/ui/button'; import { PageLayout } from '@/components/organisms/PageLayout'; import { cn } from '@/lib/utils'; import { toast } from 'sonner'; -import { getMyWorkOrders, completeWorkOrder } from './actions'; +import { getMyWorkOrders, completeWorkOrder, saveItemInspection, getWorkOrderInspectionData } from './actions'; import { getProcessList } from '@/components/process-management/actions'; import type { InspectionSetting, Process } from '@/types/process'; import type { WorkOrder } from '../ProductionDashboard/types'; @@ -445,6 +445,37 @@ export default function WorkerScreen() { } }, [activeTab, processListCache]); + // ===== 작업지시 선택 시 기존 검사 데이터 로드 ===== + useEffect(() => { + if (!selectedSidebarOrderId) return; + // 목업 ID면 건너뛰기 + if (selectedSidebarOrderId.startsWith('order-')) return; + + const loadInspectionData = async () => { + try { + const result = await getWorkOrderInspectionData(selectedSidebarOrderId); + if (result.success && result.data?.items) { + setInspectionDataMap((prev) => { + const next = new Map(prev); + for (const apiItem of result.data!.items) { + if (!apiItem.inspection_data) continue; + // workItems에서 apiItemId가 일치하는 항목 찾기 + const match = workItems.find((w) => w.apiItemId === apiItem.item_id); + if (match) { + next.set(match.id, apiItem.inspection_data as unknown as InspectionData); + } + } + return next; + }); + } + } catch { + // 검사 데이터 로드 실패는 무시 (새 작업일 수 있음) + } + }; + loadInspectionData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedSidebarOrderId]); + // ===== 탭별 필터링된 작업 ===== const filteredWorkOrders = useMemo(() => { const selectedProcess = processListCache.find((p) => p.id === activeTab); @@ -530,6 +561,8 @@ export default function WorkerScreen() { apiItems.push({ id: `${selectedOrder.id}-node-${nodeKey}`, + apiItemId: group.items[0]?.id as number | undefined, + workOrderId: selectedOrder.id, itemNo: index + 1, itemCode: selectedOrder.orderNo || '-', itemName: `${group.nodeName} : ${itemSummary}`, @@ -557,6 +590,7 @@ export default function WorkerScreen() { }); apiItems.push({ id: selectedOrder.id, + workOrderId: selectedOrder.id, itemNo: 1, itemCode: selectedOrder.orderNo || '-', itemName: selectedOrder.productName || '-', @@ -858,17 +892,40 @@ export default function WorkerScreen() { } }, [getTargetOrder]); - // 중간검사 완료 핸들러 - const handleInspectionComplete = useCallback((data: InspectionData) => { - if (selectedOrder) { - setInspectionDataMap((prev) => { - const next = new Map(prev); - next.set(selectedOrder.id, data); - return next; - }); + // 중간검사 완료 핸들러 (API 저장 + 메모리 업데이트) + const handleInspectionComplete = useCallback(async (data: InspectionData) => { + if (!selectedOrder) return; + + // 메모리에 즉시 반영 + setInspectionDataMap((prev) => { + const next = new Map(prev); + next.set(selectedOrder.id, data); + return next; + }); + + // 실제 API item인 경우 서버에 저장 + const targetItem = workItems.find((w) => w.id === selectedOrder.id); + if (targetItem?.apiItemId && targetItem?.workOrderId) { + try { + const result = await saveItemInspection( + targetItem.workOrderId, + targetItem.apiItemId, + getInspectionProcessType(), + data as unknown as Record + ); + if (result.success) { + toast.success('중간검사가 저장되었습니다.'); + } else { + toast.error(result.error || '검사 데이터 저장에 실패했습니다.'); + } + } catch { + toast.error('검사 데이터 저장 중 오류가 발생했습니다.'); + } + } else { + // 목업 데이터는 메모리만 저장 toast.success('중간검사가 완료되었습니다.'); } - }, [selectedOrder]); + }, [selectedOrder, workItems, getInspectionProcessType]); // ===== 재공품 감지 ===== const hasWipItems = useMemo(() => { @@ -1224,6 +1281,7 @@ export default function WorkerScreen() { processType={getInspectionProcessType()} productName={selectedOrder?.productName || workItems[0]?.itemName || ''} specification={workItems[0]?.slatJointBarInfo?.specification || workItems[0]?.wipInfo?.specification || ''} + initialData={selectedOrder ? inspectionDataMap.get(selectedOrder.id) : undefined} onComplete={handleInspectionComplete} /> diff --git a/src/components/production/WorkerScreen/types.ts b/src/components/production/WorkerScreen/types.ts index aa9116a0..822a1558 100644 --- a/src/components/production/WorkerScreen/types.ts +++ b/src/components/production/WorkerScreen/types.ts @@ -31,6 +31,8 @@ export interface WorkInfo { // ===== 작업 아이템 (카드 1개 단위) ===== export interface WorkItemData { id: string; + apiItemId?: number; // 실제 work_order_items.id (API 호출용) + workOrderId?: string; // 소속 작업지시 ID (API 호출용) itemNo: number; // 번호 (1, 2, 3...) itemCode: string; // 품목코드 (KWWS03) itemName: string; // 품목명 (와이어)