From a9ae162c906d168cb4d09caa642476b95d9ef3ca 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 17:37:49 +0900 Subject: [PATCH] =?UTF-8?q?feat(WEB):=20Phase=204=20=EC=A4=91=EA=B0=84?= =?UTF-8?q?=EA=B2=80=EC=82=AC=20=EC=84=B1=EC=A0=81=EC=84=9C=20API=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20=EB=B0=8F=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Phase 4.1: InspectionReportModal API 연동 (getInspectionReport 서버 액션) - Phase 4.2: 5개 InspectionContent 공통 코드 추출 (inspection-shared.tsx) - 공통 컴포넌트: InspectionLayout, CheckStatusCell, JudgmentCell, InspectionFooter - 공통 유틸: convertToCheckStatus, calculateOverallResult, getOrderInfo - 총 코드량 2,376줄 → 1,583줄 (33% 감소) - InspectionInputModal 기본값 null로 수정 (적합 버튼 미선택 상태 시작) --- .../production/WorkOrders/actions.ts | 981 +++++++++++++----- .../documents/BendingInspectionContent.tsx | 199 +--- .../documents/BendingWipInspectionContent.tsx | 274 ++--- .../documents/InspectionReportModal.tsx | 134 ++- .../documents/ScreenInspectionContent.tsx | 319 ++---- .../documents/SlatInspectionContent.tsx | 267 ++--- .../SlatJointBarInspectionContent.tsx | 273 ++--- .../documents/inspection-shared.tsx | 253 +++++ .../WorkerScreen/InspectionInputModal.tsx | 26 +- 9 files changed, 1448 insertions(+), 1278 deletions(-) create mode 100644 src/components/production/WorkOrders/documents/inspection-shared.tsx diff --git a/src/components/production/WorkOrders/actions.ts b/src/components/production/WorkOrders/actions.ts index 5e92954b..57b3a18a 100644 --- a/src/components/production/WorkOrders/actions.ts +++ b/src/components/production/WorkOrders/actions.ts @@ -19,12 +19,12 @@ 'use server'; -import { executeServerAction } from '@/lib/api/execute-server-action'; +import { isNextRedirectError } from '@/lib/utils/redirect-error'; +import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { WorkOrder, WorkOrderStats, WorkOrderStatus, - WorkOrderApi, WorkOrderApiPaginatedResponse, WorkOrderStatsApi, } from './types'; @@ -42,16 +42,14 @@ interface PaginationMeta { total: number; } -const API_URL = process.env.NEXT_PUBLIC_API_URL; - // ===== 작업지시 목록 조회 ===== export async function getWorkOrders(params?: { page?: number; perPage?: number; status?: WorkOrderStatus | 'all'; - processId?: number | 'all' | 'none'; - processType?: 'screen' | 'slat' | 'bending'; - priority?: string; + processId?: number | 'all' | 'none'; // 공정 ID (FK → processes.id), 'none' = 미지정 + processType?: 'screen' | 'slat' | 'bending'; // 공정 타입 필터 + priority?: string; // 우선순위 필터 (urgent/priority/normal) search?: string; startDate?: string; endDate?: string; @@ -61,39 +59,84 @@ export async function getWorkOrders(params?: { pagination: PaginationMeta; error?: string; }> { - const emptyPagination = { currentPage: 1, lastPage: 1, perPage: 20, total: 0 }; - const searchParams = new URLSearchParams(); - if (params?.page) searchParams.set('page', String(params.page)); - if (params?.perPage) searchParams.set('per_page', String(params.perPage)); - if (params?.status && params.status !== 'all') searchParams.set('status', params.status); - if (params?.processId && params.processId !== 'all') searchParams.set('process_id', String(params.processId)); - if (params?.processType) searchParams.set('process_type', params.processType); - if (params?.priority && params.priority !== 'all') searchParams.set('priority', params.priority); - if (params?.search) searchParams.set('search', params.search); - if (params?.startDate) searchParams.set('start_date', params.startDate); - if (params?.endDate) searchParams.set('end_date', params.endDate); - - const queryString = searchParams.toString(); - const result = await executeServerAction({ - url: `${API_URL}/api/v1/work-orders${queryString ? `?${queryString}` : ''}`, - errorMessage: '작업지시 목록 조회에 실패했습니다.', - }); - - if (!result.success || !result.data) { - return { success: false, data: [], pagination: emptyPagination, error: result.error }; - } - - const paginatedData = result.data; - return { - success: true, - data: (paginatedData.data || []).map(transformApiToFrontend), - pagination: { - currentPage: paginatedData.current_page, - lastPage: paginatedData.last_page, - perPage: paginatedData.per_page, - total: paginatedData.total, - }, + const emptyResponse = { + success: false, + data: [] as WorkOrder[], + pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 }, }; + + try { + const searchParams = new URLSearchParams(); + + if (params?.page) searchParams.set('page', String(params.page)); + if (params?.perPage) searchParams.set('per_page', String(params.perPage)); + if (params?.status && params.status !== 'all') { + searchParams.set('status', params.status); + } + if (params?.processId && params.processId !== 'all') { + // 'none': 공정 미지정 필터 (process_id IS NULL) + searchParams.set('process_id', String(params.processId)); + } + if (params?.processType) { + searchParams.set('process_type', params.processType); + } + if (params?.priority && params.priority !== 'all') { + searchParams.set('priority', params.priority); + } + if (params?.search) searchParams.set('search', params.search); + if (params?.startDate) searchParams.set('start_date', params.startDate); + if (params?.endDate) searchParams.set('end_date', params.endDate); + + const queryString = searchParams.toString(); + const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders${queryString ? `?${queryString}` : ''}`; + + console.log('[WorkOrderActions] GET work-orders:', url); + + const { response, error } = await serverFetch(url, { method: 'GET' }); + + if (error || !response) { + return { ...emptyResponse, error: error?.message || 'API 요청 실패' }; + } + + if (!response.ok) { + console.warn('[WorkOrderActions] GET work-orders error:', response.status); + return { ...emptyResponse, error: `API 오류: ${response.status}` }; + } + + const result = await response.json(); + + if (!result.success) { + return { + ...emptyResponse, + error: result.message || '작업지시 목록 조회에 실패했습니다.', + }; + } + + const paginatedData: WorkOrderApiPaginatedResponse = result.data || { + data: [], + current_page: 1, + last_page: 1, + per_page: 20, + total: 0, + }; + + const workOrders = (paginatedData.data || []).map(transformApiToFrontend); + + return { + success: true, + data: workOrders, + pagination: { + currentPage: paginatedData.current_page, + lastPage: paginatedData.last_page, + perPage: paginatedData.per_page, + total: paginatedData.total, + }, + }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[WorkOrderActions] getWorkOrders error:', error); + return { ...emptyResponse, error: '서버 오류가 발생했습니다.' }; + } } // ===== 작업지시 통계 조회 ===== @@ -102,12 +145,42 @@ export async function getWorkOrderStats(): Promise<{ data?: WorkOrderStats; error?: string; }> { - const result = await executeServerAction({ - url: `${API_URL}/api/v1/work-orders/stats`, - transform: (data: WorkOrderStatsApi) => transformStatsApiToFrontend(data), - errorMessage: '통계 조회에 실패했습니다.', - }); - return { success: result.success, data: result.data, error: result.error }; + try { + const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/stats`; + + console.log('[WorkOrderActions] GET stats:', url); + + const { response, error } = await serverFetch(url, { method: 'GET' }); + + if (error || !response) { + return { success: false, error: error?.message || 'API 요청 실패' }; + } + + if (!response.ok) { + console.warn('[WorkOrderActions] GET stats error:', response.status); + return { success: false, error: `API 오류: ${response.status}` }; + } + + const result = await response.json(); + + if (!result.success) { + return { + success: false, + error: result.message || '통계 조회에 실패했습니다.', + }; + } + + const statsApi: WorkOrderStatsApi = result.data; + + return { + success: true, + data: transformStatsApiToFrontend(statsApi), + }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[WorkOrderActions] getWorkOrderStats error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } } // ===== 작업지시 상세 조회 ===== @@ -116,42 +189,99 @@ export async function getWorkOrderById(id: string): Promise<{ data?: WorkOrder; error?: string; }> { - const result = await executeServerAction({ - url: `${API_URL}/api/v1/work-orders/${id}`, - transform: (data: WorkOrderApi) => transformApiToFrontend(data), - errorMessage: '작업지시 조회에 실패했습니다.', - }); - return { success: result.success, data: result.data, error: result.error }; + try { + const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}`; + + console.log('[WorkOrderActions] GET work-order:', url); + + const { response, error } = await serverFetch(url, { method: 'GET' }); + + if (error || !response) { + return { success: false, error: error?.message || 'API 요청 실패' }; + } + + if (!response.ok) { + console.error('[WorkOrderActions] GET work-order error:', response.status); + return { success: false, error: `API 오류: ${response.status}` }; + } + + const result = await response.json(); + + if (!result.success || !result.data) { + return { + success: false, + error: result.message || '작업지시 조회에 실패했습니다.', + }; + } + + return { + success: true, + data: transformApiToFrontend(result.data), + }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[WorkOrderActions] getWorkOrderById error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } } // ===== 작업지시 등록 ===== export async function createWorkOrder( data: Partial & { salesOrderId?: number; - assigneeId?: number; - assigneeIds?: number[]; + assigneeId?: number; // 단일 담당자 (하위 호환) + assigneeIds?: number[]; // 다중 담당자 teamId?: number; } ): Promise<{ success: boolean; data?: WorkOrder; error?: string }> { - const assigneeIds = data.assigneeIds && data.assigneeIds.length > 0 - ? data.assigneeIds - : data.assigneeId ? [data.assigneeId] : undefined; + try { + // 다중 담당자 우선, 없으면 단일 담당자 배열로 변환 + const assigneeIds = data.assigneeIds && data.assigneeIds.length > 0 + ? data.assigneeIds + : data.assigneeId + ? [data.assigneeId] + : undefined; - const apiData = { - ...transformFrontendToApi(data), - sales_order_id: data.salesOrderId, - assignee_ids: assigneeIds, - team_id: data.teamId, - }; + const apiData = { + ...transformFrontendToApi(data), + sales_order_id: data.salesOrderId, + assignee_ids: assigneeIds, // 배열로 전송 + team_id: data.teamId, + }; - const result = await executeServerAction({ - url: `${API_URL}/api/v1/work-orders`, - method: 'POST', - body: apiData, - transform: (d: WorkOrderApi) => transformApiToFrontend(d), - errorMessage: '작업지시 등록에 실패했습니다.', - }); - return { success: result.success, data: result.data, error: result.error }; + console.log('[WorkOrderActions] POST work-order request:', apiData); + + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders`, + { + method: 'POST', + body: JSON.stringify(apiData), + } + ); + + if (error || !response) { + return { success: false, error: error?.message || 'API 요청 실패' }; + } + + const result = await response.json(); + console.log('[WorkOrderActions] POST work-order response:', result); + + if (!response.ok || !result.success) { + return { + success: false, + error: result.message || '작업지시 등록에 실패했습니다.', + }; + } + + return { + success: true, + data: transformApiToFrontend(result.data), + }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[WorkOrderActions] createWorkOrder error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } } // ===== 작업지시 수정 ===== @@ -159,25 +289,72 @@ export async function updateWorkOrder( id: string, data: Partial & { assigneeIds?: number[] } ): Promise<{ success: boolean; data?: WorkOrder; error?: string }> { - const apiData = transformFrontendToApi(data); - const result = await executeServerAction({ - url: `${API_URL}/api/v1/work-orders/${id}`, - method: 'PUT', - body: apiData, - transform: (d: WorkOrderApi) => transformApiToFrontend(d), - errorMessage: '작업지시 수정에 실패했습니다.', - }); - return { success: result.success, data: result.data, error: result.error }; + try { + const apiData = transformFrontendToApi(data); + + console.log('[WorkOrderActions] PUT work-order request:', apiData); + + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}`, + { + method: 'PUT', + body: JSON.stringify(apiData), + } + ); + + if (error || !response) { + return { success: false, error: error?.message || 'API 요청 실패' }; + } + + const result = await response.json(); + console.log('[WorkOrderActions] PUT work-order response:', result); + + if (!response.ok || !result.success) { + return { + success: false, + error: result.message || '작업지시 수정에 실패했습니다.', + }; + } + + return { + success: true, + data: transformApiToFrontend(result.data), + }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[WorkOrderActions] updateWorkOrder error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } } // ===== 작업지시 삭제 ===== export async function deleteWorkOrder(id: string): Promise<{ success: boolean; error?: string }> { - const result = await executeServerAction({ - url: `${API_URL}/api/v1/work-orders/${id}`, - method: 'DELETE', - errorMessage: '작업지시 삭제에 실패했습니다.', - }); - return { success: result.success, error: result.error }; + try { + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}`, + { method: 'DELETE' } + ); + + if (error || !response) { + return { success: false, error: error?.message || 'API 요청 실패' }; + } + + const result = await response.json(); + console.log('[WorkOrderActions] DELETE work-order response:', result); + + if (!response.ok || !result.success) { + return { + success: false, + error: result.message || '작업지시 삭제에 실패했습니다.', + }; + } + + return { success: true }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[WorkOrderActions] deleteWorkOrder error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } } // ===== 작업지시 상태 변경 ===== @@ -185,34 +362,87 @@ export async function updateWorkOrderStatus( id: string, status: WorkOrderStatus ): Promise<{ success: boolean; data?: WorkOrder; error?: string }> { - const result = await executeServerAction({ - url: `${API_URL}/api/v1/work-orders/${id}/status`, - method: 'PATCH', - body: { status }, - transform: (d: WorkOrderApi) => transformApiToFrontend(d), - errorMessage: '상태 변경에 실패했습니다.', - }); - return { success: result.success, data: result.data, error: result.error }; + try { + console.log('[WorkOrderActions] PATCH status request:', { status }); + + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/status`, + { + method: 'PATCH', + body: JSON.stringify({ status }), + } + ); + + if (error || !response) { + return { success: false, error: error?.message || 'API 요청 실패' }; + } + + const result = await response.json(); + console.log('[WorkOrderActions] PATCH status response:', result); + + if (!response.ok || !result.success) { + return { + success: false, + error: result.message || '상태 변경에 실패했습니다.', + }; + } + + return { + success: true, + data: transformApiToFrontend(result.data), + }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[WorkOrderActions] updateWorkOrderStatus error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } } // ===== 담당자 배정 ===== export async function assignWorkOrder( id: string, - assigneeIds: number | number[], + assigneeIds: number | number[], // 단일 또는 다중 담당자 teamId?: number ): Promise<{ success: boolean; data?: WorkOrder; error?: string }> { - const ids = Array.isArray(assigneeIds) ? assigneeIds : [assigneeIds]; - const body: { assignee_ids: number[]; team_id?: number } = { assignee_ids: ids }; - if (teamId) body.team_id = teamId; + try { + // 배열로 통일 + const ids = Array.isArray(assigneeIds) ? assigneeIds : [assigneeIds]; + const body: { assignee_ids: number[]; team_id?: number } = { assignee_ids: ids }; + if (teamId) body.team_id = teamId; - const result = await executeServerAction({ - url: `${API_URL}/api/v1/work-orders/${id}/assign`, - method: 'PATCH', - body, - transform: (d: WorkOrderApi) => transformApiToFrontend(d), - errorMessage: '담당자 배정에 실패했습니다.', - }); - return { success: result.success, data: result.data, error: result.error }; + console.log('[WorkOrderActions] PATCH assign request:', body); + + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/assign`, + { + method: 'PATCH', + body: JSON.stringify(body), + } + ); + + if (error || !response) { + return { success: false, error: error?.message || 'API 요청 실패' }; + } + + const result = await response.json(); + console.log('[WorkOrderActions] PATCH assign response:', result); + + if (!response.ok || !result.success) { + return { + success: false, + error: result.message || '담당자 배정에 실패했습니다.', + }; + } + + return { + success: true, + data: transformApiToFrontend(result.data), + }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[WorkOrderActions] assignWorkOrder error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } } // ===== 벤딩 필드 토글 ===== @@ -220,14 +450,40 @@ export async function toggleBendingField( id: string, field: string ): Promise<{ success: boolean; data?: WorkOrder; error?: string }> { - const result = await executeServerAction({ - url: `${API_URL}/api/v1/work-orders/${id}/bending/toggle`, - method: 'PATCH', - body: { field }, - transform: (d: WorkOrderApi) => transformApiToFrontend(d), - errorMessage: '벤딩 필드 토글에 실패했습니다.', - }); - return { success: result.success, data: result.data, error: result.error }; + try { + console.log('[WorkOrderActions] PATCH bending toggle request:', { field }); + + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/bending/toggle`, + { + method: 'PATCH', + body: JSON.stringify({ field }), + } + ); + + if (error || !response) { + return { success: false, error: error?.message || 'API 요청 실패' }; + } + + const result = await response.json(); + console.log('[WorkOrderActions] PATCH bending toggle response:', result); + + if (!response.ok || !result.success) { + return { + success: false, + error: result.message || '벤딩 필드 토글에 실패했습니다.', + }; + } + + return { + success: true, + data: transformApiToFrontend(result.data), + }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[WorkOrderActions] toggleBendingField error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } } // ===== 이슈 등록 ===== @@ -239,14 +495,40 @@ export async function addWorkOrderIssue( priority?: 'low' | 'medium' | 'high'; } ): Promise<{ success: boolean; data?: WorkOrder; error?: string }> { - const result = await executeServerAction({ - url: `${API_URL}/api/v1/work-orders/${id}/issues`, - method: 'POST', - body: data, - transform: (d: WorkOrderApi) => transformApiToFrontend(d), - errorMessage: '이슈 등록에 실패했습니다.', - }); - return { success: result.success, data: result.data, error: result.error }; + try { + console.log('[WorkOrderActions] POST issue request:', data); + + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/issues`, + { + method: 'POST', + body: JSON.stringify(data), + } + ); + + if (error || !response) { + return { success: false, error: error?.message || 'API 요청 실패' }; + } + + const result = await response.json(); + console.log('[WorkOrderActions] POST issue response:', result); + + if (!response.ok || !result.success) { + return { + success: false, + error: result.message || '이슈 등록에 실패했습니다.', + }; + } + + return { + success: true, + data: transformApiToFrontend(result.data), + }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[WorkOrderActions] addWorkOrderIssue error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } } // ===== 이슈 해결 ===== @@ -254,13 +536,37 @@ export async function resolveWorkOrderIssue( workOrderId: string, issueId: string ): Promise<{ success: boolean; data?: WorkOrder; error?: string }> { - const result = await executeServerAction({ - url: `${API_URL}/api/v1/work-orders/${workOrderId}/issues/${issueId}/resolve`, - method: 'PATCH', - transform: (d: WorkOrderApi) => transformApiToFrontend(d), - errorMessage: '이슈 해결 처리에 실패했습니다.', - }); - return { success: result.success, data: result.data, error: result.error }; + try { + console.log('[WorkOrderActions] PATCH issue resolve:', { workOrderId, issueId }); + + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/issues/${issueId}/resolve`, + { method: 'PATCH' } + ); + + if (error || !response) { + return { success: false, error: error?.message || 'API 요청 실패' }; + } + + const result = await response.json(); + console.log('[WorkOrderActions] PATCH issue resolve response:', result); + + if (!response.ok || !result.success) { + return { + success: false, + error: result.message || '이슈 해결 처리에 실패했습니다.', + }; + } + + return { + success: true, + data: transformApiToFrontend(result.data), + }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[WorkOrderActions] resolveWorkOrderIssue error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } } // ===== 품목 상태 변경 ===== @@ -278,29 +584,111 @@ export async function updateWorkOrderItemStatus( workOrderStatusChanged?: boolean; error?: string; }> { - interface ItemStatusResponse { - item?: { status: WorkOrderItemStatus }; - work_order_status?: string; - work_order_status_changed?: boolean; - } - const result = await executeServerAction({ - url: `${API_URL}/api/v1/work-orders/${workOrderId}/items/${itemId}/status`, - method: 'PATCH', - body: { status }, - errorMessage: '품목 상태 변경에 실패했습니다.', - }); + try { + console.log('[WorkOrderActions] PATCH item status request:', { workOrderId, itemId, status }); - if (!result.success || !result.data) { - return { success: false, itemId, status, error: result.error }; - } + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/items/${itemId}/status`, + { + method: 'PATCH', + body: JSON.stringify({ status }), + } + ); - return { - success: true, - itemId, - status: result.data.item?.status || status, - workOrderStatus: result.data.work_order_status, - workOrderStatusChanged: result.data.work_order_status_changed || false, + if (error || !response) { + return { success: false, itemId, status, error: error?.message || 'API 요청 실패' }; + } + + const result = await response.json(); + console.log('[WorkOrderActions] PATCH item status response:', result); + + if (!response.ok || !result.success) { + return { + success: false, + itemId, + status, + error: result.message || '품목 상태 변경에 실패했습니다.', + }; + } + + return { + success: true, + itemId, + status: result.data?.item?.status || status, + workOrderStatus: result.data?.work_order_status, + workOrderStatusChanged: result.data?.work_order_status_changed || false, + }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[WorkOrderActions] updateWorkOrderItemStatus error:', error); + return { success: false, itemId, status, error: '서버 오류가 발생했습니다.' }; + } +} + +// ===== 중간검사 성적서 데이터 조회 ===== +export interface InspectionReportItem { + id: number; + item_name: string; + specification: string; + quantity: number; + sort_order: number; + status: string; + options: Record | null; + inspection_data: Record | null; +} + +export interface InspectionReportData { + work_order: { + id: number; + order_no: string; + status: string; + planned_date: string | null; + due_date: string | null; }; + order: { + id: number; + order_no: string; + client_name: string | null; + site_name: string | null; + order_date: string | null; + } | null; + items: InspectionReportItem[]; + summary: { + total_items: number; + inspected_items: number; + passed_items: number; + failed_items: number; + }; +} + +export async function getInspectionReport( + workOrderId: string +): Promise<{ success: boolean; data?: InspectionReportData; error?: string }> { + try { + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/inspection-report`, + { 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('[WorkOrderActions] getInspectionReport error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } } // ===== 중간검사 데이터 저장 (deprecated: WorkerScreen/actions.ts의 saveItemInspection 사용) ===== @@ -310,13 +698,40 @@ export async function saveInspectionData( processType: string, data: unknown ): Promise<{ success: boolean; error?: string }> { - const result = await executeServerAction({ - url: `${API_URL}/api/v1/work-orders/${workOrderId}/inspection`, - method: 'POST', - body: { process_type: processType, inspection_data: data }, - errorMessage: '검사 데이터 저장에 실패했습니다.', - }); - return { success: result.success, error: result.error }; + try { + console.log('[WorkOrderActions] POST inspection data:', { workOrderId, processType }); + + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/inspection`, + { + method: 'POST', + body: JSON.stringify({ + process_type: processType, + inspection_data: data, + }), + } + ); + + if (error || !response) { + return { success: false, error: error?.message || 'API 요청 실패' }; + } + + const result = await response.json(); + console.log('[WorkOrderActions] POST inspection response:', result); + + if (!response.ok || !result.success) { + return { + success: false, + error: result.message || '검사 데이터 저장에 실패했습니다.', + }; + } + + return { success: true }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[WorkOrderActions] saveInspectionData error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } } // ===== 수주 목록 조회 (작업지시 생성용) ===== @@ -339,54 +754,72 @@ export async function getSalesOrdersForWorkOrder(params?: { data: SalesOrderForWorkOrder[]; error?: string; }> { - const searchParams = new URLSearchParams(); - searchParams.set('for_work_order', '1'); - if (params?.q) searchParams.set('q', params.q); - if (params?.status) searchParams.set('status', params.status); + try { + const searchParams = new URLSearchParams(); - const queryString = searchParams.toString(); + // 작업지시 생성 가능한 상태만 조회 (예: 회계확인 완료) + searchParams.set('for_work_order', '1'); + if (params?.q) searchParams.set('q', params.q); + if (params?.status) searchParams.set('status', params.status); - interface SalesOrderApiItem { - id: number; - order_no: string; - client?: { name: string }; - project_name?: string; - due_date?: string; - status: string; - items_count?: number; - split_count?: number; + const queryString = searchParams.toString(); + const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders${queryString ? `?${queryString}` : ''}`; + + console.log('[WorkOrderActions] GET orders for work-order:', url); + + const { response, error } = await serverFetch(url, { method: 'GET' }); + + if (error || !response) { + return { success: false, data: [], error: error?.message || 'API 요청 실패' }; + } + + if (!response.ok) { + console.warn('[WorkOrderActions] GET orders error:', response.status); + return { success: false, data: [], error: `API 오류: ${response.status}` }; + } + + const result = await response.json(); + + if (!result.success) { + return { + success: false, + data: [], + error: result.message || '수주 목록 조회에 실패했습니다.', + }; + } + + // API 응답 변환 + const salesOrders: SalesOrderForWorkOrder[] = (result.data?.data || result.data || []).map( + (item: { + id: number; + order_no: string; + client?: { name: string }; + project_name?: string; + due_date?: string; + status: string; + items_count?: number; + split_count?: number; + }) => ({ + id: item.id, + orderNo: item.order_no, + client: item.client?.name || '-', + projectName: item.project_name || '-', + dueDate: item.due_date || '-', + status: item.status, + itemCount: item.items_count || 0, + splitCount: item.split_count || 0, + }) + ); + + return { + success: true, + data: salesOrders, + }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[WorkOrderActions] getSalesOrdersForWorkOrder error:', error); + return { success: false, data: [], error: '서버 오류가 발생했습니다.' }; } - interface SalesOrderApiResponse { - data?: SalesOrderApiItem[]; - } - - const result = await executeServerAction({ - url: `${API_URL}/api/v1/orders${queryString ? `?${queryString}` : ''}`, - errorMessage: '수주 목록 조회에 실패했습니다.', - }); - - if (!result.success || !result.data) { - return { success: false, data: [], error: result.error }; - } - - const rawData = result.data; - const items: SalesOrderApiItem[] = Array.isArray(rawData) - ? rawData - : (rawData as SalesOrderApiResponse).data || []; - - return { - success: true, - data: items.map((item) => ({ - id: item.id, - orderNo: item.order_no, - client: item.client?.name || '-', - projectName: item.project_name || '-', - dueDate: item.due_date || '-', - status: item.status, - itemCount: item.items_count || 0, - splitCount: item.split_count || 0, - })), - }; } // ===== 부서 + 사용자 조회 (담당자 선택용) ===== @@ -409,32 +842,64 @@ export async function getDepartmentsWithUsers(): Promise<{ data: DepartmentWithUsers[]; error?: string; }> { - interface DeptApiItem { - id: number; - name: string; - code: string | null; - users?: { id: number; name: string; email: string }[]; - children?: DeptApiItem[]; + try { + const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/departments/tree?with_users=1`; + + console.log('[WorkOrderActions] GET departments with users:', url); + + const { response, error } = await serverFetch(url, { method: 'GET' }); + + if (error || !response) { + return { success: false, data: [], error: error?.message || 'API 요청 실패' }; + } + + if (!response.ok) { + console.warn('[WorkOrderActions] GET departments error:', response.status); + return { success: false, data: [], error: `API 오류: ${response.status}` }; + } + + const result = await response.json(); + + if (!result.success) { + return { + success: false, + data: [], + error: result.message || '부서 목록 조회에 실패했습니다.', + }; + } + + // API 응답을 프론트엔드 형식으로 변환 + const transformDepartment = (dept: { + id: number; + name: string; + code: string | null; + users?: { id: number; name: string; email: string }[]; + children?: unknown[]; + }): DepartmentWithUsers => ({ + id: dept.id, + name: dept.name, + code: dept.code, + users: (dept.users || []).map((u) => ({ + id: u.id, + name: u.name, + email: u.email, + })), + children: (dept.children || []).map((child) => + transformDepartment(child as typeof dept) + ), + }); + + const departments = (result.data || []).map(transformDepartment); + + return { + success: true, + data: departments, + }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[WorkOrderActions] getDepartmentsWithUsers error:', error); + return { success: false, data: [], error: '서버 오류가 발생했습니다.' }; } - - const result = await executeServerAction({ - url: `${API_URL}/api/v1/departments/tree?with_users=1`, - errorMessage: '부서 목록 조회에 실패했습니다.', - }); - - if (!result.success || !result.data) { - return { success: false, data: [], error: result.error }; - } - - const transformDepartment = (dept: DeptApiItem): DepartmentWithUsers => ({ - id: dept.id, - name: dept.name, - code: dept.code, - users: (dept.users || []).map((u) => ({ id: u.id, name: u.name, email: u.email })), - children: (dept.children || []).map(transformDepartment), - }); - - return { success: true, data: result.data.map(transformDepartment) }; } // ===== 공정 목록 조회 (작업지시 생성용) ===== @@ -449,26 +914,52 @@ export async function getProcessOptions(): Promise<{ data: ProcessOption[]; error?: string; }> { - interface ProcessApiItem { - id: number; - process_code: string; - process_name: string; - } - const result = await executeServerAction({ - url: `${API_URL}/api/v1/processes/options`, - errorMessage: '공정 목록 조회에 실패했습니다.', - }); + try { + const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes/options`; - if (!result.success || !result.data) { - return { success: false, data: [], error: result.error }; - } + console.log('[WorkOrderActions] GET process options:', url); - return { - success: true, - data: result.data.map((item) => ({ - id: item.id, - processCode: item.process_code, - processName: item.process_name, - })), - }; + const { response, error } = await serverFetch(url, { method: 'GET' }); + + if (error || !response) { + return { success: false, data: [], error: error?.message || 'API 요청 실패' }; + } + + if (!response.ok) { + console.warn('[WorkOrderActions] GET process options error:', response.status); + return { success: false, data: [], error: `API 오류: ${response.status}` }; + } + + const result = await response.json(); + + if (!result.success) { + return { + success: false, + data: [], + error: result.message || '공정 목록 조회에 실패했습니다.', + }; + } + + // API 응답 변환 + const processes: ProcessOption[] = (result.data || []).map( + (item: { + id: number; + process_code: string; + process_name: string; + }) => ({ + id: item.id, + processCode: item.process_code, + processName: item.process_name, + }) + ); + + return { + success: true, + data: processes, + }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[WorkOrderActions] getProcessOptions error:', error); + return { success: false, data: [], error: '서버 오류가 발생했습니다.' }; + } } diff --git a/src/components/production/WorkOrders/documents/BendingInspectionContent.tsx b/src/components/production/WorkOrders/documents/BendingInspectionContent.tsx index eba56c5e..e27b8b18 100644 --- a/src/components/production/WorkOrders/documents/BendingInspectionContent.tsx +++ b/src/components/production/WorkOrders/documents/BendingInspectionContent.tsx @@ -19,31 +19,37 @@ import type { WorkOrder } from '../types'; import type { InspectionData } from '@/components/production/WorkerScreen/InspectionInputModal'; import type { WorkItemData } from '@/components/production/WorkerScreen/types'; import type { InspectionDataMap } from './InspectionReportModal'; +import { + type CheckStatus, + type InspectionContentRef, + convertToCheckStatus, + getFullDate, + getToday, + getOrderInfo, + calculateOverallResult, + INPUT_CLASS, + InspectionCheckbox, + JudgmentCell, + InspectionLayout, + InspectionFooter, +} from './inspection-shared'; -export interface InspectionContentRef { - getInspectionData: () => unknown; -} +export type { InspectionContentRef }; export interface BendingInspectionContentProps { data: WorkOrder; readOnly?: boolean; inspectionData?: InspectionData; - /** 작업 아이템 목록 - 행 개수 동적 생성용 */ workItems?: WorkItemData[]; - /** 아이템별 검사 데이터 맵 */ inspectionDataMap?: InspectionDataMap; - /** 기준서 도해 이미지 URL */ schematicImage?: string; - /** 검사기준 이미지 URL */ inspectionStandardImage?: string; } -type CheckStatus = '양호' | '불량' | null; - interface GapPoint { - point: string; // ①②③④⑤ - designValue: string; // 도면치수 - measured: string; // 측정값 (입력) + point: string; + designValue: string; + measured: string; } interface ProductRow { @@ -59,20 +65,7 @@ interface ProductRow { gapPoints: GapPoint[]; } -/** - * 절곡 검사성적서 - 가이드레일 타입별 행 구조 - * - * | 타입 조합 | 가이드레일 행 개수 | - * |-----------------------|-------------------| - * | 벽면형/벽면형 (벽벽) | 1행 | - * | 측면형/측면형 (측측) | 1행 | - * | 벽면형/측면형 (혼합형) | 2행 (규격이 달라서) | - * - * TODO: 실제 구현 시 공정 데이터에서 타입 정보를 받아서 - * INITIAL_PRODUCTS를 동적으로 생성해야 함 - */ const INITIAL_PRODUCTS: Omit[] = [ - // 현재 목업: 혼합형(벽/측)인 경우 가이드레일 2행 { id: 'guide-rail-wall', category: 'KWE01', productName: '가이드레일', productType: '벽면형', lengthDesign: '3000', widthDesign: 'N/A', @@ -138,13 +131,6 @@ const INITIAL_PRODUCTS: Omit { - if (status === 'good') return '양호'; - if (status === 'bad') return '불량'; - return null; -}; - export const BendingInspectionContent = forwardRef(function BendingInspectionContent({ data: order, readOnly = false, @@ -153,20 +139,9 @@ export const BendingInspectionContent = forwardRef a.isPrimary)?.name || order.assignee || '-'; + const fullDate = getFullDate(); + const today = getToday(); + const { documentNo, primaryAssignee } = getOrderInfo(order); const [products, setProducts] = useState(() => INITIAL_PRODUCTS.map(p => ({ @@ -180,7 +155,6 @@ export const BendingInspectionContent = forwardRef { if (workItems && workItems.length > 0 && inspectionDataMap) { const firstItem = workItems[0]; @@ -220,20 +194,13 @@ export const BendingInspectionContent = forwardRef { if (product.bendingStatus === '불량') return '부'; if (product.bendingStatus === '양호') return '적'; return null; }, []); - // 종합판정 자동 계산 - const overallResult = useMemo(() => { - const judgments = products.map(getProductJudgment); - if (judgments.some(j => j === '부')) return '불합격'; - if (judgments.every(j => j === '적')) return '합격'; - return null; - }, [products, getProductJudgment]); + const overallResult = useMemo(() => calculateOverallResult(products.map(getProductJudgment)), [products, getProductJudgment]); useImperativeHandle(ref, () => ({ getInspectionData: () => ({ @@ -256,62 +223,8 @@ export const BendingInspectionContent = forwardRef void) => ( - !readOnly && onClick()} - role="checkbox" - aria-checked={checked} - > - {checked ? '✓' : ''} - - ); - - const inputClass = 'w-full text-center border-0 bg-transparent focus:outline-none focus:ring-1 focus:ring-blue-500 rounded text-xs'; - - // 전체 행 수 계산 (간격 포인트 수 합계) - const totalRows = products.reduce((sum, p) => sum + p.gapPoints.length, 0); - return ( -
- {/* ===== 헤더 영역 ===== */} -
-
-

중간검사성적서 (절곡)

-

- 문서번호: {documentNo} | 작성일자: {fullDate} -

-
- - {/* 결재란 */} - - - - - - - - - - - - - - - - - - - - - - -

작성승인승인승인
{primaryAssignee}이름이름이름
부서명부서명부서명부서명
-
- + {/* ===== 기본 정보 ===== */} @@ -362,7 +275,6 @@ export const BendingInspectionContent = forwardRef - {/* 도해 3개 (가이드레일 / 케이스 / 하단마감재) */} - {/* 기준서 헤더 */} @@ -399,7 +310,6 @@ export const BendingInspectionContent = forwardRef검사주기 - {/* 겉모양 | 절곡상태 */} - {/* 치수 > 길이 */} @@ -419,7 +328,6 @@ export const BendingInspectionContent = forwardRef체크검사 - {/* 치수 > 간격 */} @@ -441,7 +349,6 @@ export const BendingInspectionContent = forwardRef - {/* 헤더 */} - {/* 겉모양 | 절곡상태 (row 1) */} - {/* 겉모양 | 절곡상태 (row 2 - 관련규정 분리) */} - {/* 치수 > 길이 */} @@ -481,12 +385,10 @@ export const BendingInspectionContent = forwardRef체크검사 - {/* 치수 > 나비 */} - {/* 치수 > 간격 */} @@ -525,50 +427,48 @@ export const BendingInspectionContent = forwardRef ( - {/* 첫 번째 간격 행에만 rowSpan 적용 */} {gapIdx === 0 && ( <> - {/* 절곡상태 - 양호/불량 체크 */} - {/* 길이 */} - {/* 너비 */} )} - {/* 간격 - 포인트별 개별 행 */} - {/* 판정 - 자동 (첫 행에만) */} {gapIdx === 0 && ( - + )} )); @@ -577,23 +477,12 @@ export const BendingInspectionContent = forwardRef {/* ===== 부적합 내용 + 종합판정 ===== */} -
가이드레일
케이스
하단마감재
하단 L-BAR @@ -390,7 +302,6 @@ export const BendingInspectionContent = forwardRef
도해 검사항목관련규정
절곡류 중간검사
상세도면 참조 @@ -411,7 +321,6 @@ export const BendingInspectionContent = forwardRefn = 1, c = 0
KS F 4510 5.1항
치수
(mm)
길이KS F 4510 7항
표9
간격 도면치수 ± 2
연기차단재 @@ -453,7 +360,6 @@ export const BendingInspectionContent = forwardRef검사주기 관련규정
{inspectionStandardImage ? ( @@ -469,11 +375,9 @@ export const BendingInspectionContent = forwardRefn = 1, c = 0 KS F 4510 5.1항
KS F 4510 7항
표9 인용
치수
(mm)
길이자체규정
나비 W50 : 50 ± 5
W80 : 80 ± 5
간격 도면치수 ± 2
{product.category} {product.productName} {product.productType}
{product.lengthDesign} - handleInputChange(product.id, 'lengthMeasured', e.target.value)} disabled={readOnly} className={inputClass} placeholder="-" /> + handleInputChange(product.id, 'lengthMeasured', e.target.value)} disabled={readOnly} className={INPUT_CLASS} placeholder="-" /> {product.widthDesign || 'N/A'} - handleInputChange(product.id, 'widthMeasured', e.target.value)} disabled={readOnly} className={inputClass} placeholder="-" /> + handleInputChange(product.id, 'widthMeasured', e.target.value)} disabled={readOnly} className={INPUT_CLASS} placeholder="-" /> {gap.point} {gap.designValue} - handleGapMeasuredChange(product.id, gapIdx, e.target.value)} disabled={readOnly} className={inputClass} placeholder="-" /> + handleGapMeasuredChange(product.id, gapIdx, e.target.value)} disabled={readOnly} className={INPUT_CLASS} placeholder="-" /> - {judgment || '-'} -
- - - -
부적합 내용 -