fix: [bending] API 연결 실패 시 안내 메시지 표시

- API 401/403/연결실패 시 구체적인 안내 메시지 표시
- 데이터 없음과 API 오류를 구분하여 사용자에게 안내
This commit is contained in:
김보곤
2026-03-21 09:43:57 +09:00
parent fc1a28e552
commit cd06925b87
4 changed files with 470 additions and 12 deletions

View File

@@ -37,8 +37,27 @@ public function index(Request $request): View|\Illuminate\Http\Response
$params = $request->only(['item_sep', 'item_bending', 'material', 'model_UA', 'item_name', 'search', 'page', 'size']);
$params['size'] = $params['size'] ?? 30;
$response = $this->api()->get('/api/v1/bending-items', $params);
$body = $response->successful() ? $response->json('data', []) : [];
$apiError = null;
try {
$response = $this->api()->get('/api/v1/bending-items', $params);
} catch (\Illuminate\Http\Client\ConnectionException $e) {
$apiError = 'API 서버에 연결할 수 없습니다. API 서비스 상태를 확인해 주세요.';
$response = null;
}
if ($response && $response->successful()) {
$body = $response->json('data', []);
} else {
$body = [];
if (! $apiError && $response) {
$apiError = match ($response->status()) {
401 => 'API 인증이 필요합니다. SAM 서비스에 로그인하여 API를 연결해 주세요.',
403 => 'API 접근 권한이 없습니다. 관리자에게 문의해 주세요.',
default => "API 오류가 발생했습니다. (HTTP {$response->status()})",
};
}
}
$data = [
'data' => $body['data'] ?? [],
'total' => $body['total'] ?? 0,
@@ -46,12 +65,15 @@ public function index(Request $request): View|\Illuminate\Http\Response
'last_page' => $body['last_page'] ?? 1,
];
$filterResponse = $this->api()->get('/api/v1/bending-items/filters');
$filterOptions = $filterResponse->successful() ? $filterResponse->json('data', []) : [];
$filterOptions = [];
if (! $apiError) {
$filterResponse = $this->api()->get('/api/v1/bending-items/filters');
$filterOptions = $filterResponse->successful() ? $filterResponse->json('data', []) : [];
}
if ($request->header('HX-Request')) {
if ($request->header('HX-Target') === 'items-table') {
return view('bending.base.partials.table', ['items' => $data]);
return view('bending.base.partials.table', ['items' => $data, 'apiError' => $apiError]);
}
return response('', 200)->header('HX-Redirect', route('bending.base.index', $request->query()));
@@ -60,6 +82,7 @@ public function index(Request $request): View|\Illuminate\Http\Response
return view('bending.base.index', [
'items' => $data,
'filterOptions' => $filterOptions,
'apiError' => $apiError,
]);
}

View File

@@ -49,8 +49,27 @@ public function index(Request $request, string $category = 'GUIDERAIL_MODEL'): V
$params['size'] = $params['size'] ?? 30;
$params['item_category'] = $category;
$response = $this->api()->get('/api/v1/guiderail-models', $params);
$body = $response->successful() ? $response->json('data', []) : [];
$apiError = null;
try {
$response = $this->api()->get('/api/v1/guiderail-models', $params);
} catch (\Illuminate\Http\Client\ConnectionException $e) {
$apiError = 'API 서버에 연결할 수 없습니다. API 서비스 상태를 확인해 주세요.';
$response = null;
}
if ($response && $response->successful()) {
$body = $response->json('data', []);
} else {
$body = [];
if (! $apiError && $response) {
$apiError = match ($response->status()) {
401 => 'API 인증이 필요합니다. SAM 서비스에 로그인하여 API를 연결해 주세요.',
403 => 'API 접근 권한이 없습니다. 관리자에게 문의해 주세요.',
default => "API 오류가 발생했습니다. (HTTP {$response->status()})",
};
}
}
$data = [
'data' => $body['data'] ?? [],
'total' => $body['total'] ?? 0,
@@ -58,16 +77,17 @@ public function index(Request $request, string $category = 'GUIDERAIL_MODEL'): V
'last_page' => $body['last_page'] ?? 1,
];
$filterResponse = $this->api()->get('/api/v1/guiderail-models/filters');
$filterOptions = $filterResponse->successful() ? $filterResponse->json('data', []) : [];
$filterOptions = [];
if (! $apiError) {
$filterResponse = $this->api()->get('/api/v1/guiderail-models/filters');
$filterOptions = $filterResponse->successful() ? $filterResponse->json('data', []) : [];
}
if ($request->header('HX-Request')) {
// 필터/검색 HTMX (hx-target="#items-table") → 파셜 반환
if ($request->header('HX-Target') === 'items-table') {
return view('bending.products.partials.table', ['items' => $data, 'config' => $config, 'category' => $category]);
return view('bending.products.partials.table', ['items' => $data, 'config' => $config, 'category' => $category, 'apiError' => $apiError]);
}
// 사이드바 등 그 외 HTMX → 전체 페이지 리로드
return response('', 200)->header('HX-Redirect', route("bending.{$config['prefix']}.index", $request->query()));
}
@@ -76,6 +96,7 @@ public function index(Request $request, string $category = 'GUIDERAIL_MODEL'): V
'filterOptions' => $filterOptions,
'config' => $config,
'category' => $category,
'apiError' => $apiError,
]);
}

View File

@@ -0,0 +1,179 @@
@php
$itemList = $items['data'] ?? $items ?? [];
$total = $items['total'] ?? count($itemList);
$currentPage = $items['current_page'] ?? 1;
$lastPage = $items['last_page'] ?? 1;
@endphp
<div class="overflow-x-auto bg-white rounded-lg shadow-sm mt-4">
<table class="min-w-full text-sm">
<thead class="bg-gray-50 border-b">
<tr>
<th class="px-2 py-2 text-left text-sm font-semibold text-gray-700">NO</th>
<th class="px-2 py-2 text-left text-sm font-semibold text-gray-700">코드</th>
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700">대분류</th>
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700">인정</th>
<th class="px-2 py-2 text-left text-sm font-semibold text-gray-700">분류</th>
<th class="px-2 py-2 text-left text-sm font-semibold text-gray-700">품명</th>
<th class="px-2 py-2 text-left text-sm font-semibold text-gray-700">규격</th>
<th class="px-2 py-2 text-left text-sm font-semibold text-gray-700">재질</th>
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700">이미지</th>
<th class="px-2 py-2 text-left text-sm font-semibold text-gray-700">모델</th>
<th class="px-2 py-2 text-right text-sm font-semibold text-gray-700">폭합</th>
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700">절곡수</th>
<th class="px-2 py-2 text-left text-sm font-semibold text-gray-700">등록일</th>
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700">수정자</th>
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700">작업</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
@forelse($itemList as $item)
@php
$itemSep = $item['item_sep'] ?? '-';
$widthSum = $item['width_sum'] ?? null;
$bendCount = $item['bend_count'] ?? 0;
$modelUA = $item['model_UA'] ?? null;
$createdAt = $item['created_at'] ?? null;
@endphp
<tr class="hover:bg-blue-50 cursor-pointer" onclick="window.location='{{ route('bending.base.show', $item['id']) }}'">
<td class="px-2 py-2 text-gray-500 text-xs">{{ $item['id'] }}</td>
<td class="px-2 py-2 font-mono text-xs">{{ $item['code'] }}</td>
<td class="px-2 py-2 text-center">
<span class="px-1.5 py-0.5 rounded text-xs {{ $itemSep === '스크린' ? 'bg-blue-100 text-blue-700' : 'bg-orange-100 text-orange-700' }}">
{{ $itemSep }}
</span>
</td>
<td class="px-2 py-2 text-center text-xs">
@if($modelUA)
<span class="{{ $modelUA === '인정' ? 'text-green-600' : 'text-gray-400' }}">{{ $modelUA }}</span>
@else
<span class="text-gray-300">-</span>
@endif
</td>
<td class="px-2 py-2 text-xs">{{ $item['item_bending'] ?? '-' }}</td>
<td class="px-2 py-2 font-medium text-xs">{{ $item['item_name'] ?? $item['name'] }}</td>
<td class="px-2 py-2 text-gray-600 text-xs">{{ $item['item_spec'] ?? '-' }}</td>
<td class="px-2 py-2 text-gray-600 text-xs">{{ $item['material'] ?? '-' }}</td>
<td class="px-2 py-2 text-center">
@if(!empty($item['image_file_id']))
<div class="relative inline-block img-preview-wrap">
<img src="{{ $item['image_url'] ?? route('files.view', $item['image_file_id']) }}" width="24" height="24" style="width:24px; height:24px; object-fit:contain;" class="rounded inline-block" alt="">
<div class="img-preview-popup hidden absolute z-50 bg-white border shadow-xl rounded-lg p-1" style="left:50%; transform:translateX(-50%); width:300px;">
<img src="{{ $item['image_url'] ?? route('files.view', $item['image_file_id']) }}" class="w-full rounded" alt="">
</div>
</div>
@else
<span class="text-gray-300">-</span>
@endif
</td>
<td class="px-2 py-2 text-gray-600 text-xs">{{ $item['model_name'] ?? '-' }}</td>
<td class="px-2 py-2 text-right font-mono text-xs">{{ $widthSum ?? '-' }}</td>
<td class="px-2 py-2 text-center text-xs">
@if($bendCount > 0)
<span class="text-blue-600 font-medium">{{ $bendCount }}</span>
@else
<span class="text-gray-300">-</span>
@endif
</td>
<td class="px-2 py-2 text-gray-500 text-xs">{{ $createdAt ? \Illuminate\Support\Str::before($createdAt, ' ') : '-' }}</td>
<td class="px-2 py-2 text-center text-xs text-gray-500">{{ $item['modified_by'] ?? '-' }}</td>
<td class="px-2 py-2 text-center">
<a href="{{ route('bending.base.edit', $item['id']) }}" class="text-blue-600 hover:underline text-xs"
onclick="event.stopPropagation()">수정</a>
</td>
</tr>
@empty
<tr>
<td colspan="15" class="px-3 py-8 text-center">
@if(!empty($apiError))
<div class="text-amber-600">
<svg class="w-8 h-8 mx-auto mb-2 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/>
</svg>
<p class="text-sm font-medium">{{ $apiError }}</p>
</div>
@else
<span class="text-gray-400">데이터가 없습니다.</span>
@endif
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<script>
document.querySelectorAll('.img-preview-wrap').forEach(wrap => {
const popup = wrap.querySelector('.img-preview-popup');
if (!popup) return;
popup.style.position = 'fixed';
wrap.addEventListener('mouseenter', () => {
const rect = wrap.getBoundingClientRect();
const popW = 300;
let left = rect.left + rect.width / 2 - popW / 2;
if (left < 4) left = 4;
if (left + popW > window.innerWidth - 4) left = window.innerWidth - popW - 4;
popup.style.left = left + 'px';
popup.style.width = popW + 'px';
popup.style.transform = 'none';
if (rect.top > window.innerHeight / 2) {
popup.style.bottom = (window.innerHeight - rect.top + 4) + 'px';
popup.style.top = 'auto';
} else {
popup.style.top = (rect.bottom + 4) + 'px';
popup.style.bottom = 'auto';
}
popup.classList.remove('hidden');
});
wrap.addEventListener('mouseleave', () => { popup.classList.add('hidden'); });
});
</script>
{{-- 페이지네이션 --}}
@if($lastPage > 1)
<div class="bg-white px-4 py-3 mt-4 rounded-lg shadow-sm">
<div class="flex items-center justify-between">
<p class="text-sm text-gray-700">전체 <span class="font-medium">{{ $total }}</span></p>
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
@if($currentPage > 1)
<a href="{{ request()->fullUrlWithQuery(['page' => 1]) }}" class="relative inline-flex items-center px-3 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">처음</a>
<a href="{{ request()->fullUrlWithQuery(['page' => $currentPage - 1]) }}" class="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>
</a>
@else
<span class="relative inline-flex items-center px-3 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-300 cursor-not-allowed">처음</span>
<span class="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-300 cursor-not-allowed">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>
</span>
@endif
@php
$maxPages = 10;
$startPage = max(1, $currentPage - floor($maxPages / 2));
$endPage = min($lastPage, $startPage + $maxPages - 1);
if ($endPage - $startPage + 1 < $maxPages) { $startPage = max(1, $endPage - $maxPages + 1); }
@endphp
@for($p = $startPage; $p <= $endPage; $p++)
@if($p == $currentPage)
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-blue-50 text-sm font-medium text-blue-600">{{ $p }}</span>
@else
<a href="{{ request()->fullUrlWithQuery(['page' => $p]) }}" class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">{{ $p }}</a>
@endif
@endfor
@if($currentPage < $lastPage)
<a href="{{ request()->fullUrlWithQuery(['page' => $currentPage + 1]) }}" class="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/></svg>
</a>
<a href="{{ request()->fullUrlWithQuery(['page' => $lastPage]) }}" class="relative inline-flex items-center px-3 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"></a>
@else
<span class="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-300 cursor-not-allowed">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/></svg>
</span>
<span class="relative inline-flex items-center px-3 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-300 cursor-not-allowed"></span>
@endif
</nav>
</div>
</div>
@endif

View File

@@ -0,0 +1,235 @@
@php
$itemList = $items['data'] ?? [];
$total = $items['total'] ?? count($itemList);
$currentPage = $items['current_page'] ?? 1;
$lastPage = $items['last_page'] ?? 1;
$cat = $category ?? ($config['category'] ?? 'GUIDERAIL_MODEL');
$isCase = $cat === 'SHUTTERBOX_MODEL';
$isBottom = $cat === 'BOTTOMBAR_MODEL';
$isGuiderail = !$isCase && !$isBottom;
@endphp
<div class="overflow-x-auto bg-white rounded-lg shadow-sm mt-4">
<table class="min-w-full text-sm">
<thead class="bg-gray-50 border-b">
<tr>
<th class="px-2 py-2 text-left text-sm font-semibold text-gray-700">NO</th>
@if($isCase)
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700">박스(가로×세로)</th>
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700">점검구</th>
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700">전면밑</th>
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700">레일폭</th>
@elseif($isBottom)
<th class="px-2 py-2 text-left text-sm font-semibold text-gray-700">모델명</th>
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700">대분류</th>
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700">인정</th>
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700">가로×세로</th>
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700">마감</th>
@else
<th class="px-2 py-2 text-left text-sm font-semibold text-gray-700">모델명</th>
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700">대분류</th>
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700">인정</th>
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700">형상</th>
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700">레일폭×높이</th>
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700">마감</th>
@endif
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700">이미지</th>
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700">부품수</th>
<th class="px-2 py-2 text-left text-sm font-semibold text-gray-700">소요자재량</th>
<th class="px-2 py-2 text-left text-sm font-semibold text-gray-700">검색어</th>
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700">수정자</th>
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700">작업지시서</th>
<th class="px-2 py-2 text-center text-sm font-semibold text-gray-700">작업</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
@forelse($itemList as $item)
@php
$itemCat = $item['item_category'] ?? 'GUIDERAIL_MODEL';
$routePrefix = match($itemCat) { 'SHUTTERBOX_MODEL' => 'cases', 'BOTTOMBAR_MODEL' => 'bottombars', default => 'products' };
@endphp
<tr class="hover:bg-blue-50 cursor-pointer" onclick="window.location='{{ route("bending.{$routePrefix}.show", $item['id']) }}'">
<td class="px-2 py-2 text-gray-500 text-xs">{{ $item['id'] }}</td>
@if($isCase)
<td class="px-2 py-2 text-center text-xs font-mono font-bold">
<span class="text-blue-600">{{ $item['box_width'] ?? '-' }}</span>×<span class="text-red-600">{{ $item['box_height'] ?? '-' }}</span>
</td>
<td class="px-2 py-2 text-center text-xs">{{ $item['exit_direction'] ?? '-' }}</td>
<td class="px-2 py-2 text-center text-xs">{{ $item['front_bottom_width'] ?? '-' }}</td>
<td class="px-2 py-2 text-center text-xs">{{ $item['rail_width'] ?? '-' }}</td>
@elseif($isBottom)
<td class="px-2 py-2 font-medium"><span class="text-blue-700">{{ $item['model_name'] ?? $item['name'] }}</span></td>
<td class="px-2 py-2 text-center">
@php $sep = $item['item_sep'] ?? '-'; @endphp
<span class="px-1.5 py-0.5 rounded text-xs {{ $sep === '스크린' ? 'bg-blue-100 text-blue-700' : 'bg-orange-100 text-orange-700' }}">{{ $sep }}</span>
</td>
<td class="px-2 py-2 text-center text-xs">
@if($item['model_UA'] ?? null)
<span class="{{ $item['model_UA'] === '인정' ? 'text-green-600' : 'text-gray-400' }}">{{ $item['model_UA'] }}</span>
@else - @endif
</td>
<td class="px-2 py-2 text-center text-xs font-mono">{{ ($item['bar_width'] ?? '-') }}×{{ ($item['bar_height'] ?? '-') }}</td>
<td class="px-2 py-2 text-center text-xs">{{ $item['finishing_type'] ?? '-' }}</td>
@else
<td class="px-2 py-2 font-medium"><span class="text-blue-700">{{ $item['model_name'] ?? $item['name'] }}</span></td>
<td class="px-2 py-2 text-center">
@php $sep = $item['item_sep'] ?? '-'; @endphp
<span class="px-1.5 py-0.5 rounded text-xs {{ $sep === '스크린' ? 'bg-blue-100 text-blue-700' : 'bg-orange-100 text-orange-700' }}">{{ $sep }}</span>
</td>
<td class="px-2 py-2 text-center text-xs">
@if($item['model_UA'] ?? null)
<span class="{{ $item['model_UA'] === '인정' ? 'text-green-600' : 'text-gray-400' }}">{{ $item['model_UA'] }}</span>
@else - @endif
</td>
<td class="px-2 py-2 text-center text-xs">{{ $item['check_type'] ?? '-' }}</td>
<td class="px-2 py-2 text-center text-xs font-mono">{{ ($item['rail_width'] ?? '-') }}×{{ ($item['rail_length'] ?? '-') }}</td>
<td class="px-2 py-2 text-center text-xs">{{ $item['finishing_type'] ?? '-' }}</td>
@endif
<td class="px-2 py-2 text-center">
@if(!empty($item['image_file_id']))
<div class="relative inline-block img-preview-wrap">
<img src="{{ $item['image_url'] ?? route('files.view', $item['image_file_id']) }}" width="24" height="24" style="width:24px; height:24px; object-fit:contain;" class="rounded inline-block" alt="">
<div class="img-preview-popup hidden absolute z-50 bg-white border shadow-xl rounded-lg p-1" style="left:50%; transform:translateX(-50%); width:300px;">
<img src="{{ $item['image_url'] ?? route('files.view', $item['image_file_id']) }}" class="w-full rounded" alt="">
</div>
</div>
@else
<span class="text-gray-300">-</span>
@endif
</td>
<td class="px-2 py-2 text-center text-xs">
<span class="px-1.5 py-0.5 bg-gray-100 rounded">{{ $item['component_count'] ?? 0 }}</span>
</td>
<td class="px-2 py-2 text-xs">
@foreach(($item['material_summary'] ?? []) as $mat => $total)
<span class="text-gray-600">{{ $mat }}: <strong>{{ $total }}</strong></span>
@if(!$loop->last) | @endif
@endforeach
@if(empty($item['material_summary'])) - @endif
</td>
<td class="px-2 py-2 text-xs text-gray-500">{{ $item['search_keyword'] ?? '-' }}</td>
<td class="px-2 py-2 text-xs text-gray-500">{{ $item['modified_by'] ?? '-' }}</td>
<td class="px-2 py-2 text-center">
@if(!empty($item['id']))
<button type="button" onclick="event.stopPropagation(); openPrintModal('{{ route("bending.{$routePrefix}.print", $item['id']) }}')"
class="px-2 py-0.5 bg-gray-600 text-white rounded text-xs hover:bg-gray-700">보기</button>
@endif
</td>
<td class="px-2 py-2 text-center">
<a href="{{ route("bending.{$routePrefix}.edit", $item['id']) }}" class="text-blue-600 hover:underline text-xs"
onclick="event.stopPropagation()">수정</a>
</td>
</tr>
@empty
<tr>
<td colspan="20" class="px-3 py-8 text-center">
@if(!empty($apiError))
<div class="text-amber-600">
<svg class="w-8 h-8 mx-auto mb-2 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/>
</svg>
<p class="text-sm font-medium">{{ $apiError }}</p>
</div>
@else
<span class="text-gray-400">데이터가 없습니다.</span>
@endif
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<script>
document.querySelectorAll('.img-preview-wrap').forEach(wrap => {
const popup = wrap.querySelector('.img-preview-popup');
if (!popup) return;
popup.style.position = 'fixed';
wrap.addEventListener('mouseenter', () => {
const rect = wrap.getBoundingClientRect();
const popW = 300;
let left = rect.left + rect.width / 2 - popW / 2;
if (left < 4) left = 4;
if (left + popW > window.innerWidth - 4) left = window.innerWidth - popW - 4;
popup.style.left = left + 'px';
popup.style.width = popW + 'px';
popup.style.transform = 'none';
if (rect.top > window.innerHeight / 2) {
popup.style.bottom = (window.innerHeight - rect.top + 4) + 'px';
popup.style.top = 'auto';
} else {
popup.style.top = (rect.bottom + 4) + 'px';
popup.style.bottom = 'auto';
}
popup.classList.remove('hidden');
});
wrap.addEventListener('mouseleave', () => { popup.classList.add('hidden'); });
});
</script>
{{-- 작업지시서 모달 (iframe 방식) --}}
<dialog id="printModal" class="rounded-lg shadow-2xl p-0 backdrop:bg-black/50" style="max-width:95vw; max-height:95vh; width:1400px; height:90vh; border:none;">
<div class="flex items-center justify-between px-4 py-2 bg-gray-100 border-b shrink-0">
<span class="text-sm font-bold text-gray-700">작업지시서</span>
<div class="flex gap-2">
<button type="button" onclick="document.getElementById('printFrame').contentWindow.print()" class="px-3 py-1 bg-blue-600 text-white rounded text-xs hover:bg-blue-700">PDF 다운로드</button>
<button type="button" onclick="document.getElementById('printModal').close()" class="px-3 py-1 bg-gray-400 text-white rounded text-xs hover:bg-gray-500">닫기</button>
</div>
</div>
<iframe id="printFrame" class="w-full" style="height:calc(90vh - 44px); border:none;"></iframe>
</dialog>
<script>
function openPrintModal(url) {
document.getElementById('printFrame').src = url;
document.getElementById('printModal').showModal();
}
</script>
@if($lastPage > 1)
<div class="bg-white px-4 py-3 mt-4 rounded-lg shadow-sm">
<div class="flex items-center justify-between">
<p class="text-sm text-gray-700">전체 <span class="font-medium">{{ $total }}</span></p>
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
@if($currentPage > 1)
<a href="{{ request()->fullUrlWithQuery(['page' => 1]) }}" class="relative inline-flex items-center px-3 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">처음</a>
<a href="{{ request()->fullUrlWithQuery(['page' => $currentPage - 1]) }}" class="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>
</a>
@else
<span class="relative inline-flex items-center px-3 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-300 cursor-not-allowed">처음</span>
<span class="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-300 cursor-not-allowed">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>
</span>
@endif
@php
$maxPages = 10;
$startPage = max(1, $currentPage - floor($maxPages / 2));
$endPage = min($lastPage, $startPage + $maxPages - 1);
if ($endPage - $startPage + 1 < $maxPages) { $startPage = max(1, $endPage - $maxPages + 1); }
@endphp
@for($p = $startPage; $p <= $endPage; $p++)
@if($p == $currentPage)
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-blue-50 text-sm font-medium text-blue-600">{{ $p }}</span>
@else
<a href="{{ request()->fullUrlWithQuery(['page' => $p]) }}" class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">{{ $p }}</a>
@endif
@endfor
@if($currentPage < $lastPage)
<a href="{{ request()->fullUrlWithQuery(['page' => $currentPage + 1]) }}" class="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/></svg>
</a>
<a href="{{ request()->fullUrlWithQuery(['page' => $lastPage]) }}" class="relative inline-flex items-center px-3 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"></a>
@else
<span class="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-300 cursor-not-allowed">
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/></svg>
</span>
<span class="relative inline-flex items-center px-3 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-300 cursor-not-allowed"></span>
@endif
</nav>
</div>
</div>
@endif