feat: [boards] 게시판 API 시스템 구현
- BoardController, PostController 추가 - Board, BoardSetting 모델 수정 - BoardService 추가 - FormRequest 클래스 추가 - Swagger 문서 추가 (BoardApi, PostApi) - 게시판 시스템 필드 마이그레이션 추가
This commit is contained in:
124
app/Http/Controllers/Api/V1/BoardController.php
Normal file
124
app/Http/Controllers/Api/V1/BoardController.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Boards\BoardStoreRequest;
|
||||
use App\Http\Requests\Boards\BoardUpdateRequest;
|
||||
use App\Services\Boards\BoardService;
|
||||
|
||||
/**
|
||||
* 게시판 API 컨트롤러 (테넌트용)
|
||||
*
|
||||
* 테넌트에서 접근 가능한 게시판:
|
||||
* - 시스템 게시판 (is_system=true) - 읽기만 가능
|
||||
* - 테넌트 게시판 (tenant_id=현재 테넌트) - CRUD 가능
|
||||
*/
|
||||
class BoardController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected BoardService $boardService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 접근 가능한 게시판 목록 조회
|
||||
* - 시스템 게시판 + 테넌트 게시판
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
$filters = request()->only(['board_type', 'search']);
|
||||
|
||||
return $this->boardService->getAccessibleBoards($filters);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 상세 조회 (코드 기반)
|
||||
*/
|
||||
public function show(string $code)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($code) {
|
||||
$board = $this->boardService->getBoardByCode($code);
|
||||
|
||||
if (! $board) {
|
||||
abort(404, __('error.board.not_found'));
|
||||
}
|
||||
|
||||
return $board->load('customFields');
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 게시판 생성
|
||||
*/
|
||||
public function store(BoardStoreRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->boardService->createTenantBoard($request->validated());
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 게시판 수정
|
||||
* - 시스템 게시판은 수정 불가
|
||||
*/
|
||||
public function update(BoardUpdateRequest $request, int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
$board = $this->boardService->updateTenantBoard($id, $request->validated());
|
||||
|
||||
if (! $board) {
|
||||
abort(404, __('error.board.not_found'));
|
||||
}
|
||||
|
||||
return $board;
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 게시판 삭제
|
||||
* - 시스템 게시판은 삭제 불가
|
||||
*/
|
||||
public function destroy(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
$deleted = $this->boardService->deleteTenantBoard($id);
|
||||
|
||||
if (! $deleted) {
|
||||
abort(404, __('error.board.not_found'));
|
||||
}
|
||||
|
||||
return ['deleted' => true];
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 게시판 목록만 조회
|
||||
*/
|
||||
public function tenantBoards()
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
$filters = request()->only(['board_type', 'search']);
|
||||
|
||||
return $this->boardService->getTenantBoards($filters);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 필드 목록 조회
|
||||
*/
|
||||
public function fields(string $code)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($code) {
|
||||
$board = $this->boardService->getBoardByCode($code);
|
||||
|
||||
if (! $board) {
|
||||
abort(404, __('error.board.not_found'));
|
||||
}
|
||||
|
||||
return $this->boardService->getBoardFields($board->id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
}
|
||||
152
app/Http/Controllers/Api/V1/PostController.php
Normal file
152
app/Http/Controllers/Api/V1/PostController.php
Normal file
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Boards\CommentStoreRequest;
|
||||
use App\Http\Requests\Boards\PostStoreRequest;
|
||||
use App\Http\Requests\Boards\PostUpdateRequest;
|
||||
use App\Services\Boards\PostService;
|
||||
|
||||
/**
|
||||
* 게시글 API 컨트롤러
|
||||
*
|
||||
* URL: /v1/boards/{code}/posts
|
||||
* - {code}: 게시판 코드 (board_code)
|
||||
*/
|
||||
class PostController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected PostService $postService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 게시글 목록 조회
|
||||
*/
|
||||
public function index(string $code)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($code) {
|
||||
$filters = request()->only(['search', 'is_notice', 'status']);
|
||||
$perPage = (int) request()->get('per_page', 15);
|
||||
|
||||
return $this->postService->getPostsByBoardCode($code, $filters, $perPage);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 상세 조회
|
||||
*/
|
||||
public function show(string $code, int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($code, $id) {
|
||||
$post = $this->postService->getPostByCodeAndId($code, $id);
|
||||
|
||||
if (! $post) {
|
||||
abort(404, __('error.post.not_found'));
|
||||
}
|
||||
|
||||
// 조회수 증가
|
||||
$post->increment('views');
|
||||
|
||||
// 커스텀 필드 값 추가
|
||||
$post->custom_field_values = $this->postService->getCustomFieldValues($id);
|
||||
|
||||
return $post;
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 작성
|
||||
*/
|
||||
public function store(PostStoreRequest $request, string $code)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $code) {
|
||||
return $this->postService->createPostByBoardCode($code, $request->validated());
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 수정
|
||||
*/
|
||||
public function update(PostUpdateRequest $request, string $code, int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $code, $id) {
|
||||
$post = $this->postService->updatePostByBoardCode($code, $id, $request->validated());
|
||||
|
||||
if (! $post) {
|
||||
abort(404, __('error.post.not_found'));
|
||||
}
|
||||
|
||||
return $post;
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 삭제
|
||||
*/
|
||||
public function destroy(string $code, int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($code, $id) {
|
||||
$deleted = $this->postService->deletePostByBoardCode($code, $id);
|
||||
|
||||
if (! $deleted) {
|
||||
abort(404, __('error.post.not_found'));
|
||||
}
|
||||
|
||||
return ['deleted' => true];
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 댓글 목록 조회
|
||||
*/
|
||||
public function comments(string $code, int $postId)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($postId) {
|
||||
return $this->postService->getComments($postId);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 댓글 작성
|
||||
*/
|
||||
public function storeComment(CommentStoreRequest $request, string $code, int $postId)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $postId) {
|
||||
return $this->postService->createComment($postId, $request->validated());
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 댓글 수정
|
||||
*/
|
||||
public function updateComment(CommentStoreRequest $request, string $code, int $postId, int $commentId)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $commentId) {
|
||||
$comment = $this->postService->updateComment($commentId, $request->validated());
|
||||
|
||||
if (! $comment) {
|
||||
abort(404, __('error.comment.not_found'));
|
||||
}
|
||||
|
||||
return $comment;
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 댓글 삭제
|
||||
*/
|
||||
public function destroyComment(string $code, int $postId, int $commentId)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($commentId) {
|
||||
$deleted = $this->postService->deleteComment($commentId);
|
||||
|
||||
if (! $deleted) {
|
||||
abort(404, __('error.comment.not_found'));
|
||||
}
|
||||
|
||||
return ['deleted' => true];
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
}
|
||||
42
app/Http/Requests/Boards/BoardStoreRequest.php
Normal file
42
app/Http/Requests/Boards/BoardStoreRequest.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Boards;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class BoardStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'board_code' => 'required|string|max:50|unique:boards,board_code',
|
||||
'board_type' => 'nullable|string|max:50',
|
||||
'name' => 'required|string|max:100',
|
||||
'description' => 'nullable|string|max:500',
|
||||
'editor_type' => 'sometimes|string|in:wysiwyg,markdown,text',
|
||||
'allow_files' => 'sometimes|boolean',
|
||||
'max_file_count' => 'sometimes|integer|min:0|max:20',
|
||||
'max_file_size' => 'sometimes|integer|min:0|max:102400',
|
||||
'extra_settings' => 'nullable|array',
|
||||
'extra_settings.permissions' => 'nullable|array',
|
||||
'extra_settings.permissions.read' => 'nullable|array',
|
||||
'extra_settings.permissions.write' => 'nullable|array',
|
||||
'extra_settings.permissions.manage' => 'nullable|array',
|
||||
'is_active' => 'sometimes|boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'board_code.required' => __('validation.required', ['attribute' => '게시판 코드']),
|
||||
'board_code.unique' => __('validation.unique', ['attribute' => '게시판 코드']),
|
||||
'name.required' => __('validation.required', ['attribute' => '게시판명']),
|
||||
];
|
||||
}
|
||||
}
|
||||
48
app/Http/Requests/Boards/BoardUpdateRequest.php
Normal file
48
app/Http/Requests/Boards/BoardUpdateRequest.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Boards;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class BoardUpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$boardId = $this->route('id');
|
||||
|
||||
return [
|
||||
'board_code' => [
|
||||
'sometimes',
|
||||
'string',
|
||||
'max:50',
|
||||
Rule::unique('boards', 'board_code')->ignore($boardId),
|
||||
],
|
||||
'board_type' => 'nullable|string|max:50',
|
||||
'name' => 'sometimes|string|max:100',
|
||||
'description' => 'nullable|string|max:500',
|
||||
'editor_type' => 'sometimes|string|in:wysiwyg,markdown,text',
|
||||
'allow_files' => 'sometimes|boolean',
|
||||
'max_file_count' => 'sometimes|integer|min:0|max:20',
|
||||
'max_file_size' => 'sometimes|integer|min:0|max:102400',
|
||||
'extra_settings' => 'nullable|array',
|
||||
'extra_settings.permissions' => 'nullable|array',
|
||||
'extra_settings.permissions.read' => 'nullable|array',
|
||||
'extra_settings.permissions.write' => 'nullable|array',
|
||||
'extra_settings.permissions.manage' => 'nullable|array',
|
||||
'is_active' => 'sometimes|boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'board_code.unique' => __('validation.unique', ['attribute' => '게시판 코드']),
|
||||
];
|
||||
}
|
||||
}
|
||||
29
app/Http/Requests/Boards/CommentStoreRequest.php
Normal file
29
app/Http/Requests/Boards/CommentStoreRequest.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Boards;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CommentStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'content' => 'required|string|max:2000',
|
||||
'parent_id' => 'nullable|integer|exists:board_comments,id',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'content.required' => __('validation.required', ['attribute' => '댓글 내용']),
|
||||
'content.max' => __('validation.max.string', ['attribute' => '댓글 내용', 'max' => 2000]),
|
||||
];
|
||||
}
|
||||
}
|
||||
35
app/Http/Requests/Boards/PostStoreRequest.php
Normal file
35
app/Http/Requests/Boards/PostStoreRequest.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Boards;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class PostStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => 'required|string|max:200',
|
||||
'content' => 'required|string',
|
||||
'editor_type' => 'sometimes|string|in:wysiwyg,markdown,text',
|
||||
'is_notice' => 'sometimes|boolean',
|
||||
'is_secret' => 'sometimes|boolean',
|
||||
'status' => 'sometimes|string|in:draft,published,hidden',
|
||||
'custom_fields' => 'nullable|array',
|
||||
'custom_fields.*' => 'nullable',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'title.required' => __('validation.required', ['attribute' => '제목']),
|
||||
'content.required' => __('validation.required', ['attribute' => '내용']),
|
||||
];
|
||||
}
|
||||
}
|
||||
27
app/Http/Requests/Boards/PostUpdateRequest.php
Normal file
27
app/Http/Requests/Boards/PostUpdateRequest.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Boards;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class PostUpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => 'sometimes|string|max:200',
|
||||
'content' => 'sometimes|string',
|
||||
'editor_type' => 'sometimes|string|in:wysiwyg,markdown,text',
|
||||
'is_notice' => 'sometimes|boolean',
|
||||
'is_secret' => 'sometimes|boolean',
|
||||
'status' => 'sometimes|string|in:draft,published,hidden',
|
||||
'custom_fields' => 'nullable|array',
|
||||
'custom_fields.*' => 'nullable',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,27 +2,214 @@
|
||||
|
||||
namespace App\Models\Boards;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* 게시판 모델
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $tenant_id
|
||||
* @property bool $is_system 시스템 게시판 여부 (true=전체 테넌트 공개)
|
||||
* @property string|null $board_type 게시판 유형 (notice, qna, faq, free 등)
|
||||
* @property string $board_code 게시판 코드
|
||||
* @property string $name 게시판명
|
||||
* @property string|null $description 설명
|
||||
* @property string $editor_type 에디터 타입
|
||||
* @property bool $allow_files 파일 첨부 허용
|
||||
* @property int $max_file_count 최대 파일 수
|
||||
* @property int $max_file_size 최대 파일 크기 (KB)
|
||||
* @property array|null $extra_settings 추가 설정 (권한, 옵션 등)
|
||||
* @property bool $is_active 활성 여부
|
||||
*
|
||||
* @mixin IdeHelperBoard
|
||||
*/
|
||||
class Board extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'boards';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id', 'board_code', 'name', 'description', 'editor_type',
|
||||
'allow_files', 'max_file_count', 'max_file_size', 'extra_settings', 'is_active',
|
||||
'tenant_id',
|
||||
'is_system',
|
||||
'board_type',
|
||||
'board_code',
|
||||
'name',
|
||||
'description',
|
||||
'editor_type',
|
||||
'allow_files',
|
||||
'max_file_count',
|
||||
'max_file_size',
|
||||
'extra_settings',
|
||||
'is_active',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_system' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
'allow_files' => 'boolean',
|
||||
'extra_settings' => 'array',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'is_system' => false,
|
||||
'is_active' => true,
|
||||
'allow_files' => true,
|
||||
'max_file_count' => 5,
|
||||
'max_file_size' => 20480,
|
||||
'editor_type' => 'wysiwyg',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// Scopes
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 현재 테넌트에서 접근 가능한 게시판
|
||||
* - 시스템 게시판 (is_system = true)
|
||||
* - 해당 테넌트 게시판 (tenant_id = 현재 테넌트)
|
||||
*/
|
||||
public function scopeAccessible(Builder $query, int $tenantId): Builder
|
||||
{
|
||||
return $query->where(function ($q) use ($tenantId) {
|
||||
$q->where('is_system', true)
|
||||
->orWhere('tenant_id', $tenantId);
|
||||
})->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 시스템 게시판만 (mng용)
|
||||
*/
|
||||
public function scopeSystemOnly(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_system', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 게시판만
|
||||
*/
|
||||
public function scopeTenantOnly(Builder $query, int $tenantId): Builder
|
||||
{
|
||||
return $query->where('is_system', false)
|
||||
->where('tenant_id', $tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 유형으로 필터링
|
||||
*/
|
||||
public function scopeOfType(Builder $query, string $type): Builder
|
||||
{
|
||||
return $query->where('board_type', $type);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Relationships
|
||||
// =========================================================================
|
||||
|
||||
public function customFields()
|
||||
{
|
||||
return $this->hasMany(BoardSetting::class, 'board_id');
|
||||
return $this->hasMany(BoardSetting::class, 'board_id')
|
||||
->orderBy('sort_order');
|
||||
}
|
||||
|
||||
public function fields()
|
||||
{
|
||||
return $this->customFields();
|
||||
}
|
||||
|
||||
public function posts()
|
||||
{
|
||||
return $this->hasMany(Post::class, 'board_id');
|
||||
}
|
||||
|
||||
public function tenant()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Tenant::class);
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function updater()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helper Methods
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* extra_settings에서 특정 키 값 가져오기
|
||||
*/
|
||||
public function getSetting(string $key, $default = null)
|
||||
{
|
||||
return data_get($this->extra_settings, $key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* extra_settings에 값 설정하기
|
||||
*/
|
||||
public function setSetting(string $key, $value): self
|
||||
{
|
||||
$settings = $this->extra_settings ?? [];
|
||||
data_set($settings, $key, $value);
|
||||
$this->extra_settings = $settings;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 읽기 권한 확인
|
||||
*/
|
||||
public function canRead(User $user): bool
|
||||
{
|
||||
$roles = $this->getSetting('permissions.read', ['*']);
|
||||
|
||||
return in_array('*', $roles) || $user->hasAnyRole($roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* 쓰기 권한 확인
|
||||
*/
|
||||
public function canWrite(User $user): bool
|
||||
{
|
||||
$roles = $this->getSetting('permissions.write', ['*']);
|
||||
|
||||
return in_array('*', $roles) || $user->hasAnyRole($roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리 권한 확인
|
||||
*/
|
||||
public function canManage(User $user): bool
|
||||
{
|
||||
$roles = $this->getSetting('permissions.manage', ['admin']);
|
||||
|
||||
return $user->hasAnyRole($roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* 시스템 게시판 여부 확인
|
||||
*/
|
||||
public function isSystemBoard(): bool
|
||||
{
|
||||
return $this->is_system === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 게시판 여부 확인
|
||||
*/
|
||||
public function isTenantBoard(): bool
|
||||
{
|
||||
return $this->is_system === false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,17 @@
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* 게시판 필드 설정 (EAV 스키마 정의)
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $board_id
|
||||
* @property string $name 필드명
|
||||
* @property string $field_key 필드 키
|
||||
* @property string $field_type 필드 타입 (text, number, select, date 등)
|
||||
* @property array|null $field_meta 필드 메타 (옵션, 유효성 등)
|
||||
* @property bool $is_required 필수 여부
|
||||
* @property int $sort_order 정렬 순서
|
||||
*
|
||||
* @mixin IdeHelperBoardSetting
|
||||
*/
|
||||
class BoardSetting extends Model
|
||||
@@ -12,11 +23,38 @@ class BoardSetting extends Model
|
||||
protected $table = 'board_settings';
|
||||
|
||||
protected $fillable = [
|
||||
'board_id', 'name', 'field_key', 'field_type', 'field_meta', 'is_required', 'sort_order',
|
||||
'board_id',
|
||||
'name',
|
||||
'field_key',
|
||||
'field_type',
|
||||
'field_meta',
|
||||
'is_required',
|
||||
'sort_order',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'field_meta' => 'array',
|
||||
'is_required' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'is_required' => false,
|
||||
'sort_order' => 0,
|
||||
];
|
||||
|
||||
public function board()
|
||||
{
|
||||
return $this->belongsTo(Board::class, 'board_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* field_meta에서 특정 키 값 가져오기
|
||||
*/
|
||||
public function getMeta(string $key, $default = null)
|
||||
{
|
||||
return data_get($this->field_meta, $key, $default);
|
||||
}
|
||||
}
|
||||
|
||||
320
app/Services/Boards/BoardService.php
Normal file
320
app/Services/Boards/BoardService.php
Normal file
@@ -0,0 +1,320 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Boards;
|
||||
|
||||
use App\Models\Boards\Board;
|
||||
use App\Models\Boards\BoardSetting;
|
||||
use App\Services\Service;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
class BoardService extends Service
|
||||
{
|
||||
// =========================================================================
|
||||
// 조회 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 접근 가능한 게시판 목록 (시스템 + 테넌트)
|
||||
*/
|
||||
public function getAccessibleBoards(array $filters = []): Collection
|
||||
{
|
||||
return Board::accessible($this->tenantId())
|
||||
->when(isset($filters['board_type']), fn ($q) => $q->where('board_type', $filters['board_type']))
|
||||
->when(isset($filters['search']), fn ($q) => $q->where('name', 'like', "%{$filters['search']}%"))
|
||||
->orderBy('is_system', 'desc')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 시스템 게시판 목록 (mng용)
|
||||
*/
|
||||
public function getSystemBoards(array $filters = []): Collection
|
||||
{
|
||||
return Board::systemOnly()
|
||||
->when(isset($filters['board_type']), fn ($q) => $q->where('board_type', $filters['board_type']))
|
||||
->when(isset($filters['search']), fn ($q) => $q->where('name', 'like', "%{$filters['search']}%"))
|
||||
->orderBy('name')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 시스템 게시판 페이징 목록 (mng용)
|
||||
*/
|
||||
public function getSystemBoardsPaginated(array $filters = [], int $perPage = 15): LengthAwarePaginator
|
||||
{
|
||||
return Board::systemOnly()
|
||||
->when(isset($filters['board_type']), fn ($q) => $q->where('board_type', $filters['board_type']))
|
||||
->when(isset($filters['search']), fn ($q) => $q->where('name', 'like', "%{$filters['search']}%"))
|
||||
->when(isset($filters['is_active']), fn ($q) => $q->where('is_active', $filters['is_active']))
|
||||
->orderBy('name')
|
||||
->paginate($perPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 게시판만 가져오기
|
||||
*/
|
||||
public function getTenantBoards(array $filters = []): Collection
|
||||
{
|
||||
return Board::tenantOnly($this->tenantId())
|
||||
->when(isset($filters['board_type']), fn ($q) => $q->where('board_type', $filters['board_type']))
|
||||
->when(isset($filters['search']), fn ($q) => $q->where('name', 'like', "%{$filters['search']}%"))
|
||||
->orderBy('name')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 코드로 조회 (접근 가능한 게시판 중에서)
|
||||
*/
|
||||
public function getBoardByCode(string $code): ?Board
|
||||
{
|
||||
return Board::accessible($this->tenantId())
|
||||
->where('board_code', $code)
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* 시스템 게시판 ID로 조회 (mng용)
|
||||
*/
|
||||
public function getSystemBoardById(int $id): ?Board
|
||||
{
|
||||
return Board::systemOnly()
|
||||
->with('customFields')
|
||||
->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 ID로 조회 (상세)
|
||||
*/
|
||||
public function getBoardDetail(int $id): ?Board
|
||||
{
|
||||
return Board::with('customFields')
|
||||
->find($id);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 생성 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 시스템 게시판 생성 (mng용)
|
||||
*/
|
||||
public function createSystemBoard(array $data): Board
|
||||
{
|
||||
$data['is_system'] = true;
|
||||
$data['tenant_id'] = null;
|
||||
$data['created_by'] = $this->apiUserId();
|
||||
|
||||
return Board::create($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 게시판 생성 (sam용)
|
||||
*/
|
||||
public function createTenantBoard(array $data): Board
|
||||
{
|
||||
$data['is_system'] = false;
|
||||
$data['tenant_id'] = $this->tenantId();
|
||||
$data['created_by'] = $this->apiUserId();
|
||||
|
||||
return Board::create($data);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 수정 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 시스템 게시판 수정 (mng용)
|
||||
*/
|
||||
public function updateSystemBoard(int $id, array $data): ?Board
|
||||
{
|
||||
$board = Board::systemOnly()->find($id);
|
||||
|
||||
if (! $board) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data['updated_by'] = $this->apiUserId();
|
||||
$board->update($data);
|
||||
|
||||
return $board->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 게시판 수정 (sam용)
|
||||
*/
|
||||
public function updateTenantBoard(int $id, array $data): ?Board
|
||||
{
|
||||
$board = Board::tenantOnly($this->tenantId())->find($id);
|
||||
|
||||
if (! $board) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data['updated_by'] = $this->apiUserId();
|
||||
$board->update($data);
|
||||
|
||||
return $board->fresh();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 삭제 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 시스템 게시판 삭제 (mng용)
|
||||
*/
|
||||
public function deleteSystemBoard(int $id): bool
|
||||
{
|
||||
$board = Board::systemOnly()->find($id);
|
||||
|
||||
if (! $board) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$board->deleted_by = $this->apiUserId();
|
||||
$board->save();
|
||||
$board->delete();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 게시판 삭제 (sam용)
|
||||
*/
|
||||
public function deleteTenantBoard(int $id): bool
|
||||
{
|
||||
$board = Board::tenantOnly($this->tenantId())->find($id);
|
||||
|
||||
if (! $board) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$board->deleted_by = $this->apiUserId();
|
||||
$board->save();
|
||||
$board->delete();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 필드 관리 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 게시판 필드 목록 조회
|
||||
*/
|
||||
public function getBoardFields(int $boardId): Collection
|
||||
{
|
||||
return BoardSetting::where('board_id', $boardId)
|
||||
->orderBy('sort_order')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 필드 추가
|
||||
*/
|
||||
public function addBoardField(int $boardId, array $data): BoardSetting
|
||||
{
|
||||
$data['board_id'] = $boardId;
|
||||
$data['created_by'] = $this->apiUserId();
|
||||
|
||||
// 기본 정렬 순서 설정
|
||||
if (! isset($data['sort_order'])) {
|
||||
$maxOrder = BoardSetting::where('board_id', $boardId)->max('sort_order') ?? 0;
|
||||
$data['sort_order'] = $maxOrder + 1;
|
||||
}
|
||||
|
||||
return BoardSetting::create($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 필드 수정
|
||||
*/
|
||||
public function updateBoardField(int $fieldId, array $data): ?BoardSetting
|
||||
{
|
||||
$field = BoardSetting::find($fieldId);
|
||||
|
||||
if (! $field) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data['updated_by'] = $this->apiUserId();
|
||||
$field->update($data);
|
||||
|
||||
return $field->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 필드 삭제
|
||||
*/
|
||||
public function deleteBoardField(int $fieldId): bool
|
||||
{
|
||||
$field = BoardSetting::find($fieldId);
|
||||
|
||||
if (! $field) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $field->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 필드 순서 변경
|
||||
*/
|
||||
public function reorderBoardFields(int $boardId, array $fieldIds): bool
|
||||
{
|
||||
foreach ($fieldIds as $order => $fieldId) {
|
||||
BoardSetting::where('id', $fieldId)
|
||||
->where('board_id', $boardId)
|
||||
->update(['sort_order' => $order + 1]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 유틸리티 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 게시판 코드 중복 확인
|
||||
*/
|
||||
public function isCodeExists(string $code, ?int $excludeId = null): bool
|
||||
{
|
||||
$query = Board::where('board_code', $code);
|
||||
|
||||
// 시스템 게시판이면 시스템 게시판 중에서 확인
|
||||
// 테넌트 게시판이면 해당 테넌트 중에서 확인
|
||||
$query->where(function ($q) {
|
||||
$q->where('is_system', true)
|
||||
->orWhere('tenant_id', $this->tenantIdOrNull());
|
||||
});
|
||||
|
||||
if ($excludeId) {
|
||||
$query->where('id', '!=', $excludeId);
|
||||
}
|
||||
|
||||
return $query->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 활성/비활성 토글
|
||||
*/
|
||||
public function toggleActive(int $id): ?Board
|
||||
{
|
||||
$board = Board::find($id);
|
||||
|
||||
if (! $board) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$board->is_active = ! $board->is_active;
|
||||
$board->updated_by = $this->apiUserId();
|
||||
$board->save();
|
||||
|
||||
return $board;
|
||||
}
|
||||
}
|
||||
327
app/Services/Boards/PostService.php
Normal file
327
app/Services/Boards/PostService.php
Normal file
@@ -0,0 +1,327 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Boards;
|
||||
|
||||
use App\Models\Boards\Board;
|
||||
use App\Models\Boards\BoardComment;
|
||||
use App\Models\Boards\Post;
|
||||
use App\Models\Boards\PostCustomFieldValue;
|
||||
use App\Services\Service;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
class PostService extends Service
|
||||
{
|
||||
// =========================================================================
|
||||
// 게시글 조회 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 게시글 목록 조회 (페이징)
|
||||
*/
|
||||
public function getPostsByBoard(int $boardId, array $filters = [], int $perPage = 15): LengthAwarePaginator
|
||||
{
|
||||
return Post::where('board_id', $boardId)
|
||||
->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']}%")
|
||||
->orWhere('content', 'like', "%{$filters['search']}%");
|
||||
});
|
||||
})
|
||||
->when(isset($filters['is_notice']), fn ($q) => $q->where('is_notice', $filters['is_notice']))
|
||||
->when(isset($filters['status']), fn ($q) => $q->where('status', $filters['status']))
|
||||
->orderByDesc('is_notice')
|
||||
->orderByDesc('created_at')
|
||||
->paginate($perPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 목록 조회 (게시판 코드 기반)
|
||||
*/
|
||||
public function getPostsByBoardCode(string $boardCode, array $filters = [], int $perPage = 15): LengthAwarePaginator
|
||||
{
|
||||
$board = Board::accessible($this->tenantId())
|
||||
->where('board_code', $boardCode)
|
||||
->firstOrFail();
|
||||
|
||||
return $this->getPostsByBoard($board->id, $filters, $perPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 단건 조회
|
||||
*/
|
||||
public function getPost(int $postId): ?Post
|
||||
{
|
||||
return Post::with(['files', 'comments.replies'])
|
||||
->where('tenant_id', $this->tenantId())
|
||||
->find($postId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 상세 조회 (조회수 증가)
|
||||
*/
|
||||
public function getPostWithViewIncrement(int $postId): ?Post
|
||||
{
|
||||
$post = $this->getPost($postId);
|
||||
|
||||
if ($post) {
|
||||
$post->increment('views');
|
||||
}
|
||||
|
||||
return $post;
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 코드와 게시글 ID로 조회
|
||||
*/
|
||||
public function getPostByCodeAndId(string $boardCode, int $postId): ?Post
|
||||
{
|
||||
$board = Board::accessible($this->tenantId())
|
||||
->where('board_code', $boardCode)
|
||||
->first();
|
||||
|
||||
if (! $board) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Post::with(['files', 'comments.replies', 'board'])
|
||||
->where('board_id', $board->id)
|
||||
->where('tenant_id', $this->tenantId())
|
||||
->find($postId);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 게시글 생성 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 게시글 생성
|
||||
*/
|
||||
public function createPost(int $boardId, array $data): Post
|
||||
{
|
||||
$data['board_id'] = $boardId;
|
||||
$data['tenant_id'] = $this->tenantId();
|
||||
$data['user_id'] = $this->apiUserId();
|
||||
$data['ip_address'] = request()->ip();
|
||||
$data['status'] = $data['status'] ?? 'published';
|
||||
$data['views'] = 0;
|
||||
|
||||
$post = Post::create($data);
|
||||
|
||||
// 커스텀 필드 저장
|
||||
if (isset($data['custom_fields'])) {
|
||||
$this->saveCustomFields($post->id, $data['custom_fields']);
|
||||
}
|
||||
|
||||
return $post->fresh(['board', 'files']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 생성 (게시판 코드 기반)
|
||||
*/
|
||||
public function createPostByBoardCode(string $boardCode, array $data): Post
|
||||
{
|
||||
$board = Board::accessible($this->tenantId())
|
||||
->where('board_code', $boardCode)
|
||||
->firstOrFail();
|
||||
|
||||
return $this->createPost($board->id, $data);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 게시글 수정 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 게시글 수정
|
||||
*/
|
||||
public function updatePost(int $postId, array $data): ?Post
|
||||
{
|
||||
$post = Post::where('tenant_id', $this->tenantId())
|
||||
->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 updatePostByBoardCode(string $boardCode, int $postId, array $data): ?Post
|
||||
{
|
||||
$board = Board::accessible($this->tenantId())
|
||||
->where('board_code', $boardCode)
|
||||
->first();
|
||||
|
||||
if (! $board) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$post = Post::where('board_id', $board->id)
|
||||
->where('tenant_id', $this->tenantId())
|
||||
->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']);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 게시글 삭제 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 게시글 삭제 (Soft Delete)
|
||||
*/
|
||||
public function deletePost(int $postId): bool
|
||||
{
|
||||
$post = Post::where('tenant_id', $this->tenantId())
|
||||
->find($postId);
|
||||
|
||||
if (! $post) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $post->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 삭제 (게시판 코드 기반)
|
||||
*/
|
||||
public function deletePostByBoardCode(string $boardCode, int $postId): bool
|
||||
{
|
||||
$board = Board::accessible($this->tenantId())
|
||||
->where('board_code', $boardCode)
|
||||
->first();
|
||||
|
||||
if (! $board) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$post = Post::where('board_id', $board->id)
|
||||
->where('tenant_id', $this->tenantId())
|
||||
->find($postId);
|
||||
|
||||
if (! $post) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $post->delete();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 댓글 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 댓글 목록 조회
|
||||
*/
|
||||
public function getComments(int $postId): Collection
|
||||
{
|
||||
return BoardComment::where('post_id', $postId)
|
||||
->whereNull('parent_id')
|
||||
->where('status', 'active')
|
||||
->with('replies')
|
||||
->orderBy('created_at')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 댓글 작성
|
||||
*/
|
||||
public function createComment(int $postId, array $data): BoardComment
|
||||
{
|
||||
$data['post_id'] = $postId;
|
||||
$data['user_id'] = $this->apiUserId();
|
||||
$data['ip_address'] = request()->ip();
|
||||
$data['status'] = 'active';
|
||||
|
||||
return BoardComment::create($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 댓글 수정
|
||||
*/
|
||||
public function updateComment(int $commentId, array $data): ?BoardComment
|
||||
{
|
||||
$comment = BoardComment::where('user_id', $this->apiUserId())
|
||||
->find($commentId);
|
||||
|
||||
if (! $comment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$comment->update($data);
|
||||
|
||||
return $comment->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* 댓글 삭제
|
||||
*/
|
||||
public function deleteComment(int $commentId): bool
|
||||
{
|
||||
$comment = BoardComment::where('user_id', $this->apiUserId())
|
||||
->find($commentId);
|
||||
|
||||
if (! $comment) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$comment->status = 'deleted';
|
||||
$comment->save();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 커스텀 필드 메서드
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 커스텀 필드 값 저장
|
||||
*/
|
||||
protected function saveCustomFields(int $postId, array $customFields): void
|
||||
{
|
||||
foreach ($customFields as $fieldId => $value) {
|
||||
PostCustomFieldValue::updateOrCreate(
|
||||
[
|
||||
'post_id' => $postId,
|
||||
'field_id' => $fieldId,
|
||||
],
|
||||
[
|
||||
'value' => is_array($value) ? json_encode($value) : $value,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 커스텀 필드 값 조회
|
||||
*/
|
||||
public function getCustomFieldValues(int $postId): Collection
|
||||
{
|
||||
return PostCustomFieldValue::where('post_id', $postId)
|
||||
->with('field')
|
||||
->get();
|
||||
}
|
||||
}
|
||||
236
app/Swagger/v1/BoardApi.php
Normal file
236
app/Swagger/v1/BoardApi.php
Normal file
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
|
||||
namespace App\Swagger\v1;
|
||||
|
||||
/**
|
||||
* @OA\Tag(name="Board", description="게시판 관리")
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="Board",
|
||||
* type="object",
|
||||
* required={"id","board_code","name"},
|
||||
*
|
||||
* @OA\Property(property="id", type="integer", example=1),
|
||||
* @OA\Property(property="tenant_id", type="integer", nullable=true, example=1, description="테넌트 ID (시스템 게시판은 null)"),
|
||||
* @OA\Property(property="is_system", type="boolean", example=false, description="시스템 게시판 여부"),
|
||||
* @OA\Property(property="board_type", type="string", nullable=true, example="notice", description="게시판 유형 (notice, qna, faq 등)"),
|
||||
* @OA\Property(property="board_code", type="string", example="NOTICE", description="게시판 코드"),
|
||||
* @OA\Property(property="name", type="string", example="공지사항", description="게시판명"),
|
||||
* @OA\Property(property="description", type="string", nullable=true, example="전사 공지사항 게시판", description="설명"),
|
||||
* @OA\Property(property="editor_type", type="string", enum={"wysiwyg","markdown","text"}, example="wysiwyg", description="에디터 타입"),
|
||||
* @OA\Property(property="allow_files", type="boolean", example=true, description="파일 첨부 허용"),
|
||||
* @OA\Property(property="max_file_count", type="integer", example=5, description="최대 파일 수"),
|
||||
* @OA\Property(property="max_file_size", type="integer", example=20480, description="최대 파일 크기 (KB)"),
|
||||
* @OA\Property(property="extra_settings", type="object", nullable=true, description="추가 설정"),
|
||||
* @OA\Property(property="is_active", type="boolean", example=true, description="활성 여부"),
|
||||
* @OA\Property(property="created_at", type="string", example="2025-01-01 12:00:00"),
|
||||
* @OA\Property(property="updated_at", type="string", example="2025-01-01 12:00:00")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="BoardField",
|
||||
* type="object",
|
||||
*
|
||||
* @OA\Property(property="id", type="integer", example=1),
|
||||
* @OA\Property(property="board_id", type="integer", example=1),
|
||||
* @OA\Property(property="field_key", type="string", example="category", description="필드 키"),
|
||||
* @OA\Property(property="field_label", type="string", example="카테고리", description="필드 라벨"),
|
||||
* @OA\Property(property="field_type", type="string", example="select", description="필드 타입"),
|
||||
* @OA\Property(property="is_required", type="boolean", example=false),
|
||||
* @OA\Property(property="sort_order", type="integer", example=1),
|
||||
* @OA\Property(property="options", type="object", nullable=true)
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="BoardCreateRequest",
|
||||
* type="object",
|
||||
* required={"board_code","name"},
|
||||
*
|
||||
* @OA\Property(property="board_code", type="string", maxLength=50, example="NOTICE"),
|
||||
* @OA\Property(property="board_type", type="string", nullable=true, maxLength=50, example="notice"),
|
||||
* @OA\Property(property="name", type="string", maxLength=100, example="공지사항"),
|
||||
* @OA\Property(property="description", type="string", nullable=true, maxLength=500),
|
||||
* @OA\Property(property="editor_type", type="string", enum={"wysiwyg","markdown","text"}, example="wysiwyg"),
|
||||
* @OA\Property(property="allow_files", type="boolean", example=true),
|
||||
* @OA\Property(property="max_file_count", type="integer", example=5),
|
||||
* @OA\Property(property="max_file_size", type="integer", example=20480),
|
||||
* @OA\Property(property="extra_settings", type="object", nullable=true),
|
||||
* @OA\Property(property="is_active", type="boolean", example=true)
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="BoardUpdateRequest",
|
||||
* type="object",
|
||||
*
|
||||
* @OA\Property(property="board_code", type="string", maxLength=50),
|
||||
* @OA\Property(property="board_type", type="string", nullable=true, maxLength=50),
|
||||
* @OA\Property(property="name", type="string", maxLength=100),
|
||||
* @OA\Property(property="description", type="string", nullable=true, maxLength=500),
|
||||
* @OA\Property(property="editor_type", type="string", enum={"wysiwyg","markdown","text"}),
|
||||
* @OA\Property(property="allow_files", type="boolean"),
|
||||
* @OA\Property(property="max_file_count", type="integer"),
|
||||
* @OA\Property(property="max_file_size", type="integer"),
|
||||
* @OA\Property(property="extra_settings", type="object", nullable=true),
|
||||
* @OA\Property(property="is_active", type="boolean")
|
||||
* )
|
||||
*/
|
||||
class BoardApi
|
||||
{
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/boards",
|
||||
* tags={"Board"},
|
||||
* summary="접근 가능한 게시판 목록 (시스템 + 테넌트)",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="board_type", in="query", description="게시판 유형 필터", @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="search", in="query", description="게시판명 검색", @OA\Schema(type="string")),
|
||||
*
|
||||
* @OA\Response(response=200, description="조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/Board")))
|
||||
* })
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function index() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/boards/tenant",
|
||||
* tags={"Board"},
|
||||
* summary="테넌트 게시판 목록만 조회",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="board_type", in="query", @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="search", in="query", @OA\Schema(type="string")),
|
||||
*
|
||||
* @OA\Response(response=200, description="조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/Board")))
|
||||
* })
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function tenantBoards() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/boards/{code}",
|
||||
* tags={"Board"},
|
||||
* summary="게시판 상세 조회 (코드 기반)",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="code", in="path", required=true, description="게시판 코드", @OA\Schema(type="string")),
|
||||
*
|
||||
* @OA\Response(response=200, description="조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Board"))
|
||||
* })
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=404, description="게시판 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function show() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/boards",
|
||||
* tags={"Board"},
|
||||
* summary="테넌트 게시판 생성",
|
||||
* description="현재 테넌트에 새 게시판을 생성합니다. 시스템 게시판은 mng에서만 생성 가능합니다.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/BoardCreateRequest")),
|
||||
*
|
||||
* @OA\Response(response=200, description="생성 성공",
|
||||
*
|
||||
* @OA\JsonContent(allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Board"))
|
||||
* })
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=400, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function store() {}
|
||||
|
||||
/**
|
||||
* @OA\Put(
|
||||
* path="/api/v1/boards/{id}",
|
||||
* tags={"Board"},
|
||||
* summary="테넌트 게시판 수정",
|
||||
* description="테넌트 게시판만 수정 가능. 시스템 게시판은 수정 불가.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/BoardUpdateRequest")),
|
||||
*
|
||||
* @OA\Response(response=200, description="수정 성공",
|
||||
*
|
||||
* @OA\JsonContent(allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Board"))
|
||||
* })
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=404, description="게시판 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function update() {}
|
||||
|
||||
/**
|
||||
* @OA\Delete(
|
||||
* path="/api/v1/boards/{id}",
|
||||
* tags={"Board"},
|
||||
* summary="테넌트 게시판 삭제",
|
||||
* description="테넌트 게시판만 삭제 가능. 시스템 게시판은 삭제 불가.",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\Response(response=200, description="삭제 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")),
|
||||
* @OA\Response(response=404, description="게시판 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function destroy() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/boards/{code}/fields",
|
||||
* tags={"Board"},
|
||||
* summary="게시판 커스텀 필드 목록",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="code", in="path", required=true, description="게시판 코드", @OA\Schema(type="string")),
|
||||
*
|
||||
* @OA\Response(response=200, description="조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/BoardField")))
|
||||
* })
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=404, description="게시판 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function fields() {}
|
||||
}
|
||||
314
app/Swagger/v1/PostApi.php
Normal file
314
app/Swagger/v1/PostApi.php
Normal file
@@ -0,0 +1,314 @@
|
||||
<?php
|
||||
|
||||
namespace App\Swagger\v1;
|
||||
|
||||
/**
|
||||
* @OA\Tag(name="Post", description="게시글 관리")
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="Post",
|
||||
* type="object",
|
||||
* required={"id","board_id","title"},
|
||||
*
|
||||
* @OA\Property(property="id", type="integer", example=1),
|
||||
* @OA\Property(property="tenant_id", type="integer", example=1),
|
||||
* @OA\Property(property="board_id", type="integer", example=1),
|
||||
* @OA\Property(property="user_id", type="integer", example=1, description="작성자 ID"),
|
||||
* @OA\Property(property="title", type="string", example="공지사항 제목", description="제목"),
|
||||
* @OA\Property(property="content", type="string", example="공지사항 내용입니다.", description="내용"),
|
||||
* @OA\Property(property="editor_type", type="string", enum={"wysiwyg","markdown","text"}, example="wysiwyg"),
|
||||
* @OA\Property(property="ip_address", type="string", nullable=true, example="127.0.0.1"),
|
||||
* @OA\Property(property="is_notice", type="boolean", example=false, description="공지 여부"),
|
||||
* @OA\Property(property="is_secret", type="boolean", example=false, description="비밀글 여부"),
|
||||
* @OA\Property(property="views", type="integer", example=0, description="조회수"),
|
||||
* @OA\Property(property="status", type="string", enum={"draft","published","hidden"}, example="published"),
|
||||
* @OA\Property(property="created_at", type="string", example="2025-01-01 12:00:00"),
|
||||
* @OA\Property(property="updated_at", type="string", example="2025-01-01 12:00:00"),
|
||||
* @OA\Property(property="board", ref="#/components/schemas/Board"),
|
||||
* @OA\Property(property="files", type="array", @OA\Items(type="object"))
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="PostPagination",
|
||||
* type="object",
|
||||
*
|
||||
* @OA\Property(property="current_page", type="integer", example=1),
|
||||
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/Post")),
|
||||
* @OA\Property(property="first_page_url", type="string"),
|
||||
* @OA\Property(property="from", type="integer", example=1),
|
||||
* @OA\Property(property="last_page", type="integer", example=3),
|
||||
* @OA\Property(property="last_page_url", type="string"),
|
||||
* @OA\Property(property="next_page_url", type="string", nullable=true),
|
||||
* @OA\Property(property="path", type="string"),
|
||||
* @OA\Property(property="per_page", type="integer", example=15),
|
||||
* @OA\Property(property="prev_page_url", type="string", nullable=true),
|
||||
* @OA\Property(property="to", type="integer", example=15),
|
||||
* @OA\Property(property="total", type="integer", example=50)
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="PostCreateRequest",
|
||||
* type="object",
|
||||
* required={"title","content"},
|
||||
*
|
||||
* @OA\Property(property="title", type="string", maxLength=200, example="게시글 제목"),
|
||||
* @OA\Property(property="content", type="string", example="게시글 내용입니다."),
|
||||
* @OA\Property(property="editor_type", type="string", enum={"wysiwyg","markdown","text"}, example="wysiwyg"),
|
||||
* @OA\Property(property="is_notice", type="boolean", example=false),
|
||||
* @OA\Property(property="is_secret", type="boolean", example=false),
|
||||
* @OA\Property(property="status", type="string", enum={"draft","published","hidden"}, example="published"),
|
||||
* @OA\Property(property="custom_fields", type="object", nullable=true, description="커스텀 필드 값 (field_id: value)")
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="PostUpdateRequest",
|
||||
* type="object",
|
||||
*
|
||||
* @OA\Property(property="title", type="string", maxLength=200),
|
||||
* @OA\Property(property="content", type="string"),
|
||||
* @OA\Property(property="editor_type", type="string", enum={"wysiwyg","markdown","text"}),
|
||||
* @OA\Property(property="is_notice", type="boolean"),
|
||||
* @OA\Property(property="is_secret", type="boolean"),
|
||||
* @OA\Property(property="status", type="string", enum={"draft","published","hidden"}),
|
||||
* @OA\Property(property="custom_fields", type="object", nullable=true)
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="Comment",
|
||||
* type="object",
|
||||
*
|
||||
* @OA\Property(property="id", type="integer", example=1),
|
||||
* @OA\Property(property="post_id", type="integer", example=1),
|
||||
* @OA\Property(property="user_id", type="integer", example=1),
|
||||
* @OA\Property(property="parent_id", type="integer", nullable=true, description="부모 댓글 ID (대댓글인 경우)"),
|
||||
* @OA\Property(property="content", type="string", example="댓글 내용"),
|
||||
* @OA\Property(property="status", type="string", enum={"active","deleted"}, example="active"),
|
||||
* @OA\Property(property="created_at", type="string", example="2025-01-01 12:00:00"),
|
||||
* @OA\Property(property="replies", type="array", @OA\Items(ref="#/components/schemas/Comment"))
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="CommentCreateRequest",
|
||||
* type="object",
|
||||
* required={"content"},
|
||||
*
|
||||
* @OA\Property(property="content", type="string", maxLength=2000, example="댓글 내용"),
|
||||
* @OA\Property(property="parent_id", type="integer", nullable=true, description="대댓글인 경우 부모 댓글 ID")
|
||||
* )
|
||||
*/
|
||||
class PostApi
|
||||
{
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/boards/{code}/posts",
|
||||
* tags={"Post"},
|
||||
* summary="게시글 목록",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="code", in="path", required=true, description="게시판 코드", @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="page", in="query", @OA\Schema(type="integer", example=1)),
|
||||
* @OA\Parameter(name="per_page", in="query", @OA\Schema(type="integer", example=15)),
|
||||
* @OA\Parameter(name="search", in="query", description="제목/내용 검색", @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="is_notice", in="query", @OA\Schema(type="boolean")),
|
||||
* @OA\Parameter(name="status", in="query", @OA\Schema(type="string", enum={"draft","published","hidden"})),
|
||||
*
|
||||
* @OA\Response(response=200, description="조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/PostPagination"))
|
||||
* })
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=404, description="게시판 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function index() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/boards/{code}/posts/{id}",
|
||||
* tags={"Post"},
|
||||
* summary="게시글 상세 (조회수 자동 증가)",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="code", in="path", required=true, description="게시판 코드", @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\Response(response=200, description="조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Post"))
|
||||
* })
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=404, description="게시글 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function show() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/boards/{code}/posts",
|
||||
* tags={"Post"},
|
||||
* summary="게시글 작성",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="code", in="path", required=true, description="게시판 코드", @OA\Schema(type="string")),
|
||||
*
|
||||
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/PostCreateRequest")),
|
||||
*
|
||||
* @OA\Response(response=200, description="작성 성공",
|
||||
*
|
||||
* @OA\JsonContent(allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Post"))
|
||||
* })
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=400, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||
* @OA\Response(response=404, description="게시판 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function store() {}
|
||||
|
||||
/**
|
||||
* @OA\Put(
|
||||
* path="/api/v1/boards/{code}/posts/{id}",
|
||||
* tags={"Post"},
|
||||
* summary="게시글 수정",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="code", in="path", required=true, description="게시판 코드", @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/PostUpdateRequest")),
|
||||
*
|
||||
* @OA\Response(response=200, description="수정 성공",
|
||||
*
|
||||
* @OA\JsonContent(allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Post"))
|
||||
* })
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=404, description="게시글 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function update() {}
|
||||
|
||||
/**
|
||||
* @OA\Delete(
|
||||
* path="/api/v1/boards/{code}/posts/{id}",
|
||||
* tags={"Post"},
|
||||
* summary="게시글 삭제",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="code", in="path", required=true, description="게시판 코드", @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\Response(response=200, description="삭제 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")),
|
||||
* @OA\Response(response=404, description="게시글 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function destroy() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/boards/{code}/posts/{postId}/comments",
|
||||
* tags={"Post"},
|
||||
* summary="댓글 목록",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="code", in="path", required=true, description="게시판 코드", @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="postId", in="path", required=true, @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\Response(response=200, description="조회 성공",
|
||||
*
|
||||
* @OA\JsonContent(allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/Comment")))
|
||||
* })
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function comments() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/boards/{code}/posts/{postId}/comments",
|
||||
* tags={"Post"},
|
||||
* summary="댓글 작성",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="code", in="path", required=true, description="게시판 코드", @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="postId", in="path", required=true, @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/CommentCreateRequest")),
|
||||
*
|
||||
* @OA\Response(response=200, description="작성 성공",
|
||||
*
|
||||
* @OA\JsonContent(allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Comment"))
|
||||
* })
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=400, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function storeComment() {}
|
||||
|
||||
/**
|
||||
* @OA\Put(
|
||||
* path="/api/v1/boards/{code}/posts/{postId}/comments/{commentId}",
|
||||
* tags={"Post"},
|
||||
* summary="댓글 수정",
|
||||
* description="본인이 작성한 댓글만 수정 가능",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="code", in="path", required=true, description="게시판 코드", @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="postId", in="path", required=true, @OA\Schema(type="integer")),
|
||||
* @OA\Parameter(name="commentId", in="path", required=true, @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\RequestBody(required=true, @OA\JsonContent(ref="#/components/schemas/CommentCreateRequest")),
|
||||
*
|
||||
* @OA\Response(response=200, description="수정 성공",
|
||||
*
|
||||
* @OA\JsonContent(allOf={
|
||||
*
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(@OA\Property(property="data", ref="#/components/schemas/Comment"))
|
||||
* })
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(response=404, description="댓글 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function updateComment() {}
|
||||
|
||||
/**
|
||||
* @OA\Delete(
|
||||
* path="/api/v1/boards/{code}/posts/{postId}/comments/{commentId}",
|
||||
* tags={"Post"},
|
||||
* summary="댓글 삭제",
|
||||
* description="본인이 작성한 댓글만 삭제 가능 (상태가 deleted로 변경됨)",
|
||||
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||
*
|
||||
* @OA\Parameter(name="code", in="path", required=true, description="게시판 코드", @OA\Schema(type="string")),
|
||||
* @OA\Parameter(name="postId", in="path", required=true, @OA\Schema(type="integer")),
|
||||
* @OA\Parameter(name="commentId", in="path", required=true, @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\Response(response=200, description="삭제 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")),
|
||||
* @OA\Response(response=404, description="댓글 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||
* )
|
||||
*/
|
||||
public function destroyComment() {}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('boards', function (Blueprint $table) {
|
||||
// tenant_id를 nullable로 변경 (시스템 게시판은 tenant_id가 NULL)
|
||||
$table->unsignedBigInteger('tenant_id')->nullable()->change();
|
||||
|
||||
// 시스템 게시판 여부 (true=전체 테넌트 공개)
|
||||
$table->boolean('is_system')
|
||||
->default(false)
|
||||
->after('tenant_id')
|
||||
->comment('시스템 게시판 여부 (1=전체 테넌트 공개)');
|
||||
|
||||
// 게시판 유형 (자유 입력: notice, qna, faq, free 등)
|
||||
$table->string('board_type', 50)
|
||||
->nullable()
|
||||
->after('is_system')
|
||||
->comment('게시판 유형 (notice, qna, faq, free 등)');
|
||||
|
||||
// Soft Delete 컬럼 추가
|
||||
$table->softDeletes()->comment('삭제 일시');
|
||||
$table->unsignedBigInteger('deleted_by')
|
||||
->nullable()
|
||||
->after('deleted_at')
|
||||
->comment('삭제자 ID');
|
||||
|
||||
// 인덱스 추가
|
||||
$table->index('is_system', 'idx_boards_is_system');
|
||||
$table->index('board_type', 'idx_boards_board_type');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('boards', function (Blueprint $table) {
|
||||
// 인덱스 삭제
|
||||
$table->dropIndex('idx_boards_is_system');
|
||||
$table->dropIndex('idx_boards_board_type');
|
||||
|
||||
// 컬럼 삭제
|
||||
$table->dropColumn(['is_system', 'board_type', 'deleted_by']);
|
||||
$table->dropSoftDeletes();
|
||||
|
||||
// tenant_id를 NOT NULL로 복원
|
||||
$table->unsignedBigInteger('tenant_id')->nullable(false)->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user