feat: [equipment] 설비관리 모듈 구현
- 모델 6개 (Equipment, InspectionTemplate, Inspection, InspectionDetail, Repair, Process) - 서비스 3개 (Equipment, Inspection, Repair) - API 컨트롤러 3개 + FormRequest 4개 - Blade 컨트롤러 + 라우트 등록 - 뷰: 대시보드, 등록대장(CRUD), 일상점검표(캘린더 그리드), 수리이력
This commit is contained in:
141
app/Http/Controllers/Api/Admin/EquipmentController.php
Normal file
141
app/Http/Controllers/Api/Admin/EquipmentController.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreEquipmentRequest;
|
||||
use App\Http\Requests\UpdateEquipmentRequest;
|
||||
use App\Services\EquipmentService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EquipmentController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private EquipmentService $equipmentService
|
||||
) {}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$equipments = $this->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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
161
app/Http/Controllers/Api/Admin/EquipmentInspectionController.php
Normal file
161
app/Http/Controllers/Api/Admin/EquipmentInspectionController.php
Normal file
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\EquipmentInspectionService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EquipmentInspectionController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private EquipmentInspectionService $inspectionService
|
||||
) {}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$yearMonth = $request->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
92
app/Http/Controllers/Api/Admin/EquipmentRepairController.php
Normal file
92
app/Http/Controllers/Api/Admin/EquipmentRepairController.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreEquipmentRepairRequest;
|
||||
use App\Services\EquipmentRepairService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EquipmentRepairController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private EquipmentRepairService $repairService
|
||||
) {}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$repairs = $this->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
97
app/Http/Controllers/EquipmentController.php
Normal file
97
app/Http/Controllers/EquipmentController.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\EquipmentInspectionService;
|
||||
use App\Services\EquipmentRepairService;
|
||||
use App\Services\EquipmentService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class EquipmentController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private EquipmentService $equipmentService,
|
||||
private EquipmentInspectionService $inspectionService,
|
||||
private EquipmentRepairService $repairService
|
||||
) {}
|
||||
|
||||
public function dashboard(Request $request): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('equipment.dashboard'));
|
||||
}
|
||||
|
||||
$stats = $this->equipmentService->getDashboardStats();
|
||||
$typeStats = $this->equipmentService->getTypeStats();
|
||||
$inspectionStats = $this->inspectionService->getMonthlyStats(now()->format('Y-m'));
|
||||
$recentRepairs = $this->repairService->getRecentRepairs(5);
|
||||
|
||||
return view('equipment.dashboard', compact('stats', 'typeStats', 'inspectionStats', 'recentRepairs'));
|
||||
}
|
||||
|
||||
public function index(Request $request): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('equipment.index'));
|
||||
}
|
||||
|
||||
return view('equipment.index');
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
$users = \App\Models\User::orderBy('name')->get(['id', 'name']);
|
||||
|
||||
return view('equipment.create', compact('users'));
|
||||
}
|
||||
|
||||
public function show(int $id): View
|
||||
{
|
||||
$equipment = $this->equipmentService->getEquipmentById($id);
|
||||
|
||||
if (! $equipment) {
|
||||
abort(404, '설비를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
return view('equipment.show', compact('equipment'));
|
||||
}
|
||||
|
||||
public function edit(int $id): View
|
||||
{
|
||||
$users = \App\Models\User::orderBy('name')->get(['id', 'name']);
|
||||
|
||||
return view('equipment.edit', compact('id', 'users'));
|
||||
}
|
||||
|
||||
public function inspections(Request $request): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('equipment.inspections'));
|
||||
}
|
||||
|
||||
$equipmentList = $this->equipmentService->getEquipmentList();
|
||||
|
||||
return view('equipment.inspections.index', compact('equipmentList'));
|
||||
}
|
||||
|
||||
public function repairs(Request $request): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('equipment.repairs'));
|
||||
}
|
||||
|
||||
$equipmentList = $this->equipmentService->getEquipmentList();
|
||||
|
||||
return view('equipment.repairs.index', compact('equipmentList'));
|
||||
}
|
||||
|
||||
public function repairCreate(): View
|
||||
{
|
||||
$equipmentList = $this->equipmentService->getEquipmentList();
|
||||
$users = \App\Models\User::orderBy('name')->get(['id', 'name']);
|
||||
|
||||
return view('equipment.repairs.create', compact('equipmentList', 'users'));
|
||||
}
|
||||
}
|
||||
31
app/Http/Requests/StoreEquipmentInspectionRequest.php
Normal file
31
app/Http/Requests/StoreEquipmentInspectionRequest.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreEquipmentInspectionRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'equipment_id' => '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' => '점검일',
|
||||
];
|
||||
}
|
||||
}
|
||||
48
app/Http/Requests/StoreEquipmentRepairRequest.php
Normal file
48
app/Http/Requests/StoreEquipmentRepairRequest.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreEquipmentRepairRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'equipment_id' => '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' => '보전구분을 선택해주세요.',
|
||||
];
|
||||
}
|
||||
}
|
||||
68
app/Http/Requests/StoreEquipmentRequest.php
Normal file
68
app/Http/Requests/StoreEquipmentRequest.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreEquipmentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
|
||||
return [
|
||||
'equipment_code' => [
|
||||
'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' => '설비명은 필수입니다.',
|
||||
];
|
||||
}
|
||||
}
|
||||
56
app/Http/Requests/UpdateEquipmentRequest.php
Normal file
56
app/Http/Requests/UpdateEquipmentRequest.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateEquipmentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
$id = $this->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' => '설비명',
|
||||
];
|
||||
}
|
||||
}
|
||||
133
app/Models/Equipment/Equipment.php
Normal file
133
app/Models/Equipment/Equipment.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Equipment;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Equipment extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $table = 'equipments';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'equipment_code',
|
||||
'name',
|
||||
'equipment_type',
|
||||
'specification',
|
||||
'manufacturer',
|
||||
'model_name',
|
||||
'serial_no',
|
||||
'location',
|
||||
'production_line',
|
||||
'purchase_date',
|
||||
'install_date',
|
||||
'purchase_price',
|
||||
'useful_life',
|
||||
'status',
|
||||
'disposed_date',
|
||||
'manager_id',
|
||||
'photo_path',
|
||||
'memo',
|
||||
'is_active',
|
||||
'sort_order',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'purchase_date' => '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' => '폐기',
|
||||
];
|
||||
}
|
||||
}
|
||||
58
app/Models/Equipment/EquipmentInspection.php
Normal file
58
app/Models/Equipment/EquipmentInspection.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Equipment;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class EquipmentInspection extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'equipment_id',
|
||||
'year_month',
|
||||
'overall_judgment',
|
||||
'inspector_id',
|
||||
'repair_note',
|
||||
'issue_note',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
public function equipment(): BelongsTo
|
||||
{
|
||||
return $this->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',
|
||||
};
|
||||
}
|
||||
}
|
||||
62
app/Models/Equipment/EquipmentInspectionDetail.php
Normal file
62
app/Models/Equipment/EquipmentInspectionDetail.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Equipment;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class EquipmentInspectionDetail extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'inspection_id',
|
||||
'template_item_id',
|
||||
'check_date',
|
||||
'result',
|
||||
'note',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'check_date' => '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',
|
||||
};
|
||||
}
|
||||
}
|
||||
48
app/Models/Equipment/EquipmentInspectionTemplate.php
Normal file
48
app/Models/Equipment/EquipmentInspectionTemplate.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Equipment;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class EquipmentInspectionTemplate extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'equipment_id',
|
||||
'item_no',
|
||||
'check_point',
|
||||
'check_item',
|
||||
'check_timing',
|
||||
'check_frequency',
|
||||
'check_method',
|
||||
'sort_order',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => '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 ?? '-',
|
||||
};
|
||||
}
|
||||
}
|
||||
31
app/Models/Equipment/EquipmentProcess.php
Normal file
31
app/Models/Equipment/EquipmentProcess.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Equipment;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class EquipmentProcess extends Model
|
||||
{
|
||||
protected $table = 'equipment_process';
|
||||
|
||||
protected $fillable = [
|
||||
'equipment_id',
|
||||
'process_id',
|
||||
'is_primary',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_primary' => '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');
|
||||
}
|
||||
}
|
||||
62
app/Models/Equipment/EquipmentRepair.php
Normal file
62
app/Models/Equipment/EquipmentRepair.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Equipment;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class EquipmentRepair extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'equipment_id',
|
||||
'repair_date',
|
||||
'repair_type',
|
||||
'repair_hours',
|
||||
'description',
|
||||
'cost',
|
||||
'vendor',
|
||||
'repaired_by',
|
||||
'memo',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'repair_date' => '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).'원';
|
||||
}
|
||||
}
|
||||
178
app/Services/EquipmentInspectionService.php
Normal file
178
app/Services/EquipmentInspectionService.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Equipment\Equipment;
|
||||
use App\Models\Equipment\EquipmentInspection;
|
||||
use App\Models\Equipment\EquipmentInspectionDetail;
|
||||
use App\Models\Equipment\EquipmentInspectionTemplate;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class EquipmentInspectionService
|
||||
{
|
||||
public function getMonthlyInspections(string $yearMonth, ?string $productionLine = null, ?int $equipmentId = null): array
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
|
||||
$equipmentQuery = Equipment::where('is_active', true)->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();
|
||||
}
|
||||
}
|
||||
80
app/Services/EquipmentRepairService.php
Normal file
80
app/Services/EquipmentRepairService.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Equipment\EquipmentRepair;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
|
||||
class EquipmentRepairService
|
||||
{
|
||||
public function getRepairs(array $filters = [], int $perPage = 20): LengthAwarePaginator
|
||||
{
|
||||
$query = EquipmentRepair::query()->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();
|
||||
}
|
||||
}
|
||||
105
app/Services/EquipmentService.php
Normal file
105
app/Services/EquipmentService.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Equipment\Equipment;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
|
||||
class EquipmentService
|
||||
{
|
||||
public function getEquipments(array $filters = [], int $perPage = 20): LengthAwarePaginator
|
||||
{
|
||||
$query = Equipment::query()->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']);
|
||||
}
|
||||
}
|
||||
198
resources/views/equipment/create.blade.php
Normal file
198
resources/views/equipment/create.blade.php
Normal file
@@ -0,0 +1,198 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', '설비 등록')
|
||||
@section('content')
|
||||
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800">설비 등록</h1>
|
||||
<a href="{{ route('equipment.index') }}" class="text-gray-600 hover:text-gray-800">
|
||||
← 목록으로
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form id="equipmentForm" class="space-y-6">
|
||||
@csrf
|
||||
|
||||
<!-- 기본정보 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4 pb-2 border-b">기본정보</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||
설비코드 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="text" name="equipment_code" required placeholder="KD-M-001"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<p class="mt-1 text-sm text-gray-500">예: KD-M-001, KD-S-002</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||
설비명 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="text" name="name" required placeholder="포밍기#1"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">설비유형</label>
|
||||
<select name="equipment_type"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">선택</option>
|
||||
<option value="포밍기">포밍기</option>
|
||||
<option value="미싱기">미싱기</option>
|
||||
<option value="샤링기">샤링기</option>
|
||||
<option value="V컷팅기">V컷팅기</option>
|
||||
<option value="절곡기">절곡기</option>
|
||||
<option value="프레스">프레스</option>
|
||||
<option value="드릴">드릴</option>
|
||||
<option value="기타">기타</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">규격</label>
|
||||
<input type="text" name="specification"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 제조사 정보 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4 pb-2 border-b">제조사 정보</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">제조사</label>
|
||||
<input type="text" name="manufacturer"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">모델명</label>
|
||||
<input type="text" name="model_name"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">제조번호</label>
|
||||
<input type="text" name="serial_no"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 설치 정보 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4 pb-2 border-b">설치 정보</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">위치</label>
|
||||
<input type="text" name="location" placeholder="1공장-1F"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">생산라인</label>
|
||||
<select name="production_line"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">선택</option>
|
||||
<option value="스라트">스라트</option>
|
||||
<option value="스크린">스크린</option>
|
||||
<option value="절곡">절곡</option>
|
||||
<option value="기타">기타</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">구입일</label>
|
||||
<input type="date" name="purchase_date"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">설치일</label>
|
||||
<input type="date" name="install_date"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">구입가격 (원)</label>
|
||||
<input type="number" name="purchase_price" min="0" step="1"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">내용연수 (년)</label>
|
||||
<input type="number" name="useful_life" min="0"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 담당자/비고 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4 pb-2 border-b">담당자 / 비고</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">담당자</label>
|
||||
<select name="manager_id"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">선택</option>
|
||||
@foreach($users as $user)
|
||||
<option value="{{ $user->id }}">{{ $user->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">상태</label>
|
||||
<select name="status"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="active" selected>가동</option>
|
||||
<option value="idle">유휴</option>
|
||||
<option value="disposed">폐기</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">비고</label>
|
||||
<textarea name="memo" rows="3"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 버튼 -->
|
||||
<div class="flex gap-3">
|
||||
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg transition">
|
||||
등록
|
||||
</button>
|
||||
<a href="{{ route('equipment.index') }}"
|
||||
class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-6 py-2 rounded-lg transition">
|
||||
취소
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.getElementById('equipmentForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
fetch('/admin/equipment', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast(data.message, 'success');
|
||||
window.location.href = '{{ route("equipment.index") }}';
|
||||
} else {
|
||||
showToast(data.message || '등록에 실패했습니다.', 'error');
|
||||
}
|
||||
})
|
||||
.catch(() => showToast('서버 오류가 발생했습니다.', 'error'));
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
138
resources/views/equipment/dashboard.blade.php
Normal file
138
resources/views/equipment/dashboard.blade.php
Normal file
@@ -0,0 +1,138 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', '설비 현황')
|
||||
@section('content')
|
||||
|
||||
<!-- 헤더 -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800">설비 현황</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">{{ now()->format('Y년 m월 d일') }} 기준</p>
|
||||
</div>
|
||||
|
||||
<!-- 상단 카드 -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<div class="bg-white rounded-lg shadow-sm p-5">
|
||||
<div class="text-sm text-gray-500 mb-1">총 설비</div>
|
||||
<div class="text-3xl font-bold text-gray-800">{{ $stats['total'] }}</div>
|
||||
<div class="text-xs text-gray-400 mt-1">대</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow-sm p-5">
|
||||
<div class="text-sm text-gray-500 mb-1">가동 중</div>
|
||||
<div class="text-3xl font-bold text-green-600">{{ $stats['active'] }}</div>
|
||||
<div class="text-xs text-gray-400 mt-1">대</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow-sm p-5">
|
||||
<div class="text-sm text-gray-500 mb-1">유휴</div>
|
||||
<div class="text-3xl font-bold text-yellow-600">{{ $stats['idle'] }}</div>
|
||||
<div class="text-xs text-gray-400 mt-1">대</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow-sm p-5">
|
||||
<div class="text-sm text-gray-500 mb-1">폐기</div>
|
||||
<div class="text-3xl font-bold text-gray-400">{{ $stats['disposed'] }}</div>
|
||||
<div class="text-xs text-gray-400 mt-1">대</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
<!-- 이번달 점검 현황 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4 pb-2 border-b">
|
||||
이번달 점검 현황
|
||||
<span class="text-sm font-normal text-gray-500 ml-2">{{ now()->format('Y년 m월') }}</span>
|
||||
</h2>
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600">점검 대상</span>
|
||||
<span class="font-semibold">{{ $inspectionStats['total'] }}대</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600">점검 완료</span>
|
||||
<span class="font-semibold text-green-600">{{ $inspectionStats['inspected'] }}대</span>
|
||||
</div>
|
||||
@if($inspectionStats['total'] > 0)
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div class="bg-green-500 h-2 rounded-full"
|
||||
style="width: {{ min(100, round($inspectionStats['inspected'] / $inspectionStats['total'] * 100)) }}%"></div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 text-right">
|
||||
{{ round($inspectionStats['inspected'] / $inspectionStats['total'] * 100) }}% 완료
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600">이상 발견</span>
|
||||
<span class="font-semibold {{ $inspectionStats['issue_count'] > 0 ? 'text-red-600' : 'text-gray-400' }}">
|
||||
{{ $inspectionStats['issue_count'] }}건
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 설비 유형별 현황 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4 pb-2 border-b">설비 유형별 현황</h2>
|
||||
@if(!empty($typeStats))
|
||||
<div class="space-y-2">
|
||||
@foreach($typeStats as $type => $count)
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600">{{ $type ?? '미분류' }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="bg-gray-200 rounded-full h-2" style="width: 100px;">
|
||||
@php $maxCount = max($typeStats); @endphp
|
||||
<div class="bg-blue-500 h-2 rounded-full"
|
||||
style="width: {{ $maxCount > 0 ? round($count / $maxCount * 100) : 0 }}%"></div>
|
||||
</div>
|
||||
<span class="font-semibold text-sm" style="min-width: 30px; text-align: right;">{{ $count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<p class="text-gray-500 text-center py-4">데이터가 없습니다.</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 최근 수리이력 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<div class="flex justify-between items-center mb-4 pb-2 border-b">
|
||||
<h2 class="text-lg font-semibold text-gray-800">최근 수리이력</h2>
|
||||
<a href="{{ route('equipment.repairs') }}" class="text-blue-600 hover:text-blue-800 text-sm">
|
||||
전체보기 →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if($recentRepairs->isNotEmpty())
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">수리일</th>
|
||||
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700">설비</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">보전구분</th>
|
||||
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700">수리내용</th>
|
||||
<th class="px-3 py-2 text-right text-sm font-semibold text-gray-700">비용</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
@foreach($recentRepairs as $repair)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-3 py-2 text-sm text-center">{{ $repair->repair_date->format('m-d') }}</td>
|
||||
<td class="px-3 py-2 text-sm">{{ $repair->equipment?->name ?? '-' }}</td>
|
||||
<td class="px-3 py-2 text-sm text-center">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium
|
||||
{{ $repair->repair_type === 'internal' ? 'bg-blue-100 text-blue-800' : 'bg-orange-100 text-orange-800' }}">
|
||||
{{ $repair->repair_type_label }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-sm">{{ Str::limit($repair->description, 40) ?? '-' }}</td>
|
||||
<td class="px-3 py-2 text-sm text-right font-mono">{{ $repair->formatted_cost }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-gray-500 text-center py-8">최근 수리이력이 없습니다.</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
230
resources/views/equipment/edit.blade.php
Normal file
230
resources/views/equipment/edit.blade.php
Normal file
@@ -0,0 +1,230 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', '설비 수정')
|
||||
@section('content')
|
||||
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800">설비 수정</h1>
|
||||
<a href="{{ route('equipment.index') }}" class="text-gray-600 hover:text-gray-800">
|
||||
← 목록으로
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 로딩 상태 -->
|
||||
<div id="loadingState" class="bg-white rounded-lg shadow-sm p-6">
|
||||
<div class="flex justify-center items-center p-12">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 폼 -->
|
||||
<div id="formContainer" style="display: none;">
|
||||
<form id="equipmentForm" class="space-y-6">
|
||||
@csrf
|
||||
|
||||
<!-- 기본정보 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4 pb-2 border-b">기본정보</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||
설비코드 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="text" name="equipment_code" id="equipment_code" required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||
설비명 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="text" name="name" id="name" required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">설비유형</label>
|
||||
<select name="equipment_type" id="equipment_type"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">선택</option>
|
||||
<option value="포밍기">포밍기</option>
|
||||
<option value="미싱기">미싱기</option>
|
||||
<option value="샤링기">샤링기</option>
|
||||
<option value="V컷팅기">V컷팅기</option>
|
||||
<option value="절곡기">절곡기</option>
|
||||
<option value="프레스">프레스</option>
|
||||
<option value="드릴">드릴</option>
|
||||
<option value="기타">기타</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">규격</label>
|
||||
<input type="text" name="specification" id="specification"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 제조사 정보 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4 pb-2 border-b">제조사 정보</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">제조사</label>
|
||||
<input type="text" name="manufacturer" id="manufacturer"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">모델명</label>
|
||||
<input type="text" name="model_name" id="model_name"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">제조번호</label>
|
||||
<input type="text" name="serial_no" id="serial_no"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 설치 정보 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4 pb-2 border-b">설치 정보</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">위치</label>
|
||||
<input type="text" name="location" id="location"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">생산라인</label>
|
||||
<select name="production_line" id="production_line"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">선택</option>
|
||||
<option value="스라트">스라트</option>
|
||||
<option value="스크린">스크린</option>
|
||||
<option value="절곡">절곡</option>
|
||||
<option value="기타">기타</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">구입일</label>
|
||||
<input type="date" name="purchase_date" id="purchase_date"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">설치일</label>
|
||||
<input type="date" name="install_date" id="install_date"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">구입가격 (원)</label>
|
||||
<input type="number" name="purchase_price" id="purchase_price" min="0" step="1"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">내용연수 (년)</label>
|
||||
<input type="number" name="useful_life" id="useful_life" min="0"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 담당자/비고 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4 pb-2 border-b">담당자 / 비고</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">담당자</label>
|
||||
<select name="manager_id" id="manager_id"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">선택</option>
|
||||
@foreach($users as $user)
|
||||
<option value="{{ $user->id }}">{{ $user->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">상태</label>
|
||||
<select name="status" id="status"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="active">가동</option>
|
||||
<option value="idle">유휴</option>
|
||||
<option value="disposed">폐기</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">비고</label>
|
||||
<textarea name="memo" id="memo" rows="3"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 버튼 -->
|
||||
<div class="flex gap-3">
|
||||
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg transition">
|
||||
수정
|
||||
</button>
|
||||
<a href="{{ route('equipment.index') }}"
|
||||
class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-6 py-2 rounded-lg transition">
|
||||
취소
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
const equipmentId = {{ $id }};
|
||||
const fields = ['equipment_code', 'name', 'equipment_type', 'specification', 'manufacturer',
|
||||
'model_name', 'serial_no', 'location', 'production_line', 'purchase_date', 'install_date',
|
||||
'purchase_price', 'useful_life', 'status', 'manager_id', 'memo'];
|
||||
|
||||
fetch(`/admin/equipment/${equipmentId}`, {
|
||||
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const eq = data.data;
|
||||
fields.forEach(f => {
|
||||
const el = document.getElementById(f);
|
||||
if (el && eq[f] != null) el.value = eq[f];
|
||||
});
|
||||
document.getElementById('loadingState').style.display = 'none';
|
||||
document.getElementById('formContainer').style.display = 'block';
|
||||
} else {
|
||||
showToast('설비 정보를 불러올 수 없습니다.', 'error');
|
||||
window.location.href = '{{ route("equipment.index") }}';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('equipmentForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(this);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
fetch(`/admin/equipment/${equipmentId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast(data.message, 'success');
|
||||
window.location.href = '{{ route("equipment.index") }}';
|
||||
} else {
|
||||
showToast(data.message || '수정에 실패했습니다.', 'error');
|
||||
}
|
||||
})
|
||||
.catch(() => showToast('서버 오류가 발생했습니다.', 'error'));
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
107
resources/views/equipment/index.blade.php
Normal file
107
resources/views/equipment/index.blade.php
Normal file
@@ -0,0 +1,107 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', '설비 등록대장')
|
||||
@section('content')
|
||||
|
||||
<!-- 헤더 -->
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800">설비 등록대장</h1>
|
||||
<a href="{{ route('equipment.create') }}"
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition text-center">
|
||||
+ 설비 등록
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 필터 -->
|
||||
<x-filter-collapsible id="filterForm">
|
||||
<form id="filterForm" class="flex flex-wrap gap-2 sm:gap-4">
|
||||
<input type="hidden" name="per_page" id="perPageInput" value="20">
|
||||
<input type="hidden" name="page" id="pageInput" value="1">
|
||||
|
||||
<div style="flex: 1 1 200px; max-width: 300px;">
|
||||
<input type="text" name="search" placeholder="설비번호/설비명 검색..."
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<div class="shrink-0" style="width: 140px;">
|
||||
<select name="status" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">상태 전체</option>
|
||||
<option value="active">가동</option>
|
||||
<option value="idle">유휴</option>
|
||||
<option value="disposed">폐기</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="shrink-0" style="width: 140px;">
|
||||
<select name="production_line" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">라인 전체</option>
|
||||
<option value="스라트">스라트</option>
|
||||
<option value="스크린">스크린</option>
|
||||
<option value="절곡">절곡</option>
|
||||
<option value="기타">기타</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="shrink-0" style="width: 140px;">
|
||||
<select name="equipment_type" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">유형 전체</option>
|
||||
<option value="포밍기">포밍기</option>
|
||||
<option value="미싱기">미싱기</option>
|
||||
<option value="샤링기">샤링기</option>
|
||||
<option value="V컷팅기">V컷팅기</option>
|
||||
<option value="절곡기">절곡기</option>
|
||||
<option value="프레스">프레스</option>
|
||||
<option value="드릴">드릴</option>
|
||||
<option value="기타">기타</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition shrink-0">
|
||||
검색
|
||||
</button>
|
||||
</form>
|
||||
</x-filter-collapsible>
|
||||
|
||||
<!-- 테이블 영역 -->
|
||||
<div id="equipment-table"
|
||||
hx-get="/admin/equipment"
|
||||
hx-trigger="load, filterSubmit from:body"
|
||||
hx-include="#filterForm"
|
||||
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
|
||||
class="bg-white rounded-lg shadow-sm">
|
||||
<div class="flex justify-center items-center p-12">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.getElementById('filterForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
document.getElementById('pageInput').value = 1;
|
||||
htmx.trigger('#equipment-table', 'filterSubmit');
|
||||
});
|
||||
|
||||
function confirmDelete(id, name) {
|
||||
showDeleteConfirm(name, () => {
|
||||
fetch(`/admin/equipment/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
htmx.trigger('#equipment-table', 'filterSubmit');
|
||||
showToast(data.message, 'success');
|
||||
} else {
|
||||
showToast(data.message, 'error');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
90
resources/views/equipment/inspections/index.blade.php
Normal file
90
resources/views/equipment/inspections/index.blade.php
Normal file
@@ -0,0 +1,90 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', '일상점검표')
|
||||
@section('content')
|
||||
|
||||
<!-- 헤더 -->
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800">일상점검표</h1>
|
||||
</div>
|
||||
|
||||
<!-- 필터 -->
|
||||
<x-filter-collapsible id="inspectionFilter" :defaultOpen="true">
|
||||
<form id="inspectionFilter" class="flex flex-wrap gap-2 sm:gap-4">
|
||||
<div class="shrink-0" style="width: 160px;">
|
||||
<label class="block text-xs text-gray-500 mb-1">점검년월</label>
|
||||
<input type="month" name="year_month" id="yearMonth" value="{{ now()->format('Y-m') }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div class="shrink-0" style="width: 140px;">
|
||||
<label class="block text-xs text-gray-500 mb-1">생산라인</label>
|
||||
<select name="production_line"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">전체</option>
|
||||
<option value="스라트">스라트</option>
|
||||
<option value="스크린">스크린</option>
|
||||
<option value="절곡">절곡</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="shrink-0" style="width: 200px;">
|
||||
<label class="block text-xs text-gray-500 mb-1">설비</label>
|
||||
<select name="equipment_id"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">전체</option>
|
||||
@foreach($equipmentList as $eq)
|
||||
<option value="{{ $eq->id }}">{{ $eq->equipment_code }} - {{ $eq->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="self-end">
|
||||
<button type="submit" class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition">
|
||||
조회
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</x-filter-collapsible>
|
||||
|
||||
<!-- 점검 그리드 -->
|
||||
<div id="inspection-grid"
|
||||
hx-get="/admin/equipment/inspections"
|
||||
hx-trigger="load, filterSubmit from:body"
|
||||
hx-include="#inspectionFilter"
|
||||
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
|
||||
class="bg-white rounded-lg shadow-sm">
|
||||
<div class="flex justify-center items-center p-12">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.getElementById('inspectionFilter').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
htmx.trigger('#inspection-grid', 'filterSubmit');
|
||||
});
|
||||
|
||||
function toggleCell(equipmentId, templateItemId, checkDate, cell) {
|
||||
fetch('/admin/equipment/inspections/detail', {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
equipment_id: equipmentId,
|
||||
template_item_id: templateItemId,
|
||||
check_date: checkDate,
|
||||
})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
cell.textContent = data.data.symbol;
|
||||
cell.className = 'inspection-cell cursor-pointer text-center text-lg font-bold select-none ' + data.data.color;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
97
resources/views/equipment/partials/inspection-grid.blade.php
Normal file
97
resources/views/equipment/partials/inspection-grid.blade.php
Normal file
@@ -0,0 +1,97 @@
|
||||
@if(empty($inspections))
|
||||
<div class="p-12 text-center text-gray-500">
|
||||
<p>점검 가능한 설비가 없습니다.</p>
|
||||
<p class="text-sm mt-2">설비 등록대장에서 점검항목을 추가해주세요.</p>
|
||||
</div>
|
||||
@else
|
||||
@php
|
||||
$date = \Carbon\Carbon::createFromFormat('Y-m', $yearMonth);
|
||||
$daysInMonth = $date->daysInMonth;
|
||||
@endphp
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full border-collapse text-xs">
|
||||
<thead>
|
||||
<tr class="bg-gray-100">
|
||||
<th class="border border-gray-300 px-2 py-1 text-center whitespace-nowrap sticky left-0 bg-gray-100 z-10" style="min-width: 80px;">설비</th>
|
||||
<th class="border border-gray-300 px-2 py-1 text-center whitespace-nowrap sticky bg-gray-100 z-10" style="left: 80px; min-width: 80px;">점검항목</th>
|
||||
@for($d = 1; $d <= $daysInMonth; $d++)
|
||||
@php
|
||||
$dayDate = $date->copy()->day($d);
|
||||
$dayOfWeek = $dayDate->dayOfWeek;
|
||||
$isWeekend = in_array($dayOfWeek, [0, 6]);
|
||||
@endphp
|
||||
<th class="border border-gray-300 px-1 py-1 text-center {{ $isWeekend ? 'bg-red-50 text-red-600' : '' }}" style="min-width: 32px;">
|
||||
{{ $d }}
|
||||
</th>
|
||||
@endfor
|
||||
<th class="border border-gray-300 px-2 py-1 text-center whitespace-nowrap" style="min-width: 60px;">판정</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@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)
|
||||
<tr class="{{ $idx === 0 ? 'border-t-2 border-gray-400' : '' }}">
|
||||
@if($idx === 0)
|
||||
<td class="border border-gray-300 px-2 py-1 text-center font-medium whitespace-nowrap sticky left-0 bg-white z-10"
|
||||
rowspan="{{ $rowCount }}" style="min-width: 80px;">
|
||||
<div class="text-xs text-blue-600">{{ $equipment->equipment_code }}</div>
|
||||
<div class="text-xs">{{ Str::limit($equipment->name, 8) }}</div>
|
||||
</td>
|
||||
@endif
|
||||
<td class="border border-gray-300 px-2 py-1 whitespace-nowrap sticky bg-white z-10" style="left: 80px; min-width: 80px;">
|
||||
<span class="text-gray-600">{{ $tmpl->check_point }}</span>
|
||||
</td>
|
||||
|
||||
@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
|
||||
<td class="border border-gray-300 text-center cursor-pointer select-none {{ $isWeekend ? 'bg-red-50' : '' }}"
|
||||
style="min-width: 32px; padding: 2px;"
|
||||
onclick="toggleCell({{ $equipment->id }}, {{ $tmpl->id }}, '{{ $checkDate }}', this)">
|
||||
<span class="inspection-cell text-lg font-bold {{ $color }}">{{ $symbol }}</span>
|
||||
</td>
|
||||
@endfor
|
||||
|
||||
@if($idx === 0)
|
||||
<td class="border border-gray-300 px-2 py-1 text-center whitespace-nowrap"
|
||||
rowspan="{{ $rowCount }}">
|
||||
@if($inspection && $inspection->overall_judgment)
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium {{ $inspection->judgment_color }}">
|
||||
{{ $inspection->judgment_label }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-gray-400">-</span>
|
||||
@endif
|
||||
</td>
|
||||
@endif
|
||||
</tr>
|
||||
@endforeach
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 범례 -->
|
||||
<div class="p-4 border-t flex flex-wrap gap-4 text-sm text-gray-600">
|
||||
<span><span class="text-green-600 font-bold text-lg">○</span> 양호</span>
|
||||
<span><span class="text-red-600 font-bold text-lg">X</span> 이상</span>
|
||||
<span><span class="text-yellow-600 font-bold text-lg">△</span> 수리완료</span>
|
||||
<span class="text-gray-400">셀 클릭: 빈칸 → ○ → X → △ → 빈칸</span>
|
||||
</div>
|
||||
@endif
|
||||
63
resources/views/equipment/partials/repair-table.blade.php
Normal file
63
resources/views/equipment/partials/repair-table.blade.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
<x-table-swipe>
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">수리일</th>
|
||||
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700">설비</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">보전구분</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">수리시간</th>
|
||||
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700">수리내용</th>
|
||||
<th class="px-3 py-2 text-right text-sm font-semibold text-gray-700">비용</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">외주업체</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">액션</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
@forelse($repairs as $repair)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-3 py-3 whitespace-nowrap text-sm text-center">
|
||||
{{ $repair->repair_date->format('Y-m-d') }}
|
||||
</td>
|
||||
<td class="px-3 py-3 whitespace-nowrap text-sm">
|
||||
<span class="font-mono text-blue-600 text-xs">{{ $repair->equipment?->equipment_code }}</span>
|
||||
<span class="ml-1">{{ $repair->equipment?->name }}</span>
|
||||
</td>
|
||||
<td class="px-3 py-3 whitespace-nowrap text-sm text-center">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium
|
||||
{{ $repair->repair_type === 'internal' ? 'bg-blue-100 text-blue-800' : 'bg-orange-100 text-orange-800' }}">
|
||||
{{ $repair->repair_type_label }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-3 py-3 whitespace-nowrap text-sm text-center">
|
||||
{{ $repair->repair_hours ? $repair->repair_hours . 'h' : '-' }}
|
||||
</td>
|
||||
<td class="px-3 py-3 text-sm">
|
||||
{{ Str::limit($repair->description, 40) ?? '-' }}
|
||||
</td>
|
||||
<td class="px-3 py-3 whitespace-nowrap text-sm text-right font-mono">
|
||||
{{ $repair->formatted_cost }}
|
||||
</td>
|
||||
<td class="px-3 py-3 whitespace-nowrap text-sm text-center">
|
||||
{{ $repair->vendor ?? '-' }}
|
||||
</td>
|
||||
<td class="px-3 py-3 whitespace-nowrap text-sm text-center">
|
||||
<button onclick="confirmDeleteRepair({{ $repair->id }})"
|
||||
class="text-red-600 hover:text-red-900">삭제</button>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="8" class="px-6 py-12 text-center text-gray-500">
|
||||
수리이력이 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</x-table-swipe>
|
||||
</div>
|
||||
|
||||
@if($repairs->hasPages())
|
||||
@include('partials.pagination', ['paginator' => $repairs, 'target' => '#repair-table'])
|
||||
@endif
|
||||
66
resources/views/equipment/partials/table.blade.php
Normal file
66
resources/views/equipment/partials/table.blade.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
<x-table-swipe>
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">설비번호</th>
|
||||
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700">설비명</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">유형</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">위치</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">생산라인</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">상태</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">담당자</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">구입일</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">액션</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
@forelse($equipments as $eq)
|
||||
<tr class="hover:bg-gray-50 cursor-pointer" onclick="window.location='{{ route('equipment.show', $eq->id) }}'">
|
||||
<td class="px-3 py-3 whitespace-nowrap text-sm text-center font-mono text-blue-600">
|
||||
{{ $eq->equipment_code }}
|
||||
</td>
|
||||
<td class="px-3 py-3 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{{ $eq->name }}
|
||||
</td>
|
||||
<td class="px-3 py-3 whitespace-nowrap text-sm text-center text-gray-600">
|
||||
{{ $eq->equipment_type ?? '-' }}
|
||||
</td>
|
||||
<td class="px-3 py-3 whitespace-nowrap text-sm text-center text-gray-600">
|
||||
{{ $eq->location ?? '-' }}
|
||||
</td>
|
||||
<td class="px-3 py-3 whitespace-nowrap text-sm text-center text-gray-600">
|
||||
{{ $eq->production_line ?? '-' }}
|
||||
</td>
|
||||
<td class="px-3 py-3 whitespace-nowrap text-sm text-center">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ $eq->status_color }}">
|
||||
{{ $eq->status_label }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-3 py-3 whitespace-nowrap text-sm text-center text-gray-600">
|
||||
{{ $eq->manager?->name ?? '-' }}
|
||||
</td>
|
||||
<td class="px-3 py-3 whitespace-nowrap text-sm text-center text-gray-600">
|
||||
{{ $eq->purchase_date?->format('Y-m-d') ?? '-' }}
|
||||
</td>
|
||||
<td class="px-3 py-3 whitespace-nowrap text-sm text-center" onclick="event.stopPropagation()">
|
||||
<a href="{{ route('equipment.edit', $eq->id) }}" class="text-blue-600 hover:text-blue-900 mr-2">수정</a>
|
||||
<button onclick="confirmDelete({{ $eq->id }}, '{{ $eq->name }}')"
|
||||
class="text-red-600 hover:text-red-900">삭제</button>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="9" class="px-6 py-12 text-center text-gray-500">
|
||||
등록된 설비가 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</x-table-swipe>
|
||||
</div>
|
||||
|
||||
@if($equipments->hasPages())
|
||||
@include('partials.pagination', ['paginator' => $equipments, 'target' => '#equipment-table'])
|
||||
@endif
|
||||
102
resources/views/equipment/partials/tabs/basic-info.blade.php
Normal file
102
resources/views/equipment/partials/tabs/basic-info.blade.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<!-- 기본정보 탭 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4 pb-2 border-b">기본정보</h2>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-500 mb-1">설비코드</label>
|
||||
<p class="text-gray-900 font-mono font-medium">{{ $equipment->equipment_code }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-500 mb-1">설비명</label>
|
||||
<p class="text-gray-900 font-medium">{{ $equipment->name }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-500 mb-1">설비유형</label>
|
||||
<p class="text-gray-900">{{ $equipment->equipment_type ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-500 mb-1">규격</label>
|
||||
<p class="text-gray-900">{{ $equipment->specification ?? '-' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4 pb-2 border-b">제조사 정보</h2>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-500 mb-1">제조사</label>
|
||||
<p class="text-gray-900">{{ $equipment->manufacturer ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-500 mb-1">모델명</label>
|
||||
<p class="text-gray-900">{{ $equipment->model_name ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-500 mb-1">제조번호</label>
|
||||
<p class="text-gray-900 font-mono">{{ $equipment->serial_no ?? '-' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4 pb-2 border-b">설치 정보</h2>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-500 mb-1">위치</label>
|
||||
<p class="text-gray-900">{{ $equipment->location ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-500 mb-1">생산라인</label>
|
||||
<p class="text-gray-900">{{ $equipment->production_line ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-500 mb-1">구입일</label>
|
||||
<p class="text-gray-900">{{ $equipment->purchase_date?->format('Y-m-d') ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-500 mb-1">설치일</label>
|
||||
<p class="text-gray-900">{{ $equipment->install_date?->format('Y-m-d') ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-500 mb-1">구입가격</label>
|
||||
<p class="text-gray-900">{{ $equipment->purchase_price ? number_format($equipment->purchase_price) . '원' : '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-500 mb-1">내용연수</label>
|
||||
<p class="text-gray-900">{{ $equipment->useful_life ? $equipment->useful_life . '년' : '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-500 mb-1">담당자</label>
|
||||
<p class="text-gray-900">{{ $equipment->manager?->name ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-500 mb-1">상태</label>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ $equipment->status_color }}">
|
||||
{{ $equipment->status_label }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@if($equipment->memo)
|
||||
<div class="mt-4">
|
||||
<label class="block text-sm text-gray-500 mb-1">비고</label>
|
||||
<p class="text-gray-900 whitespace-pre-wrap">{{ $equipment->memo }}</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($equipment->processes->isNotEmpty())
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4 pb-2 border-b">연결된 공정</h2>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach($equipment->processes as $process)
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
|
||||
{{ $process->process_name }}
|
||||
@if($process->pivot->is_primary)
|
||||
<span class="ml-1 text-xs text-blue-600">(주)</span>
|
||||
@endif
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,104 @@
|
||||
<!-- 점검항목 탭 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
|
||||
<div class="flex justify-between items-center mb-4 pb-2 border-b">
|
||||
<h2 class="text-lg font-semibold text-gray-800">점검항목 템플릿</h2>
|
||||
<button onclick="document.getElementById('templateModal').classList.remove('hidden')"
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-lg text-sm transition">
|
||||
+ 항목 추가
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if($equipment->inspectionTemplates->isNotEmpty())
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">번호</th>
|
||||
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700">점검개소</th>
|
||||
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700">점검항목</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">시기</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">주기</th>
|
||||
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700">점검방법</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">액션</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
@foreach($equipment->inspectionTemplates as $tmpl)
|
||||
<tr>
|
||||
<td class="px-3 py-2 text-sm text-center">{{ $tmpl->item_no }}</td>
|
||||
<td class="px-3 py-2 text-sm">{{ $tmpl->check_point }}</td>
|
||||
<td class="px-3 py-2 text-sm">{{ $tmpl->check_item }}</td>
|
||||
<td class="px-3 py-2 text-sm text-center">{{ $tmpl->timing_label }}</td>
|
||||
<td class="px-3 py-2 text-sm text-center">{{ $tmpl->check_frequency ?? '-' }}</td>
|
||||
<td class="px-3 py-2 text-sm text-gray-600">{{ $tmpl->check_method ?? '-' }}</td>
|
||||
<td class="px-3 py-2 text-sm text-center">
|
||||
<button onclick="deleteTemplate({{ $tmpl->id }})" class="text-red-600 hover:text-red-900">삭제</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-gray-500 text-center py-8">등록된 점검항목이 없습니다.</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- 점검항목 추가 모달 -->
|
||||
<div id="templateModal" class="fixed inset-0 z-50 hidden">
|
||||
<div class="fixed inset-0 bg-black/50" onclick="document.getElementById('templateModal').classList.add('hidden')"></div>
|
||||
<div class="fixed inset-0 flex items-center justify-center p-4">
|
||||
<div class="bg-white rounded-xl shadow-xl w-full max-w-lg">
|
||||
<div class="flex justify-between items-center p-6 border-b">
|
||||
<h3 class="text-lg font-semibold">점검항목 추가</h3>
|
||||
<button onclick="document.getElementById('templateModal').classList.add('hidden')" class="text-gray-400 hover:text-gray-600">×</button>
|
||||
</div>
|
||||
<form id="templateForm" class="p-6 space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-1">항목번호 <span class="text-red-500">*</span></label>
|
||||
<input type="number" name="item_no" required min="1"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-1">점검개소 <span class="text-red-500">*</span></label>
|
||||
<input type="text" name="check_point" required placeholder="겉모양"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-1">점검항목 <span class="text-red-500">*</span></label>
|
||||
<input type="text" name="check_item" required placeholder="청결상태"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-1">시기</label>
|
||||
<select name="check_timing"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">선택</option>
|
||||
<option value="operating">가동 중</option>
|
||||
<option value="stopped">정지 시</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-1">주기</label>
|
||||
<input type="text" name="check_frequency" placeholder="1회/일"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-1">점검방법</label>
|
||||
<textarea name="check_method" rows="2" placeholder="육안 확인"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3 pt-2">
|
||||
<button type="button" onclick="document.getElementById('templateModal').classList.add('hidden')"
|
||||
class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-4 py-2 rounded-lg">취소</button>
|
||||
<button type="button" onclick="addTemplate()"
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg">추가</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,46 @@
|
||||
<!-- 수리이력 탭 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<div class="flex justify-between items-center mb-4 pb-2 border-b">
|
||||
<h2 class="text-lg font-semibold text-gray-800">수리이력</h2>
|
||||
</div>
|
||||
|
||||
@if($equipment->repairs->isNotEmpty())
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">수리일</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">보전구분</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">수리시간</th>
|
||||
<th class="px-3 py-2 text-left text-sm font-semibold text-gray-700">수리내용</th>
|
||||
<th class="px-3 py-2 text-right text-sm font-semibold text-gray-700">비용</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">외주업체</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700">액션</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
@foreach($equipment->repairs->sortByDesc('repair_date') as $repair)
|
||||
<tr>
|
||||
<td class="px-3 py-2 text-sm text-center">{{ $repair->repair_date->format('Y-m-d') }}</td>
|
||||
<td class="px-3 py-2 text-sm text-center">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium
|
||||
{{ $repair->repair_type === 'internal' ? 'bg-blue-100 text-blue-800' : 'bg-orange-100 text-orange-800' }}">
|
||||
{{ $repair->repair_type_label }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-sm text-center">{{ $repair->repair_hours ? $repair->repair_hours . 'h' : '-' }}</td>
|
||||
<td class="px-3 py-2 text-sm">{{ Str::limit($repair->description, 50) ?? '-' }}</td>
|
||||
<td class="px-3 py-2 text-sm text-right font-mono">{{ $repair->formatted_cost }}</td>
|
||||
<td class="px-3 py-2 text-sm text-center">{{ $repair->vendor ?? '-' }}</td>
|
||||
<td class="px-3 py-2 text-sm text-center">
|
||||
<button onclick="deleteRepair({{ $repair->id }})" class="text-red-600 hover:text-red-900">삭제</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-gray-500 text-center py-8">수리이력이 없습니다.</p>
|
||||
@endif
|
||||
</div>
|
||||
134
resources/views/equipment/repairs/create.blade.php
Normal file
134
resources/views/equipment/repairs/create.blade.php
Normal file
@@ -0,0 +1,134 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', '수리이력 등록')
|
||||
@section('content')
|
||||
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800">수리이력 등록</h1>
|
||||
<a href="{{ route('equipment.repairs') }}" class="text-gray-600 hover:text-gray-800">
|
||||
← 목록으로
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<form id="repairForm" class="space-y-4">
|
||||
@csrf
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||
설비 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select name="equipment_id" required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">선택</option>
|
||||
@foreach($equipmentList as $eq)
|
||||
<option value="{{ $eq->id }}">{{ $eq->equipment_code }} - {{ $eq->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||
수리일 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="date" name="repair_date" required value="{{ now()->format('Y-m-d') }}"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||
보전구분 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select name="repair_type" required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">선택</option>
|
||||
<option value="internal">사내</option>
|
||||
<option value="external">외주</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">수리시간 (h)</label>
|
||||
<input type="number" name="repair_hours" min="0" step="0.5"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">수리내용</label>
|
||||
<textarea name="description" rows="3"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">수리비용 (원)</label>
|
||||
<input type="number" name="cost" min="0" step="1"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">외주업체</label>
|
||||
<input type="text" name="vendor"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">수리자</label>
|
||||
<select name="repaired_by"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">선택</option>
|
||||
@foreach($users as $user)
|
||||
<option value="{{ $user->id }}">{{ $user->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-2">비고</label>
|
||||
<textarea name="memo" rows="2"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 pt-2">
|
||||
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg transition">
|
||||
등록
|
||||
</button>
|
||||
<a href="{{ route('equipment.repairs') }}"
|
||||
class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-6 py-2 rounded-lg transition">
|
||||
취소
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.getElementById('repairForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
fetch('/admin/equipment/repairs', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast(data.message, 'success');
|
||||
window.location.href = '{{ route("equipment.repairs") }}';
|
||||
} else {
|
||||
showToast(data.message || '등록에 실패했습니다.', 'error');
|
||||
}
|
||||
})
|
||||
.catch(() => showToast('서버 오류가 발생했습니다.', 'error'));
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
100
resources/views/equipment/repairs/index.blade.php
Normal file
100
resources/views/equipment/repairs/index.blade.php
Normal file
@@ -0,0 +1,100 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', '수리이력')
|
||||
@section('content')
|
||||
|
||||
<!-- 헤더 -->
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800">수리이력</h1>
|
||||
<a href="{{ route('equipment.repairs.create') }}"
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition text-center">
|
||||
+ 수리이력 등록
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 필터 -->
|
||||
<x-filter-collapsible id="repairFilter">
|
||||
<form id="repairFilter" class="flex flex-wrap gap-2 sm:gap-4">
|
||||
<input type="hidden" name="per_page" id="perPageInput" value="20">
|
||||
<input type="hidden" name="page" id="pageInput" value="1">
|
||||
|
||||
<div style="flex: 1 1 200px; max-width: 300px;">
|
||||
<input type="text" name="search" placeholder="설비명/수리내용 검색..."
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<div class="shrink-0" style="width: 200px;">
|
||||
<select name="equipment_id"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">설비 전체</option>
|
||||
@foreach($equipmentList as $eq)
|
||||
<option value="{{ $eq->id }}">{{ $eq->equipment_code }} - {{ $eq->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="shrink-0" style="width: 140px;">
|
||||
<select name="repair_type"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">구분 전체</option>
|
||||
<option value="internal">사내</option>
|
||||
<option value="external">외주</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="shrink-0" style="width: 150px;">
|
||||
<input type="date" name="date_from" placeholder="시작일"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div class="shrink-0" style="width: 150px;">
|
||||
<input type="date" name="date_to" placeholder="종료일"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition shrink-0">
|
||||
검색
|
||||
</button>
|
||||
</form>
|
||||
</x-filter-collapsible>
|
||||
|
||||
<!-- 테이블 -->
|
||||
<div id="repair-table"
|
||||
hx-get="/admin/equipment/repairs"
|
||||
hx-trigger="load, filterSubmit from:body"
|
||||
hx-include="#repairFilter"
|
||||
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
|
||||
class="bg-white rounded-lg shadow-sm">
|
||||
<div class="flex justify-center items-center p-12">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.getElementById('repairFilter').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
document.getElementById('pageInput').value = 1;
|
||||
htmx.trigger('#repair-table', 'filterSubmit');
|
||||
});
|
||||
|
||||
function confirmDeleteRepair(id) {
|
||||
showDeleteConfirm('수리이력', () => {
|
||||
fetch(`/admin/equipment/repairs/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
htmx.trigger('#repair-table', 'filterSubmit');
|
||||
showToast(data.message, 'success');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
137
resources/views/equipment/show.blade.php
Normal file
137
resources/views/equipment/show.blade.php
Normal file
@@ -0,0 +1,137 @@
|
||||
@extends('layouts.app')
|
||||
@section('title', $equipment->name . ' - 설비 상세')
|
||||
@section('content')
|
||||
|
||||
<!-- 헤더 -->
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="{{ route('equipment.index') }}"
|
||||
class="p-2 text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-lg">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
</a>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-800">{{ $equipment->name }}</h1>
|
||||
<p class="text-sm text-gray-500 font-mono">{{ $equipment->equipment_code }}</p>
|
||||
</div>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ $equipment->status_color }}">
|
||||
{{ $equipment->status_label }}
|
||||
</span>
|
||||
</div>
|
||||
<a href="{{ route('equipment.edit', $equipment->id) }}"
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition text-center">
|
||||
수정
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 탭 네비게이션 -->
|
||||
<div class="border-b border-gray-200 mb-6">
|
||||
<nav class="flex space-x-8" id="tabNav">
|
||||
<button class="tab-btn border-b-2 border-blue-500 text-blue-600 px-1 py-3 text-sm font-medium" data-tab="basic">
|
||||
기본정보
|
||||
</button>
|
||||
<button class="tab-btn border-b-2 border-transparent text-gray-500 hover:text-gray-700 px-1 py-3 text-sm font-medium" data-tab="inspection">
|
||||
점검항목
|
||||
</button>
|
||||
<button class="tab-btn border-b-2 border-transparent text-gray-500 hover:text-gray-700 px-1 py-3 text-sm font-medium" data-tab="repair">
|
||||
수리이력
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- 탭 콘텐츠 -->
|
||||
<div id="tab-basic" class="tab-content">
|
||||
@include('equipment.partials.tabs.basic-info', ['equipment' => $equipment])
|
||||
</div>
|
||||
|
||||
<div id="tab-inspection" class="tab-content" style="display: none;">
|
||||
@include('equipment.partials.tabs.inspection-items', ['equipment' => $equipment])
|
||||
</div>
|
||||
|
||||
<div id="tab-repair" class="tab-content" style="display: none;">
|
||||
@include('equipment.partials.tabs.repair-history', ['equipment' => $equipment])
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
const equipmentId = {{ $equipment->id }};
|
||||
|
||||
// 탭 전환
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
document.querySelectorAll('.tab-btn').forEach(b => {
|
||||
b.classList.remove('border-blue-500', 'text-blue-600');
|
||||
b.classList.add('border-transparent', 'text-gray-500');
|
||||
});
|
||||
this.classList.remove('border-transparent', 'text-gray-500');
|
||||
this.classList.add('border-blue-500', 'text-blue-600');
|
||||
|
||||
document.querySelectorAll('.tab-content').forEach(c => c.style.display = 'none');
|
||||
document.getElementById('tab-' + this.dataset.tab).style.display = 'block';
|
||||
});
|
||||
});
|
||||
|
||||
// 점검항목 추가
|
||||
function addTemplate() {
|
||||
const form = document.getElementById('templateForm');
|
||||
const formData = new FormData(form);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
fetch(`/admin/equipment/${equipmentId}/templates`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast(data.message, 'success');
|
||||
location.reload();
|
||||
} else {
|
||||
showToast(data.message, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 점검항목 삭제
|
||||
function deleteTemplate(id) {
|
||||
showDeleteConfirm('점검항목', () => {
|
||||
fetch(`/admin/equipment/templates/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}', 'Accept': 'application/json' }
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast(data.message, 'success');
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 수리이력 삭제
|
||||
function deleteRepair(id) {
|
||||
showDeleteConfirm('수리이력', () => {
|
||||
fetch(`/admin/equipment/repairs/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}', 'Accept': 'application/json' }
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast(data.message, 'success');
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -957,6 +957,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');
|
||||
|
||||
@@ -1592,6 +1592,22 @@
|
||||
Route::delete('/history', [\App\Http\Controllers\Video\TutorialVideoController::class, 'destroy'])->name('destroy');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 설비관리 (Equipment Management)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
Route::prefix('equipment')->name('equipment.')->middleware(['auth', 'hq.member', 'password.changed'])->group(function () {
|
||||
Route::get('/', [\App\Http\Controllers\EquipmentController::class, 'dashboard'])->name('dashboard');
|
||||
Route::get('/registry', [\App\Http\Controllers\EquipmentController::class, 'index'])->name('index');
|
||||
Route::get('/registry/create', [\App\Http\Controllers\EquipmentController::class, 'create'])->name('create');
|
||||
Route::get('/registry/{id}', [\App\Http\Controllers\EquipmentController::class, 'show'])->whereNumber('id')->name('show');
|
||||
Route::get('/registry/{id}/edit', [\App\Http\Controllers\EquipmentController::class, 'edit'])->whereNumber('id')->name('edit');
|
||||
Route::get('/inspections', [\App\Http\Controllers\EquipmentController::class, 'inspections'])->name('inspections');
|
||||
Route::get('/repairs', [\App\Http\Controllers\EquipmentController::class, 'repairs'])->name('repairs');
|
||||
Route::get('/repairs/create', [\App\Http\Controllers\EquipmentController::class, 'repairCreate'])->name('repairs.create');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| SAM E-Sign Public Routes (인증 불필요 - 서명자용)
|
||||
|
||||
Reference in New Issue
Block a user