535 lines
21 KiB
PHP
535 lines
21 KiB
PHP
<?php
|
|
|
|
namespace Modules\Sirsoft\Board\Tests\Feature\User;
|
|
|
|
// ModuleTestCase를 수동으로 require (autoload 전에 로드 필요)
|
|
require_once __DIR__.'/../../ModuleTestCase.php';
|
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Modules\Sirsoft\Board\Enums\PostStatus;
|
|
use Modules\Sirsoft\Board\Enums\SecretMode;
|
|
use Modules\Sirsoft\Board\Models\Board;
|
|
use Modules\Sirsoft\Board\Services\PostService;
|
|
use Modules\Sirsoft\Board\Tests\ModuleTestCase;
|
|
|
|
/**
|
|
* 게시글 이전/다음 네비게이션 API 회귀 테스트 (이슈 #269)
|
|
*
|
|
* PostController::navigation() 이 어떤 상황에서도 500 응답을 내지 않고
|
|
* graceful degradation 으로 `{prev, next}` 또는 404 만 반환하도록 보장합니다.
|
|
*
|
|
* @group board
|
|
* @group board-navigation
|
|
*/
|
|
class PostNavigationTest extends ModuleTestCase
|
|
{
|
|
private string $boardSlug = 'nav-test';
|
|
|
|
private Board $board;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
Cache::flush();
|
|
DB::table('boards')->where('slug', $this->boardSlug)->delete();
|
|
DB::table('boards')->where('slug', 'nav-test-inactive')->delete();
|
|
|
|
$this->board = Board::factory()->create([
|
|
'slug' => $this->boardSlug,
|
|
'is_active' => true,
|
|
'order_by' => 'created_at',
|
|
'order_direction' => 'DESC',
|
|
'secret_mode' => SecretMode::Disabled->value,
|
|
]);
|
|
|
|
$this->ensureGuestCanReadPosts();
|
|
}
|
|
|
|
/**
|
|
* navigation 라우트는 permission:user,posts.read 로 보호됩니다.
|
|
* guest 역할에 해당 권한을 부여해 401 차단을 우회합니다.
|
|
*/
|
|
private function ensureGuestCanReadPosts(): void
|
|
{
|
|
$identifier = 'sirsoft-board.'.$this->boardSlug.'.posts.read';
|
|
$permId = DB::table('permissions')->where('identifier', $identifier)->value('id');
|
|
|
|
if (! $permId) {
|
|
$permId = DB::table('permissions')->insertGetId([
|
|
'identifier' => $identifier,
|
|
'name' => json_encode(['ko' => 'navigation 테스트 read', 'en' => 'nav-test read']),
|
|
'type' => 'user',
|
|
'extension_type' => 'module',
|
|
'extension_identifier' => 'sirsoft-board',
|
|
'resource_route_key' => 'post',
|
|
'owner_key' => 'user_id',
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
}
|
|
|
|
$guestRoleId = DB::table('roles')->where('identifier', 'guest')->value('id');
|
|
if (! $guestRoleId) {
|
|
return;
|
|
}
|
|
|
|
DB::table('role_permissions')->updateOrInsert(
|
|
['role_id' => $guestRoleId, 'permission_id' => $permId],
|
|
['scope_type' => null, 'granted_at' => now(), 'created_at' => now(), 'updated_at' => now()]
|
|
);
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
DB::table('board_posts')->where('board_id', $this->board->id)->delete();
|
|
DB::table('boards')->whereIn('slug', [$this->boardSlug, 'nav-test-inactive'])->delete();
|
|
parent::tearDown();
|
|
}
|
|
|
|
/**
|
|
* 존재하지 않는 글 ID → 200 + 빈 값 (500 금지)
|
|
*/
|
|
public function test_returns_empty_when_post_not_found(): void
|
|
{
|
|
$response = $this->getJson("/api/modules/sirsoft-board/boards/{$this->boardSlug}/posts/999999/navigation");
|
|
|
|
$response->assertStatus(200);
|
|
$response->assertJson([
|
|
'data' => ['prev' => null, 'next' => null],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 공지글 → 200 + 빈 값 (네비게이션 미제공)
|
|
*
|
|
* @scenario post_kind=notice
|
|
*
|
|
* @effects navigation_null_for_notice
|
|
*/
|
|
public function test_returns_empty_for_notice_post(): void
|
|
{
|
|
$noticeId = $this->insertPost([
|
|
'is_notice' => true,
|
|
'title' => '공지',
|
|
]);
|
|
|
|
$response = $this->getJson("/api/modules/sirsoft-board/boards/{$this->boardSlug}/posts/{$noticeId}/navigation");
|
|
|
|
$response->assertStatus(200);
|
|
$response->assertJson([
|
|
'data' => ['prev' => null, 'next' => null],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 일반글 양옆 글 없음 → 200 + 빈 값
|
|
*/
|
|
public function test_returns_empty_when_no_adjacent_posts(): void
|
|
{
|
|
$postId = $this->insertPost(['title' => '혼자']);
|
|
|
|
$response = $this->getJson("/api/modules/sirsoft-board/boards/{$this->boardSlug}/posts/{$postId}/navigation");
|
|
|
|
$response->assertStatus(200);
|
|
$response->assertJson([
|
|
'data' => ['prev' => null, 'next' => null],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 일반글 + 이전/다음 글 존재 → 정상 반환 (created_at DESC 정렬)
|
|
*
|
|
* created_at DESC 정렬에서 prev는 created_at이 더 나중인 글(newer),
|
|
* next는 created_at이 더 이전인 글(older).
|
|
*/
|
|
public function test_returns_adjacent_posts_when_available(): void
|
|
{
|
|
$olderId = $this->insertPost(['title' => '이전글', 'created_at' => now()->subMinutes(30)]);
|
|
$currentId = $this->insertPost(['title' => '현재글', 'created_at' => now()->subMinutes(20)]);
|
|
$newerId = $this->insertPost(['title' => '최신글', 'created_at' => now()->subMinutes(10)]);
|
|
|
|
$response = $this->getJson("/api/modules/sirsoft-board/boards/{$this->boardSlug}/posts/{$currentId}/navigation");
|
|
|
|
$response->assertStatus(200);
|
|
$response->assertJson([
|
|
'data' => [
|
|
'prev' => ['id' => $newerId], // DESC 에서 prev = created_at 더 나중
|
|
'next' => ['id' => $olderId], // DESC 에서 next = created_at 더 이전
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 비밀글(secret_mode=always)에서도 navigation 은 is_secret 필터를 적용하지 않고
|
|
* 목록과 동일 범위로 prev/next 를 실제로 반환해야 합니다.
|
|
*
|
|
* getAdjacentPosts 쿼리에 is_secret 필터가 없음을 실증적으로 검증합니다.
|
|
*
|
|
* @scenario post_kind=original
|
|
*
|
|
* @effects navigation_secret_included
|
|
*/
|
|
public function test_secret_always_board_returns_real_prev_next(): void
|
|
{
|
|
DB::table('boards')->where('id', $this->board->id)->update([
|
|
'secret_mode' => SecretMode::Always->value,
|
|
]);
|
|
|
|
$olderId = $this->insertPost([
|
|
'is_secret' => true,
|
|
'title' => '비밀 이전글',
|
|
'created_at' => now()->subMinutes(30),
|
|
]);
|
|
$currentId = $this->insertPost([
|
|
'is_secret' => true,
|
|
'title' => '비밀 현재글',
|
|
'created_at' => now()->subMinutes(20),
|
|
]);
|
|
$newerId = $this->insertPost([
|
|
'is_secret' => true,
|
|
'title' => '비밀 최신글',
|
|
'created_at' => now()->subMinutes(10),
|
|
]);
|
|
|
|
$response = $this->getJson("/api/modules/sirsoft-board/boards/{$this->boardSlug}/posts/{$currentId}/navigation");
|
|
|
|
$response->assertStatus(200);
|
|
$response->assertJson([
|
|
'data' => [
|
|
'prev' => ['id' => $newerId], // DESC
|
|
'next' => ['id' => $olderId],
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 핵심 회귀 방지: Controller 가 PostService::getPost 를 호출하지 않음을 실증합니다.
|
|
*
|
|
* beta.2 의 500 경로는 getPost 내부의 PermissionHelper::checkScopeAccess 가 false 를
|
|
* 반환하면서 AccessDeniedHttpException 을 throw 할 때 발생했습니다. 수정 코드가
|
|
* getPost 호출을 실제로 제거했는지 spy 로 검증합니다.
|
|
*/
|
|
public function test_controller_does_not_invoke_getpost(): void
|
|
{
|
|
$postId = $this->insertPost(['title' => 'getPost 미호출 검증']);
|
|
|
|
$postService = $this->spy(PostService::class);
|
|
|
|
$response = $this->getJson("/api/modules/sirsoft-board/boards/{$this->boardSlug}/posts/{$postId}/navigation");
|
|
|
|
$this->assertNotSame(500, $response->status());
|
|
$postService->shouldNotHaveReceived('getPost');
|
|
}
|
|
|
|
/**
|
|
* 핵심 회귀 방지: getAdjacentPosts 가 예외를 던져도 Controller 는 500 이 아닌
|
|
* 200 + 빈 값 을 반환하고 Log::warning 을 한 번 호출해야 합니다.
|
|
*/
|
|
public function test_controller_degrades_gracefully_when_getadjacent_throws(): void
|
|
{
|
|
$postId = $this->insertPost(['title' => '예외 유도']);
|
|
|
|
Log::spy();
|
|
|
|
$postService = $this->mock(PostService::class);
|
|
$postService->shouldReceive('isPostNotice')->once()->andReturn(false);
|
|
// 원글(parent_id null) 메타 → 답글 가드 통과 후 getAdjacentPosts 진입
|
|
$postService->shouldReceive('getPostNavigationMeta')->once()->andReturn(['category' => null, 'parent_id' => null]);
|
|
$postService->shouldReceive('getAdjacentPosts')->once()->andThrow(
|
|
new \RuntimeException('boom')
|
|
);
|
|
|
|
$response = $this->getJson("/api/modules/sirsoft-board/boards/{$this->boardSlug}/posts/{$postId}/navigation");
|
|
|
|
$response->assertStatus(200);
|
|
$response->assertJson([
|
|
'data' => ['prev' => null, 'next' => null],
|
|
]);
|
|
|
|
Log::shouldHaveReceived('warning')
|
|
->once()
|
|
->withArgs(function ($message, $context) use ($postId) {
|
|
return str_contains($message, 'Post navigation')
|
|
&& ($context['post_id'] ?? null) === $postId
|
|
&& ($context['slug'] ?? null) === $this->boardSlug;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 정상 경로(예외 없음) 에서는 Log::warning 이 호출되지 않아야 합니다.
|
|
*/
|
|
public function test_controller_does_not_log_warning_on_success(): void
|
|
{
|
|
$this->insertPost(['title' => '이전', 'created_at' => now()->subMinutes(20)]);
|
|
$currentId = $this->insertPost(['title' => '현재', 'created_at' => now()->subMinutes(15)]);
|
|
$this->insertPost(['title' => '최신', 'created_at' => now()->subMinutes(10)]);
|
|
|
|
Log::spy();
|
|
|
|
$response = $this->getJson("/api/modules/sirsoft-board/boards/{$this->boardSlug}/posts/{$currentId}/navigation");
|
|
|
|
$response->assertStatus(200);
|
|
Log::shouldNotHaveReceived('warning');
|
|
}
|
|
|
|
/**
|
|
* [A-1] 존재하지 않는 게시판은 404 또는 permission 단계의 401 을 반환해야 합니다 (500 금지).
|
|
*/
|
|
public function test_returns_error_for_nonexistent_board(): void
|
|
{
|
|
$response = $this->getJson('/api/modules/sirsoft-board/boards/non-existent-board/posts/1/navigation');
|
|
|
|
$this->assertNotSame(500, $response->status(), '존재하지 않는 게시판에서 500 이 아니어야 합니다.');
|
|
$this->assertContains($response->status(), [401, 403, 404], 'permission 또는 board-not-found 경로로 처리되어야 합니다.');
|
|
}
|
|
|
|
/**
|
|
* [A-2] 비활성 게시판은 404 또는 permission 단계의 401 을 반환해야 합니다 (500 금지).
|
|
*/
|
|
public function test_returns_error_for_inactive_board(): void
|
|
{
|
|
Board::factory()->create([
|
|
'slug' => 'nav-test-inactive',
|
|
'is_active' => false,
|
|
'order_by' => 'created_at',
|
|
'order_direction' => 'DESC',
|
|
'secret_mode' => SecretMode::Disabled->value,
|
|
]);
|
|
|
|
$response = $this->getJson('/api/modules/sirsoft-board/boards/nav-test-inactive/posts/1/navigation');
|
|
|
|
$this->assertNotSame(500, $response->status(), '비활성 게시판에서 500 이 아니어야 합니다.');
|
|
$this->assertContains($response->status(), [401, 403, 404]);
|
|
}
|
|
|
|
/**
|
|
* [B-1] order_direction=ASC 에서 prev 는 created_at 더 이전 글, next 는 더 나중 글이어야 합니다.
|
|
*/
|
|
public function test_asc_order_flips_prev_next_direction(): void
|
|
{
|
|
DB::table('boards')->where('id', $this->board->id)->update(['order_direction' => 'ASC']);
|
|
|
|
$olderId = $this->insertPost(['title' => '이전글', 'created_at' => now()->subMinutes(30)]);
|
|
$currentId = $this->insertPost(['title' => '현재글', 'created_at' => now()->subMinutes(20)]);
|
|
$newerId = $this->insertPost(['title' => '최신글', 'created_at' => now()->subMinutes(10)]);
|
|
|
|
$response = $this->getJson("/api/modules/sirsoft-board/boards/{$this->boardSlug}/posts/{$currentId}/navigation");
|
|
|
|
$response->assertStatus(200);
|
|
$response->assertJson([
|
|
'data' => [
|
|
'prev' => ['id' => $olderId],
|
|
'next' => ['id' => $newerId],
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* [D] 일반글 사이에 공지글이 끼어 있어도 navigation 은 공지를 건너뛰어야 합니다.
|
|
*/
|
|
public function test_notice_posts_excluded_from_navigation(): void
|
|
{
|
|
$olderId = $this->insertPost(['title' => '이전글', 'created_at' => now()->subMinutes(30)]);
|
|
$this->insertPost(['title' => '공지', 'is_notice' => true, 'created_at' => now()->subMinutes(25)]);
|
|
$currentId = $this->insertPost(['title' => '현재글', 'created_at' => now()->subMinutes(20)]);
|
|
$this->insertPost(['title' => '공지2', 'is_notice' => true, 'created_at' => now()->subMinutes(15)]);
|
|
$newerId = $this->insertPost(['title' => '최신글', 'created_at' => now()->subMinutes(10)]);
|
|
|
|
$response = $this->getJson("/api/modules/sirsoft-board/boards/{$this->boardSlug}/posts/{$currentId}/navigation");
|
|
|
|
$response->assertStatus(200);
|
|
$response->assertJson([
|
|
'data' => [
|
|
'prev' => ['id' => $newerId], // created_at DESC
|
|
'next' => ['id' => $olderId],
|
|
],
|
|
]);
|
|
$response->assertJsonMissingPath('data.prev.is_notice');
|
|
}
|
|
|
|
/**
|
|
* [G] published 가 아닌(블라인드 등) 글은 navigation 에 포함되면 안 됩니다.
|
|
*
|
|
* @scenario post_kind=original
|
|
*
|
|
* @effects navigation_blinded_excluded
|
|
*/
|
|
public function test_non_published_posts_excluded_from_navigation(): void
|
|
{
|
|
$olderId = $this->insertPost(['title' => '이전글', 'created_at' => now()->subMinutes(30)]);
|
|
$this->insertPost([
|
|
'title' => '블라인드글',
|
|
'status' => PostStatus::Blinded->value,
|
|
'created_at' => now()->subMinutes(25),
|
|
]);
|
|
$currentId = $this->insertPost(['title' => '현재글', 'created_at' => now()->subMinutes(20)]);
|
|
$this->insertPost([
|
|
'title' => '삭제상태글',
|
|
'status' => PostStatus::Deleted->value,
|
|
'created_at' => now()->subMinutes(15),
|
|
]);
|
|
$newerId = $this->insertPost(['title' => '최신글', 'created_at' => now()->subMinutes(10)]);
|
|
|
|
$response = $this->getJson("/api/modules/sirsoft-board/boards/{$this->boardSlug}/posts/{$currentId}/navigation");
|
|
|
|
$response->assertStatus(200);
|
|
$response->assertJson([
|
|
'data' => [
|
|
'prev' => ['id' => $newerId],
|
|
'next' => ['id' => $olderId],
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* [J-1] 가장 오래된 글(DESC 정렬에서 next 방향 끝)은 next 가 null 이어야 합니다.
|
|
*
|
|
* @scenario post_kind=original
|
|
*
|
|
* @effects navigation_boundary_null
|
|
*/
|
|
public function test_oldest_post_has_no_next(): void
|
|
{
|
|
$oldestId = $this->insertPost(['title' => '가장 오래된 글', 'created_at' => now()->subMinutes(30)]);
|
|
$this->insertPost(['title' => '중간글', 'created_at' => now()->subMinutes(20)]);
|
|
$this->insertPost(['title' => '최신글', 'created_at' => now()->subMinutes(10)]);
|
|
|
|
$response = $this->getJson("/api/modules/sirsoft-board/boards/{$this->boardSlug}/posts/{$oldestId}/navigation");
|
|
|
|
$response->assertStatus(200);
|
|
$data = $response->json('data');
|
|
$this->assertNotNull($data['prev'], 'DESC 정렬 첫글 기준 prev 는 더 이전(older) 이 없으므로 next 가 null 이어야 합니다.');
|
|
$this->assertNull($data['next'], 'DESC 정렬에서 가장 오래된 글의 next 는 null 이어야 합니다.');
|
|
}
|
|
|
|
/**
|
|
* [J-2] 가장 최신 글(DESC 정렬에서 prev 방향 끝)은 prev 가 null 이어야 합니다.
|
|
*/
|
|
public function test_newest_post_has_no_prev(): void
|
|
{
|
|
$this->insertPost(['title' => '이전글', 'created_at' => now()->subMinutes(30)]);
|
|
$this->insertPost(['title' => '중간글', 'created_at' => now()->subMinutes(20)]);
|
|
$newestId = $this->insertPost(['title' => '최신글', 'created_at' => now()->subMinutes(10)]);
|
|
|
|
$response = $this->getJson("/api/modules/sirsoft-board/boards/{$this->boardSlug}/posts/{$newestId}/navigation");
|
|
|
|
$response->assertStatus(200);
|
|
$data = $response->json('data');
|
|
$this->assertNull($data['prev'], 'DESC 정렬에서 가장 최신 글의 prev 는 null 이어야 합니다.');
|
|
$this->assertNotNull($data['next']);
|
|
}
|
|
|
|
/**
|
|
* [47-1] 카테고리가 섞인 게시판에서 navigation 은 현재 글과 동일 카테고리 글만
|
|
* 이전/다음으로 반환해야 합니다. (다른 카테고리 글은 건너뜀)
|
|
*
|
|
* @scenario post_kind=original
|
|
*
|
|
* @effects navigation_limited_to_same_category
|
|
*/
|
|
public function test_navigation_applies_current_post_category(): void
|
|
{
|
|
// created_at DESC 정렬. 카테고리를 번갈아 배치하여 '동일 카테고리만' 을 검증.
|
|
$freeOlder = $this->insertPost(['title' => '자유-이전', 'category' => '자유', 'created_at' => now()->subMinutes(50)]);
|
|
$this->insertPost(['title' => '질문-사이1', 'category' => '질문', 'created_at' => now()->subMinutes(40)]);
|
|
$current = $this->insertPost(['title' => '자유-현재', 'category' => '자유', 'created_at' => now()->subMinutes(30)]);
|
|
$this->insertPost(['title' => '질문-사이2', 'category' => '질문', 'created_at' => now()->subMinutes(20)]);
|
|
$freeNewer = $this->insertPost(['title' => '자유-최신', 'category' => '자유', 'created_at' => now()->subMinutes(10)]);
|
|
|
|
$response = $this->getJson("/api/modules/sirsoft-board/boards/{$this->boardSlug}/posts/{$current}/navigation");
|
|
|
|
$response->assertStatus(200);
|
|
$response->assertJson([
|
|
'data' => [
|
|
'prev' => ['id' => $freeNewer], // DESC: 더 최신 자유글
|
|
'next' => ['id' => $freeOlder], // DESC: 더 이전 자유글
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* [47-1] 미분류(category=null) 글은 카테고리 필터 없이 전체 원글을 순회해야 합니다.
|
|
*
|
|
* @scenario post_kind=original
|
|
*
|
|
* @effects navigation_uncategorized_traverses_all
|
|
*/
|
|
public function test_navigation_for_uncategorized_post_traverses_all(): void
|
|
{
|
|
$older = $this->insertPost(['title' => '이전(질문)', 'category' => '질문', 'created_at' => now()->subMinutes(30)]);
|
|
$current = $this->insertPost(['title' => '현재(미분류)', 'category' => null, 'created_at' => now()->subMinutes(20)]);
|
|
$newer = $this->insertPost(['title' => '최신(자유)', 'category' => '자유', 'created_at' => now()->subMinutes(10)]);
|
|
|
|
$response = $this->getJson("/api/modules/sirsoft-board/boards/{$this->boardSlug}/posts/{$current}/navigation");
|
|
|
|
$response->assertStatus(200);
|
|
$response->assertJson([
|
|
'data' => [
|
|
'prev' => ['id' => $newer],
|
|
'next' => ['id' => $older],
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* [47-4] 답글(parent_id != null) 상세의 navigation 은 prev/next 모두 null 이어야 합니다.
|
|
*
|
|
* 답글은 원글 후보 쿼리(whereNull parent_id)에서 제외되므로, 답글 상세에서 이전/다음
|
|
* 버튼이 뜨면 자기 스레드와 무관한 원글로 이동하는 깨진 동작이 발생합니다.
|
|
*
|
|
* @scenario post_kind=reply
|
|
*
|
|
* @effects navigation_null_for_reply
|
|
*/
|
|
public function test_navigation_returns_null_for_reply(): void
|
|
{
|
|
// 원글 3개 (정상 순회 가능한 상태)
|
|
$this->insertPost(['title' => '원글1', 'created_at' => now()->subMinutes(40)]);
|
|
$parentId = $this->insertPost(['title' => '원글2', 'created_at' => now()->subMinutes(30)]);
|
|
$this->insertPost(['title' => '원글3', 'created_at' => now()->subMinutes(20)]);
|
|
|
|
// 원글2 에 대한 답글
|
|
$replyId = $this->insertPost([
|
|
'title' => '답글',
|
|
'parent_id' => $parentId,
|
|
'created_at' => now()->subMinutes(10),
|
|
]);
|
|
|
|
$response = $this->getJson("/api/modules/sirsoft-board/boards/{$this->boardSlug}/posts/{$replyId}/navigation");
|
|
|
|
$response->assertStatus(200);
|
|
$response->assertJson([
|
|
'data' => ['prev' => null, 'next' => null],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* board_posts 레코드를 직접 INSERT 합니다.
|
|
*
|
|
* @param array $overrides 오버라이드할 속성
|
|
* @return int 생성된 post id
|
|
*/
|
|
private function insertPost(array $overrides = []): int
|
|
{
|
|
$defaults = [
|
|
'board_id' => $this->board->id,
|
|
'title' => '테스트글',
|
|
'content' => '본문',
|
|
'content_mode' => 'text',
|
|
'author_name' => '작성자',
|
|
'ip_address' => '127.0.0.1',
|
|
'is_notice' => false,
|
|
'is_secret' => false,
|
|
'status' => PostStatus::Published->value,
|
|
'view_count' => 0,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
];
|
|
|
|
return DB::table('board_posts')->insertGetId(array_merge($defaults, $overrides));
|
|
}
|
|
}
|