공개 이슈 gnuboard/g7 이 지목한 병목은 "같은 일을 반복 실행한다" 축이다. 응답·행의 무게를 다룬 · 과 겹치지 않는다. 공통 계약 — 총 건수 상한과 페이지 이동 범위는 별개 결정이다. 묶으면 필요 없이 기능이 깎인다. 총 건수만 상한을 받고(파생 테이블 COUNT), "다음" 이동은 per_page + 1 실측으로 끝까지 열어 둔다. 계산이 불가능해지는 것은 마지막 페이지 번호 하나뿐이며 그 사실은 last_page: null 이 알린다. 최신순처럼 실제 컬럼으로 정렬하는 목록은 커서로 전환해 깊이와 무관하게 일정 속도로 이동한다. 관련도순은 계산값 정렬이라 커서 키로 쓸 수 없어 offset 을 유지한다. 계약의 입구는 표준 paginate 와 같은 폭이어야 한다. 관계·쿼리 빌더를 받지 못하면 그 좁은 만큼이 그대로 운영 500 이 되고, 실제로 관리자 알림 목록에서 그렇게 터졌다. 응답 조립도 컬렉션마다 손으로 하면 형태가 늘어나는 순간 없는 값을 부르거나 새 필드를 흘리므로, 형태 판정을 paginationMeta 한 곳에 모았다. 표준 paginate 응답은 필드 단위로 이전과 동일하다. 요청당 반복 비용 — 훅 구독마다 남기던 로그 400줄, 요청당 스무 번 넘던 설정 파일 재읽기, 이미 캐시된 목록의 DB 재조회를 없앴다. 권한 판정은 요청 스코프 메모를 두어 화면 요소마다 나가던 조회를 한 번으로 줄였고, 크로스 요청 캐시는 두지 않아 권한 변경이 종전처럼 다음 요청에 반영된다. 검색 질의는 활성 엔진이 만든다. 저장소가 구체 엔진을 지목하면 플러그인이 등록한 엔진은 호출될 기회 자체를 잃고 오류 없이 다른 방식으로 동작한다. 해석기를 두어 활성 엔진에 위임하고, 전문검색이 없는 DBMS 의 부분일치 폴백도 드라이버명 하드코딩 대신 선언형 config 로 옮겼다. 총 건수가 잘린 목록에서 순번을 역산하면 0 과 음수가 나온다. 지어내지 않고 null 을 돌려주며, 그 원칙을 last_page 와 동일하게 적용했다.
175 lines
6.2 KiB
PHP
175 lines
6.2 KiB
PHP
<?php
|
|
|
|
namespace App\Repositories;
|
|
|
|
use App\Contracts\Repositories\ScheduleHistoryRepositoryInterface;
|
|
use App\Models\ScheduleHistory;
|
|
use App\Repositories\Concerns\PaginatesWithDeferredJoin;
|
|
use App\Repositories\Concerns\ResolvesSortSpec;
|
|
use App\Support\Query\PaginationLimits;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
|
|
class ScheduleHistoryRepository implements ScheduleHistoryRepositoryInterface
|
|
{
|
|
use PaginatesWithDeferredJoin;
|
|
use ResolvesSortSpec;
|
|
|
|
/**
|
|
* ID로 실행 이력을 찾습니다.
|
|
*
|
|
* @param int $id 이력 ID
|
|
* @return ScheduleHistory|null 찾은 이력 모델 또는 null
|
|
*/
|
|
public function findById(int $id): ?ScheduleHistory
|
|
{
|
|
return ScheduleHistory::with(['schedule', 'triggeredBy'])->find($id);
|
|
}
|
|
|
|
/**
|
|
* 새로운 실행 이력을 생성합니다.
|
|
*
|
|
* @param array $data 이력 생성 데이터
|
|
* @return ScheduleHistory 생성된 이력 모델
|
|
*/
|
|
public function create(array $data): ScheduleHistory
|
|
{
|
|
return ScheduleHistory::create($data);
|
|
}
|
|
|
|
/**
|
|
* 기존 실행 이력을 업데이트합니다.
|
|
*
|
|
* @param ScheduleHistory $history 업데이트할 이력 모델
|
|
* @param array $data 업데이트할 데이터
|
|
* @return bool 업데이트 성공 여부
|
|
*/
|
|
public function update(ScheduleHistory $history, array $data): bool
|
|
{
|
|
return $history->update($data);
|
|
}
|
|
|
|
/**
|
|
* 실행 이력을 삭제합니다.
|
|
*
|
|
* @param ScheduleHistory $history 삭제할 이력 모델
|
|
* @return bool 삭제 성공 여부
|
|
*/
|
|
public function delete(ScheduleHistory $history): bool
|
|
{
|
|
return $history->delete();
|
|
}
|
|
|
|
/**
|
|
* 특정 스케줄의 실행 이력을 페이지네이션하여 조회합니다.
|
|
*
|
|
* @param int $scheduleId 스케줄 ID
|
|
* @param array $filters 필터 조건 배열
|
|
* @return LengthAwarePaginator 페이지네이션된 이력 목록
|
|
*/
|
|
public function getPaginatedByScheduleId(int $scheduleId, array $filters = []): LengthAwarePaginator
|
|
{
|
|
// 관계는 쿼리에 미리 붙이지 않는다 — 지연 조인 트레이트가 inner/outer 양쪽에서
|
|
// setEagerLoads([]) 로 지우므로, 여기서 with() 하면 outer 에서도 사라져
|
|
// ScheduleHistoryResource 의 whenLoaded('triggeredBy') 가 항상 비게 된다.
|
|
// 로드는 relations: 인자로 넘긴다.
|
|
$query = ScheduleHistory::query()
|
|
->where('schedule_id', $scheduleId);
|
|
|
|
// 상태 필터
|
|
if (! empty($filters['status'])) {
|
|
$query->where('status', $filters['status']);
|
|
}
|
|
|
|
// 트리거 유형 필터
|
|
if (! empty($filters['trigger_type'])) {
|
|
$query->where('trigger_type', $filters['trigger_type']);
|
|
}
|
|
|
|
// 기간 필터 — whereDate 는 컬럼에 DATE() 를 씌워 인덱스를 못 쓰게 만든다.
|
|
// 같은 결과를 내는 범위 조건으로 바꿔 started_at 인덱스를 살린다
|
|
// (종료일은 그날 23:59:59.999999 까지 포함해야 whereDate 와 동일한 경계를 갖는다).
|
|
if (! empty($filters['started_from'])) {
|
|
$query->where('started_at', '>=', Carbon::parse($filters['started_from'])->startOfDay());
|
|
}
|
|
|
|
if (! empty($filters['started_to'])) {
|
|
$query->where('started_at', '<=', Carbon::parse($filters['started_to'])->endOfDay());
|
|
}
|
|
|
|
// 정렬 — 요청 값은 허용 목록으로만 해석한다.
|
|
// 허용 집합은 ScheduleHistoryListRequest 의 `in:` 규칙과 동일해야 한다. 저장소 목록이
|
|
// 그보다 좁으면 게이트를 통과한 정렬이 조용히 기본값으로 되돌아간다 (종료시각/소요시간).
|
|
$sort = $this->resolveSortSpec(
|
|
$filters,
|
|
['started_at', 'ended_at', 'duration', 'status'],
|
|
'started_at'
|
|
);
|
|
|
|
$perPage = (int) ($filters['per_page'] ?? 15);
|
|
|
|
// 실행 이력은 계속 쌓이며 출력/에러 메시지를 그대로 노출하므로, 컬럼은 좁히지 않고
|
|
// 지연 조인으로 넓은 컬럼을 읽는 행 수만 이번 페이지 분량으로 고정한다.
|
|
return $this->paginateWithDeferredJoin(
|
|
query: $query,
|
|
columns: ['*'],
|
|
sort: $sort,
|
|
perPage: $perPage,
|
|
relations: ['triggeredBy'],
|
|
// 로그 테이블은 계속 쌓이기만 한다. 총 건수는 상한까지만 세고 "다음" 이동은
|
|
// per_page + 1 실측으로 끝까지 열어 둔다 (계산 불가는 마지막 페이지 번호 하나뿐).
|
|
resultCap: PaginationLimits::resultCap('admin.schedule_histories'),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 특정 스케줄의 최근 실행 이력을 조회합니다.
|
|
*
|
|
* @param int $scheduleId 스케줄 ID
|
|
* @param int $limit 조회 개수
|
|
* @return Collection 최근 이력 컬렉션
|
|
*/
|
|
public function getRecentByScheduleId(int $scheduleId, int $limit = 10): Collection
|
|
{
|
|
return ScheduleHistory::with('triggeredBy')
|
|
->where('schedule_id', $scheduleId)
|
|
->orderByDesc('started_at')
|
|
->limit($limit)
|
|
->get();
|
|
}
|
|
|
|
/**
|
|
* 여러 이력을 일괄 삭제합니다.
|
|
*
|
|
* @param array $ids 이력 ID 배열
|
|
* @return int 삭제된 레코드 수
|
|
*/
|
|
public function bulkDelete(array $ids): int
|
|
{
|
|
return ScheduleHistory::whereIn('id', $ids)->delete();
|
|
}
|
|
|
|
/**
|
|
* 특정 스케줄의 모든 이력을 삭제합니다.
|
|
*
|
|
* @param int $scheduleId 스케줄 ID
|
|
* @return int 삭제된 레코드 수
|
|
*/
|
|
public function deleteByScheduleId(int $scheduleId): int
|
|
{
|
|
return ScheduleHistory::where('schedule_id', $scheduleId)->delete();
|
|
}
|
|
|
|
/**
|
|
* 특정 기간 이전의 이력을 삭제합니다.
|
|
*
|
|
* @param int $days 보관 기간 (일)
|
|
* @return int 삭제된 레코드 수
|
|
*/
|
|
public function deleteOlderThan(int $days): int
|
|
{
|
|
return ScheduleHistory::where('started_at', '<', now()->subDays($days))->delete();
|
|
}
|
|
}
|