feat: [equipment] 다중 점검주기 + 정/부 담당자 체계 구현

- InspectionCycle enum: 6종 점검주기 상수, 열 라벨, check_date 계산
- Equipment 모델: subManager 관계, canInspect() 권한 체크
- Template/Inspection 모델: inspection_cycle fillable 추가
- EquipmentInspectionService: 주기별 점검 조회/토글/권한 체크
- 점검표 UI: 주기 탭, 동적 필터(월/연도), 주기별 그리드 열
- 점검항목 템플릿: 주기별 탭 그룹핑, 모달에 주기 선택
- 설비 등록/수정/상세: 부 담당자 필드 추가
- 권한 없는 장비 셀 비활성(cursor-not-allowed, opacity-50)
This commit is contained in:
김보곤
2026-02-28 12:37:37 +09:00
parent 0aab609dcc
commit beecf0851e
14 changed files with 554 additions and 90 deletions

View File

@@ -0,0 +1,221 @@
<?php
namespace App\Enums;
use Carbon\Carbon;
class InspectionCycle
{
const DAILY = 'daily';
const WEEKLY = 'weekly';
const MONTHLY = 'monthly';
const BIMONTHLY = 'bimonthly';
const QUARTERLY = 'quarterly';
const SEMIANNUAL = 'semiannual';
/**
* 전체 주기 목록 (코드 → 라벨)
*/
public static function all(): array
{
return [
self::DAILY => '일일',
self::WEEKLY => '주간',
self::MONTHLY => '월간',
self::BIMONTHLY => '2개월',
self::QUARTERLY => '분기',
self::SEMIANNUAL => '반년',
];
}
/**
* 주기별 라벨 반환
*/
public static function label(string $cycle): string
{
return self::all()[$cycle] ?? $cycle;
}
/**
* 주기별 기간 필터 타입 반환
*/
public static function periodType(string $cycle): string
{
return $cycle === self::DAILY ? 'month' : 'year';
}
/**
* 주기별 그리드 열 라벨 반환
*
* @return array<int, string> [colIndex => label]
*/
public static function columnLabels(string $cycle, ?string $period = null): array
{
return match ($cycle) {
self::DAILY => self::dailyLabels($period),
self::WEEKLY => self::weeklyLabels(),
self::MONTHLY => self::monthlyLabels(),
self::BIMONTHLY => self::bimonthlyLabels(),
self::QUARTERLY => self::quarterlyLabels(),
self::SEMIANNUAL => self::semiannualLabels(),
default => self::dailyLabels($period),
};
}
/**
* 열 인덱스 → check_date 변환
*/
public static function resolveCheckDate(string $cycle, string $period, int $colIndex): string
{
return match ($cycle) {
self::DAILY => self::dailyCheckDate($period, $colIndex),
self::WEEKLY => self::weeklyCheckDate($period, $colIndex),
self::MONTHLY => self::monthlyCheckDate($period, $colIndex),
self::BIMONTHLY => self::bimonthlyCheckDate($period, $colIndex),
self::QUARTERLY => self::quarterlyCheckDate($period, $colIndex),
self::SEMIANNUAL => self::semiannualCheckDate($period, $colIndex),
default => self::dailyCheckDate($period, $colIndex),
};
}
/**
* check_date → year_month(period) 역산
*/
public static function resolvePeriod(string $cycle, string $checkDate): string
{
$date = Carbon::parse($checkDate);
return match ($cycle) {
self::DAILY => $date->format('Y-m'),
default => $date->format('Y'),
};
}
/**
* 주기별 그리드 열 수
*/
public static function columnCount(string $cycle, ?string $period = null): int
{
return count(self::columnLabels($cycle, $period));
}
/**
* 주말 여부 (daily 전용)
*/
public static function isWeekend(string $period, int $colIndex): bool
{
$date = Carbon::createFromFormat('Y-m', $period)->startOfMonth()->addDays($colIndex - 1);
return in_array($date->dayOfWeek, [0, 6]);
}
// --- Daily ---
private static function dailyLabels(?string $period): array
{
$date = Carbon::createFromFormat('Y-m', $period ?? now()->format('Y-m'));
$days = $date->daysInMonth;
$labels = [];
for ($d = 1; $d <= $days; $d++) {
$labels[$d] = (string) $d;
}
return $labels;
}
private static function dailyCheckDate(string $period, int $colIndex): string
{
return Carbon::createFromFormat('Y-m', $period)->startOfMonth()->addDays($colIndex - 1)->format('Y-m-d');
}
// --- Weekly ---
private static function weeklyLabels(): array
{
$labels = [];
for ($w = 1; $w <= 52; $w++) {
$labels[$w] = $w.'주';
}
return $labels;
}
private static function weeklyCheckDate(string $year, int $colIndex): string
{
// ISO 주차의 월요일
return Carbon::create((int) $year)->setISODate((int) $year, $colIndex, 1)->format('Y-m-d');
}
// --- Monthly ---
private static function monthlyLabels(): array
{
$labels = [];
for ($m = 1; $m <= 12; $m++) {
$labels[$m] = $m.'월';
}
return $labels;
}
private static function monthlyCheckDate(string $year, int $colIndex): string
{
return Carbon::create((int) $year, $colIndex, 1)->format('Y-m-d');
}
// --- Bimonthly ---
private static function bimonthlyLabels(): array
{
return [
1 => '1~2월',
2 => '3~4월',
3 => '5~6월',
4 => '7~8월',
5 => '9~10월',
6 => '11~12월',
];
}
private static function bimonthlyCheckDate(string $year, int $colIndex): string
{
$month = ($colIndex - 1) * 2 + 1;
return Carbon::create((int) $year, $month, 1)->format('Y-m-d');
}
// --- Quarterly ---
private static function quarterlyLabels(): array
{
return [
1 => '1분기',
2 => '2분기',
3 => '3분기',
4 => '4분기',
];
}
private static function quarterlyCheckDate(string $year, int $colIndex): string
{
$month = ($colIndex - 1) * 3 + 1;
return Carbon::create((int) $year, $month, 1)->format('Y-m-d');
}
// --- Semiannual ---
private static function semiannualLabels(): array
{
return [
1 => '상반기',
2 => '하반기',
];
}
private static function semiannualCheckDate(string $year, int $colIndex): string
{
$month = $colIndex === 1 ? 1 : 7;
return Carbon::create((int) $year, $month, 1)->format('Y-m-d');
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers\Api\Admin;
use App\Enums\InspectionCycle;
use App\Http\Controllers\Controller;
use App\Services\EquipmentInspectionService;
use Illuminate\Http\JsonResponse;
@@ -15,12 +16,20 @@ public function __construct(
public function index(Request $request)
{
$yearMonth = $request->input('year_month', now()->format('Y-m'));
$cycle = $request->input('cycle', InspectionCycle::DAILY);
$period = $request->input('period') ?? $request->input('year_month', now()->format('Y-m'));
// daily가 아닌 주기에서 period가 없으면 현재 연도
if ($cycle !== InspectionCycle::DAILY && ! $request->input('period')) {
$period = now()->format('Y');
}
$productionLine = $request->input('production_line');
$equipmentId = $request->input('equipment_id');
$inspections = $this->inspectionService->getMonthlyInspections(
$yearMonth,
$inspections = $this->inspectionService->getInspections(
$cycle,
$period,
$productionLine,
$equipmentId ? (int) $equipmentId : null
);
@@ -28,7 +37,8 @@ public function index(Request $request)
if ($request->header('HX-Request')) {
return view('equipment.partials.inspection-grid', [
'inspections' => $inspections,
'yearMonth' => $yearMonth,
'cycle' => $cycle,
'period' => $period,
]);
}
@@ -44,13 +54,15 @@ public function toggleDetail(Request $request): JsonResponse
'equipment_id' => 'required|integer',
'template_item_id' => 'required|integer',
'check_date' => 'required|date',
'cycle' => 'nullable|string',
]);
try {
$result = $this->inspectionService->toggleDetail(
$request->input('equipment_id'),
$request->input('template_item_id'),
$request->input('check_date')
$request->input('check_date'),
$request->input('cycle', InspectionCycle::DAILY)
);
return response()->json([
@@ -58,10 +70,12 @@ public function toggleDetail(Request $request): JsonResponse
'data' => $result,
]);
} catch (\Exception $e) {
$status = $e->getMessage() === '점검 권한이 없습니다.' ? 403 : 400;
return response()->json([
'success' => false,
'message' => $e->getMessage(),
], 400);
], $status);
}
}
@@ -70,6 +84,7 @@ public function updateNotes(Request $request): JsonResponse
$request->validate([
'equipment_id' => 'required|integer',
'year_month' => 'required|string',
'cycle' => 'nullable|string',
'overall_judgment' => 'nullable|in:OK,NG',
'repair_note' => 'nullable|string',
'issue_note' => 'nullable|string',
@@ -80,7 +95,8 @@ public function updateNotes(Request $request): JsonResponse
$inspection = $this->inspectionService->updateInspectionNotes(
$request->input('equipment_id'),
$request->input('year_month'),
$request->only(['overall_judgment', 'repair_note', 'issue_note', 'inspector_id'])
$request->only(['overall_judgment', 'repair_note', 'issue_note', 'inspector_id']),
$request->input('cycle', InspectionCycle::DAILY)
);
return response()->json([
@@ -100,6 +116,7 @@ public function storeTemplate(Request $request, int $equipmentId): JsonResponse
{
$request->validate([
'item_no' => 'required|integer',
'inspection_cycle' => 'nullable|string',
'check_point' => 'required|string|max:50',
'check_item' => 'required|string|max:100',
'check_timing' => 'nullable|in:operating,stopped',
@@ -109,7 +126,12 @@ public function storeTemplate(Request $request, int $equipmentId): JsonResponse
]);
try {
$template = $this->inspectionService->saveTemplate($equipmentId, $request->all());
$data = $request->all();
if (empty($data['inspection_cycle'])) {
$data['inspection_cycle'] = InspectionCycle::DAILY;
}
$template = $this->inspectionService->saveTemplate($equipmentId, $data);
return response()->json([
'success' => true,

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Enums\InspectionCycle;
use App\Services\EquipmentInspectionService;
use App\Services\EquipmentRepairService;
use App\Services\EquipmentService;
@@ -72,8 +73,9 @@ public function inspections(Request $request): View|Response
}
$equipmentList = $this->equipmentService->getEquipmentList();
$cycles = InspectionCycle::all();
return view('equipment.inspections.index', compact('equipmentList'));
return view('equipment.inspections.index', compact('equipmentList', 'cycles'));
}
public function repairs(Request $request): View|Response

View File

@@ -34,6 +34,7 @@ class Equipment extends Model
'status',
'disposed_date',
'manager_id',
'sub_manager_id',
'photo_path',
'memo',
'is_active',
@@ -56,6 +57,24 @@ public function manager(): BelongsTo
return $this->belongsTo(\App\Models\User::class, 'manager_id');
}
public function subManager(): BelongsTo
{
return $this->belongsTo(\App\Models\User::class, 'sub_manager_id');
}
/**
* 해당 유저가 이 설비의 점검 권한이 있는지 확인
*/
public function canInspect(?int $userId = null): bool
{
$userId = $userId ?? auth()->id();
if (auth()->user()?->isAdmin()) {
return true;
}
return $this->manager_id === $userId || $this->sub_manager_id === $userId;
}
public function inspectionTemplates(): HasMany
{
return $this->hasMany(EquipmentInspectionTemplate::class, 'equipment_id')->orderBy('sort_order');

View File

@@ -14,6 +14,7 @@ class EquipmentInspection extends Model
protected $fillable = [
'tenant_id',
'equipment_id',
'inspection_cycle',
'year_month',
'overall_judgment',
'inspector_id',

View File

@@ -13,6 +13,7 @@ class EquipmentInspectionTemplate extends Model
protected $fillable = [
'tenant_id',
'equipment_id',
'inspection_cycle',
'item_no',
'check_point',
'check_item',
@@ -37,6 +38,11 @@ public function scopeActive($query)
return $query->where('is_active', true);
}
public function scopeByCycle($query, string $cycle)
{
return $query->where('inspection_cycle', $cycle);
}
public function getTimingLabelAttribute(): string
{
return match ($this->check_timing) {

View File

@@ -2,19 +2,25 @@
namespace App\Services;
use App\Enums\InspectionCycle;
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
/**
* 주기별 점검 데이터 조회
*
* @param string $cycle 점검주기 (daily/weekly/monthly/bimonthly/quarterly/semiannual)
* @param string $period 기간 (daily: 2026-02, 그 외: 2026)
*/
public function getInspections(string $cycle, string $period, ?string $productionLine = null, ?int $equipmentId = null): array
{
$tenantId = session('selected_tenant_id', 1);
$equipmentQuery = Equipment::where('is_active', true)->where('status', '!=', 'disposed');
$equipmentQuery = Equipment::where('is_active', true)
->where('status', '!=', 'disposed')
->with(['manager', 'subManager']);
if ($productionLine) {
$equipmentQuery->where('production_line', $productionLine);
@@ -26,13 +32,13 @@ public function getMonthlyInspections(string $yearMonth, ?string $productionLine
$equipments = $equipmentQuery->orderBy('sort_order')->orderBy('name')->get();
$date = Carbon::createFromFormat('Y-m', $yearMonth);
$daysInMonth = $date->daysInMonth;
$labels = InspectionCycle::columnLabels($cycle, $period);
$result = [];
foreach ($equipments as $equipment) {
$templates = EquipmentInspectionTemplate::where('equipment_id', $equipment->id)
->where('inspection_cycle', $cycle)
->where('is_active', true)
->orderBy('sort_order')
->get();
@@ -42,7 +48,8 @@ public function getMonthlyInspections(string $yearMonth, ?string $productionLine
}
$inspection = EquipmentInspection::where('equipment_id', $equipment->id)
->where('year_month', $yearMonth)
->where('inspection_cycle', $cycle)
->where('year_month', $period)
->first();
$details = [];
@@ -59,23 +66,41 @@ public function getMonthlyInspections(string $yearMonth, ?string $productionLine
'templates' => $templates,
'inspection' => $inspection,
'details' => $details,
'days_in_month' => $daysInMonth,
'labels' => $labels,
'can_inspect' => $equipment->canInspect(),
];
}
return $result;
}
public function toggleDetail(int $equipmentId, int $templateItemId, string $checkDate): array
/**
* 하위호환: 기존 getMonthlyInspections 유지
*/
public function getMonthlyInspections(string $yearMonth, ?string $productionLine = null, ?int $equipmentId = null): array
{
return $this->getInspections(InspectionCycle::DAILY, $yearMonth, $productionLine, $equipmentId);
}
/**
* 셀 토글 (점검 결과 순환)
*/
public function toggleDetail(int $equipmentId, int $templateItemId, string $checkDate, string $cycle = 'daily'): array
{
$equipment = Equipment::findOrFail($equipmentId);
if (! $equipment->canInspect()) {
throw new \Exception('점검 권한이 없습니다.');
}
$tenantId = session('selected_tenant_id', 1);
$yearMonth = Carbon::parse($checkDate)->format('Y-m');
$period = InspectionCycle::resolvePeriod($cycle, $checkDate);
$inspection = EquipmentInspection::firstOrCreate(
[
'tenant_id' => $tenantId,
'equipment_id' => $equipmentId,
'year_month' => $yearMonth,
'inspection_cycle' => $cycle,
'year_month' => $period,
],
[
'created_by' => auth()->id(),
@@ -112,7 +137,7 @@ public function toggleDetail(int $equipmentId, int $templateItemId, string $chec
];
}
public function updateInspectionNotes(int $equipmentId, string $yearMonth, array $data): EquipmentInspection
public function updateInspectionNotes(int $equipmentId, string $yearMonth, array $data, string $cycle = 'daily'): EquipmentInspection
{
$tenantId = session('selected_tenant_id', 1);
@@ -120,6 +145,7 @@ public function updateInspectionNotes(int $equipmentId, string $yearMonth, array
[
'tenant_id' => $tenantId,
'equipment_id' => $equipmentId,
'inspection_cycle' => $cycle,
'year_month' => $yearMonth,
],
[
@@ -134,8 +160,6 @@ public function updateInspectionNotes(int $equipmentId, string $yearMonth, array
public function getMonthlyStats(string $yearMonth): array
{
$tenantId = session('selected_tenant_id', 1);
$totalEquipments = Equipment::where('is_active', true)
->where('status', '!=', 'disposed')
->count();
@@ -175,4 +199,16 @@ public function deleteTemplate(int $id): bool
{
return EquipmentInspectionTemplate::findOrFail($id)->delete();
}
/**
* 설비에 등록된 점검주기 목록 반환 (항목이 있는 주기만)
*/
public function getActiveCycles(int $equipmentId): array
{
return EquipmentInspectionTemplate::where('equipment_id', $equipmentId)
->where('is_active', true)
->distinct()
->pluck('inspection_cycle')
->toArray();
}
}

View File

@@ -9,7 +9,7 @@ class EquipmentService
{
public function getEquipments(array $filters = [], int $perPage = 20): LengthAwarePaginator
{
$query = Equipment::query()->with('manager');
$query = Equipment::query()->with(['manager', 'subManager']);
if (! empty($filters['search'])) {
$search = $filters['search'];
@@ -40,7 +40,7 @@ public function getEquipments(array $filters = [], int $perPage = 20): LengthAwa
public function getEquipmentById(int $id): ?Equipment
{
return Equipment::with(['manager', 'inspectionTemplates', 'repairs', 'processes', 'photos'])->find($id);
return Equipment::with(['manager', 'subManager', 'inspectionTemplates', 'repairs', 'processes', 'photos'])->find($id);
}
public function createEquipment(array $data): Equipment

View File

@@ -125,7 +125,7 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
<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>
<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>
@@ -134,6 +134,16 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2"> 담당자</label>
<select name="sub_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"

View File

@@ -133,7 +133,7 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
<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>
<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>
@@ -142,6 +142,16 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-2"> 담당자</label>
<select name="sub_manager_id" id="sub_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"
@@ -203,7 +213,7 @@ class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-6 py-2 rounded-lg transiti
const csrfToken = '{{ csrf_token() }}';
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'];
'purchase_price', 'useful_life', 'status', 'manager_id', 'sub_manager_id', 'memo'];
// 설비 데이터 로드
fetch(`/api/admin/equipment/${equipmentId}`, {

View File

@@ -1,20 +1,47 @@
@extends('layouts.app')
@section('title', '일상점검표')
@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>
<h1 class="text-2xl font-bold text-gray-800">설비 점검표</h1>
</div>
<!-- 주기 -->
<div class="flex gap-1 mb-4 bg-gray-100 rounded-lg p-1" style="width: fit-content;">
@foreach($cycles as $code => $label)
<button type="button"
class="cycle-tab px-4 py-2 rounded-lg text-sm font-medium transition {{ $code === 'daily' ? 'bg-white text-blue-700 shadow-sm' : 'text-gray-600 hover:text-gray-800 hover:bg-gray-50' }}"
data-cycle="{{ $code }}"
onclick="switchCycle('{{ $code }}')">
{{ $label }}
</button>
@endforeach
</div>
<!-- 필터 -->
<x-filter-collapsible id="inspectionFilter" :defaultOpen="true">
<x-filter-collapsible id="inspectionFilterWrap" :defaultOpen="true">
<form id="inspectionFilter" class="flex flex-wrap gap-2 sm:gap-4">
<div class="shrink-0" style="width: 160px;">
<input type="hidden" name="cycle" id="filterCycle" value="daily">
<!-- daily 전용: 점검년월 -->
<div class="shrink-0 filter-daily" 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') }}"
<input type="month" name="period" id="periodMonth" 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 filter-year" style="width: 120px; display: none;">
<label class="block text-xs text-gray-500 mb-1">점검연도</label>
<select name="period_year" id="periodYear"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
@for($y = now()->year; $y >= now()->year - 3; $y--)
<option value="{{ $y }}">{{ $y }}</option>
@endfor
</select>
</div>
<div class="shrink-0" style="width: 140px;">
<label class="block text-xs text-gray-500 mb-1">생산라인</label>
<select name="production_line"
@@ -59,11 +86,47 @@ class="bg-white rounded-lg shadow-sm">
@push('scripts')
<script>
let currentCycle = 'daily';
document.getElementById('inspectionFilter').addEventListener('submit', function(e) {
e.preventDefault();
updatePeriodValue();
htmx.trigger('#inspection-grid', 'filterSubmit');
});
function switchCycle(cycle) {
currentCycle = cycle;
document.getElementById('filterCycle').value = cycle;
// 탭 UI 전환
document.querySelectorAll('.cycle-tab').forEach(btn => {
if (btn.dataset.cycle === cycle) {
btn.className = 'cycle-tab px-4 py-2 rounded-lg text-sm font-medium transition bg-white text-blue-700 shadow-sm';
} else {
btn.className = 'cycle-tab px-4 py-2 rounded-lg text-sm font-medium transition text-gray-600 hover:text-gray-800 hover:bg-gray-50';
}
});
// 필터 전환
const isDaily = cycle === 'daily';
document.querySelectorAll('.filter-daily').forEach(el => el.style.display = isDaily ? '' : 'none');
document.querySelectorAll('.filter-year').forEach(el => el.style.display = isDaily ? 'none' : '');
updatePeriodValue();
htmx.trigger('#inspection-grid', 'filterSubmit');
}
function updatePeriodValue() {
// hidden period 필드에 올바른 값 설정
if (currentCycle === 'daily') {
document.getElementById('periodMonth').name = 'period';
document.getElementById('periodYear').name = 'period_year';
} else {
document.getElementById('periodMonth').name = 'period_month_unused';
document.getElementById('periodYear').name = 'period';
}
}
function toggleCell(equipmentId, templateItemId, checkDate, cell) {
fetch('/api/admin/equipment/inspections/detail', {
method: 'PATCH',
@@ -76,13 +139,17 @@ function toggleCell(equipmentId, templateItemId, checkDate, cell) {
equipment_id: equipmentId,
template_item_id: templateItemId,
check_date: checkDate,
cycle: currentCycle,
})
})
.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;
const span = cell.querySelector('.inspection-cell') || cell;
span.textContent = data.data.symbol;
span.className = 'inspection-cell text-lg font-bold select-none ' + data.data.color;
} else if (data.message) {
showToast(data.message, 'error');
}
});
}

View File

@@ -1,12 +1,14 @@
@if(empty($inspections))
<div class="p-12 text-center text-gray-500">
<p>점검 가능한 설비가 없습니다.</p>
<p class="text-sm mt-2">설비 등록대장에서 점검항목을 추가해주세요.</p>
<p class="text-sm mt-2">설비 등록대장에서 해당 주기의 점검항목을 추가해주세요.</p>
</div>
@else
@php
$date = \Carbon\Carbon::createFromFormat('Y-m', $yearMonth);
$daysInMonth = $date->daysInMonth;
$cycle = $cycle ?? 'daily';
$isDaily = $cycle === 'daily';
// 첫 번째 설비의 labels 사용
$labels = $inspections[0]['labels'] ?? [];
@endphp
<div class="overflow-x-auto">
@@ -15,16 +17,14 @@
<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++)
@foreach($labels as $colIndex => $label)
@php
$dayDate = $date->copy()->day($d);
$dayOfWeek = $dayDate->dayOfWeek;
$isWeekend = in_array($dayOfWeek, [0, 6]);
$isWeekend = $isDaily && \App\Enums\InspectionCycle::isWeekend($period, $colIndex);
@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 }}
{{ $label }}
</th>
@endfor
@endforeach
<th class="border border-gray-300 px-2 py-1 text-center whitespace-nowrap" style="min-width: 60px;">판정</th>
</tr>
</thead>
@@ -35,6 +35,7 @@
$templates = $item['templates'];
$inspection = $item['inspection'];
$details = $item['details'];
$canInspect = $item['can_inspect'] ?? true;
$rowCount = $templates->count();
@endphp
@@ -51,22 +52,28 @@
<span class="text-gray-600">{{ $tmpl->check_point }}</span>
</td>
@for($d = 1; $d <= $daysInMonth; $d++)
@foreach($labels as $colIndex => $label)
@php
$checkDate = $date->copy()->day($d)->format('Y-m-d');
$checkDate = \App\Enums\InspectionCycle::resolveCheckDate($cycle, $period, $colIndex);
$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]);
$isWeekend = $isDaily && \App\Enums\InspectionCycle::isWeekend($period, $colIndex);
@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($canInspect)
<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>
@else
<td class="border border-gray-300 text-center select-none cursor-not-allowed opacity-50 {{ $isWeekend ? 'bg-red-50' : '' }}"
style="min-width: 32px; padding: 2px;">
<span class="inspection-cell text-lg font-bold {{ $color }}">{{ $symbol }}</span>
</td>
@endif
@endforeach
@if($idx === 0)
<td class="border border-gray-300 px-2 py-1 text-center whitespace-nowrap"

View File

@@ -67,9 +67,13 @@
<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>
<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>
<p class="text-gray-900">{{ $equipment->subManager?->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 }}">

View File

@@ -8,37 +8,67 @@ class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-lg text-sm t
</button>
</div>
@php
$allCycles = \App\Enums\InspectionCycle::all();
$templatesByCycle = $equipment->inspectionTemplates->groupBy('inspection_cycle');
@endphp
@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 class="flex gap-1 mb-4 bg-gray-50 rounded-lg p-1" style="width: fit-content;">
@foreach($allCycles as $code => $label)
@php $count = isset($templatesByCycle[$code]) ? $templatesByCycle[$code]->count() : 0; @endphp
<button type="button"
class="tmpl-cycle-tab px-3 py-1.5 rounded-md text-xs font-medium transition {{ $loop->first ? 'bg-white text-blue-700 shadow-sm' : 'text-gray-500 hover:text-gray-700' }}"
data-cycle="{{ $code }}"
onclick="switchTemplateTab('{{ $code }}')">
{{ $label }}
@if($count > 0)
<span class="ml-1 inline-flex items-center justify-center w-4 h-4 text-[10px] font-bold bg-blue-100 text-blue-700 rounded-full">{{ $count }}</span>
@endif
</button>
@endforeach
</div>
<!-- 주기별 테이블 -->
@foreach($allCycles as $code => $label)
<div class="tmpl-cycle-content {{ $loop->first ? '' : 'hidden' }}" data-cycle="{{ $code }}">
@if(isset($templatesByCycle[$code]) && $templatesByCycle[$code]->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($templatesByCycle[$code] 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-400 text-center py-4 text-sm">{{ $label }} 점검항목이 없습니다.</p>
@endif
</div>
@endforeach
@else
<p class="text-gray-500 text-center py-8">등록된 점검항목이 없습니다.</p>
@endif
@@ -55,21 +85,32 @@ class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-lg text-sm t
</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>
<select name="inspection_cycle" id="tmplInspectionCycle"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
@foreach($allCycles as $code => $label)
<option value="{{ $code }}">{{ $label }}</option>
@endforeach
</select>
</div>
<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>
<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="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>
<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>
<div class="grid grid-cols-2 gap-4">
<div>
@@ -102,3 +143,21 @@ class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg">추가</bu
</div>
</div>
</div>
<script>
function switchTemplateTab(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';
} else {
btn.className = 'tmpl-cycle-tab px-3 py-1.5 rounded-md text-xs font-medium transition text-gray-500 hover:text-gray-700';
}
});
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;
}
</script>