'use server'; import { executeServerAction } from '@/lib/api/execute-server-action'; import { buildApiUrl } from '@/lib/api/query-params'; import type { PaginatedApiResponse } from '@/lib/api/types'; import type { Process, ProcessFormData, ClassificationRule, IndividualItem, ProcessStep } from '@/types/process'; // ============================================================================ // API 타입 정의 // ============================================================================ interface ApiProcess { id: number; tenant_id: number; process_code: string; process_name: string; description: string | null; process_type: string; department: string | null; manager: string | null; process_category: string | null; use_production_date: boolean; work_log_template: string | null; document_template_id: number | null; document_template?: { id: number; name: string; category: string } | null; work_log_template_id: number | null; work_log_template_relation?: { id: number; name: string; category: string } | null; options?: { needs_inspection?: boolean; needs_work_log?: boolean; } | null; required_workers: number; equipment_info: string | null; work_steps: string[] | null; note: string | null; is_active: boolean; created_at: string; updated_at: string; classification_rules?: ApiClassificationRule[]; process_items?: ApiProcessItem[]; steps?: ApiProcessStep[]; } interface ApiClassificationRule { id: number; process_id: number; registration_type: string; rule_type: string; matching_type: string; condition_value: string; priority: number; description: string | null; is_active: boolean; created_at: string; updated_at: string; } interface ApiProcessItem { id: number; process_id: number; item_id: number; priority: number; is_active: boolean; item?: { id: number; code: string; name: string; }; } interface ApiResponse { success: boolean; message: string; data: T; } // ============================================================================ // 데이터 변환 함수 // ============================================================================ function transformApiToFrontend(apiData: ApiProcess): Process { // Pattern 규칙 변환 const patternRules = (apiData.classification_rules ?? []).map(transformRuleApiToFrontend); // 개별 품목 → individual 분류 규칙으로 변환 const individualRules = transformProcessItemsToRules(apiData.process_items ?? []); return { id: String(apiData.id), processCode: apiData.process_code, processName: apiData.process_name, description: apiData.description ?? undefined, processType: apiData.process_type as Process['processType'], department: apiData.department ?? '', manager: apiData.manager ?? undefined, processCategory: apiData.process_category ?? undefined, useProductionDate: apiData.use_production_date ?? false, workLogTemplate: apiData.work_log_template ?? undefined, documentTemplateId: apiData.document_template_id ?? undefined, documentTemplateName: apiData.document_template?.name ?? undefined, workLogTemplateId: apiData.work_log_template_id ?? undefined, workLogTemplateName: apiData.work_log_template_relation?.name ?? undefined, needsInspection: apiData.options?.needs_inspection ?? false, needsWorkLog: apiData.options?.needs_work_log ?? false, classificationRules: [...patternRules, ...individualRules], 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, updatedAt: apiData.updated_at, }; } /** * process_items 배열을 individual 분류 규칙으로 변환 * 모든 개별 품목을 하나의 규칙으로 통합 */ function transformProcessItemsToRules(processItems: ApiProcessItem[]): ClassificationRule[] { if (processItems.length === 0) return []; const activeItems = processItems.filter(pi => pi.is_active); if (activeItems.length === 0) return []; // 모든 품목 ID를 쉼표로 구분하여 하나의 규칙으로 통합 const itemIds = activeItems .map(pi => String(pi.item_id)) .join(','); // 품목 상세 정보 추출 (code, name 포함) const items: IndividualItem[] = activeItems .filter(pi => pi.item) // item 정보가 있는 것만 .map(pi => ({ id: String(pi.item!.id), code: pi.item!.code, name: pi.item!.name, })); return [{ id: `individual-${Date.now()}`, registrationType: 'individual', ruleType: '품목코드', matchingType: 'equals', conditionValue: itemIds, priority: 0, description: `개별 품목 ${activeItems.length}개`, isActive: true, createdAt: new Date().toISOString(), items, // 품목 상세 정보 추가 }]; } function transformRuleApiToFrontend(apiRule: ApiClassificationRule): ClassificationRule { return { id: String(apiRule.id), registrationType: apiRule.registration_type as ClassificationRule['registrationType'], ruleType: apiRule.rule_type as ClassificationRule['ruleType'], matchingType: apiRule.matching_type as ClassificationRule['matchingType'], conditionValue: apiRule.condition_value, priority: apiRule.priority, description: apiRule.description ?? undefined, isActive: apiRule.is_active, createdAt: apiRule.created_at, }; } function transformFrontendToApi(data: ProcessFormData): Record { // 패턴 규칙만 분리 (individual 제외) const patternRules = data.classificationRules.filter( (rule) => rule.registrationType === 'pattern' ); // 개별 품목 규칙에서 item_ids 추출 const individualRules = data.classificationRules.filter( (rule) => rule.registrationType === 'individual' ); // 개별 품목의 conditionValue에서 ID 배열 추출 (쉼표 구분) const itemIds: number[] = individualRules.flatMap((rule) => rule.conditionValue .split(',') .map((id) => parseInt(id.trim(), 10)) .filter((n) => !isNaN(n) && n > 0) ); return { process_name: data.processName, process_type: data.processType, department: data.department || null, manager: data.manager || null, process_category: data.processCategory || null, use_production_date: data.useProductionDate ?? false, work_log_template: data.workLogTemplate || null, document_template_id: data.documentTemplateId || null, work_log_template_id: data.workLogTemplateId || null, options: { needs_inspection: data.needsInspection ?? false, needs_work_log: data.needsWorkLog ?? false, }, required_workers: data.requiredWorkers, equipment_info: data.equipmentInfo || null, work_steps: data.workSteps ? data.workSteps.split(',').map((s) => s.trim()).filter(Boolean) : [], note: data.note || null, is_active: data.isActive, // 패턴 규칙만 전송 (registration_type 제외) classification_rules: patternRules.map((rule) => ({ rule_type: rule.ruleType, matching_type: rule.matchingType, condition_value: rule.conditionValue, priority: rule.priority, description: rule.description || null, is_active: rule.isActive, })), // 개별 품목 ID 배열 전송 item_ids: itemIds, }; } // ============================================================================ // API 함수 // ============================================================================ /** * 공정 목록 조회 */ export async function getProcessList(params?: { page?: number; size?: number; q?: string; status?: string; process_type?: string; }): Promise<{ success: boolean; data?: { items: Process[]; total: number; page: number; totalPages: number }; error?: string; __authError?: boolean }> { const result = await executeServerAction>({ url: buildApiUrl('/api/v1/processes', { page: params?.page, size: params?.size, q: params?.q, status: params?.status, process_type: params?.process_type, }), errorMessage: '공정 목록 조회에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; if (!result.success || !result.data) return { success: false, error: result.error }; return { success: true, data: { items: result.data.data.map(transformApiToFrontend), total: result.data.total, page: result.data.current_page, totalPages: result.data.last_page, }, }; } /** * 공정 상세 조회 */ export async function getProcessById(id: string): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> { const result = await executeServerAction({ url: buildApiUrl(`/api/v1/processes/${id}`), transform: (data: ApiProcess) => transformApiToFrontend(data), errorMessage: '공정 조회에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; return { success: result.success, data: result.data, error: result.error }; } /** * 공정 생성 */ export async function createProcess(data: ProcessFormData): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> { const result = await executeServerAction({ url: buildApiUrl('/api/v1/processes'), method: 'POST', body: transformFrontendToApi(data), transform: (d: ApiProcess) => transformApiToFrontend(d), errorMessage: '공정 등록에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; return { success: result.success, data: result.data, error: result.error }; } /** * 공정 수정 */ export async function updateProcess(id: string, data: ProcessFormData): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> { const result = await executeServerAction({ url: buildApiUrl(`/api/v1/processes/${id}`), method: 'PUT', body: transformFrontendToApi(data), transform: (d: ApiProcess) => transformApiToFrontend(d), errorMessage: '공정 수정에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; return { success: result.success, data: result.data, error: result.error }; } /** * 공정 품목 제거 (item_ids만 업데이트) */ export async function removeProcessItem(processId: string, remainingItemIds: number[]): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> { const result = await executeServerAction({ url: buildApiUrl(`/api/v1/processes/${processId}`), method: 'PUT', body: { item_ids: remainingItemIds }, transform: (d: ApiProcess) => transformApiToFrontend(d), errorMessage: '품목 제거에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; return { success: result.success, data: result.data, error: result.error }; } /** * 공정 삭제 */ export async function deleteProcess(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> { const result = await executeServerAction({ url: buildApiUrl(`/api/v1/processes/${id}`), method: 'DELETE', errorMessage: '공정 삭제에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; return { success: result.success, error: result.error }; } /** * 공정 일괄 삭제 */ export async function deleteProcesses(ids: string[]): Promise<{ success: boolean; deletedCount?: number; error?: string; __authError?: boolean }> { const result = await executeServerAction<{ deleted_count: number }>({ url: buildApiUrl('/api/v1/processes'), method: 'DELETE', body: { ids: ids.map((id) => parseInt(id, 10)) }, errorMessage: '공정 일괄 삭제에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; if (!result.success || !result.data) return { success: false, error: result.error }; return { success: true, deletedCount: result.data.deleted_count }; } /** * 공정 상태 토글 */ export async function toggleProcessActive(id: string): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> { const result = await executeServerAction({ url: buildApiUrl(`/api/v1/processes/${id}/toggle`), method: 'PATCH', transform: (d: ApiProcess) => transformApiToFrontend(d), errorMessage: '공정 상태 변경에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; return { success: result.success, data: result.data, error: result.error }; } /** * 공정 순서 변경 */ export async function reorderProcesses( processes: { id: string; order: number }[] ): Promise<{ success: boolean; error?: string; __authError?: boolean }> { const result = await executeServerAction({ url: buildApiUrl('/api/v1/processes/reorder'), method: 'PATCH', body: { items: processes.map((p) => ({ id: parseInt(p.id, 10), sort_order: p.order, })), }, errorMessage: '공정 순서 변경에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; return { success: result.success, error: result.error }; } /** * 공정 옵션 목록 (드롭다운용) */ export async function getProcessOptions(): Promise<{ success: boolean; data?: Array<{ id: string; processCode: string; processName: string; processType: string; department: string }>; error?: string; __authError?: boolean; }> { interface ApiOptionItem { id: number; process_code: string; process_name: string; process_type: string; department: string } const result = await executeServerAction({ url: buildApiUrl('/api/v1/processes/options'), errorMessage: '공정 옵션 조회에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; if (!result.success || !result.data) return { success: false, error: result.error }; return { success: true, data: result.data.map((item) => ({ id: String(item.id), processCode: item.process_code, processName: item.process_name, processType: item.process_type, department: item.department ?? '', })), }; } /** * 공정 통계 */ export async function getProcessStats(): Promise<{ success: boolean; data?: { total: number; active: number; inactive: number; byType: Record }; error?: string; __authError?: boolean; }> { interface ApiStats { total: number; active: number; inactive: number; by_type: Record } const result = await executeServerAction({ url: buildApiUrl('/api/v1/processes/stats'), errorMessage: '공정 통계 조회에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; if (!result.success || !result.data) return { success: false, error: result.error }; return { success: true, data: { total: result.data.total, active: result.data.active, inactive: result.data.inactive, byType: result.data.by_type, }, }; } // ============================================================================ // 부서 옵션 타입 및 함수 // ============================================================================ export interface DepartmentOption { id: string; value: string; label: string; } /** * 부서 목록 조회 */ export async function getDepartmentOptions(): Promise { const defaultOptions: DepartmentOption[] = [ { id: 'default-1', value: '생산부', label: '생산부' }, { id: 'default-2', value: '품질관리부', label: '품질관리부' }, { id: 'default-3', value: '물류부', label: '물류부' }, { id: 'default-4', value: '영업부', label: '영업부' }, ]; interface DeptResponse { data: Array<{ id: number; name: string }> } const result = await executeServerAction({ url: buildApiUrl('/api/v1/departments'), errorMessage: '부서 목록 조회에 실패했습니다.', }); if (!result.success || !result.data?.data) return defaultOptions; const seenNames = new Set(); return result.data.data .filter((dept) => { if (seenNames.has(dept.name)) return false; seenNames.add(dept.name); return true; }) .map((dept) => ({ id: String(dept.id), value: dept.name, label: dept.name, })); } // ============================================================================ // 품목 옵션 타입 및 함수 // ============================================================================ export interface ItemOption { value: string; label: string; code: string; id: string; fullName: string; type: string; // TODO: API 응답에 process_name, process_category 필드 추가 후 활성화 processName?: string; processCategory?: string; } interface GetItemListParams { q?: string; itemType?: string; size?: number; /** 해당 공정 외 다른 공정에 이미 배정된 품목 제외 (공정 ID) */ excludeProcessId?: string; } /** * 품목 목록 조회 (분류 규칙용) * - excludeProcessId: 다른 공정에 이미 배정된 품목 제외 (중복 방지) * * TODO: 백엔드 API 수정 요청 * - 응답에 process_name, process_category 필드 추가 필요 (공정 품목 선택 팝업에서 공정/구분 컬럼 표시용) * - 파라미터에 processName, processCategory 필터 추가 필요 (공정/구분 필터링용) */ export async function getItemList(params?: GetItemListParams): Promise { interface ItemListResponse { data: Array<{ id: number; name: string; item_code?: string; item_type?: string; item_type_name?: string }> } const result = await executeServerAction({ url: buildApiUrl('/api/v1/items', { size: params?.size || 1000, q: params?.q, item_type: params?.itemType, exclude_process_id: params?.excludeProcessId, }), errorMessage: '품목 목록 조회에 실패했습니다.', }); if (!result.success || !result.data?.data) return []; return result.data.data.map((item) => ({ value: String(item.id), label: item.name, code: item.item_code || '', id: String(item.id), fullName: item.name, type: item.item_type_name || item.item_type || '', })); } /** * 품목 유형 옵션 조회 (common_codes에서 동적 조회) */ export async function getItemTypeOptions(): Promise> { const result = await executeServerAction>({ url: buildApiUrl('/api/v1/settings/common/item_type'), errorMessage: '품목 유형 옵션 조회에 실패했습니다.', }); if (!result.success || !result.data) return []; return result.data.map((item) => ({ value: item.code, label: item.name })); } // ============================================================================ // 문서 양식 (Document Template) API // ============================================================================ export interface DocumentTemplateOption { id: number; name: string; category: string; } /** * 문서 양식 목록 조회 (드롭다운용) */ export async function getDocumentTemplates(): Promise<{ success: boolean; data?: DocumentTemplateOption[]; error?: string; }> { interface ApiTemplateItem { id: number; name: string; category: string } const result = await executeServerAction<{ data: ApiTemplateItem[] }>({ url: buildApiUrl('/api/v1/document-templates', { is_active: 1, per_page: 100 }), errorMessage: '문서 양식 목록 조회에 실패했습니다.', }); if (!result.success || !result.data?.data) return { success: false, error: result.error }; return { success: true, data: result.data.data.map((item) => ({ id: item.id, name: item.name, category: item.category, })), }; } // ============================================================================ // 공정 단계 (Process Step) API // ============================================================================ interface ApiProcessStepOptions { inspection_setting?: Record; inspection_scope?: { type: string; sample_size?: number; sample_base?: string; }; } 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; options: ApiProcessStepOptions | null; created_at: string; updated_at: string; } function transformStepApiToFrontend(apiStep: ApiProcessStep): ProcessStep { const opts = apiStep.options; 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']) || 'click_complete', inspectionSetting: opts?.inspection_setting as ProcessStep['inspectionSetting'], inspectionScope: opts?.inspection_scope ? { type: opts.inspection_scope.type as 'all' | 'sampling' | 'group', sampleSize: opts.inspection_scope.sample_size, sampleBase: opts.inspection_scope.sample_base as 'order' | 'lot' | undefined, } : undefined, }; } /** * 공정 단계 목록 조회 */ export async function getProcessSteps(processId: string): Promise<{ success: boolean; data?: ProcessStep[]; error?: string; }> { const result = await executeServerAction({ url: buildApiUrl(`/api/v1/processes/${processId}/steps`), errorMessage: '공정 단계 목록 조회에 실패했습니다.', }); if (!result.success || !result.data) return { success: false, error: result.error }; return { success: true, data: result.data.map(transformStepApiToFrontend) }; } /** * 공정 단계 상세 조회 */ export async function getProcessStepById(processId: string, stepId: string): Promise<{ success: boolean; data?: ProcessStep; error?: string; }> { const result = await executeServerAction({ url: buildApiUrl(`/api/v1/processes/${processId}/steps/${stepId}`), transform: (d: ApiProcessStep) => transformStepApiToFrontend(d), errorMessage: '공정 단계 조회에 실패했습니다.', }); return { success: result.success, data: result.data, error: result.error }; } /** * 공정 단계 생성 */ export async function createProcessStep( processId: string, data: Omit ): Promise<{ success: boolean; data?: ProcessStep; error?: string }> { const result = await executeServerAction({ url: buildApiUrl(`/api/v1/processes/${processId}/steps`), method: 'POST', body: { 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, options: (data.inspectionSetting || data.inspectionScope) ? { inspection_setting: data.inspectionSetting || null, inspection_scope: data.inspectionScope ? { type: data.inspectionScope.type, sample_size: data.inspectionScope.sampleSize, sample_base: data.inspectionScope.sampleBase, } : null, } : null, }, transform: (d: ApiProcessStep) => transformStepApiToFrontend(d), errorMessage: '공정 단계 등록에 실패했습니다.', }); return { success: result.success, data: result.data, error: result.error }; } /** * 공정 단계 수정 */ export async function updateProcessStep( processId: string, stepId: string, data: Partial ): Promise<{ success: boolean; data?: ProcessStep; error?: string }> { 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; if (data.inspectionSetting !== undefined || data.inspectionScope !== undefined) { apiData.options = { inspection_setting: data.inspectionSetting || null, inspection_scope: data.inspectionScope ? { type: data.inspectionScope.type, sample_size: data.inspectionScope.sampleSize, sample_base: data.inspectionScope.sampleBase, } : null, }; } const result = await executeServerAction({ url: buildApiUrl(`/api/v1/processes/${processId}/steps/${stepId}`), method: 'PUT', body: apiData, transform: (d: ApiProcessStep) => transformStepApiToFrontend(d), errorMessage: '공정 단계 수정에 실패했습니다.', }); return { success: result.success, data: result.data, error: result.error }; } /** * 공정 단계 삭제 */ export async function deleteProcessStep( processId: string, stepId: string ): Promise<{ success: boolean; error?: string }> { const result = await executeServerAction({ url: buildApiUrl(`/api/v1/processes/${processId}/steps/${stepId}`), method: 'DELETE', errorMessage: '공정 단계 삭제에 실패했습니다.', }); return { success: result.success, error: result.error }; } /** * 공정 단계 순서 변경 */ export async function reorderProcessSteps( processId: string, steps: { id: string; order: number }[] ): Promise<{ success: boolean; data?: ProcessStep[]; error?: string }> { const result = await executeServerAction({ url: buildApiUrl(`/api/v1/processes/${processId}/steps/reorder`), method: 'PATCH', body: { items: steps.map((s) => ({ id: parseInt(s.id, 10), sort_order: s.order, })), }, errorMessage: '공정 단계 순서 변경에 실패했습니다.', }); if (!result.success || !result.data) return { success: false, error: result.error }; return { success: true, data: result.data.map(transformStepApiToFrontend) }; }