Files
HeuJung 41fe812f2c perf(board): 목록·검색·상세의 중복 실행 제거 및 상한 계약 소비
검색 — 이 코어에 추가한 BOOLEAN MODE 정제기가 게시판에는 도달하지 않아
원문 키워드가 그대로 바인딩되고 있었다. + - * " < > 를 넣으면 오류 화면이 떴다.
코어 정제기 경유로 옮겼고, 같은 술어를 count 와 get 으로 두 번 실행하던 것을
페이지네이터 한 번으로 합쳤다. 구체 엔진 직접 호출도 해석기 경유로 바꿔 플러그인
검색엔진이 게시글·신고 검색에 적용된다.

상세 — 같은 글을 3회, 같은 게시판을 5회 읽고 있었다. 조회한 인스턴스를 넘겨
글 1회·게시판 1회로 줄였고, 조회수 증가를 권한 확인 뒤로 옮겨 열람 권한이 없는
요청이 조회수를 올리지 않게 했다.

목록 — 답변 트리의 무제한 조회에 상한과 순환 가드를 두고, O(n²) 병합을 한 패스로
바꿨다. 댓글도 무제한 조회에 상한을 두되 잘렸다는 사실과 전체 건수를 함께 알린다.
조용히 잘리면 "댓글이 그만큼뿐" 으로 보인다. 원댓글 단위 페이지네이션을 더해
답글이 부모와 떨어지지 않게 했다.

관리자 화면 — 총 건수가 잘린 목록에서 페이저가 "1페이지뿐" 으로 접히던 자리를
고쳤다. 저장소가 상한을 붙인 목록인데 화면이 그 값을 1 로 채우고 있어, 기록이
상한을 넘으면 2페이지에 도달할 방법이 없었다.

같은 파일에서 드러난 사전 결함도 함께 처리했다 — 반복 렌더 안의 정적 HTML id,
Icon 의 박스 크기 지정, 단색 버튼의 다크 모드 대비.
2026-08-06 11:13:51 +09:00

122 lines
4.5 KiB
PHP

<?php
namespace Modules\Sirsoft\Board\Tests\Feature;
use App\Enums\TotalRelation;
use App\Support\Query\BoundedCount;
use Modules\Sirsoft\Board\Models\Comment;
use Modules\Sirsoft\Board\Models\Post;
use Modules\Sirsoft\Board\Repositories\Contracts\CommentRepositoryInterface;
use Modules\Sirsoft\Board\Tests\BoardTestCase;
/**
* 댓글 목록 절단 노출 계약 테스트 (#519)
*
* 한 글의 댓글은 상한을 넘으면 그 지점에서 끊긴다. 끊긴 사실을 알리지 않으면
* 사용자에게는 "댓글이 그만큼뿐" 으로 보인다 — 오류도 빈 화면도 아니라서
* 화면만 봐서는 알아챌 수 없는 유형이다.
*/
class CommentTruncationDisclosureTest extends BoardTestCase
{
/**
* 게시글 하나와 댓글 N 건을 만듭니다.
*
* @param int $commentCount 만들 댓글 수
* @return Post 생성된 게시글
*/
private function seedPostWithComments(int $commentCount): Post
{
$post = Post::create([
'board_id' => $this->board->id,
'title' => '댓글 절단 테스트',
'content' => '본문',
'author_name' => '작성자',
'status' => 'published',
'ip_address' => '127.0.0.1',
]);
for ($i = 0; $i < $commentCount; $i++) {
Comment::create([
'board_id' => $this->board->id,
'post_id' => $post->id,
'content' => '댓글 '.$i,
'author_name' => '댓글쓴이',
'ip_address' => '127.0.0.1',
]);
}
return $post;
}
/**
* 상한 이하면 댓글 전량이 나오고 절단 표시가 없는지 확인
*/
public function test_comments_are_not_truncated_under_cap(): void
{
config(['g7_settings.core.pagination.result_cap' => 100]);
$post = $this->seedPostWithComments(5);
$repository = app(CommentRepositoryInterface::class);
$comments = $repository->getByPostId($this->board->slug, $post->id, boardId: $this->board->id);
$this->assertCount(5, $comments);
$count = $repository->countByPostId($this->board->slug, $post->id, boardId: $this->board->id);
$this->assertInstanceOf(BoundedCount::class, $count);
$this->assertSame(5, $count->total);
$this->assertFalse($count->isTruncated());
}
/**
* 상한을 넘으면 목록이 그 지점에서 끊기고 총 건수가 "이상" 으로 보고되는지 확인
*/
public function test_comment_count_reports_truncation_over_cap(): void
{
config(['g7_settings.core.pagination.result_cap' => 3]);
$post = $this->seedPostWithComments(7);
$repository = app(CommentRepositoryInterface::class);
$comments = $repository->getByPostId($this->board->slug, $post->id, boardId: $this->board->id);
$this->assertLessThanOrEqual(3, $comments->count(), '상한을 넘겨 조회됐다 — 목록이 끊기지 않았다');
$count = $repository->countByPostId($this->board->slug, $post->id, boardId: $this->board->id);
$this->assertSame(3, $count->total);
$this->assertSame(TotalRelation::AtLeast, $count->totalRelation());
$this->assertTrue($count->isTruncated());
}
/**
* 상세 응답이 절단 사실을 필드로 알리는지 확인
*
* 저장소가 정확도를 돌려줘도 화면까지 도달하지 않으면 아무 소용이 없다.
*/
public function test_post_detail_response_discloses_truncation(): void
{
config(['g7_settings.core.pagination.result_cap' => 3]);
$post = $this->seedPostWithComments(7);
$response = $this->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$post->id}");
$response->assertOk();
$response->assertJsonPath('data.comments_truncated', true);
$response->assertJsonPath('data.comments_total', 3);
$response->assertJsonPath('data.comments_total_is_exact', false);
}
/**
* 끊기지 않은 경우에는 절단 플래그가 서지 않는지 확인
*/
public function test_post_detail_response_reports_no_truncation_when_under_cap(): void
{
config(['g7_settings.core.pagination.result_cap' => 100]);
$post = $this->seedPostWithComments(4);
$response = $this->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$post->id}");
$response->assertOk();
$response->assertJsonPath('data.comments_truncated', false);
$response->assertJsonPath('data.comments_total_is_exact', true);
}
}