feat: [equipment] 점검항목 다른 주기로 복사 기능 추가
- 서비스: copyTemplatesToCycles 메서드 추가 (중복 항목 스킵) - 컨트롤러: copyTemplates API 엔드포인트 추가 - UI: 다른 주기에 복사 버튼 + 체크박스 모달
This commit is contained in:
@@ -164,6 +164,34 @@ public function updateTemplate(Request $request, int $templateId): JsonResponse
|
||||
}
|
||||
}
|
||||
|
||||
public function copyTemplates(Request $request, int $equipmentId): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'source_cycle' => 'required|string',
|
||||
'target_cycles' => 'required|array|min:1',
|
||||
'target_cycles.*' => 'required|string',
|
||||
]);
|
||||
|
||||
try {
|
||||
$result = $this->inspectionService->copyTemplatesToCycles(
|
||||
$equipmentId,
|
||||
$request->input('source_cycle'),
|
||||
$request->input('target_cycles')
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => "{$result['copied']}개 항목이 복사되었습니다.".($result['skipped'] > 0 ? " (중복 {$result['skipped']}개 건너뜀)" : ''),
|
||||
'data' => $result,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteTemplate(int $templateId): JsonResponse
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -200,6 +200,64 @@ public function deleteTemplate(int $id): bool
|
||||
return EquipmentInspectionTemplate::findOrFail($id)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 점검항목을 다른 주기로 복사
|
||||
*/
|
||||
public function copyTemplatesToCycles(int $equipmentId, string $sourceCycle, array $targetCycles): array
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
|
||||
$sourceTemplates = EquipmentInspectionTemplate::where('equipment_id', $equipmentId)
|
||||
->where('inspection_cycle', $sourceCycle)
|
||||
->where('is_active', true)
|
||||
->orderBy('sort_order')
|
||||
->get();
|
||||
|
||||
if ($sourceTemplates->isEmpty()) {
|
||||
throw new \Exception('복사할 점검항목이 없습니다.');
|
||||
}
|
||||
|
||||
$copiedCount = 0;
|
||||
$skippedCount = 0;
|
||||
|
||||
foreach ($targetCycles as $targetCycle) {
|
||||
foreach ($sourceTemplates as $template) {
|
||||
$exists = EquipmentInspectionTemplate::where('equipment_id', $equipmentId)
|
||||
->where('inspection_cycle', $targetCycle)
|
||||
->where('item_no', $template->item_no)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
$skippedCount++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
EquipmentInspectionTemplate::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'equipment_id' => $equipmentId,
|
||||
'inspection_cycle' => $targetCycle,
|
||||
'item_no' => $template->item_no,
|
||||
'check_point' => $template->check_point,
|
||||
'check_item' => $template->check_item,
|
||||
'check_timing' => $template->check_timing,
|
||||
'check_frequency' => $template->check_frequency,
|
||||
'check_method' => $template->check_method,
|
||||
'sort_order' => $template->sort_order,
|
||||
'is_active' => true,
|
||||
]);
|
||||
$copiedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'copied' => $copiedCount,
|
||||
'skipped' => $skippedCount,
|
||||
'source_count' => $sourceTemplates->count(),
|
||||
'target_cycles' => $targetCycles,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 설비에 등록된 점검주기 목록 반환 (항목이 있는 주기만)
|
||||
*/
|
||||
|
||||
@@ -2,10 +2,17 @@
|
||||
<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 class="flex gap-2">
|
||||
<button onclick="openCopyModal()"
|
||||
class="bg-green-600 hover:bg-green-700 text-white px-3 py-1.5 rounded-lg text-sm transition"
|
||||
id="copyTemplateBtn" style="display: none;">
|
||||
다른 주기에 복사
|
||||
</button>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@php
|
||||
@@ -144,8 +151,45 @@ class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg">추가</bu
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 다른 주기에 복사 모달 -->
|
||||
<div id="copyTemplateModal" class="fixed inset-0 z-50 hidden">
|
||||
<div class="fixed inset-0 bg-black/50" onclick="document.getElementById('copyTemplateModal').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-md">
|
||||
<div class="flex justify-between items-center p-6 border-b">
|
||||
<h3 class="text-lg font-semibold">점검항목 복사</h3>
|
||||
<button onclick="document.getElementById('copyTemplateModal').classList.add('hidden')" class="text-gray-400 hover:text-gray-600">×</button>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
<span id="copySourceLabel" class="font-semibold text-blue-700"></span> 점검항목을 선택한 주기에 복사합니다.
|
||||
</p>
|
||||
<div class="space-y-2 mb-6" id="copyTargetCycles">
|
||||
@foreach($allCycles as $code => $label)
|
||||
<label class="copy-cycle-option flex items-center gap-3 p-2 rounded-lg hover:bg-gray-50 cursor-pointer" data-cycle="{{ $code }}">
|
||||
<input type="checkbox" name="target_cycles[]" value="{{ $code }}"
|
||||
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
|
||||
<span class="text-sm text-gray-700">{{ $label }}</span>
|
||||
<span class="copy-cycle-count text-xs text-gray-400 ml-auto"></span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button type="button" onclick="document.getElementById('copyTemplateModal').classList.add('hidden')"
|
||||
class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-4 py-2 rounded-lg text-sm">취소</button>
|
||||
<button type="button" onclick="executeCopyTemplates()"
|
||||
class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg text-sm" id="copyExecuteBtn">복사</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentTemplateTab = 'daily';
|
||||
|
||||
function switchTemplateTab(cycle) {
|
||||
currentTemplateTab = cycle;
|
||||
document.querySelectorAll('.tmpl-cycle-tab').forEach(btn => {
|
||||
if (btn.dataset.cycle === cycle) {
|
||||
btn.className = 'tmpl-cycle-tab px-3 py-1.5 rounded-md text-xs font-medium transition bg-white text-blue-700 shadow-sm';
|
||||
@@ -156,8 +200,80 @@ function switchTemplateTab(cycle) {
|
||||
document.querySelectorAll('.tmpl-cycle-content').forEach(el => {
|
||||
el.classList.toggle('hidden', el.dataset.cycle !== cycle);
|
||||
});
|
||||
// 모달의 주기 기본값도 전환
|
||||
const cycleSelect = document.getElementById('tmplInspectionCycle');
|
||||
if (cycleSelect) cycleSelect.value = cycle;
|
||||
updateCopyButton();
|
||||
}
|
||||
|
||||
function updateCopyButton() {
|
||||
const btn = document.getElementById('copyTemplateBtn');
|
||||
const content = document.querySelector(`.tmpl-cycle-content[data-cycle="${currentTemplateTab}"]`);
|
||||
const hasItems = content && content.querySelector('table');
|
||||
btn.style.display = hasItems ? '' : 'none';
|
||||
}
|
||||
|
||||
function openCopyModal() {
|
||||
const cycleLabels = @json($allCycles);
|
||||
document.getElementById('copySourceLabel').textContent = cycleLabels[currentTemplateTab];
|
||||
|
||||
document.querySelectorAll('.copy-cycle-option').forEach(el => {
|
||||
const code = el.dataset.cycle;
|
||||
const checkbox = el.querySelector('input[type="checkbox"]');
|
||||
if (code === currentTemplateTab) {
|
||||
el.style.display = 'none';
|
||||
checkbox.checked = false;
|
||||
} else {
|
||||
el.style.display = '';
|
||||
checkbox.checked = false;
|
||||
const countEl = el.querySelector('.copy-cycle-count');
|
||||
const tab = document.querySelector(`.tmpl-cycle-tab[data-cycle="${code}"]`);
|
||||
const badge = tab ? tab.querySelector('span') : null;
|
||||
countEl.textContent = badge ? `${badge.textContent.trim()}개 항목` : '';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('copyTemplateModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function executeCopyTemplates() {
|
||||
const checked = document.querySelectorAll('#copyTargetCycles input[type="checkbox"]:checked');
|
||||
if (checked.length === 0) {
|
||||
alert('복사할 주기를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const targetCycles = Array.from(checked).map(cb => cb.value);
|
||||
const equipmentId = @json($equipment->id);
|
||||
const btn = document.getElementById('copyExecuteBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '복사 중...';
|
||||
|
||||
fetch(`/api/admin/equipment/${equipmentId}/templates/copy`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
source_cycle: currentTemplateTab,
|
||||
target_cycles: targetCycles,
|
||||
}),
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert(data.message);
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.message || '복사 실패');
|
||||
}
|
||||
})
|
||||
.catch(() => alert('오류가 발생했습니다.'))
|
||||
.finally(() => {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '복사';
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => updateCopyButton());
|
||||
</script>
|
||||
|
||||
@@ -1023,6 +1023,7 @@
|
||||
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::post('/{id}/templates/copy', [\App\Http\Controllers\Api\Admin\EquipmentInspectionController::class, 'copyTemplates'])->whereNumber('id')->name('templates.copy');
|
||||
|
||||
// 점검 기록
|
||||
Route::get('/inspections', [\App\Http\Controllers\Api\Admin\EquipmentInspectionController::class, 'index'])->name('inspections.index');
|
||||
|
||||
Reference in New Issue
Block a user