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

277 lines
9.4 KiB
PHP

<?php
namespace Modules\Sirsoft\Board\Tests;
use App\Http\Middleware\PermissionMiddleware;
use App\Models\Permission;
use App\Models\Role;
use Illuminate\Support\Facades\DB;
use Modules\Sirsoft\Board\Models\Board;
/**
* 단일 테이블을 사용하는 테스트 베이스 클래스
*
* PostAccessTest, PostGuestTest 등 게시판별 데이터(board_posts, board_comments)를
* 다루어야 하는 테스트에서 사용합니다.
*
* 전략:
* - RefreshDatabase를 오버라이드하여 비활성화 (TRUNCATE 없이 board_id 기반 삭제)
* - setUp에서 해당 게시판의 데이터만 삭제하여 초기화
* - ModuleTestCase의 마이그레이션은 유지
*/
abstract class BoardTestCase extends ModuleTestCase
{
/**
* RefreshDatabase 비활성화
*
* @return void
*/
protected function refreshTestDatabase(): void
{
// 트랜잭션 대신 수동 DELETE 방식으로 데이터 정리
// (DDL implicit commit으로 인해 DatabaseTransactions 사용 불가)
}
/**
* 테스트용 게시판
*/
protected Board $board;
/**
* 테스트 게시판 slug (하위 클래스에서 오버라이드 가능)
*/
protected function getTestBoardSlug(): string
{
// 클래스명 기반으로 고유한 slug 생성 (예: PostGuestTest -> post-guest)
$className = (new \ReflectionClass($this))->getShortName();
return strtolower(preg_replace('/([a-z])([A-Z])/', '$1-$2', str_replace('Test', '', $className)));
}
/**
* 테스트 환경 설정
*/
protected function setUp(): void
{
parent::setUp();
// 테스트 게시판 및 데이터 설정
$this->setupTestBoard();
}
/**
* 테스트 게시판을 설정합니다.
* 게시판이 이미 존재하면 해당 게시판의 데이터만 초기화합니다.
*/
protected function setupTestBoard(): void
{
$slug = $this->getTestBoardSlug();
// 게시판 생성 또는 조회 (updateOrCreate로 매번 기본 속성 보장)
// firstOrCreate는 기존 레코드 속성을 갱신하지 않아 이전 테스트의
// is_active=false 등이 잔류하는 문제가 있음
$this->board = Board::updateOrCreate(
['slug' => $slug],
$this->getDefaultBoardAttributes($slug)
);
// 해당 게시판의 게시글/댓글/첨부파일/신고 데이터만 삭제
DB::table('boards_reports')->where('board_id', $this->board->id)->delete();
DB::table('board_attachments')->where('board_id', $this->board->id)->delete();
DB::table('board_comments')->where('board_id', $this->board->id)->delete();
DB::table('board_posts')->where('board_id', $this->board->id)->delete();
// users 테이블도 초기화 (각 테스트마다 새로운 사용자 생성)
$this->truncateUserTables();
// PermissionMiddleware의 static guest role 캐시 초기화
// DELETE로 roles를 삭제하면 AUTO_INCREMENT가 바뀌므로 캐시된 Role 객체가 stale 됨
$this->resetPermissionMiddlewareCache();
// 기본 비회원 권한 부여 (roles/permissions 시스템 사용)
// truncateUserTables() 이후에 roles가 재생성되므로 여기서 호출
$this->createDefaultRoles();
$this->grantDefaultGuestPermissions();
}
/**
* 사용자 관련 테이블 초기화
*/
protected function truncateUserTables(): void
{
// DELETE(DML) 사용 — TRUNCATE(DDL)는 암묵적 커밋 + prepared statement 무효화 문제 유발
// FK 의존성 역순으로 삭제하여 제약조건 위반 방지
DB::table('role_permissions')->delete();
DB::table('user_roles')->delete();
DB::table('permissions')->delete();
DB::table('roles')->delete();
DB::table('users')->delete();
}
/**
* 기본 게시판 속성을 반환합니다.
* 하위 클래스에서 오버라이드하여 커스터마이징 가능합니다.
*
* @param string $slug 게시판 slug
* @return array 게시판 속성
*/
protected function getDefaultBoardAttributes(string $slug): array
{
return [
'slug' => $slug,
'name' => ['ko' => '테스트 게시판', 'en' => 'Test Board'],
'is_active' => true,
'use_file_upload' => true,
'secret_mode' => 'enabled',
'blocked_keywords' => [],
];
}
/**
* 비회원(guest) 역할에 게시판 권한을 정확히 설정합니다.
*
* 기존 guest 권한을 모두 제거하고, 지정한 권한만 부여합니다.
* guest_permissions 컬럼 대신 role/permission 시스템을 사용합니다.
*
* @param array $permissionKeys 권한 키 배열 (예: ['posts.read', 'posts.write'])
*/
protected function setGuestPermissions(array $permissionKeys): void
{
$guestRole = Role::where('identifier', 'guest')->first();
if (! $guestRole) {
return;
}
$slug = $this->board->slug;
// 이 게시판의 기존 guest 권한 모두 해제
$boardPermIds = $guestRole->permissions()
->where('identifier', 'like', "sirsoft-board.{$slug}.%")
->pluck('permissions.id');
$guestRole->permissions()->detach($boardPermIds);
// 새 권한 부여
foreach ($permissionKeys as $key) {
$perm = Permission::firstOrCreate(
['identifier' => "sirsoft-board.{$slug}.{$key}"],
['name' => ['ko' => $key, 'en' => $key], 'type' => 'user']
);
$guestRole->permissions()->syncWithoutDetaching([$perm->id]);
}
// 미들웨어 캐시 초기화 (eager-loaded permissions 갱신)
$this->resetPermissionMiddlewareCache();
}
/**
* 기본 비회원 권한을 부여합니다.
*
* posts.read, posts.write, attachments.upload 권한을 guest 역할에 부여합니다.
*/
protected function grantDefaultGuestPermissions(): void
{
$this->setGuestPermissions(['posts.read', 'posts.write', 'attachments.upload']);
}
/**
* PermissionMiddleware의 static guest role 캐시를 초기화합니다.
*
* DELETE로 roles 테이블을 비우면 AUTO_INCREMENT가 변경되어
* 캐시된 Role 객체의 ID와 실제 DB ID가 불일치합니다.
*/
protected function resetPermissionMiddlewareCache(): void
{
PermissionMiddleware::clearGuestRoleCache();
}
/**
* user role에 게시판 권한을 부여합니다.
*
* 인증된 사용자(member)가 게시판에 접근하려면 user 역할에
* 해당 게시판 권한이 필요합니다.
*
* @param array $permissionKeys 권한 키 배열 (예: ['posts.read', 'posts.write'])
*/
protected function grantUserRolePermissions(array $permissionKeys = ['posts.read', 'posts.write', 'attachments.upload']): void
{
$userRole = Role::where('identifier', 'user')->first();
if (! $userRole) {
return;
}
$slug = $this->board->slug;
foreach ($permissionKeys as $key) {
$perm = Permission::firstOrCreate(
['identifier' => "sirsoft-board.{$slug}.{$key}"],
['name' => ['ko' => $key, 'en' => $key], 'type' => 'user']
);
$userRole->permissions()->syncWithoutDetaching([$perm->id]);
}
}
/**
* 게시판 설정을 업데이트합니다.
*
* @param array $settings 업데이트할 설정
*/
protected function updateBoardSettings(array $settings): void
{
$this->board->update($settings);
$this->board->refresh();
}
/**
* 테스트 게시글을 직접 DB에 생성합니다.
*
* @param array $attributes 게시글 속성
* @return int 생성된 게시글 ID
*/
protected function createTestPost(array $attributes = []): int
{
$defaults = [
'board_id' => $this->board->id,
'title' => '테스트 게시글',
'content' => '테스트 내용입니다.',
'user_id' => null,
'author_name' => '테스트',
'password' => null,
'ip_address' => '127.0.0.1',
'is_notice' => false,
'is_secret' => false,
'status' => 'published',
'trigger_type' => 'admin',
'view_count' => 0,
'created_at' => now(),
'updated_at' => now(),
];
return DB::table('board_posts')->insertGetId(array_merge($defaults, $attributes));
}
/**
* 테스트 댓글을 직접 DB에 생성합니다.
*
* @param int $postId 게시글 ID
* @param array $attributes 댓글 속성
* @return int 생성된 댓글 ID
*/
protected function createTestComment(int $postId, array $attributes = []): int
{
$defaults = [
'board_id' => $this->board->id,
'post_id' => $postId,
'user_id' => null,
'author_name' => '테스트',
'content' => '테스트 댓글입니다.',
'password' => null,
'ip_address' => '127.0.0.1',
'status' => 'published',
'created_at' => now(),
'updated_at' => now(),
];
return DB::table('board_comments')->insertGetId(array_merge($defaults, $attributes));
}
}