페이지네이션 기능 개선 및 공통화

- 페이지네이션 로직을 별도 JS 파일로 분리 (public/js/pagination.js)
  * 쿠키 기반 per_page 값 저장 및 유지
  * 페이지네이션 이벤트 핸들러 통합
  * 중복 코드 제거

- 페이지네이션 UI 개선
  * 처음으로/끝으로 이동 버튼 추가
  * selectbox 너비 조정 (90px)
  * 서버사이드 selected 속성으로 옵션 매칭 개선

- 초기 페이지당 항목 수를 10개로 변경
  * TenantController, UserController, DepartmentController, RoleController 기본값 수정

- layouts/app.blade.php
  * pagination.js 로드 추가
  * 기존 인라인 스크립트 제거

변경 파일:
- public/js/pagination.js (신규)
- resources/views/layouts/app.blade.php
- resources/views/partials/pagination.blade.php
- app/Http/Controllers/Api/Admin/{Tenant,User,Department,Role}Controller.php
This commit is contained in:
2025-11-24 21:21:22 +09:00
parent 894055786e
commit 43af1a5779
7 changed files with 201 additions and 28 deletions

View File

@@ -22,7 +22,7 @@ public function index(Request $request): JsonResponse
{
$departments = $this->departmentService->getDepartments(
$request->all(),
$request->integer('per_page', 15)
$request->integer('per_page', 10)
);
// HTMX 요청 시 HTML 반환

View File

@@ -22,7 +22,7 @@ public function index(Request $request): JsonResponse
{
$roles = $this->roleService->getRoles(
$request->all(),
$request->integer('per_page', 15)
$request->integer('per_page', 10)
);
// HTMX 요청 시 HTML 반환

View File

@@ -22,7 +22,7 @@ public function index(Request $request): JsonResponse
{
$tenants = $this->tenantService->getTenants(
$request->all(),
$request->integer('per_page', 15)
$request->integer('per_page', 10)
);
// HTMX 요청 시 HTML 반환

View File

@@ -22,7 +22,7 @@ public function index(Request $request): JsonResponse
{
$users = $this->userService->getUsers(
$request->all(),
$request->integer('per_page', 15)
$request->integer('per_page', 10)
);
// HTMX 요청인 경우 HTML 반환

126
public/js/pagination.js Normal file
View File

@@ -0,0 +1,126 @@
/**
* 공통 페이지네이션 스크립트
*
* 기능:
* - 쿠키 기반 per_page 값 관리
* - 페이지네이션 이벤트 핸들러
* - HTMX 연동
*/
// ============================================
// 쿠키 헬퍼 함수
// ============================================
window.setCookie = function(name, value, days = 365) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
const expires = "expires=" + date.toUTCString();
document.cookie = name + "=" + value + ";" + expires + ";path=/";
};
window.getCookie = function(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
}
return null;
};
window.getPerPageFromCookie = function() {
const savedPerPage = getCookie('pagination_per_page');
return savedPerPage ? savedPerPage : '10'; // 기본값 10
};
// ============================================
// 페이지네이션 이벤트 핸들러
// ============================================
// 페이지당 항목 수 변경 핸들러
window.handlePerPageChange = function(perPage) {
console.log('handlePerPageChange called with:', perPage);
// 쿠키에 저장
setCookie('pagination_per_page', perPage);
console.log('Cookie saved. Reading back:', getCookie('pagination_per_page'));
// 현재 페이지의 HTMX 타겟 찾기
const target = document.querySelector('[hx-trigger*="filterSubmit"]');
if (target) {
const perPageInput = document.getElementById('perPageInput');
const pageInput = document.getElementById('pageInput');
if (perPageInput && pageInput) {
perPageInput.value = perPage;
pageInput.value = 1; // 페이지를 1로 초기화
console.log('Triggering HTMX with per_page:', perPageInput.value);
htmx.trigger(target, 'filterSubmit');
}
}
};
// 페이지 변경 핸들러
window.handlePageChange = function(page) {
const target = document.querySelector('[hx-trigger*="filterSubmit"]');
if (target) {
const pageInput = document.getElementById('pageInput');
if (pageInput) {
pageInput.value = page;
htmx.trigger(target, 'filterSubmit');
}
}
};
// ============================================
// 초기화
// ============================================
document.addEventListener('DOMContentLoaded', function() {
// filterForm 찾기
const filterForm = document.getElementById('filterForm');
if (!filterForm) return;
// hidden input이 이미 존재하는지 확인
let perPageInput = document.getElementById('perPageInput');
let pageInput = document.getElementById('pageInput');
// per_page input 생성 또는 업데이트
if (!perPageInput) {
perPageInput = document.createElement('input');
perPageInput.type = 'hidden';
perPageInput.name = 'per_page';
perPageInput.id = 'perPageInput';
filterForm.appendChild(perPageInput);
}
perPageInput.value = getPerPageFromCookie();
// page input 생성 또는 업데이트
if (!pageInput) {
pageInput = document.createElement('input');
pageInput.type = 'hidden';
pageInput.name = 'page';
pageInput.id = 'pageInput';
pageInput.value = '1';
filterForm.appendChild(pageInput);
}
});
// ============================================
// HTMX 이벤트 핸들러
// ============================================
// HTMX afterSwap: 테이블 새로고침 시 selectbox 재설정
document.body.addEventListener('htmx:afterSwap', function(event) {
// selectbox 설정을 약간 지연시켜 DOM이 완전히 렌더링된 후 실행
setTimeout(function() {
const perPageSelect = document.getElementById('perPageSelect');
if (perPageSelect) {
const savedPerPage = getPerPageFromCookie();
console.log('HTMX afterSwap - Setting selectbox to:', savedPerPage);
perPageSelect.value = savedPerPage;
}
}, 50);
});

View File

@@ -7,6 +7,7 @@
<title>@yield('title', 'Dashboard') - {{ config('app.name') }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<script src="{{ asset('js/pagination.js') }}"></script>
@stack('styles')
</head>
<body class="bg-gray-100">

View File

@@ -18,18 +18,16 @@
이전
</span>
@else
<button hx-get="{{ $paginator->previousPageUrl() }}"
hx-target="{{ $target }}"
hx-include="{{ $includeForm ?? '' }}"
<button type="button"
onclick="handlePageChange({{ $paginator->currentPage() - 1 }})"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
이전
</button>
@endif
@if($paginator->hasMorePages())
<button hx-get="{{ $paginator->nextPageUrl() }}"
hx-target="{{ $target }}"
hx-include="{{ $includeForm ?? '' }}"
<button type="button"
onclick="handlePageChange({{ $paginator->currentPage() + 1 }})"
class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
다음
</button>
@@ -42,29 +40,53 @@ class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 t
<!-- 데스크톱 네비게이션 -->
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<div class="flex items-center gap-4">
<p class="text-sm text-gray-700">
전체 <span class="font-medium">{{ $paginator->total() }}</span>
<span class="font-medium">{{ $paginator->firstItem() }}</span>
~
<span class="font-medium">{{ $paginator->lastItem() }}</span>
</p>
<!-- 페이지당 항목 선택 -->
<select name="per_page" id="perPageSelect"
style="min-width: 90px;"
class="px-3 py-1 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
onchange="handlePerPageChange(this.value)">
<option value="10" {{ $paginator->perPage() == 10 ? 'selected' : '' }}>10개씩</option>
<option value="20" {{ $paginator->perPage() == 20 ? 'selected' : '' }}>20개씩</option>
<option value="30" {{ $paginator->perPage() == 30 ? 'selected' : '' }}>30개씩</option>
<option value="50" {{ $paginator->perPage() == 50 ? 'selected' : '' }}>50개씩</option>
<option value="100" {{ $paginator->perPage() == 100 ? 'selected' : '' }}>100개씩</option>
<option value="200" {{ $paginator->perPage() == 200 ? 'selected' : '' }}>200개씩</option>
<option value="500" {{ $paginator->perPage() == 500 ? 'selected' : '' }}>500개씩</option>
</select>
</div>
<div>
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
<!-- 처음으로 버튼 -->
@if ($paginator->onFirstPage())
<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>
@else
<button type="button"
onclick="handlePageChange(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">
처음
</button>
@endif
<!-- 이전 버튼 -->
@if ($paginator->onFirstPage())
<span class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-300 cursor-not-allowed">
<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>
@else
<button hx-get="{{ $paginator->previousPageUrl() }}"
hx-target="{{ $target }}"
hx-include="{{ $includeForm ?? '' }}"
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<button type="button"
onclick="handlePageChange({{ $paginator->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>
@@ -72,16 +94,29 @@ class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gr
@endif
<!-- 페이지 번호 -->
@foreach ($paginator->getUrlRange(1, $paginator->lastPage()) as $page => $url)
@php
$currentPage = $paginator->currentPage();
$lastPage = $paginator->lastPage();
$maxPages = 10;
// 시작 페이지 계산
$startPage = max(1, $currentPage - floor($maxPages / 2));
$endPage = min($lastPage, $startPage + $maxPages - 1);
// 끝에서 10개를 채우지 못하면 시작 페이지 조정
if ($endPage - $startPage + 1 < $maxPages) {
$startPage = max(1, $endPage - $maxPages + 1);
}
@endphp
@foreach ($paginator->getUrlRange($startPage, $endPage) as $page => $url)
@if ($page == $paginator->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">
{{ $page }}
</span>
@else
<button hx-get="{{ $url }}"
hx-target="{{ $target }}"
hx-include="{{ $includeForm ?? '' }}"
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
<button type="button"
onclick="handlePageChange({{ $page }})"
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">
{{ $page }}
</button>
@@ -90,22 +125,33 @@ class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-whi
<!-- 다음 버튼 -->
@if ($paginator->hasMorePages())
<button hx-get="{{ $paginator->nextPageUrl() }}"
hx-target="{{ $target }}"
hx-include="{{ $includeForm ?? '' }}"
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<button type="button"
onclick="handlePageChange({{ $paginator->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>
</button>
@else
<span class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-300 cursor-not-allowed">
<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>
@endif
<!-- 끝으로 버튼 -->
@if ($paginator->hasMorePages())
<button type="button"
onclick="handlePageChange({{ $paginator->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">
</button>
@else
<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>