/** * 작업지시 관리 서버 액션 * * API Endpoints: * - GET /api/v1/work-orders - 목록 조회 * - GET /api/v1/work-orders/stats - 통계 조회 * - GET /api/v1/work-orders/{id} - 상세 조회 * - POST /api/v1/work-orders - 등록 * - PUT /api/v1/work-orders/{id} - 수정 * - DELETE /api/v1/work-orders/{id} - 삭제 * - PATCH /api/v1/work-orders/{id}/status - 상태 변경 * - PATCH /api/v1/work-orders/{id}/assign - 담당자 배정 * - PATCH /api/v1/work-orders/{id}/bending/toggle - 벤딩 필드 토글 * - POST /api/v1/work-orders/{id}/issues - 이슈 등록 * - PATCH /api/v1/work-orders/{id}/issues/{issueId}/resolve - 이슈 해결 * - PATCH /api/v1/work-orders/{id}/items/{itemId}/status - 품목 상태 변경 */ 'use server'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { serverFetch } from '@/lib/api/fetch-wrapper'; import type { WorkOrder, WorkOrderStats, WorkOrderStatus, WorkOrderApiPaginatedResponse, WorkOrderStatsApi, } from './types'; import { transformApiToFrontend, transformFrontendToApi, transformStatsApiToFrontend, } from './types'; // ===== 페이지네이션 타입 ===== interface PaginationMeta { currentPage: number; lastPage: number; perPage: number; total: number; } // ===== 작업지시 목록 조회 ===== export async function getWorkOrders(params?: { page?: number; perPage?: number; status?: WorkOrderStatus | 'all'; processId?: number | 'all'; // 공정 ID (FK → processes.id) search?: string; startDate?: string; endDate?: string; }): Promise<{ success: boolean; data: WorkOrder[]; pagination: PaginationMeta; error?: string; }> { 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') { searchParams.set('process_id', String(params.processId)); } 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: '서버 오류가 발생했습니다.' }; } } // ===== 작업지시 통계 조회 ===== export async function getWorkOrderStats(): Promise<{ success: boolean; data?: WorkOrderStats; error?: string; }> { 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: '서버 오류가 발생했습니다.' }; } } // ===== 작업지시 상세 조회 ===== export async function getWorkOrderById(id: string): Promise<{ success: boolean; data?: WorkOrder; error?: string; }> { 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[]; // 다중 담당자 teamId?: number; } ): Promise<{ success: boolean; data?: WorkOrder; error?: string }> { 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, }; 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: '서버 오류가 발생했습니다.' }; } } // ===== 작업지시 수정 ===== export async function updateWorkOrder( id: string, data: Partial ): Promise<{ success: boolean; data?: WorkOrder; error?: string }> { 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 }> { 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: '서버 오류가 발생했습니다.' }; } } // ===== 작업지시 상태 변경 ===== export async function updateWorkOrderStatus( id: string, status: WorkOrderStatus ): Promise<{ success: boolean; data?: WorkOrder; error?: string }> { 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[], // 단일 또는 다중 담당자 teamId?: number ): Promise<{ success: boolean; data?: WorkOrder; error?: string }> { 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; 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: '서버 오류가 발생했습니다.' }; } } // ===== 벤딩 필드 토글 ===== export async function toggleBendingField( id: string, field: string ): Promise<{ success: boolean; data?: WorkOrder; error?: string }> { 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: '서버 오류가 발생했습니다.' }; } } // ===== 이슈 등록 ===== export async function addWorkOrderIssue( id: string, data: { title: string; description?: string; priority?: 'low' | 'medium' | 'high'; } ): Promise<{ success: boolean; data?: WorkOrder; error?: string }> { 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: '서버 오류가 발생했습니다.' }; } } // ===== 이슈 해결 ===== export async function resolveWorkOrderIssue( workOrderId: string, issueId: string ): Promise<{ success: boolean; data?: WorkOrder; error?: string }> { 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: '서버 오류가 발생했습니다.' }; } } // ===== 품목 상태 변경 ===== export type WorkOrderItemStatus = 'waiting' | 'in_progress' | 'completed'; export async function updateWorkOrderItemStatus( workOrderId: string, itemId: number, status: WorkOrderItemStatus ): Promise<{ success: boolean; itemId: number; status: WorkOrderItemStatus; workOrderStatus?: string; workOrderStatusChanged?: boolean; error?: string; }> { try { console.log('[WorkOrderActions] PATCH item status request:', { workOrderId, itemId, status }); 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 }), } ); 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 SalesOrderForWorkOrder { id: number; orderNo: string; client: string; projectName: string; dueDate: string; status: string; itemCount: number; splitCount: number; } export async function getSalesOrdersForWorkOrder(params?: { q?: string; status?: string; }): Promise<{ success: boolean; data: SalesOrderForWorkOrder[]; error?: string; }> { try { 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); 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: '서버 오류가 발생했습니다.' }; } } // ===== 부서 + 사용자 조회 (담당자 선택용) ===== export interface DepartmentUser { id: number; name: string; email: string; } export interface DepartmentWithUsers { id: number; name: string; code: string | null; users: DepartmentUser[]; children: DepartmentWithUsers[]; } export async function getDepartmentsWithUsers(): Promise<{ success: boolean; data: DepartmentWithUsers[]; error?: string; }> { 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: '서버 오류가 발생했습니다.' }; } } // ===== 공정 목록 조회 (작업지시 생성용) ===== export interface ProcessOption { id: number; processCode: string; processName: string; } export async function getProcessOptions(): Promise<{ success: boolean; data: ProcessOption[]; error?: string; }> { try { const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes/options`; console.log('[WorkOrderActions] GET process options:', 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 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: '서버 오류가 발생했습니다.' }; } }