tenantId(); $page = (int) ($params['page'] ?? 1); $perPage = (int) ($params['per_page'] ?? 20); $q = trim((string) ($params['q'] ?? '')); $status = $params['status'] ?? null; $inspectionType = $params['inspection_type'] ?? null; $dateFrom = $params['date_from'] ?? null; $dateTo = $params['date_to'] ?? null; $query = Inspection::query() ->where('tenant_id', $tenantId) ->with(['inspector:id,name', 'item:id,item_name']); // 검색어 (검사번호, LOT번호) if ($q !== '') { $query->where(function ($qq) use ($q) { $qq->where('inspection_no', 'like', "%{$q}%") ->orWhere('lot_no', 'like', "%{$q}%"); }); } // 상태 필터 if ($status !== null) { $query->where('status', $status); } // 검사유형 필터 if ($inspectionType !== null) { $query->where('inspection_type', $inspectionType); } // 요청일 범위 필터 if ($dateFrom !== null) { $query->where('request_date', '>=', $dateFrom); } if ($dateTo !== null) { $query->where('request_date', '<=', $dateTo); } $query->orderByDesc('created_at'); $paginated = $query->paginate($perPage, ['*'], 'page', $page); // 프론트엔드 형식에 맞게 데이터 변환 $transformedData = $paginated->getCollection()->map(fn ($item) => $this->transformToFrontend($item)); return [ 'items' => $transformedData, 'current_page' => $paginated->currentPage(), 'last_page' => $paginated->lastPage(), 'per_page' => $paginated->perPage(), 'total' => $paginated->total(), ]; } /** * 통계 조회 */ public function stats(array $params = []): array { $tenantId = $this->tenantId(); $query = Inspection::where('tenant_id', $tenantId); // 필터 적용 if (! empty($params['date_from'])) { $query->where('request_date', '>=', $params['date_from']); } if (! empty($params['date_to'])) { $query->where('request_date', '<=', $params['date_to']); } if (! empty($params['inspection_type'])) { $query->where('inspection_type', $params['inspection_type']); } // 상태별 카운트 $counts = (clone $query) ->select('status', DB::raw('count(*) as count')) ->groupBy('status') ->pluck('count', 'status') ->toArray(); // 불량률 계산 (완료된 검사 중 불합격 비율) $completedQuery = (clone $query)->where('status', Inspection::STATUS_COMPLETED); $completedCount = $completedQuery->count(); $failCount = (clone $completedQuery)->where('result', Inspection::RESULT_FAIL)->count(); $defectRate = $completedCount > 0 ? round(($failCount / $completedCount) * 100, 2) : 0; return [ 'waiting_count' => $counts[Inspection::STATUS_WAITING] ?? 0, 'in_progress_count' => $counts[Inspection::STATUS_IN_PROGRESS] ?? 0, 'completed_count' => $counts[Inspection::STATUS_COMPLETED] ?? 0, 'defect_rate' => $defectRate, ]; } /** * 단건 조회 */ public function show(int $id) { $tenantId = $this->tenantId(); $inspection = Inspection::where('tenant_id', $tenantId) ->with(['inspector:id,name', 'item:id,item_name']) ->find($id); if (! $inspection) { throw new NotFoundHttpException(__('error.not_found')); } return $this->transformToFrontend($inspection); } /** * 생성 */ public function store(array $data) { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($data, $tenantId, $userId) { // 검사번호 자동 생성 $inspectionNo = Inspection::generateInspectionNo($tenantId, $data['inspection_type']); // meta JSON 구성 $meta = [ 'process_name' => $data['process_name'] ?? null, 'quantity' => $data['quantity'] ?? null, 'unit' => $data['unit'] ?? null, ]; // extra JSON 구성 $extra = [ 'remarks' => $data['remarks'] ?? null, ]; // items JSON 구성 $items = []; if (! empty($data['items'])) { foreach ($data['items'] as $index => $item) { $items[] = [ 'id' => uniqid('item_'), 'name' => $item['name'], 'type' => $item['type'], 'spec' => $item['spec'], 'unit' => $item['unit'] ?? null, 'result' => null, 'measured_value' => null, 'judgment' => null, ]; } } $inspection = Inspection::create([ 'tenant_id' => $tenantId, 'inspection_no' => $inspectionNo, 'inspection_type' => $data['inspection_type'], 'request_date' => $data['request_date'] ?? now()->toDateString(), 'lot_no' => $data['lot_no'], 'inspector_id' => $data['inspector_id'] ?? null, 'meta' => $meta, 'items' => $items, 'extra' => $extra, 'created_by' => $userId, ]); // 감사 로그 $this->auditLogger->log( $tenantId, self::AUDIT_TARGET, $inspection->id, 'created', null, $inspection->toArray() ); return $this->transformToFrontend($inspection->load(['inspector:id,name'])); }); } /** * 수정 */ public function update(int $id, array $data) { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $inspection = Inspection::where('tenant_id', $tenantId)->find($id); if (! $inspection) { throw new NotFoundHttpException(__('error.not_found')); } $beforeData = $inspection->toArray(); return DB::transaction(function () use ($inspection, $data, $userId, $beforeData) { $updateData = ['updated_by' => $userId]; // items 업데이트 if (isset($data['items'])) { $existingItems = $inspection->items ?? []; $updatedItems = []; foreach ($data['items'] as $inputItem) { // 기존 항목 찾기 $found = false; foreach ($existingItems as $existing) { if ($existing['id'] === $inputItem['id']) { $existing['result'] = $inputItem['result'] ?? $existing['result']; $existing['measured_value'] = $inputItem['measured_value'] ?? $existing['measured_value']; $existing['judgment'] = $inputItem['judgment'] ?? $existing['judgment']; $updatedItems[] = $existing; $found = true; break; } } if (! $found) { $updatedItems[] = $inputItem; } } $updateData['items'] = $updatedItems; } // result 업데이트 if (isset($data['result'])) { $updateData['result'] = $data['result']; } // extra JSON 업데이트 $extra = $inspection->extra ?? []; if (isset($data['remarks'])) { $extra['remarks'] = $data['remarks']; } if (isset($data['opinion'])) { $extra['opinion'] = $data['opinion']; } if (! empty($extra)) { $updateData['extra'] = $extra; } $inspection->update($updateData); // 감사 로그 $this->auditLogger->log( $inspection->tenant_id, self::AUDIT_TARGET, $inspection->id, 'updated', $beforeData, $inspection->fresh()->toArray() ); return $this->transformToFrontend($inspection->load(['inspector:id,name'])); }); } /** * 삭제 */ public function destroy(int $id) { $tenantId = $this->tenantId(); $inspection = Inspection::where('tenant_id', $tenantId)->find($id); if (! $inspection) { throw new NotFoundHttpException(__('error.not_found')); } // 완료된 검사는 삭제 불가 if ($inspection->status === Inspection::STATUS_COMPLETED) { throw new BadRequestHttpException(__('error.inspection.cannot_delete_completed')); } $beforeData = $inspection->toArray(); $inspection->deleted_by = $this->apiUserId(); $inspection->save(); $inspection->delete(); // 감사 로그 $this->auditLogger->log( $tenantId, self::AUDIT_TARGET, $inspection->id, 'deleted', $beforeData, null ); return 'success'; } /** * 검사 완료 처리 */ public function complete(int $id, array $data) { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $inspection = Inspection::where('tenant_id', $tenantId)->find($id); if (! $inspection) { throw new NotFoundHttpException(__('error.not_found')); } // 이미 완료된 경우 if ($inspection->status === Inspection::STATUS_COMPLETED) { throw new BadRequestHttpException(__('error.inspection.already_completed')); } $beforeData = $inspection->toArray(); return DB::transaction(function () use ($inspection, $data, $userId, $beforeData) { $extra = $inspection->extra ?? []; if (isset($data['opinion'])) { $extra['opinion'] = $data['opinion']; } $inspection->update([ 'status' => Inspection::STATUS_COMPLETED, 'result' => $data['result'], 'inspection_date' => now()->toDateString(), 'extra' => $extra, 'updated_by' => $userId, ]); // 감사 로그 $this->auditLogger->log( $inspection->tenant_id, self::AUDIT_TARGET, $inspection->id, 'completed', $beforeData, $inspection->fresh()->toArray() ); return $this->transformToFrontend($inspection->load(['inspector:id,name'])); }); } /** * DB 데이터를 프론트엔드 형식으로 변환 */ private function transformToFrontend(Inspection $inspection): array { $meta = $inspection->meta ?? []; $extra = $inspection->extra ?? []; return [ 'id' => $inspection->id, 'inspection_no' => $inspection->inspection_no, 'inspection_type' => $inspection->inspection_type, 'request_date' => $inspection->request_date?->format('Y-m-d'), 'inspection_date' => $inspection->inspection_date?->format('Y-m-d'), 'item_name' => $inspection->item?->item_name ?? ($meta['item_name'] ?? null), 'lot_no' => $inspection->lot_no, 'process_name' => $meta['process_name'] ?? null, 'quantity' => $meta['quantity'] ?? null, 'unit' => $meta['unit'] ?? null, 'status' => $inspection->status, 'result' => $inspection->result, 'inspector_id' => $inspection->inspector_id, 'inspector' => $inspection->inspector ? [ 'id' => $inspection->inspector->id, 'name' => $inspection->inspector->name, ] : null, 'items' => $inspection->items ?? [], 'remarks' => $extra['remarks'] ?? null, 'opinion' => $extra['opinion'] ?? null, 'attachments' => $inspection->attachments ?? [], 'created_at' => $inspection->created_at?->toIso8601String(), 'updated_at' => $inspection->updated_at?->toIso8601String(), ]; } }