검색 — 이 코어에 추가한 BOOLEAN MODE 정제기가 게시판에는 도달하지 않아 원문 키워드가 그대로 바인딩되고 있었다. + - * " < > 를 넣으면 오류 화면이 떴다. 코어 정제기 경유로 옮겼고, 같은 술어를 count 와 get 으로 두 번 실행하던 것을 페이지네이터 한 번으로 합쳤다. 구체 엔진 직접 호출도 해석기 경유로 바꿔 플러그인 검색엔진이 게시글·신고 검색에 적용된다. 상세 — 같은 글을 3회, 같은 게시판을 5회 읽고 있었다. 조회한 인스턴스를 넘겨 글 1회·게시판 1회로 줄였고, 조회수 증가를 권한 확인 뒤로 옮겨 열람 권한이 없는 요청이 조회수를 올리지 않게 했다. 목록 — 답변 트리의 무제한 조회에 상한과 순환 가드를 두고, O(n²) 병합을 한 패스로 바꿨다. 댓글도 무제한 조회에 상한을 두되 잘렸다는 사실과 전체 건수를 함께 알린다. 조용히 잘리면 "댓글이 그만큼뿐" 으로 보인다. 원댓글 단위 페이지네이션을 더해 답글이 부모와 떨어지지 않게 했다. 관리자 화면 — 총 건수가 잘린 목록에서 페이저가 "1페이지뿐" 으로 접히던 자리를 고쳤다. 저장소가 상한을 붙인 목록인데 화면이 그 값을 1 로 채우고 있어, 기록이 상한을 넘으면 2페이지에 도달할 방법이 없었다. 같은 파일에서 드러난 사전 결함도 함께 처리했다 — 반복 렌더 안의 정적 HTML id, Icon 의 박스 크기 지정, 단색 버튼의 다크 모드 대비.
153 lines
5.2 KiB
PHP
153 lines
5.2 KiB
PHP
<?php
|
||
|
||
namespace Modules\Sirsoft\Board\Tests\Feature;
|
||
|
||
use App\Enums\TotalRelation;
|
||
use App\Support\Query\BoundedCount;
|
||
use Illuminate\Pagination\LengthAwarePaginator;
|
||
use Illuminate\Support\Collection;
|
||
use Modules\Sirsoft\Board\Http\Resources\PostCollection;
|
||
use Modules\Sirsoft\Board\Models\Post;
|
||
use Modules\Sirsoft\Board\Tests\BoardTestCase;
|
||
|
||
/**
|
||
* 목록 순번이 상한 총 건수로 역산되어 0·음수로 내려가는 회귀 테스트 (#519)
|
||
*
|
||
* 내림차순 순번은 "전체 몇 건 중 몇 번째" 라 총 건수를 알아야 계산된다. 총 건수가 상한에
|
||
* 걸려 잘리면 그 값으로 역산한 순번은 첫 페이지부터 이미 틀리고, 상한을 넘어가는 페이지에서는
|
||
* 0 과 음수까지 내려간다. 실측(12,027 행): 500p=20 → 501p=0/−19 → 601p=−2000.
|
||
*
|
||
* 틀린 숫자를 내보내는 것보다 내보내지 않는 편이 낫다. `last_page` 를 모를 때 null 로
|
||
* 내보내는 것과 같은 원칙으로, 잘린 총 건수에서는 순번을 null 로 둔다. 오름차순은 offset
|
||
* 기반이라 총 건수와 무관하게 정확하므로 그대로 둔다.
|
||
*
|
||
* @scenario case=post_number_bounded_total
|
||
*
|
||
* @effects post_number_null_when_total_truncated,
|
||
* post_number_never_non_positive,
|
||
* post_number_exact_when_total_exact,
|
||
* post_number_ascending_unaffected_by_truncation
|
||
*/
|
||
class PostNumberBoundedTotalRegressionTest extends BoardTestCase
|
||
{
|
||
/**
|
||
* 순번 계산에 필요한 최소 형태의 게시글 목록을 만듭니다.
|
||
*
|
||
* @param int $count 만들 개수
|
||
* @return Collection<int, Post> 게시글 컬렉션
|
||
*/
|
||
private function makePosts(int $count): Collection
|
||
{
|
||
return collect(range(1, $count))->map(function (int $i) {
|
||
$post = new Post;
|
||
$post->id = 1000 + $i;
|
||
$post->is_notice = false;
|
||
$post->parent_id = null;
|
||
|
||
return $post;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 컬렉션을 렌더링해 순번만 뽑아냅니다.
|
||
*
|
||
* @param int $page 현재 페이지
|
||
* @param int $perPage 페이지당 건수
|
||
* @param BoundedCount $total 총 건수 + 정확도
|
||
* @param string $direction 정렬 방향
|
||
* @return array<int, mixed> 순번 목록
|
||
*/
|
||
private function numbersFor(int $page, int $perPage, BoundedCount $total, string $direction = 'desc'): array
|
||
{
|
||
$paginator = new LengthAwarePaginator(
|
||
$this->makePosts($perPage),
|
||
max($total->total(), $page * $perPage),
|
||
$perPage,
|
||
$page,
|
||
);
|
||
|
||
$collection = new PostCollection($paginator);
|
||
$collection->setTotalNormalPosts($total);
|
||
$collection->setOrderDirection($direction);
|
||
|
||
$rendered = $collection->toArray(request());
|
||
|
||
return collect($rendered['data'])->pluck('number')->all();
|
||
}
|
||
|
||
/**
|
||
* 총 건수가 잘리지 않았으면 순번은 지금까지처럼 정확해야 합니다.
|
||
*/
|
||
public function test_총건수가_정확하면_순번은_총건수부터_내림차순이다(): void
|
||
{
|
||
$numbers = $this->numbersFor(
|
||
page: 1,
|
||
perPage: 5,
|
||
total: new BoundedCount(120, TotalRelation::Exact, 10000),
|
||
);
|
||
|
||
$this->assertSame([120, 119, 118, 117, 116], $numbers);
|
||
}
|
||
|
||
/**
|
||
* 총 건수가 잘렸으면 순번을 지어내지 않아야 합니다.
|
||
*/
|
||
public function test_총건수가_잘리면_순번은_null_이다(): void
|
||
{
|
||
$numbers = $this->numbersFor(
|
||
page: 1,
|
||
perPage: 5,
|
||
total: new BoundedCount(10000, TotalRelation::AtLeast, 10000),
|
||
);
|
||
|
||
$this->assertSame([null, null, null, null, null], $numbers);
|
||
}
|
||
|
||
/**
|
||
* 상한을 넘어선 깊은 페이지에서도 0·음수가 나오지 않아야 합니다.
|
||
*/
|
||
public function test_상한을_넘은_페이지에서_0이나_음수_순번이_없다(): void
|
||
{
|
||
foreach ([501, 502, 601] as $page) {
|
||
$numbers = $this->numbersFor(
|
||
page: $page,
|
||
perPage: 20,
|
||
total: new BoundedCount(10000, TotalRelation::AtLeast, 10000),
|
||
);
|
||
|
||
foreach ($numbers as $number) {
|
||
$this->assertNotIsInt(
|
||
$number,
|
||
"{$page} 페이지에서 상한 총 건수로 역산한 순번이 그대로 나왔습니다."
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 오름차순은 offset 기반이라 총 건수가 잘려도 그대로 정확해야 합니다.
|
||
*/
|
||
public function test_오름차순_순번은_총건수가_잘려도_유지된다(): void
|
||
{
|
||
$numbers = $this->numbersFor(
|
||
page: 3,
|
||
perPage: 5,
|
||
total: new BoundedCount(10000, TotalRelation::AtLeast, 10000),
|
||
direction: 'asc',
|
||
);
|
||
|
||
$this->assertSame([11, 12, 13, 14, 15], $numbers);
|
||
}
|
||
|
||
/**
|
||
* PHPUnit 11 에는 assertNotIsInt 가 없으므로 의미를 그대로 옮겨 둡니다.
|
||
*
|
||
* @param mixed $value 검사 대상
|
||
* @param string $message 실패 메시지
|
||
*/
|
||
private function assertNotIsInt(mixed $value, string $message = ''): void
|
||
{
|
||
$this->assertFalse(is_int($value), $message);
|
||
}
|
||
}
|