feat: [equipment] 설비관리 모듈 구현

- 모델 6개 (Equipment, InspectionTemplate, Inspection, InspectionDetail, Repair, Process)
- 서비스 3개 (Equipment, Inspection, Repair)
- API 컨트롤러 3개 + FormRequest 4개
- Blade 컨트롤러 + 라우트 등록
- 뷰: 대시보드, 등록대장(CRUD), 일상점검표(캘린더 그리드), 수리이력
This commit is contained in:
김보곤
2026-02-25 19:39:59 +09:00
parent f0178d8928
commit 11a7f89216
31 changed files with 2998 additions and 0 deletions

View 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">
&larr; 목록으로
</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

View 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">
전체보기 &rarr;
</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

View 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">
&larr; 목록으로
</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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -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">&times;</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>

View File

@@ -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>

View 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">
&larr; 목록으로
</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

View 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

View 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