Files
Gnuboard7/app/Http/Resources/BaseApiCollection.php
T
HeuJung 7b859d4200 fix(core,basic,ecommerce): 쇼핑 화면 호출 통합 원복·검색 주소 승계·라우트 캐시 갱신 및 상한값 설정 저장 정합
합의되지 않은 구조 변경을 되돌리고, 계획서 전수조사에서 드러난 결함을 처리했다.

쇼핑 화면 호출 통합(F4)은 API 쿼리 튜닝이 아니라 화면 구조 변경이라 원복했다.
레이아웃 9개를 develop 상태로 복원하고 전용 엔드포인트·컨트롤러·문서·테스트를 걷어냈다.
되돌리면 함께 사라지는 비-F4 수정 둘(페이저 has_more_pages 판정 3곳, Icon 크기 클래스
23줄)은 재적용했다. 성능 개선분은 전부 잔존한다.

통합검색 화면 상태를 전역 상태에서 URL 쿼리로 옮겼다. 종전에는 탭·필터·정렬·페이지가
주소에 없어 새로고침·뒤로가기에서 초기화되고 결과를 공유할 수도 없었다. 화면은 정상으로
보이고 콘솔 에러도 없어 코드만으로는 드러나지 않는 형태였다.

확장이 라우트를 추가해도 라우트 캐시가 걸린 사이트에서는 그 주소가 404 가 되던 구멍을
막았다. 훅 캐시와 달리 라우트 캐시에는 스캔 폴백이 없어 예외도 경고도 남지 않는다.
같은 문제를 이미 푼 ConfigCacheHelper 를 미러링해 갱신 지점을 한 곳으로 모았고,
코어 업데이트가 캐시를 비우기만 하고 되살리지 않던 문제도 함께 고쳤다.

상품·페이지 검색이 커서 판정에 페이지 번호를 넘기지 않아, 커서 없이 깊은 페이지를
지목한 딥링크가 조용히 1 페이지를 돌려주던 결함을 고쳤다. 호출 지점을 손으로 열거하는
대신 저장소를 훑는 패리티 테스트로 다음 도메인의 재발을 막는다.

환경설정 고급 탭의 목록 상한값이 저장되지 않던 결함을 고쳤다. 화면·검증·읽기는 모두
있었는데 저장 시 카테고리 분류표가 손으로 열거돼 있어 그 값만 버려지고 있었다. 분류표를
설정 정의에서 도출하도록 바꿔 다음 카테고리가 합류해도 같은 일이 생기지 않게 했다.
범위를 벗어난 값의 안내에 내부 식별자가 노출되던 것도 함께 정리했다.

언어팩 재설치가 활성 팩을 자기 자신과의 슬롯 충돌로 오인해 강등하던 회귀와, CLI 설치가
HTTP 권한 컨텍스트 없이 자동 활성화되는지를 고정하는 테스트를 함께 담았다.
2026-08-06 11:13:54 +09:00

140 lines
5.2 KiB
PHP

<?php
namespace App\Http\Resources;
use App\Contracts\Pagination\BoundedTotalAware;
use App\Helpers\PermissionHelper;
use App\Http\Resources\Traits\HasRowNumber;
use App\Support\Query\BoundedPage;
use App\Support\Query\KeysetPaginator;
use Illuminate\Contracts\Pagination\LengthAwarePaginator as LengthAwarePaginatorContract;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Pagination\CursorPaginator;
/**
* API 컬렉션 리소스 기본 클래스
*
* 모든 API Collection은 이 클래스를 상속해야 합니다.
* - HasRowNumber: 순번 부여
* - abilityMap(): 컬렉션 레벨 abilities (페이지 버튼 제어)
*/
abstract class BaseApiCollection extends ResourceCollection
{
use HasRowNumber;
/**
* 컬렉션 레벨 능력(can_*) 매핑을 반환합니다.
*
* 페이지 레벨 버튼 제어용 (생성, 일괄삭제 등).
* abilities가 불필요한 컬렉션은 오버라이드하지 않습니다.
*
* @return array<string, string> ['can_delete' => 'permission.identifier', ...]
*/
protected function abilityMap(): array
{
return [];
}
/**
* 컬렉션 레벨 abilities를 해석합니다.
*
* @param Request $request HTTP 요청 객체
* @return array<string, bool>
*/
public function resolveCollectionAbilities(Request $request): array
{
$map = $this->abilityMap();
if (empty($map)) {
return [];
}
$user = $request->user();
$abilities = [];
foreach ($map as $key => $permission) {
$abilities[$key] = PermissionHelper::check($permission, $user);
}
return $abilities;
}
/**
* paginator 인 경우 표준 pagination 메타를 반환합니다.
*
* 무한스크롤 화면이 전체 개수(total)와 다음 페이지 존재 여부(has_more_pages)를
* 정확히 판정할 수 있도록 노출합니다. 전체 조회(get) 등 paginator 가 아닌
* 경우에는 빈 배열을 반환하므로 toArray 에서 array_merge 로 안전하게 합칠 수 있습니다.
*
* 페이지 결과는 세 형태 중 하나이며, 형태를 스스로 판정해 그 형태가 실제로 아는
* 값만 내보냅니다. 모르는 값을 0 이나 1 로 채우면 화면이 그것을 사실로 읽습니다.
*
* | 형태 | 예 | total | last_page | 추가 |
* |---|---|---|---|---|
* | 전체건수형 | `paginate()` | 정확 | 정확 | — |
* | 상한형 | {@see BoundedPage} | 정확 또는 하한 | 하한이면 null | `total_relation` `total_is_exact` `result_cap` |
* | 단순형 | `simplePaginate()` / `cursorPaginate()` | 없음 | 없음 | 커서면 `next_cursor` `prev_cursor` |
*
* 기존 필드는 하나도 제거하지 않으므로, 표준 `paginate()` 를 쓰는 기존 컬렉션의
* 응답은 이전과 완전히 동일합니다.
*
* @return array<string, mixed> ['pagination' => [...]] 또는 빈 배열
*/
protected function paginationMeta(): array
{
$resource = $this->resource;
if ($resource instanceof CursorPaginator) {
return ['pagination' => $this->cursorPaginationMeta($resource)];
}
if (! method_exists($resource, 'currentPage')) {
return [];
}
$meta = [
'current_page' => $resource->currentPage(),
'per_page' => $resource->perPage(),
'from' => $resource->firstItem(),
'to' => $resource->lastItem(),
'has_more_pages' => $resource->hasMorePages(),
];
// 단순형(simplePaginate)은 총 건수를 아예 세지 않으므로 total/last_page 를 내보내지 않는다.
if ($resource instanceof LengthAwarePaginatorContract) {
$meta['last_page'] = $resource->lastPage();
$meta['total'] = $resource->total();
}
if ($resource instanceof BoundedTotalAware) {
$meta['total_relation'] = $resource->totalRelation()->value;
$meta['total_is_exact'] = $resource->totalRelation()->isExact();
$meta['result_cap'] = $resource->resultCap();
}
return ['pagination' => $meta];
}
/**
* 커서 페이지의 pagination 메타를 만듭니다.
*
* 커서 방식은 총 건수와 페이지 번호를 계산하지 않습니다. 대신 앞뒤 이동 커서를
* 그대로 실어 보내며, 화면은 이 값으로 이전/다음 버튼을 만듭니다.
*
* 인코딩은 {@see KeysetPaginator} 를 거칩니다. 커서 문자열의 형식은 디코딩과 짝을
* 이뤄야 하므로, 한쪽만 여기서 직접 만들면 표준이 두 벌이 됩니다.
*
* @param CursorPaginator $resource 커서 페이지 결과
* @return array<string, mixed> pagination 메타
*/
private function cursorPaginationMeta(CursorPaginator $resource): array
{
return [
'per_page' => $resource->perPage(),
'has_more_pages' => $resource->hasMorePages(),
'next_cursor' => KeysetPaginator::nextCursor($resource),
'prev_cursor' => KeysetPaginator::previousCursor($resource),
];
}
}