공개 이슈 gnuboard/g7 이 지목한 병목은 "같은 일을 반복 실행한다" 축이다. 응답·행의 무게를 다룬 · 과 겹치지 않는다. 공통 계약 — 총 건수 상한과 페이지 이동 범위는 별개 결정이다. 묶으면 필요 없이 기능이 깎인다. 총 건수만 상한을 받고(파생 테이블 COUNT), "다음" 이동은 per_page + 1 실측으로 끝까지 열어 둔다. 계산이 불가능해지는 것은 마지막 페이지 번호 하나뿐이며 그 사실은 last_page: null 이 알린다. 최신순처럼 실제 컬럼으로 정렬하는 목록은 커서로 전환해 깊이와 무관하게 일정 속도로 이동한다. 관련도순은 계산값 정렬이라 커서 키로 쓸 수 없어 offset 을 유지한다. 계약의 입구는 표준 paginate 와 같은 폭이어야 한다. 관계·쿼리 빌더를 받지 못하면 그 좁은 만큼이 그대로 운영 500 이 되고, 실제로 관리자 알림 목록에서 그렇게 터졌다. 응답 조립도 컬렉션마다 손으로 하면 형태가 늘어나는 순간 없는 값을 부르거나 새 필드를 흘리므로, 형태 판정을 paginationMeta 한 곳에 모았다. 표준 paginate 응답은 필드 단위로 이전과 동일하다. 요청당 반복 비용 — 훅 구독마다 남기던 로그 400줄, 요청당 스무 번 넘던 설정 파일 재읽기, 이미 캐시된 목록의 DB 재조회를 없앴다. 권한 판정은 요청 스코프 메모를 두어 화면 요소마다 나가던 조회를 한 번으로 줄였고, 크로스 요청 캐시는 두지 않아 권한 변경이 종전처럼 다음 요청에 반영된다. 검색 질의는 활성 엔진이 만든다. 저장소가 구체 엔진을 지목하면 플러그인이 등록한 엔진은 호출될 기회 자체를 잃고 오류 없이 다른 방식으로 동작한다. 해석기를 두어 활성 엔진에 위임하고, 전문검색이 없는 DBMS 의 부분일치 폴백도 드라이버명 하드코딩 대신 선언형 config 로 옮겼다. 총 건수가 잘린 목록에서 순번을 역산하면 0 과 음수가 나온다. 지어내지 않고 null 을 돌려주며, 그 원칙을 last_page 와 동일하게 적용했다.
90 lines
3.7 KiB
PHP
90 lines
3.7 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Resources\Traits;
|
|
|
|
use App\Contracts\Pagination\BoundedTotalAware;
|
|
use Illuminate\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Support\Collection;
|
|
|
|
/**
|
|
* 컬렉션 항목에 순번(number)을 부여하는 Trait
|
|
*
|
|
* 페이지네이션 정보와 정렬 방향을 기반으로 각 항목에 순번을 계산합니다.
|
|
* - 내림차순(desc): 역순 (total, total-1, ...)
|
|
* - 오름차순(asc): 기본순 (1, 2, 3, ...)
|
|
*/
|
|
trait HasRowNumber
|
|
{
|
|
/**
|
|
* 순번이 포함된 데이터 매핑을 수행합니다.
|
|
*
|
|
* @param callable $callback 각 항목을 변환하는 콜백 함수
|
|
* @param string|null $sortOrder 정렬 방향 (asc|desc), null인 경우 request에서 추출
|
|
* @return Collection 순번이 포함된 변환된 컬렉션
|
|
*/
|
|
protected function mapWithRowNumber(callable $callback, ?string $sortOrder = null): Collection
|
|
{
|
|
$sortOrder = $sortOrder ?? request()->input('sort_order', 'desc');
|
|
|
|
// 페이지네이션이 아닌 경우 단순 인덱스 기반 순번 부여
|
|
if (! $this->resource instanceof LengthAwarePaginator) {
|
|
return $this->collection->map(function ($item, $index) use ($callback, $sortOrder) {
|
|
$total = $this->collection->count();
|
|
$number = $sortOrder === 'asc' ? $index + 1 : $total - $index;
|
|
$data = $callback($item);
|
|
|
|
return array_merge(['number' => $number], $data);
|
|
});
|
|
}
|
|
|
|
// 페이지네이션 정보 추출
|
|
$total = $this->resource->total();
|
|
$perPage = $this->resource->perPage();
|
|
$currentPage = $this->resource->currentPage();
|
|
|
|
return $this->collection->map(function ($item, $index) use ($callback, $sortOrder, $total, $perPage, $currentPage) {
|
|
$number = $this->calculateRowNumber($index, $sortOrder, $total, $perPage, $currentPage);
|
|
$data = $callback($item);
|
|
|
|
return array_merge(['number' => $number], $data);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 순번을 계산합니다.
|
|
*
|
|
* @param int $index 현재 항목의 인덱스 (0부터 시작)
|
|
* @param string $sortOrder 정렬 방향 (asc|desc)
|
|
* @param int $total 전체 항목 수
|
|
* @param int $perPage 페이지당 항목 수
|
|
* @param int $currentPage 현재 페이지 번호
|
|
* @return int|null 계산된 순번 (총 건수가 잘린 내림차순이면 null)
|
|
*/
|
|
protected function calculateRowNumber(
|
|
int $index,
|
|
string $sortOrder,
|
|
int $total,
|
|
int $perPage,
|
|
int $currentPage
|
|
): ?int {
|
|
if ($sortOrder === 'asc') {
|
|
// 오름차순: 1, 2, 3, ... (페이지별 연속)
|
|
// 예: 2페이지 → ((2-1) * 10) + 0 + 1 = 11, 12, 13, ...
|
|
// offset 만으로 정해지므로 총 건수가 잘려도 그대로 정확하다.
|
|
return (($currentPage - 1) * $perPage) + $index + 1;
|
|
}
|
|
|
|
// 내림차순 순번은 "전체 몇 건 중 몇 번째" 라 총 건수를 알아야 나온다. 총 건수가
|
|
// 상한에 걸려 잘렸으면 그 값으로 역산한 순번은 첫 페이지부터 이미 틀리고, 상한을
|
|
// 넘어선 페이지에서는 0 과 음수까지 내려간다. 틀린 숫자를 내보내는 것보다
|
|
// 내보내지 않는 편이 낫으므로 `last_page` 와 같은 원칙으로 null 을 돌려준다.
|
|
if ($this->resource instanceof BoundedTotalAware && ! $this->resource->totalRelation()->isExact()) {
|
|
return null;
|
|
}
|
|
|
|
// 내림차순: total, total-1, ... (역순)
|
|
// 예: 총 30개, 2페이지 → 30 - ((2-1) * 10) - 0 = 20, 19, 18, ...
|
|
return $total - (($currentPage - 1) * $perPage) - $index;
|
|
}
|
|
}
|