fix: [archived-records] 페이지네이션 오류 수정

- GROUP BY + paginate() 조합 시 total count 오류 해결 (서브쿼리 방식)
- DB::table() 사용 시 page 파라미터 명시적 전달
- 커스텀 페이지네이션을 공용 컴포넌트(partials.pagination)로 교체

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-30 23:11:38 +09:00
parent 049fa7ed61
commit bbf34e9f3f
2 changed files with 28 additions and 63 deletions

View File

@@ -12,12 +12,14 @@ class ArchivedRecordService
/**
* 아카이브 레코드 목록 조회 (batch_id로 그룹핑, 페이지네이션)
* batch_id가 NULL인 기존 데이터는 각각 개별 batch로 취급
*
* NOTE: GROUP BY + paginate() 조합 시 total count가 잘못 계산되는 문제 해결
* 서브쿼리로 GROUP BY 결과를 감싼 후 외부에서 paginate() 실행
*/
public function getArchivedRecordsBatched(array $filters = [], int $perPage = 15): LengthAwarePaginator
{
// batch_id별 그룹핑하여 첫 번째 레코드만 조회
// batch_id가 NULL인 경우 id를 기반으로 가상 batch_id 생성
$query = ArchivedRecord::query()
// 1. 서브쿼리: batch_id별 그룹핑
$subQuery = ArchivedRecord::query()
->select([
DB::raw("COALESCE(batch_id, CONCAT('legacy_', id)) as batch_id"),
DB::raw("COALESCE(batch_description, CONCAT(record_type, ' 삭제 (ID: ', original_id, ')')) as batch_description"),
@@ -27,22 +29,29 @@ public function getArchivedRecordsBatched(array $filters = [], int $perPage = 15
DB::raw('MIN(deleted_by) as deleted_by'),
DB::raw('MIN(deleted_at) as deleted_at'),
])
->groupBy(DB::raw("COALESCE(batch_id, CONCAT('legacy_', id))"), DB::raw("COALESCE(batch_description, CONCAT(record_type, ' 삭제 (ID: ', original_id, ')'))"));
->groupBy(
DB::raw("COALESCE(batch_id, CONCAT('legacy_', id))"),
DB::raw("COALESCE(batch_description, CONCAT(record_type, ' 삭제 (ID: ', original_id, ')'))")
);
// 2. 외부 쿼리: 서브쿼리를 감싸서 정확한 count 계산
$query = DB::table(DB::raw("({$subQuery->toSql()}) as grouped"))
->mergeBindings($subQuery->getQuery())
->select('*');
// 레코드 타입 필터
if (! empty($filters['record_type'])) {
$query->having('record_types', 'like', "%{$filters['record_type']}%");
$query->where('record_types', 'like', "%{$filters['record_type']}%");
}
// 삭제자 필터
if (! empty($filters['deleted_by'])) {
$query->having('deleted_by', $filters['deleted_by']);
$query->where('deleted_by', $filters['deleted_by']);
}
// 검색
if (! empty($filters['search'])) {
$search = $filters['search'];
$query->where('batch_description', 'like', "%{$search}%");
$query->where('batch_description', 'like', "%{$filters['search']}%");
}
// 정렬 (기본: 삭제일시 내림차순)
@@ -50,7 +59,11 @@ public function getArchivedRecordsBatched(array $filters = [], int $perPage = 15
$sortDirection = $filters['sort_direction'] ?? 'desc';
$query->orderBy($sortBy, $sortDirection);
return $query->paginate($perPage);
// NOTE: DB::table() 사용 시 request의 page 파라미터를 자동으로 읽지 못함
// filters에서 page를 명시적으로 전달
$page = $filters['page'] ?? null;
return $query->paginate($perPage, ['*'], 'page', $page);
}
/**

View File

@@ -74,57 +74,9 @@ class="text-blue-600 hover:text-blue-900 transition"
</table>
</div>
{{-- 페이지네이션 --}}
@if($records->hasPages())
<div class="px-6 py-4 border-t border-gray-200">
<div class="flex items-center justify-between">
<div class="text-sm text-gray-700">
<span class="font-medium">{{ $records->total() }}</span> 작업
<span class="font-medium">{{ $records->firstItem() }}</span> -
<span class="font-medium">{{ $records->lastItem() }}</span> 표시
</div>
<div class="flex gap-2">
{{-- 이전 페이지 --}}
@if($records->onFirstPage())
<span class="px-3 py-1 text-sm text-gray-400 bg-gray-100 rounded cursor-not-allowed">이전</span>
@else
<button type="button"
class="px-3 py-1 text-sm text-gray-700 bg-white border border-gray-300 rounded hover:bg-gray-50"
hx-get="/api/admin/archived-records?page={{ $records->currentPage() - 1 }}"
hx-target="#archived-record-table"
hx-include="#filterForm">
이전
</button>
@endif
{{-- 페이지 번호 --}}
@foreach(range(max(1, $records->currentPage() - 2), min($records->lastPage(), $records->currentPage() + 2)) as $page)
@if($page == $records->currentPage())
<span class="px-3 py-1 text-sm text-white bg-blue-600 rounded">{{ $page }}</span>
@else
<button type="button"
class="px-3 py-1 text-sm text-gray-700 bg-white border border-gray-300 rounded hover:bg-gray-50"
hx-get="/api/admin/archived-records?page={{ $page }}"
hx-target="#archived-record-table"
hx-include="#filterForm">
{{ $page }}
</button>
@endif
@endforeach
{{-- 다음 페이지 --}}
@if($records->hasMorePages())
<button type="button"
class="px-3 py-1 text-sm text-gray-700 bg-white border border-gray-300 rounded hover:bg-gray-50"
hx-get="/api/admin/archived-records?page={{ $records->currentPage() + 1 }}"
hx-target="#archived-record-table"
hx-include="#filterForm">
다음
</button>
@else
<span class="px-3 py-1 text-sm text-gray-400 bg-gray-100 rounded cursor-not-allowed">다음</span>
@endif
</div>
</div>
</div>
@endif
{{-- 페이지네이션 (공용 컴포넌트) --}}
@include('partials.pagination', [
'paginator' => $records,
'target' => '#archived-record-table',
'includeForm' => '#filterForm'
])