diff --git a/app/Http/Controllers/Api/Admin/EquipmentController.php b/app/Http/Controllers/Api/Admin/EquipmentController.php new file mode 100644 index 00000000..5790b4d1 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/EquipmentController.php @@ -0,0 +1,141 @@ +equipmentService->getEquipments( + $request->all(), + $request->input('per_page', 20) + ); + + if ($request->header('HX-Request')) { + return view('equipment.partials.table', compact('equipments')); + } + + return response()->json([ + 'success' => true, + 'data' => $equipments->items(), + 'meta' => [ + 'current_page' => $equipments->currentPage(), + 'total' => $equipments->total(), + 'per_page' => $equipments->perPage(), + 'last_page' => $equipments->lastPage(), + ], + ]); + } + + public function store(StoreEquipmentRequest $request): JsonResponse + { + try { + $equipment = $this->equipmentService->createEquipment($request->validated()); + + return response()->json([ + 'success' => true, + 'message' => '설비가 등록되었습니다.', + 'data' => $equipment, + ], 201); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage(), + ], 400); + } + } + + public function show(int $id): JsonResponse + { + $equipment = $this->equipmentService->getEquipmentById($id); + + if (! $equipment) { + return response()->json([ + 'success' => false, + 'message' => '설비를 찾을 수 없습니다.', + ], 404); + } + + return response()->json([ + 'success' => true, + 'data' => $equipment, + ]); + } + + public function update(UpdateEquipmentRequest $request, int $id): JsonResponse + { + try { + $equipment = $this->equipmentService->updateEquipment($id, $request->validated()); + + return response()->json([ + 'success' => true, + 'message' => '설비 정보가 수정되었습니다.', + 'data' => $equipment, + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage(), + ], 400); + } + } + + public function destroy(int $id): JsonResponse + { + try { + $this->equipmentService->deleteEquipment($id); + + return response()->json([ + 'success' => true, + 'message' => '설비가 삭제되었습니다.', + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage(), + ], 400); + } + } + + public function restore(int $id): JsonResponse + { + try { + $this->equipmentService->restoreEquipment($id); + + return response()->json([ + 'success' => true, + 'message' => '설비가 복원되었습니다.', + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage(), + ], 400); + } + } + + public function templates(int $id): JsonResponse + { + $equipment = $this->equipmentService->getEquipmentById($id); + + if (! $equipment) { + return response()->json(['success' => false, 'message' => '설비를 찾을 수 없습니다.'], 404); + } + + return response()->json([ + 'success' => true, + 'data' => $equipment->inspectionTemplates, + ]); + } +} diff --git a/app/Http/Controllers/Api/Admin/EquipmentInspectionController.php b/app/Http/Controllers/Api/Admin/EquipmentInspectionController.php new file mode 100644 index 00000000..6e831c7d --- /dev/null +++ b/app/Http/Controllers/Api/Admin/EquipmentInspectionController.php @@ -0,0 +1,161 @@ +input('year_month', now()->format('Y-m')); + $productionLine = $request->input('production_line'); + $equipmentId = $request->input('equipment_id'); + + $inspections = $this->inspectionService->getMonthlyInspections( + $yearMonth, + $productionLine, + $equipmentId ? (int) $equipmentId : null + ); + + if ($request->header('HX-Request')) { + return view('equipment.partials.inspection-grid', [ + 'inspections' => $inspections, + 'yearMonth' => $yearMonth, + ]); + } + + return response()->json([ + 'success' => true, + 'data' => $inspections, + ]); + } + + public function toggleDetail(Request $request): JsonResponse + { + $request->validate([ + 'equipment_id' => 'required|integer', + 'template_item_id' => 'required|integer', + 'check_date' => 'required|date', + ]); + + try { + $result = $this->inspectionService->toggleDetail( + $request->input('equipment_id'), + $request->input('template_item_id'), + $request->input('check_date') + ); + + return response()->json([ + 'success' => true, + 'data' => $result, + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage(), + ], 400); + } + } + + public function updateNotes(Request $request): JsonResponse + { + $request->validate([ + 'equipment_id' => 'required|integer', + 'year_month' => 'required|string', + 'overall_judgment' => 'nullable|in:OK,NG', + 'repair_note' => 'nullable|string', + 'issue_note' => 'nullable|string', + 'inspector_id' => 'nullable|integer', + ]); + + try { + $inspection = $this->inspectionService->updateInspectionNotes( + $request->input('equipment_id'), + $request->input('year_month'), + $request->only(['overall_judgment', 'repair_note', 'issue_note', 'inspector_id']) + ); + + return response()->json([ + 'success' => true, + 'message' => '점검 정보가 저장되었습니다.', + 'data' => $inspection, + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage(), + ], 400); + } + } + + public function storeTemplate(Request $request, int $equipmentId): JsonResponse + { + $request->validate([ + 'item_no' => 'required|integer', + 'check_point' => 'required|string|max:50', + 'check_item' => 'required|string|max:100', + 'check_timing' => 'nullable|in:operating,stopped', + 'check_frequency' => 'nullable|string|max:50', + 'check_method' => 'nullable|string', + 'sort_order' => 'nullable|integer', + ]); + + try { + $template = $this->inspectionService->saveTemplate($equipmentId, $request->all()); + + return response()->json([ + 'success' => true, + 'message' => '점검항목이 추가되었습니다.', + 'data' => $template, + ], 201); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage(), + ], 400); + } + } + + public function updateTemplate(Request $request, int $templateId): JsonResponse + { + try { + $template = $this->inspectionService->updateTemplate($templateId, $request->all()); + + return response()->json([ + 'success' => true, + 'message' => '점검항목이 수정되었습니다.', + 'data' => $template, + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage(), + ], 400); + } + } + + public function deleteTemplate(int $templateId): JsonResponse + { + try { + $this->inspectionService->deleteTemplate($templateId); + + return response()->json([ + 'success' => true, + 'message' => '점검항목이 삭제되었습니다.', + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage(), + ], 400); + } + } +} diff --git a/app/Http/Controllers/Api/Admin/EquipmentRepairController.php b/app/Http/Controllers/Api/Admin/EquipmentRepairController.php new file mode 100644 index 00000000..5350d4fd --- /dev/null +++ b/app/Http/Controllers/Api/Admin/EquipmentRepairController.php @@ -0,0 +1,92 @@ +repairService->getRepairs( + $request->all(), + $request->input('per_page', 20) + ); + + if ($request->header('HX-Request')) { + return view('equipment.partials.repair-table', compact('repairs')); + } + + return response()->json([ + 'success' => true, + 'data' => $repairs->items(), + 'meta' => [ + 'current_page' => $repairs->currentPage(), + 'total' => $repairs->total(), + 'per_page' => $repairs->perPage(), + 'last_page' => $repairs->lastPage(), + ], + ]); + } + + public function store(StoreEquipmentRepairRequest $request): JsonResponse + { + try { + $repair = $this->repairService->createRepair($request->validated()); + + return response()->json([ + 'success' => true, + 'message' => '수리이력이 등록되었습니다.', + 'data' => $repair, + ], 201); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage(), + ], 400); + } + } + + public function update(Request $request, int $id): JsonResponse + { + try { + $repair = $this->repairService->updateRepair($id, $request->all()); + + return response()->json([ + 'success' => true, + 'message' => '수리이력이 수정되었습니다.', + 'data' => $repair, + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage(), + ], 400); + } + } + + public function destroy(int $id): JsonResponse + { + try { + $this->repairService->deleteRepair($id); + + return response()->json([ + 'success' => true, + 'message' => '수리이력이 삭제되었습니다.', + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage(), + ], 400); + } + } +} diff --git a/app/Http/Requests/StoreEquipmentInspectionRequest.php b/app/Http/Requests/StoreEquipmentInspectionRequest.php new file mode 100644 index 00000000..cf6f4de2 --- /dev/null +++ b/app/Http/Requests/StoreEquipmentInspectionRequest.php @@ -0,0 +1,31 @@ + 'required|exists:equipments,id', + 'template_item_id' => 'required|exists:equipment_inspection_templates,id', + 'check_date' => 'required|date', + ]; + } + + public function attributes(): array + { + return [ + 'equipment_id' => '설비', + 'template_item_id' => '점검항목', + 'check_date' => '점검일', + ]; + } +} diff --git a/app/Http/Requests/StoreEquipmentRepairRequest.php b/app/Http/Requests/StoreEquipmentRepairRequest.php new file mode 100644 index 00000000..4fcc55cc --- /dev/null +++ b/app/Http/Requests/StoreEquipmentRepairRequest.php @@ -0,0 +1,48 @@ + 'required|exists:equipments,id', + 'repair_date' => 'required|date', + 'repair_type' => 'required|in:internal,external', + 'repair_hours' => 'nullable|numeric|min:0', + 'description' => 'nullable|string', + 'cost' => 'nullable|numeric|min:0', + 'vendor' => 'nullable|string|max:100', + 'repaired_by' => 'nullable|exists:users,id', + 'memo' => 'nullable|string', + ]; + } + + public function attributes(): array + { + return [ + 'equipment_id' => '설비', + 'repair_date' => '수리일', + 'repair_type' => '보전구분', + 'repair_hours' => '수리시간', + 'cost' => '수리비용', + ]; + } + + public function messages(): array + { + return [ + 'equipment_id.required' => '설비를 선택해주세요.', + 'repair_date.required' => '수리일은 필수입니다.', + 'repair_type.required' => '보전구분을 선택해주세요.', + ]; + } +} diff --git a/app/Http/Requests/StoreEquipmentRequest.php b/app/Http/Requests/StoreEquipmentRequest.php new file mode 100644 index 00000000..a878e71b --- /dev/null +++ b/app/Http/Requests/StoreEquipmentRequest.php @@ -0,0 +1,68 @@ + [ + 'required', 'string', 'max:20', + Rule::unique('equipments', 'equipment_code') + ->where('tenant_id', $tenantId), + ], + 'name' => 'required|string|max:100', + 'equipment_type' => 'nullable|string|max:50', + 'specification' => 'nullable|string|max:255', + 'manufacturer' => 'nullable|string|max:100', + 'model_name' => 'nullable|string|max:100', + 'serial_no' => 'nullable|string|max:100', + 'location' => 'nullable|string|max:100', + 'production_line' => 'nullable|string|max:50', + 'purchase_date' => 'nullable|date', + 'install_date' => 'nullable|date', + 'purchase_price' => 'nullable|numeric|min:0', + 'useful_life' => 'nullable|integer|min:0', + 'status' => 'nullable|in:active,idle,disposed', + 'disposed_date' => 'nullable|date', + 'manager_id' => 'nullable|exists:users,id', + 'photo_path' => 'nullable|string|max:500', + 'memo' => 'nullable|string', + 'is_active' => 'nullable|boolean', + 'sort_order' => 'nullable|integer|min:0', + ]; + } + + public function attributes(): array + { + return [ + 'equipment_code' => '설비코드', + 'name' => '설비명', + 'equipment_type' => '설비유형', + 'manufacturer' => '제조사', + 'purchase_date' => '구입일', + 'install_date' => '설치일', + 'purchase_price' => '구입가격', + ]; + } + + public function messages(): array + { + return [ + 'equipment_code.required' => '설비코드는 필수입니다.', + 'equipment_code.unique' => '이미 존재하는 설비코드입니다.', + 'name.required' => '설비명은 필수입니다.', + ]; + } +} diff --git a/app/Http/Requests/UpdateEquipmentRequest.php b/app/Http/Requests/UpdateEquipmentRequest.php new file mode 100644 index 00000000..8a75831d --- /dev/null +++ b/app/Http/Requests/UpdateEquipmentRequest.php @@ -0,0 +1,56 @@ +route('id'); + + return [ + 'equipment_code' => [ + 'required', 'string', 'max:20', + Rule::unique('equipments', 'equipment_code') + ->where('tenant_id', $tenantId) + ->ignore($id), + ], + 'name' => 'required|string|max:100', + 'equipment_type' => 'nullable|string|max:50', + 'specification' => 'nullable|string|max:255', + 'manufacturer' => 'nullable|string|max:100', + 'model_name' => 'nullable|string|max:100', + 'serial_no' => 'nullable|string|max:100', + 'location' => 'nullable|string|max:100', + 'production_line' => 'nullable|string|max:50', + 'purchase_date' => 'nullable|date', + 'install_date' => 'nullable|date', + 'purchase_price' => 'nullable|numeric|min:0', + 'useful_life' => 'nullable|integer|min:0', + 'status' => 'nullable|in:active,idle,disposed', + 'disposed_date' => 'nullable|date', + 'manager_id' => 'nullable|exists:users,id', + 'photo_path' => 'nullable|string|max:500', + 'memo' => 'nullable|string', + 'is_active' => 'nullable|boolean', + 'sort_order' => 'nullable|integer|min:0', + ]; + } + + public function attributes(): array + { + return [ + 'equipment_code' => '설비코드', + 'name' => '설비명', + ]; + } +} diff --git a/app/Models/Equipment/Equipment.php b/app/Models/Equipment/Equipment.php new file mode 100644 index 00000000..3ecd6511 --- /dev/null +++ b/app/Models/Equipment/Equipment.php @@ -0,0 +1,133 @@ + 'date', + 'install_date' => 'date', + 'disposed_date' => 'date', + 'purchase_price' => 'decimal:2', + 'is_active' => 'boolean', + ]; + + public function manager(): BelongsTo + { + return $this->belongsTo(\App\Models\User::class, 'manager_id'); + } + + public function inspectionTemplates(): HasMany + { + return $this->hasMany(EquipmentInspectionTemplate::class, 'equipment_id')->orderBy('sort_order'); + } + + public function inspections(): HasMany + { + return $this->hasMany(EquipmentInspection::class, 'equipment_id'); + } + + public function repairs(): HasMany + { + return $this->hasMany(EquipmentRepair::class, 'equipment_id'); + } + + public function processes(): BelongsToMany + { + return $this->belongsToMany(\App\Models\Process::class, 'equipment_process') + ->withPivot('is_primary') + ->withTimestamps(); + } + + public function scopeActive($query) + { + return $query->where('status', 'active'); + } + + public function scopeByLine($query, string $line) + { + return $query->where('production_line', $line); + } + + public function scopeByType($query, string $type) + { + return $query->where('equipment_type', $type); + } + + public function getStatusLabelAttribute(): string + { + return match ($this->status) { + 'active' => '가동', + 'idle' => '유휴', + 'disposed' => '폐기', + default => $this->status, + }; + } + + public function getStatusColorAttribute(): string + { + return match ($this->status) { + 'active' => 'bg-green-100 text-green-800', + 'idle' => 'bg-yellow-100 text-yellow-800', + 'disposed' => 'bg-gray-100 text-gray-800', + default => 'bg-gray-100 text-gray-800', + }; + } + + public static function getEquipmentTypes(): array + { + return ['포밍기', '미싱기', '샤링기', 'V컷팅기', '절곡기', '프레스', '드릴', '기타']; + } + + public static function getProductionLines(): array + { + return ['스라트', '스크린', '절곡', '기타']; + } + + public static function getStatuses(): array + { + return [ + 'active' => '가동', + 'idle' => '유휴', + 'disposed' => '폐기', + ]; + } +} diff --git a/app/Models/Equipment/EquipmentInspection.php b/app/Models/Equipment/EquipmentInspection.php new file mode 100644 index 00000000..b524c3e7 --- /dev/null +++ b/app/Models/Equipment/EquipmentInspection.php @@ -0,0 +1,58 @@ +belongsTo(Equipment::class, 'equipment_id'); + } + + public function inspector(): BelongsTo + { + return $this->belongsTo(\App\Models\User::class, 'inspector_id'); + } + + public function details(): HasMany + { + return $this->hasMany(EquipmentInspectionDetail::class, 'inspection_id'); + } + + public function getJudgmentLabelAttribute(): string + { + return match ($this->overall_judgment) { + 'OK' => '양호', + 'NG' => '이상', + default => '-', + }; + } + + public function getJudgmentColorAttribute(): string + { + return match ($this->overall_judgment) { + 'OK' => 'bg-green-100 text-green-800', + 'NG' => 'bg-red-100 text-red-800', + default => 'bg-gray-100 text-gray-800', + }; + } +} diff --git a/app/Models/Equipment/EquipmentInspectionDetail.php b/app/Models/Equipment/EquipmentInspectionDetail.php new file mode 100644 index 00000000..75aca5be --- /dev/null +++ b/app/Models/Equipment/EquipmentInspectionDetail.php @@ -0,0 +1,62 @@ + 'date', + ]; + + public function inspection(): BelongsTo + { + return $this->belongsTo(EquipmentInspection::class, 'inspection_id'); + } + + public function templateItem(): BelongsTo + { + return $this->belongsTo(EquipmentInspectionTemplate::class, 'template_item_id'); + } + + public function getResultSymbolAttribute(): string + { + return match ($this->result) { + 'good' => '○', + 'bad' => 'X', + 'repaired' => '△', + default => '', + }; + } + + public function getResultColorAttribute(): string + { + return match ($this->result) { + 'good' => 'text-green-600', + 'bad' => 'text-red-600', + 'repaired' => 'text-yellow-600', + default => 'text-gray-400', + }; + } + + public static function getNextResult(?string $current): ?string + { + return match ($current) { + null, '' => 'good', + 'good' => 'bad', + 'bad' => 'repaired', + 'repaired' => null, + default => 'good', + }; + } +} diff --git a/app/Models/Equipment/EquipmentInspectionTemplate.php b/app/Models/Equipment/EquipmentInspectionTemplate.php new file mode 100644 index 00000000..dd9393f4 --- /dev/null +++ b/app/Models/Equipment/EquipmentInspectionTemplate.php @@ -0,0 +1,48 @@ + 'boolean', + ]; + + public function equipment(): BelongsTo + { + return $this->belongsTo(Equipment::class, 'equipment_id'); + } + + public function scopeActive($query) + { + return $query->where('is_active', true); + } + + public function getTimingLabelAttribute(): string + { + return match ($this->check_timing) { + 'operating' => '가동 중', + 'stopped' => '정지 시', + default => $this->check_timing ?? '-', + }; + } +} diff --git a/app/Models/Equipment/EquipmentProcess.php b/app/Models/Equipment/EquipmentProcess.php new file mode 100644 index 00000000..ca8260f6 --- /dev/null +++ b/app/Models/Equipment/EquipmentProcess.php @@ -0,0 +1,31 @@ + 'boolean', + ]; + + public function equipment(): BelongsTo + { + return $this->belongsTo(Equipment::class, 'equipment_id'); + } + + public function process(): BelongsTo + { + return $this->belongsTo(\App\Models\Process::class, 'process_id'); + } +} diff --git a/app/Models/Equipment/EquipmentRepair.php b/app/Models/Equipment/EquipmentRepair.php new file mode 100644 index 00000000..3b3ffbe8 --- /dev/null +++ b/app/Models/Equipment/EquipmentRepair.php @@ -0,0 +1,62 @@ + 'date', + 'repair_hours' => 'decimal:1', + 'cost' => 'decimal:2', + ]; + + public function equipment(): BelongsTo + { + return $this->belongsTo(Equipment::class, 'equipment_id'); + } + + public function repairer(): BelongsTo + { + return $this->belongsTo(\App\Models\User::class, 'repaired_by'); + } + + public function getRepairTypeLabelAttribute(): string + { + return match ($this->repair_type) { + 'internal' => '사내', + 'external' => '외주', + default => $this->repair_type ?? '-', + }; + } + + public function getFormattedCostAttribute(): string + { + if (! $this->cost) { + return '-'; + } + + return number_format($this->cost).'원'; + } +} diff --git a/app/Services/EquipmentInspectionService.php b/app/Services/EquipmentInspectionService.php new file mode 100644 index 00000000..3a77aa0b --- /dev/null +++ b/app/Services/EquipmentInspectionService.php @@ -0,0 +1,178 @@ +where('status', '!=', 'disposed'); + + if ($productionLine) { + $equipmentQuery->where('production_line', $productionLine); + } + + if ($equipmentId) { + $equipmentQuery->where('id', $equipmentId); + } + + $equipments = $equipmentQuery->orderBy('sort_order')->orderBy('name')->get(); + + $date = Carbon::createFromFormat('Y-m', $yearMonth); + $daysInMonth = $date->daysInMonth; + + $result = []; + + foreach ($equipments as $equipment) { + $templates = EquipmentInspectionTemplate::where('equipment_id', $equipment->id) + ->where('is_active', true) + ->orderBy('sort_order') + ->get(); + + if ($templates->isEmpty()) { + continue; + } + + $inspection = EquipmentInspection::where('equipment_id', $equipment->id) + ->where('year_month', $yearMonth) + ->first(); + + $details = []; + if ($inspection) { + $details = EquipmentInspectionDetail::where('inspection_id', $inspection->id) + ->get() + ->groupBy(function ($d) { + return $d->template_item_id.'_'.$d->check_date->format('Y-m-d'); + }); + } + + $result[] = [ + 'equipment' => $equipment, + 'templates' => $templates, + 'inspection' => $inspection, + 'details' => $details, + 'days_in_month' => $daysInMonth, + ]; + } + + return $result; + } + + public function toggleDetail(int $equipmentId, int $templateItemId, string $checkDate): array + { + $tenantId = session('selected_tenant_id', 1); + $yearMonth = Carbon::parse($checkDate)->format('Y-m'); + + $inspection = EquipmentInspection::firstOrCreate( + [ + 'tenant_id' => $tenantId, + 'equipment_id' => $equipmentId, + 'year_month' => $yearMonth, + ], + [ + 'created_by' => auth()->id(), + ] + ); + + $detail = EquipmentInspectionDetail::where('inspection_id', $inspection->id) + ->where('template_item_id', $templateItemId) + ->where('check_date', $checkDate) + ->first(); + + if ($detail) { + $nextResult = EquipmentInspectionDetail::getNextResult($detail->result); + if ($nextResult === null) { + $detail->delete(); + + return ['result' => null, 'symbol' => '', 'color' => 'text-gray-400']; + } + $detail->update(['result' => $nextResult]); + } else { + $detail = EquipmentInspectionDetail::create([ + 'inspection_id' => $inspection->id, + 'template_item_id' => $templateItemId, + 'check_date' => $checkDate, + 'result' => 'good', + ]); + $nextResult = 'good'; + } + + return [ + 'result' => $nextResult, + 'symbol' => $detail->fresh()->result_symbol, + 'color' => $detail->fresh()->result_color, + ]; + } + + public function updateInspectionNotes(int $equipmentId, string $yearMonth, array $data): EquipmentInspection + { + $tenantId = session('selected_tenant_id', 1); + + $inspection = EquipmentInspection::firstOrCreate( + [ + 'tenant_id' => $tenantId, + 'equipment_id' => $equipmentId, + 'year_month' => $yearMonth, + ], + [ + 'created_by' => auth()->id(), + ] + ); + + $inspection->update(array_merge($data, ['updated_by' => auth()->id()])); + + return $inspection->fresh(); + } + + public function getMonthlyStats(string $yearMonth): array + { + $tenantId = session('selected_tenant_id', 1); + + $totalEquipments = Equipment::where('is_active', true) + ->where('status', '!=', 'disposed') + ->count(); + + $inspected = EquipmentInspection::where('year_month', $yearMonth)->count(); + + $issueCount = EquipmentInspectionDetail::whereHas('inspection', function ($q) use ($yearMonth) { + $q->where('year_month', $yearMonth); + })->where('result', 'bad')->count(); + + return [ + 'total' => $totalEquipments, + 'inspected' => $inspected, + 'issue_count' => $issueCount, + ]; + } + + public function saveTemplate(int $equipmentId, array $data): EquipmentInspectionTemplate + { + $tenantId = session('selected_tenant_id', 1); + + return EquipmentInspectionTemplate::create(array_merge($data, [ + 'tenant_id' => $tenantId, + 'equipment_id' => $equipmentId, + ])); + } + + public function updateTemplate(int $id, array $data): EquipmentInspectionTemplate + { + $template = EquipmentInspectionTemplate::findOrFail($id); + $template->update($data); + + return $template->fresh(); + } + + public function deleteTemplate(int $id): bool + { + return EquipmentInspectionTemplate::findOrFail($id)->delete(); + } +} diff --git a/app/Services/EquipmentRepairService.php b/app/Services/EquipmentRepairService.php new file mode 100644 index 00000000..0f880cc1 --- /dev/null +++ b/app/Services/EquipmentRepairService.php @@ -0,0 +1,80 @@ +with('equipment', 'repairer'); + + if (! empty($filters['equipment_id'])) { + $query->where('equipment_id', $filters['equipment_id']); + } + + if (! empty($filters['repair_type'])) { + $query->where('repair_type', $filters['repair_type']); + } + + if (! empty($filters['date_from'])) { + $query->where('repair_date', '>=', $filters['date_from']); + } + + if (! empty($filters['date_to'])) { + $query->where('repair_date', '<=', $filters['date_to']); + } + + if (! empty($filters['search'])) { + $search = $filters['search']; + $query->where(function ($q) use ($search) { + $q->where('description', 'like', "%{$search}%") + ->orWhereHas('equipment', function ($eq) use ($search) { + $eq->where('name', 'like', "%{$search}%") + ->orWhere('equipment_code', 'like', "%{$search}%"); + }); + }); + } + + return $query->orderBy('repair_date', 'desc')->paginate($perPage); + } + + public function getRepairById(int $id): ?EquipmentRepair + { + return EquipmentRepair::with('equipment', 'repairer')->find($id); + } + + public function createRepair(array $data): EquipmentRepair + { + $data['tenant_id'] = session('selected_tenant_id', 1); + $data['created_by'] = auth()->id(); + + return EquipmentRepair::create($data); + } + + public function updateRepair(int $id, array $data): EquipmentRepair + { + $repair = EquipmentRepair::findOrFail($id); + $data['updated_by'] = auth()->id(); + $repair->update($data); + + return $repair->fresh(); + } + + public function deleteRepair(int $id): bool + { + $repair = EquipmentRepair::findOrFail($id); + + return $repair->delete(); + } + + public function getRecentRepairs(int $limit = 5): \Illuminate\Database\Eloquent\Collection + { + return EquipmentRepair::with('equipment') + ->orderBy('repair_date', 'desc') + ->limit($limit) + ->get(); + } +} diff --git a/app/Services/EquipmentService.php b/app/Services/EquipmentService.php new file mode 100644 index 00000000..2c63a59a --- /dev/null +++ b/app/Services/EquipmentService.php @@ -0,0 +1,105 @@ +with('manager'); + + if (! empty($filters['search'])) { + $search = $filters['search']; + $query->where(function ($q) use ($search) { + $q->where('equipment_code', 'like', "%{$search}%") + ->orWhere('name', 'like', "%{$search}%"); + }); + } + + if (! empty($filters['status'])) { + $query->where('status', $filters['status']); + } + + if (! empty($filters['production_line'])) { + $query->where('production_line', $filters['production_line']); + } + + if (! empty($filters['equipment_type'])) { + $query->where('equipment_type', $filters['equipment_type']); + } + + $sortBy = $filters['sort_by'] ?? 'sort_order'; + $sortDir = $filters['sort_direction'] ?? 'asc'; + $query->orderBy($sortBy, $sortDir); + + return $query->paginate($perPage); + } + + public function getEquipmentById(int $id): ?Equipment + { + return Equipment::with(['manager', 'inspectionTemplates', 'repairs', 'processes'])->find($id); + } + + public function createEquipment(array $data): Equipment + { + $data['tenant_id'] = session('selected_tenant_id', 1); + $data['created_by'] = auth()->id(); + + return Equipment::create($data); + } + + public function updateEquipment(int $id, array $data): Equipment + { + $equipment = Equipment::findOrFail($id); + $data['updated_by'] = auth()->id(); + $equipment->update($data); + + return $equipment->fresh(); + } + + public function deleteEquipment(int $id): bool + { + $equipment = Equipment::findOrFail($id); + $equipment->deleted_by = auth()->id(); + $equipment->save(); + + return $equipment->delete(); + } + + public function restoreEquipment(int $id): bool + { + $equipment = Equipment::onlyTrashed()->findOrFail($id); + + return $equipment->restore(); + } + + public function getDashboardStats(): array + { + $total = Equipment::count(); + $active = Equipment::where('status', 'active')->count(); + $idle = Equipment::where('status', 'idle')->count(); + $disposed = Equipment::where('status', 'disposed')->count(); + + return compact('total', 'active', 'idle', 'disposed'); + } + + public function getTypeStats(): array + { + return Equipment::where('status', '!=', 'disposed') + ->selectRaw('equipment_type, count(*) as count') + ->groupBy('equipment_type') + ->pluck('count', 'equipment_type') + ->toArray(); + } + + public function getEquipmentList(): \Illuminate\Database\Eloquent\Collection + { + return Equipment::where('is_active', true) + ->orderBy('sort_order') + ->orderBy('name') + ->get(['id', 'equipment_code', 'name', 'equipment_type', 'production_line']); + } +} diff --git a/resources/views/equipment/create.blade.php b/resources/views/equipment/create.blade.php new file mode 100644 index 00000000..2041dd7f --- /dev/null +++ b/resources/views/equipment/create.blade.php @@ -0,0 +1,198 @@ +@extends('layouts.app') +@section('title', '설비 등록') +@section('content') + +
+
+

설비 등록

+ + ← 목록으로 + +
+ +
+ @csrf + + +
+

기본정보

+
+
+ + +

예: KD-M-001, KD-S-002

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+

제조사 정보

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+

설치 정보

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+

담당자 / 비고

+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+ + + 취소 + +
+
+
+ +@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/equipment/dashboard.blade.php b/resources/views/equipment/dashboard.blade.php new file mode 100644 index 00000000..af37bddc --- /dev/null +++ b/resources/views/equipment/dashboard.blade.php @@ -0,0 +1,138 @@ +@extends('layouts.app') +@section('title', '설비 현황') +@section('content') + + +
+

설비 현황

+

{{ now()->format('Y년 m월 d일') }} 기준

+
+ + +
+
+
총 설비
+
{{ $stats['total'] }}
+
+
+
+
가동 중
+
{{ $stats['active'] }}
+
+
+
+
유휴
+
{{ $stats['idle'] }}
+
+
+
+
폐기
+
{{ $stats['disposed'] }}
+
+
+
+ +
+ +
+

+ 이번달 점검 현황 + {{ now()->format('Y년 m월') }} +

+
+
+ 점검 대상 + {{ $inspectionStats['total'] }}대 +
+
+ 점검 완료 + {{ $inspectionStats['inspected'] }}대 +
+ @if($inspectionStats['total'] > 0) +
+
+
+
+ {{ round($inspectionStats['inspected'] / $inspectionStats['total'] * 100) }}% 완료 +
+ @endif +
+ 이상 발견 + + {{ $inspectionStats['issue_count'] }}건 + +
+
+
+ + +
+

설비 유형별 현황

+ @if(!empty($typeStats)) +
+ @foreach($typeStats as $type => $count) +
+ {{ $type ?? '미분류' }} +
+
+ @php $maxCount = max($typeStats); @endphp +
+
+ {{ $count }} +
+
+ @endforeach +
+ @else +

데이터가 없습니다.

+ @endif +
+
+ + +
+
+

최근 수리이력

+ + 전체보기 → + +
+ + @if($recentRepairs->isNotEmpty()) +
+ + + + + + + + + + + + @foreach($recentRepairs as $repair) + + + + + + + + @endforeach + +
수리일설비보전구분수리내용비용
{{ $repair->repair_date->format('m-d') }}{{ $repair->equipment?->name ?? '-' }} + + {{ $repair->repair_type_label }} + + {{ Str::limit($repair->description, 40) ?? '-' }}{{ $repair->formatted_cost }}
+
+ @else +

최근 수리이력이 없습니다.

+ @endif +
+ +@endsection diff --git a/resources/views/equipment/edit.blade.php b/resources/views/equipment/edit.blade.php new file mode 100644 index 00000000..8f2de1ea --- /dev/null +++ b/resources/views/equipment/edit.blade.php @@ -0,0 +1,230 @@ +@extends('layouts.app') +@section('title', '설비 수정') +@section('content') + +
+
+

설비 수정

+ + ← 목록으로 + +
+ + +
+
+
+
+
+ + + +
+ +@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/equipment/index.blade.php b/resources/views/equipment/index.blade.php new file mode 100644 index 00000000..09f8c6e2 --- /dev/null +++ b/resources/views/equipment/index.blade.php @@ -0,0 +1,107 @@ +@extends('layouts.app') +@section('title', '설비 등록대장') +@section('content') + + +
+

설비 등록대장

+ + + 설비 등록 + +
+ + + +
+ + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + +
+
+ + +
+
+
+
+
+ +@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/equipment/inspections/index.blade.php b/resources/views/equipment/inspections/index.blade.php new file mode 100644 index 00000000..f878dc97 --- /dev/null +++ b/resources/views/equipment/inspections/index.blade.php @@ -0,0 +1,90 @@ +@extends('layouts.app') +@section('title', '일상점검표') +@section('content') + + +
+

일상점검표

+
+ + + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+
+
+
+
+ +@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/equipment/partials/inspection-grid.blade.php b/resources/views/equipment/partials/inspection-grid.blade.php new file mode 100644 index 00000000..bdda37db --- /dev/null +++ b/resources/views/equipment/partials/inspection-grid.blade.php @@ -0,0 +1,97 @@ +@if(empty($inspections)) +
+

점검 가능한 설비가 없습니다.

+

설비 등록대장에서 점검항목을 추가해주세요.

+
+@else + @php + $date = \Carbon\Carbon::createFromFormat('Y-m', $yearMonth); + $daysInMonth = $date->daysInMonth; + @endphp + +
+ + + + + + @for($d = 1; $d <= $daysInMonth; $d++) + @php + $dayDate = $date->copy()->day($d); + $dayOfWeek = $dayDate->dayOfWeek; + $isWeekend = in_array($dayOfWeek, [0, 6]); + @endphp + + @endfor + + + + + @foreach($inspections as $item) + @php + $equipment = $item['equipment']; + $templates = $item['templates']; + $inspection = $item['inspection']; + $details = $item['details']; + $rowCount = $templates->count(); + @endphp + + @foreach($templates as $idx => $tmpl) + + @if($idx === 0) + + @endif + + + @for($d = 1; $d <= $daysInMonth; $d++) + @php + $checkDate = $date->copy()->day($d)->format('Y-m-d'); + $key = $tmpl->id . '_' . $checkDate; + $detail = isset($details[$key]) ? $details[$key]->first() : null; + $symbol = $detail ? $detail->result_symbol : ''; + $color = $detail ? $detail->result_color : 'text-gray-400'; + $dayDate = $date->copy()->day($d); + $isWeekend = in_array($dayDate->dayOfWeek, [0, 6]); + @endphp + + @endfor + + @if($idx === 0) + + @endif + + @endforeach + @endforeach + +
설비점검항목 + {{ $d }} + 판정
+
{{ $equipment->equipment_code }}
+
{{ Str::limit($equipment->name, 8) }}
+
+ {{ $tmpl->check_point }} + + {{ $symbol }} + + @if($inspection && $inspection->overall_judgment) + + {{ $inspection->judgment_label }} + + @else + - + @endif +
+
+ + +
+ 양호 + X 이상 + 수리완료 + 셀 클릭: 빈칸 → ○ → X → △ → 빈칸 +
+@endif diff --git a/resources/views/equipment/partials/repair-table.blade.php b/resources/views/equipment/partials/repair-table.blade.php new file mode 100644 index 00000000..03fa3c82 --- /dev/null +++ b/resources/views/equipment/partials/repair-table.blade.php @@ -0,0 +1,63 @@ +
+ + + + + + + + + + + + + + + + @forelse($repairs as $repair) + + + + + + + + + + + @empty + + + + @endforelse + +
수리일설비보전구분수리시간수리내용비용외주업체액션
+ {{ $repair->repair_date->format('Y-m-d') }} + + {{ $repair->equipment?->equipment_code }} + {{ $repair->equipment?->name }} + + + {{ $repair->repair_type_label }} + + + {{ $repair->repair_hours ? $repair->repair_hours . 'h' : '-' }} + + {{ Str::limit($repair->description, 40) ?? '-' }} + + {{ $repair->formatted_cost }} + + {{ $repair->vendor ?? '-' }} + + +
+ 수리이력이 없습니다. +
+
+
+ +@if($repairs->hasPages()) + @include('partials.pagination', ['paginator' => $repairs, 'target' => '#repair-table']) +@endif diff --git a/resources/views/equipment/partials/table.blade.php b/resources/views/equipment/partials/table.blade.php new file mode 100644 index 00000000..b8d84d05 --- /dev/null +++ b/resources/views/equipment/partials/table.blade.php @@ -0,0 +1,66 @@ +
+ + + + + + + + + + + + + + + + + @forelse($equipments as $eq) + + + + + + + + + + + + @empty + + + + @endforelse + +
설비번호설비명유형위치생산라인상태담당자구입일액션
+ {{ $eq->equipment_code }} + + {{ $eq->name }} + + {{ $eq->equipment_type ?? '-' }} + + {{ $eq->location ?? '-' }} + + {{ $eq->production_line ?? '-' }} + + + {{ $eq->status_label }} + + + {{ $eq->manager?->name ?? '-' }} + + {{ $eq->purchase_date?->format('Y-m-d') ?? '-' }} + + 수정 + +
+ 등록된 설비가 없습니다. +
+
+
+ +@if($equipments->hasPages()) + @include('partials.pagination', ['paginator' => $equipments, 'target' => '#equipment-table']) +@endif diff --git a/resources/views/equipment/partials/tabs/basic-info.blade.php b/resources/views/equipment/partials/tabs/basic-info.blade.php new file mode 100644 index 00000000..8631e37e --- /dev/null +++ b/resources/views/equipment/partials/tabs/basic-info.blade.php @@ -0,0 +1,102 @@ + +
+

기본정보

+
+
+ +

{{ $equipment->equipment_code }}

+
+
+ +

{{ $equipment->name }}

+
+
+ +

{{ $equipment->equipment_type ?? '-' }}

+
+
+ +

{{ $equipment->specification ?? '-' }}

+
+
+
+ +
+

제조사 정보

+
+
+ +

{{ $equipment->manufacturer ?? '-' }}

+
+
+ +

{{ $equipment->model_name ?? '-' }}

+
+
+ +

{{ $equipment->serial_no ?? '-' }}

+
+
+
+ +
+

설치 정보

+
+
+ +

{{ $equipment->location ?? '-' }}

+
+
+ +

{{ $equipment->production_line ?? '-' }}

+
+
+ +

{{ $equipment->purchase_date?->format('Y-m-d') ?? '-' }}

+
+
+ +

{{ $equipment->install_date?->format('Y-m-d') ?? '-' }}

+
+
+ +

{{ $equipment->purchase_price ? number_format($equipment->purchase_price) . '원' : '-' }}

+
+
+ +

{{ $equipment->useful_life ? $equipment->useful_life . '년' : '-' }}

+
+
+ +

{{ $equipment->manager?->name ?? '-' }}

+
+
+ + + {{ $equipment->status_label }} + +
+
+ @if($equipment->memo) +
+ +

{{ $equipment->memo }}

+
+ @endif +
+ +@if($equipment->processes->isNotEmpty()) +
+

연결된 공정

+
+ @foreach($equipment->processes as $process) + + {{ $process->process_name }} + @if($process->pivot->is_primary) + (주) + @endif + + @endforeach +
+
+@endif diff --git a/resources/views/equipment/partials/tabs/inspection-items.blade.php b/resources/views/equipment/partials/tabs/inspection-items.blade.php new file mode 100644 index 00000000..f344e792 --- /dev/null +++ b/resources/views/equipment/partials/tabs/inspection-items.blade.php @@ -0,0 +1,104 @@ + +
+
+

점검항목 템플릿

+ +
+ + @if($equipment->inspectionTemplates->isNotEmpty()) +
+ + + + + + + + + + + + + + @foreach($equipment->inspectionTemplates as $tmpl) + + + + + + + + + + @endforeach + +
번호점검개소점검항목시기주기점검방법액션
{{ $tmpl->item_no }}{{ $tmpl->check_point }}{{ $tmpl->check_item }}{{ $tmpl->timing_label }}{{ $tmpl->check_frequency ?? '-' }}{{ $tmpl->check_method ?? '-' }} + +
+
+ @else +

등록된 점검항목이 없습니다.

+ @endif +
+ + + diff --git a/resources/views/equipment/partials/tabs/repair-history.blade.php b/resources/views/equipment/partials/tabs/repair-history.blade.php new file mode 100644 index 00000000..d9045d53 --- /dev/null +++ b/resources/views/equipment/partials/tabs/repair-history.blade.php @@ -0,0 +1,46 @@ + +
+
+

수리이력

+
+ + @if($equipment->repairs->isNotEmpty()) +
+ + + + + + + + + + + + + + @foreach($equipment->repairs->sortByDesc('repair_date') as $repair) + + + + + + + + + + @endforeach + +
수리일보전구분수리시간수리내용비용외주업체액션
{{ $repair->repair_date->format('Y-m-d') }} + + {{ $repair->repair_type_label }} + + {{ $repair->repair_hours ? $repair->repair_hours . 'h' : '-' }}{{ Str::limit($repair->description, 50) ?? '-' }}{{ $repair->formatted_cost }}{{ $repair->vendor ?? '-' }} + +
+
+ @else +

수리이력이 없습니다.

+ @endif +
diff --git a/resources/views/equipment/repairs/create.blade.php b/resources/views/equipment/repairs/create.blade.php new file mode 100644 index 00000000..72fa4029 --- /dev/null +++ b/resources/views/equipment/repairs/create.blade.php @@ -0,0 +1,134 @@ +@extends('layouts.app') +@section('title', '수리이력 등록') +@section('content') + +
+
+

수리이력 등록

+ + ← 목록으로 + +
+ +
+
+ @csrf + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + + 취소 + +
+
+
+
+ +@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/equipment/repairs/index.blade.php b/resources/views/equipment/repairs/index.blade.php new file mode 100644 index 00000000..eb031292 --- /dev/null +++ b/resources/views/equipment/repairs/index.blade.php @@ -0,0 +1,100 @@ +@extends('layouts.app') +@section('title', '수리이력') +@section('content') + + +
+

수리이력

+ + + 수리이력 등록 + +
+ + + +
+ + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+ + +
+
+ + +
+
+
+
+
+ +@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/equipment/show.blade.php b/resources/views/equipment/show.blade.php new file mode 100644 index 00000000..4f006024 --- /dev/null +++ b/resources/views/equipment/show.blade.php @@ -0,0 +1,137 @@ +@extends('layouts.app') +@section('title', $equipment->name . ' - 설비 상세') +@section('content') + + +
+
+ + + + + +
+

{{ $equipment->name }}

+

{{ $equipment->equipment_code }}

+
+ + {{ $equipment->status_label }} + +
+ + 수정 + +
+ + +
+ +
+ + +
+ @include('equipment.partials.tabs.basic-info', ['equipment' => $equipment]) +
+ + + + + +@endsection + +@push('scripts') + +@endpush diff --git a/routes/api.php b/routes/api.php index a0323907..4cb3768d 100644 --- a/routes/api.php +++ b/routes/api.php @@ -965,6 +965,38 @@ | Google STT + Claude API를 사용한 회의록 생성 | */ +/* +|-------------------------------------------------------------------------- +| 설비관리 API (Equipment Management) +|-------------------------------------------------------------------------- +*/ +Route::middleware(['web', 'auth', 'hq.member'])->prefix('admin/equipment')->name('api.admin.equipment.')->group(function () { + // 설비 CRUD + Route::get('/', [\App\Http\Controllers\Api\Admin\EquipmentController::class, 'index'])->name('index'); + Route::post('/', [\App\Http\Controllers\Api\Admin\EquipmentController::class, 'store'])->name('store'); + Route::get('/{id}', [\App\Http\Controllers\Api\Admin\EquipmentController::class, 'show'])->whereNumber('id')->name('show'); + Route::put('/{id}', [\App\Http\Controllers\Api\Admin\EquipmentController::class, 'update'])->whereNumber('id')->name('update'); + Route::delete('/{id}', [\App\Http\Controllers\Api\Admin\EquipmentController::class, 'destroy'])->whereNumber('id')->name('destroy'); + Route::post('/{id}/restore', [\App\Http\Controllers\Api\Admin\EquipmentController::class, 'restore'])->whereNumber('id')->name('restore'); + + // 점검 템플릿 + Route::get('/{id}/templates', [\App\Http\Controllers\Api\Admin\EquipmentController::class, 'templates'])->whereNumber('id')->name('templates'); + Route::post('/{id}/templates', [\App\Http\Controllers\Api\Admin\EquipmentInspectionController::class, 'storeTemplate'])->whereNumber('id')->name('templates.store'); + Route::put('/templates/{templateId}', [\App\Http\Controllers\Api\Admin\EquipmentInspectionController::class, 'updateTemplate'])->whereNumber('templateId')->name('templates.update'); + Route::delete('/templates/{templateId}', [\App\Http\Controllers\Api\Admin\EquipmentInspectionController::class, 'deleteTemplate'])->whereNumber('templateId')->name('templates.destroy'); + + // 점검 기록 + Route::get('/inspections', [\App\Http\Controllers\Api\Admin\EquipmentInspectionController::class, 'index'])->name('inspections.index'); + Route::patch('/inspections/detail', [\App\Http\Controllers\Api\Admin\EquipmentInspectionController::class, 'toggleDetail'])->name('inspections.toggle'); + Route::patch('/inspections/notes', [\App\Http\Controllers\Api\Admin\EquipmentInspectionController::class, 'updateNotes'])->name('inspections.notes'); + + // 수리이력 + Route::get('/repairs', [\App\Http\Controllers\Api\Admin\EquipmentRepairController::class, 'index'])->name('repairs.index'); + Route::post('/repairs', [\App\Http\Controllers\Api\Admin\EquipmentRepairController::class, 'store'])->name('repairs.store'); + Route::put('/repairs/{id}', [\App\Http\Controllers\Api\Admin\EquipmentRepairController::class, 'update'])->whereNumber('id')->name('repairs.update'); + Route::delete('/repairs/{id}', [\App\Http\Controllers\Api\Admin\EquipmentRepairController::class, 'destroy'])->whereNumber('id')->name('repairs.destroy'); +}); + Route::middleware(['web', 'auth'])->prefix('meeting-logs')->name('api.admin.meeting-logs.')->group(function () { // 목록 조회 (HTMX 지원) Route::get('/', [MeetingLogController::class, 'index'])->name('index');