feat: 시스템 게시판 API 추가 (/api/v1/system-boards)

- SystemBoardController: 시스템 게시판 목록/상세/필드 조회
- SystemPostController: 시스템 게시글 CRUD + 댓글 CRUD
- BoardService: getSystemBoardByCode(), getTenantBoardByCode() 추가
- PostService: 시스템/테넌트 게시판 전용 메서드 추가
- routes/api.php: /system-boards/* 엔드포인트 12개 추가
- SystemBoardApi.php: Swagger 문서

시스템 게시판 (is_system=true, tenant_id=null)과
테넌트 게시판 (is_system=false, tenant_id={current})의
board_code 중복 가능성으로 인해 별도 엔드포인트로 분리

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-28 00:53:26 +09:00
parent 472cf53289
commit ab77bab510
6 changed files with 846 additions and 18 deletions

View File

@@ -74,6 +74,28 @@ public function getBoardByCode(string $code): ?Board
->first();
}
/**
* 시스템 게시판 코드로 조회
*/
public function getSystemBoardByCode(string $code): ?Board
{
return Board::systemOnly()
->where('board_code', $code)
->where('is_active', true)
->first();
}
/**
* 테넌트 게시판 코드로 조회
*/
public function getTenantBoardByCode(string $code): ?Board
{
return Board::tenantOnly($this->tenantId())
->where('board_code', $code)
->where('is_active', true)
->first();
}
/**
* 시스템 게시판 ID로 조회 (mng용)
*/

View File

@@ -18,11 +18,15 @@ class PostService extends Service
/**
* 게시글 목록 조회 (페이징)
*
* @param bool $isSystemBoard 시스템 게시판 여부 (시스템 게시판은 tenant_id 필터 제외)
*/
public function getPostsByBoard(int $boardId, array $filters = [], int $perPage = 15): LengthAwarePaginator
public function getPostsByBoard(int $boardId, array $filters = [], int $perPage = 15, bool $isSystemBoard = false): LengthAwarePaginator
{
return Post::where('board_id', $boardId)
->where('tenant_id', $this->tenantId())
// 시스템 게시판: tenant_id 필터 없음 (모든 게시글 조회)
// 일반 게시판: 현재 테넌트 게시글만 조회
->when(! $isSystemBoard, fn ($q) => $q->where('tenant_id', $this->tenantId()))
->when(isset($filters['search']), function ($q) use ($filters) {
$q->where(function ($query) use ($filters) {
$query->where('title', 'like', "%{$filters['search']}%")
@@ -45,17 +49,58 @@ public function getPostsByBoardCode(string $boardCode, array $filters = [], int
->where('board_code', $boardCode)
->firstOrFail();
return $this->getPostsByBoard($board->id, $filters, $perPage);
return $this->getPostsByBoard($board->id, $filters, $perPage, $board->is_system);
}
/**
* 시스템 게시판 게시글 목록 조회 (게시판 코드 기반)
*/
public function getPostsBySystemBoardCode(string $boardCode, array $filters = [], int $perPage = 15): LengthAwarePaginator
{
$board = Board::systemOnly()
->where('board_code', $boardCode)
->where('is_active', true)
->firstOrFail();
return $this->getPostsByBoard($board->id, $filters, $perPage, true);
}
/**
* 테넌트 게시판 게시글 목록 조회 (게시판 코드 기반)
*/
public function getPostsByTenantBoardCode(string $boardCode, array $filters = [], int $perPage = 15): LengthAwarePaginator
{
$board = Board::tenantOnly($this->tenantId())
->where('board_code', $boardCode)
->where('is_active', true)
->firstOrFail();
return $this->getPostsByBoard($board->id, $filters, $perPage, false);
}
/**
* 게시글 단건 조회
*
* @param bool|null $isSystemBoard 시스템 게시판 여부 (null이면 자동 감지)
*/
public function getPost(int $postId): ?Post
public function getPost(int $postId, ?bool $isSystemBoard = null): ?Post
{
return Post::with(['files', 'comments.replies'])
->where('tenant_id', $this->tenantId())
->find($postId);
$query = Post::with(['files', 'comments.replies', 'board']);
if ($isSystemBoard === null) {
// 게시글에서 board 정보로 시스템 게시판 여부 판단
// 시스템 게시판이면 tenant_id 필터 없음
$query->where(function ($q) {
$q->where('tenant_id', $this->tenantId())
->orWhereHas('board', fn ($b) => $b->where('is_system', true));
});
} elseif ($isSystemBoard) {
// 시스템 게시판: tenant_id 필터 없음
} else {
$query->where('tenant_id', $this->tenantId());
}
return $query->find($postId);
}
/**
@@ -85,6 +130,51 @@ public function getPostByCodeAndId(string $boardCode, int $postId): ?Post
return null;
}
$query = Post::with(['files', 'comments.replies', 'board'])
->where('board_id', $board->id);
// 시스템 게시판: tenant_id 필터 없음
// 일반 게시판: 현재 테넌트 게시글만 조회
if (! $board->is_system) {
$query->where('tenant_id', $this->tenantId());
}
return $query->find($postId);
}
/**
* 시스템 게시판 코드와 게시글 ID로 조회
*/
public function getPostBySystemBoardCodeAndId(string $boardCode, int $postId): ?Post
{
$board = Board::systemOnly()
->where('board_code', $boardCode)
->where('is_active', true)
->first();
if (! $board) {
return null;
}
return Post::with(['files', 'comments.replies', 'board'])
->where('board_id', $board->id)
->find($postId);
}
/**
* 테넌트 게시판 코드와 게시글 ID로 조회
*/
public function getPostByTenantBoardCodeAndId(string $boardCode, int $postId): ?Post
{
$board = Board::tenantOnly($this->tenantId())
->where('board_code', $boardCode)
->where('is_active', true)
->first();
if (! $board) {
return null;
}
return Post::with(['files', 'comments.replies', 'board'])
->where('board_id', $board->id)
->where('tenant_id', $this->tenantId())
@@ -97,11 +187,13 @@ public function getPostByCodeAndId(string $boardCode, int $postId): ?Post
/**
* 게시글 생성
*
* @param bool $isSystemBoard 시스템 게시판 여부 (시스템 게시판은 tenant_id = null)
*/
public function createPost(int $boardId, array $data): Post
public function createPost(int $boardId, array $data, bool $isSystemBoard = false): Post
{
$data['board_id'] = $boardId;
$data['tenant_id'] = $this->tenantId();
$data['tenant_id'] = $isSystemBoard ? null : $this->tenantId();
$data['user_id'] = $this->apiUserId();
$data['ip_address'] = request()->ip();
$data['status'] = $data['status'] ?? 'published';
@@ -126,7 +218,33 @@ public function createPostByBoardCode(string $boardCode, array $data): Post
->where('board_code', $boardCode)
->firstOrFail();
return $this->createPost($board->id, $data);
return $this->createPost($board->id, $data, $board->is_system);
}
/**
* 시스템 게시판 게시글 생성 (게시판 코드 기반)
*/
public function createPostBySystemBoardCode(string $boardCode, array $data): Post
{
$board = Board::systemOnly()
->where('board_code', $boardCode)
->where('is_active', true)
->firstOrFail();
return $this->createPost($board->id, $data, true);
}
/**
* 테넌트 게시판 게시글 생성 (게시판 코드 기반)
*/
public function createPostByTenantBoardCode(string $boardCode, array $data): Post
{
$board = Board::tenantOnly($this->tenantId())
->where('board_code', $boardCode)
->where('is_active', true)
->firstOrFail();
return $this->createPost($board->id, $data, false);
}
// =========================================================================
@@ -135,11 +253,27 @@ public function createPostByBoardCode(string $boardCode, array $data): Post
/**
* 게시글 수정
*
* @param bool|null $isSystemBoard 시스템 게시판 여부 (null이면 자동 감지)
*/
public function updatePost(int $postId, array $data): ?Post
public function updatePost(int $postId, array $data, ?bool $isSystemBoard = null): ?Post
{
$post = Post::where('tenant_id', $this->tenantId())
->find($postId);
$query = Post::with('board');
if ($isSystemBoard === null) {
// 게시글에서 board 정보로 시스템 게시판 여부 판단
// 시스템 게시판이면 tenant_id 필터 없음
$query->where(function ($q) {
$q->where('tenant_id', $this->tenantId())
->orWhereHas('board', fn ($b) => $b->where('is_system', true));
});
} elseif ($isSystemBoard) {
// 시스템 게시판: tenant_id 필터 없음
} else {
$query->where('tenant_id', $this->tenantId());
}
$post = $query->find($postId);
if (! $post) {
return null;
@@ -168,6 +302,72 @@ public function updatePostByBoardCode(string $boardCode, int $postId, array $dat
return null;
}
$query = Post::where('board_id', $board->id);
// 시스템 게시판: tenant_id 필터 없음
// 일반 게시판: 현재 테넌트 게시글만
if (! $board->is_system) {
$query->where('tenant_id', $this->tenantId());
}
$post = $query->find($postId);
if (! $post) {
return null;
}
$post->update($data);
if (isset($data['custom_fields'])) {
$this->saveCustomFields($post->id, $data['custom_fields']);
}
return $post->fresh(['board', 'files']);
}
/**
* 시스템 게시판 게시글 수정 (게시판 코드 기반)
*/
public function updatePostBySystemBoardCode(string $boardCode, int $postId, array $data): ?Post
{
$board = Board::systemOnly()
->where('board_code', $boardCode)
->where('is_active', true)
->first();
if (! $board) {
return null;
}
$post = Post::where('board_id', $board->id)->find($postId);
if (! $post) {
return null;
}
$post->update($data);
if (isset($data['custom_fields'])) {
$this->saveCustomFields($post->id, $data['custom_fields']);
}
return $post->fresh(['board', 'files']);
}
/**
* 테넌트 게시판 게시글 수정 (게시판 코드 기반)
*/
public function updatePostByTenantBoardCode(string $boardCode, int $postId, array $data): ?Post
{
$board = Board::tenantOnly($this->tenantId())
->where('board_code', $boardCode)
->where('is_active', true)
->first();
if (! $board) {
return null;
}
$post = Post::where('board_id', $board->id)
->where('tenant_id', $this->tenantId())
->find($postId);
@@ -191,11 +391,27 @@ public function updatePostByBoardCode(string $boardCode, int $postId, array $dat
/**
* 게시글 삭제 (Soft Delete)
*
* @param bool|null $isSystemBoard 시스템 게시판 여부 (null이면 자동 감지)
*/
public function deletePost(int $postId): bool
public function deletePost(int $postId, ?bool $isSystemBoard = null): bool
{
$post = Post::where('tenant_id', $this->tenantId())
->find($postId);
$query = Post::with('board');
if ($isSystemBoard === null) {
// 게시글에서 board 정보로 시스템 게시판 여부 판단
// 시스템 게시판이면 tenant_id 필터 없음
$query->where(function ($q) {
$q->where('tenant_id', $this->tenantId())
->orWhereHas('board', fn ($b) => $b->where('is_system', true));
});
} elseif ($isSystemBoard) {
// 시스템 게시판: tenant_id 필터 없음
} else {
$query->where('tenant_id', $this->tenantId());
}
$post = $query->find($postId);
if (! $post) {
return false;
@@ -217,6 +433,60 @@ public function deletePostByBoardCode(string $boardCode, int $postId): bool
return false;
}
$query = Post::where('board_id', $board->id);
// 시스템 게시판: tenant_id 필터 없음
// 일반 게시판: 현재 테넌트 게시글만
if (! $board->is_system) {
$query->where('tenant_id', $this->tenantId());
}
$post = $query->find($postId);
if (! $post) {
return false;
}
return $post->delete();
}
/**
* 시스템 게시판 게시글 삭제 (게시판 코드 기반)
*/
public function deletePostBySystemBoardCode(string $boardCode, int $postId): bool
{
$board = Board::systemOnly()
->where('board_code', $boardCode)
->where('is_active', true)
->first();
if (! $board) {
return false;
}
$post = Post::where('board_id', $board->id)->find($postId);
if (! $post) {
return false;
}
return $post->delete();
}
/**
* 테넌트 게시판 게시글 삭제 (게시판 코드 기반)
*/
public function deletePostByTenantBoardCode(string $boardCode, int $postId): bool
{
$board = Board::tenantOnly($this->tenantId())
->where('board_code', $boardCode)
->where('is_active', true)
->first();
if (! $board) {
return false;
}
$post = Post::where('board_id', $board->id)
->where('tenant_id', $this->tenantId())
->find($postId);
@@ -331,12 +601,17 @@ public function getCustomFieldValues(int $postId): Collection
/**
* 나의 게시글 목록 조회
* 시스템 게시판(tenant_id = null) 및 테넌트 게시판의 게시글 모두 조회
*/
public function getMyPosts(array $filters = [], int $perPage = 15): LengthAwarePaginator
{
return Post::where('user_id', $this->apiUserId())
->where('tenant_id', $this->tenantId())
->with(['board:id,board_code,name'])
->where(function ($q) {
// 시스템 게시판 게시글 또는 현재 테넌트 게시글
$q->whereNull('tenant_id')
->orWhere('tenant_id', $this->tenantId());
})
->with(['board:id,board_code,name,is_system'])
->when(isset($filters['board_code']), function ($q) use ($filters) {
$q->whereHas('board', fn ($query) => $query->where('board_code', $filters['board_code']));
})