diff --git a/src/components/process-management/ProcessDetail.tsx b/src/components/process-management/ProcessDetail.tsx index 8e2685ba..87615ef9 100644 --- a/src/components/process-management/ProcessDetail.tsx +++ b/src/components/process-management/ProcessDetail.tsx @@ -19,7 +19,7 @@ import { PageLayout } from '@/components/organisms/PageLayout'; import { PageHeader } from '@/components/organisms/PageHeader'; import { useMenuStore } from '@/store/menuStore'; import { usePermission } from '@/hooks/usePermission'; -import { getProcessSteps } from './actions'; +import { getProcessSteps, reorderProcessSteps } from './actions'; import type { Process, ProcessStep } from '@/types/process'; interface ProcessDetailProps { @@ -111,12 +111,19 @@ export function ProcessDetail({ process }: ProcessDetailProps) { const updated = [...prev]; const [moved] = updated.splice(dragIndex, 1); updated.splice(dropIndex, 0, moved); - // 순서 재할당 - return updated.map((step, i) => ({ ...step, order: i + 1 })); + const reordered = updated.map((step, i) => ({ ...step, order: i + 1 })); + + // API 순서 변경 호출 + reorderProcessSteps( + process.id, + reordered.map((s) => ({ id: s.id, order: s.order })) + ); + + return reordered; }); handleDragEnd(); - }, [dragIndex, handleDragEnd]); + }, [dragIndex, handleDragEnd, process.id]); return ( diff --git a/src/components/process-management/ProcessListClient.tsx b/src/components/process-management/ProcessListClient.tsx index 3b65cb89..d5e403ab 100644 --- a/src/components/process-management/ProcessListClient.tsx +++ b/src/components/process-management/ProcessListClient.tsx @@ -329,7 +329,7 @@ export default function ProcessListClient({ initialData = [], initialStats }: Pr {process.processCode} {process.processName} {process.department} - {process.workSteps.length} + {process.steps?.length ?? 0} {itemCount > 0 ? itemCount : '-'} e.stopPropagation()}> - + 0 ? `${itemCount}개` : '-'} /> } diff --git a/src/components/process-management/actions.ts b/src/components/process-management/actions.ts index d61518c3..91d2b64d 100644 --- a/src/components/process-management/actions.ts +++ b/src/components/process-management/actions.ts @@ -27,6 +27,7 @@ interface ApiProcess { updated_at: string; classification_rules?: ApiClassificationRule[]; process_items?: ApiProcessItem[]; + steps?: ApiProcessStep[]; } interface ApiClassificationRule { @@ -93,6 +94,7 @@ function transformApiToFrontend(apiData: ApiProcess): Process { requiredWorkers: apiData.required_workers, equipmentInfo: apiData.equipment_info ?? undefined, workSteps: apiData.work_steps ?? [], + steps: (apiData.steps ?? []).map(transformStepApiToFrontend), note: apiData.note ?? undefined, status: apiData.is_active ? '사용중' : '미사용', createdAt: apiData.created_at, @@ -644,13 +646,13 @@ export async function getItemList(params?: GetItemListParams): Promise ({ + return result.data.data.map((item: { id: number; name: string; item_code?: string; item_type?: string; item_type_name?: string }) => ({ value: String(item.id), label: item.name, - code: item.code || '', + code: item.item_code || '', id: String(item.id), fullName: item.name, - type: item.item_type || '', + type: item.item_type_name || item.item_type || '', })); } @@ -662,151 +664,313 @@ export async function getItemList(params?: GetItemListParams): Promise> { + try { + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/settings/common/item_type`, + { + method: 'GET', + cache: 'no-store', + } + ); + + if (error || !response?.ok) { + return []; + } + + const result = await response.json(); + if (result.success && Array.isArray(result.data)) { + return result.data.map((item: { code: string; name: string }) => ({ + value: item.code, + label: item.name, + })); + } + + return []; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[getItemTypeOptions] Error:', error); + return []; + } +} + // ============================================================================ -// 공정 단계 (Process Step) - 목데이터 및 스텁 함수 -// 백엔드 API 미준비 → 프론트엔드 목데이터로 운영 +// 공정 단계 (Process Step) API // ============================================================================ -const MOCK_STEPS: Record = { - // processId별로 목데이터 보유 - default: [ - { - id: 'step-1', - stepCode: 'STP-001', - stepName: '자재투입', - isRequired: true, - needsApproval: false, - needsInspection: false, - isActive: true, - order: 1, - connectionType: '팝업', - connectionTarget: '입고완료 자재 목록', - completionType: '선택 완료 시 완료', - }, - { - id: 'step-2', - stepCode: 'STP-002', - stepName: '미싱', - isRequired: true, - needsApproval: false, - needsInspection: false, - isActive: true, - order: 2, - connectionType: '없음', - completionType: '클릭 시 완료', - }, - { - id: 'step-3', - stepCode: 'STP-003', - stepName: '중간검사', - isRequired: false, - needsApproval: true, - needsInspection: true, - isActive: true, - order: 3, - connectionType: '없음', - completionType: '클릭 시 완료', - }, - { - id: 'step-4', - stepCode: 'STP-004', - stepName: '포장', - isRequired: true, - needsApproval: false, - needsInspection: false, - isActive: true, - order: 4, - connectionType: '없음', - completionType: '클릭 시 완료', - }, - ], -}; +interface ApiProcessStep { + id: number; + process_id: number; + step_code: string; + step_name: string; + is_required: boolean; + needs_approval: boolean; + needs_inspection: boolean; + is_active: boolean; + sort_order: number; + connection_type: string | null; + connection_target: string | null; + completion_type: string | null; + created_at: string; + updated_at: string; +} + +function transformStepApiToFrontend(apiStep: ApiProcessStep): ProcessStep { + return { + id: String(apiStep.id), + stepCode: apiStep.step_code, + stepName: apiStep.step_name, + isRequired: apiStep.is_required, + needsApproval: apiStep.needs_approval, + needsInspection: apiStep.needs_inspection, + isActive: apiStep.is_active, + order: apiStep.sort_order, + connectionType: (apiStep.connection_type as ProcessStep['connectionType']) || '없음', + connectionTarget: apiStep.connection_target ?? undefined, + completionType: (apiStep.completion_type as ProcessStep['completionType']) || '클릭 시 완료', + }; +} /** - * 공정 단계 목록 조회 (목데이터) + * 공정 단계 목록 조회 */ export async function getProcessSteps(processId: string): Promise<{ success: boolean; data?: ProcessStep[]; error?: string; }> { - // 목데이터 반환 - const steps = MOCK_STEPS[processId] || MOCK_STEPS['default']; - return { success: true, data: steps }; + try { + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes/${processId}/steps`, + { method: 'GET', cache: 'no-store' } + ); + + if (error) { + return { success: false, error: error.message }; + } + + if (!response) { + return { success: false, error: '단계 목록 조회에 실패했습니다.' }; + } + + const result: ApiResponse = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '단계 목록 조회에 실패했습니다.' }; + } + + return { success: true, data: result.data.map(transformStepApiToFrontend) }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[getProcessSteps] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } } /** - * 공정 단계 상세 조회 (목데이터) + * 공정 단계 상세 조회 */ export async function getProcessStepById(processId: string, stepId: string): Promise<{ success: boolean; data?: ProcessStep; error?: string; }> { - const steps = MOCK_STEPS[processId] || MOCK_STEPS['default']; - const step = steps.find((s) => s.id === stepId); - if (!step) { - return { success: false, error: '단계를 찾을 수 없습니다.' }; + try { + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes/${processId}/steps/${stepId}`, + { method: 'GET', cache: 'no-store' } + ); + + if (error) { + return { success: false, error: error.message }; + } + + if (!response) { + return { success: false, error: '단계 조회에 실패했습니다.' }; + } + + const result: ApiResponse = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '단계 조회에 실패했습니다.' }; + } + + return { success: true, data: transformStepApiToFrontend(result.data) }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[getProcessStepById] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; } - return { success: true, data: step }; } /** - * 공정 단계 생성 (스텁) + * 공정 단계 생성 */ export async function createProcessStep( - _processId: string, + processId: string, data: Omit ): Promise<{ success: boolean; data?: ProcessStep; error?: string }> { - const newStep: ProcessStep = { - ...data, - id: `step-${Date.now()}`, - }; - return { success: true, data: newStep }; + try { + const apiData = { + step_name: data.stepName, + is_required: data.isRequired, + needs_approval: data.needsApproval, + needs_inspection: data.needsInspection, + is_active: data.isActive, + connection_type: data.connectionType || null, + connection_target: data.connectionTarget || null, + completion_type: data.completionType || null, + }; + + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes/${processId}/steps`, + { method: 'POST', body: JSON.stringify(apiData) } + ); + + if (error) { + return { success: false, error: error.message }; + } + + if (!response) { + return { success: false, error: '단계 등록에 실패했습니다.' }; + } + + const result: ApiResponse = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '단계 등록에 실패했습니다.' }; + } + + return { success: true, data: transformStepApiToFrontend(result.data) }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[createProcessStep] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } } /** - * 공정 단계 수정 (스텁) + * 공정 단계 수정 */ export async function updateProcessStep( - _processId: string, + processId: string, stepId: string, data: Partial ): Promise<{ success: boolean; data?: ProcessStep; error?: string }> { - return { - success: true, - data: { - id: stepId, - stepCode: data.stepCode || '', - stepName: data.stepName || '', - isRequired: data.isRequired ?? false, - needsApproval: data.needsApproval ?? false, - needsInspection: data.needsInspection ?? false, - isActive: data.isActive ?? true, - order: data.order ?? 0, - connectionType: data.connectionType || '없음', - connectionTarget: data.connectionTarget, - completionType: data.completionType || '클릭 시 완료', - }, - }; + try { + const apiData: Record = {}; + if (data.stepName !== undefined) apiData.step_name = data.stepName; + if (data.isRequired !== undefined) apiData.is_required = data.isRequired; + if (data.needsApproval !== undefined) apiData.needs_approval = data.needsApproval; + if (data.needsInspection !== undefined) apiData.needs_inspection = data.needsInspection; + if (data.isActive !== undefined) apiData.is_active = data.isActive; + if (data.connectionType !== undefined) apiData.connection_type = data.connectionType || null; + if (data.connectionTarget !== undefined) apiData.connection_target = data.connectionTarget || null; + if (data.completionType !== undefined) apiData.completion_type = data.completionType || null; + + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes/${processId}/steps/${stepId}`, + { method: 'PUT', body: JSON.stringify(apiData) } + ); + + if (error) { + return { success: false, error: error.message }; + } + + if (!response) { + return { success: false, error: '단계 수정에 실패했습니다.' }; + } + + const result: ApiResponse = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '단계 수정에 실패했습니다.' }; + } + + return { success: true, data: transformStepApiToFrontend(result.data) }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[updateProcessStep] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } } /** - * 공정 단계 삭제 (스텁) + * 공정 단계 삭제 */ export async function deleteProcessStep( - _processId: string, - _stepId: string + processId: string, + stepId: string ): Promise<{ success: boolean; error?: string }> { - return { success: true }; + try { + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes/${processId}/steps/${stepId}`, + { method: 'DELETE' } + ); + + if (error) { + return { success: false, error: error.message }; + } + + if (!response) { + return { success: false, error: '단계 삭제에 실패했습니다.' }; + } + + const result: ApiResponse = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '단계 삭제에 실패했습니다.' }; + } + + return { success: true }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[deleteProcessStep] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } } /** - * 공정 단계 순서 변경 (스텁) + * 공정 단계 순서 변경 */ export async function reorderProcessSteps( - _processId: string, - _stepIds: string[] -): Promise<{ success: boolean; error?: string }> { - return { success: true }; + processId: string, + steps: { id: string; order: number }[] +): Promise<{ success: boolean; data?: ProcessStep[]; error?: string }> { + try { + const apiData = { + items: steps.map((s) => ({ + id: parseInt(s.id, 10), + sort_order: s.order, + })), + }; + + const { response, error } = await serverFetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/processes/${processId}/steps/reorder`, + { method: 'PATCH', body: JSON.stringify(apiData) } + ); + + if (error) { + return { success: false, error: error.message }; + } + + if (!response) { + return { success: false, error: '순서 변경에 실패했습니다.' }; + } + + const result: ApiResponse = await response.json(); + + if (!response.ok || !result.success) { + return { success: false, error: result.message || '순서 변경에 실패했습니다.' }; + } + + return { success: true, data: result.data.map(transformStepApiToFrontend) }; + } catch (error) { + if (isNextRedirectError(error)) throw error; + console.error('[reorderProcessSteps] Error:', error); + return { success: false, error: '서버 오류가 발생했습니다.' }; + } }