diff --git a/app/Http/Controllers/Api/Admin/BoardController.php b/app/Http/Controllers/Api/Admin/BoardController.php new file mode 100644 index 00000000..7030c162 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/BoardController.php @@ -0,0 +1,284 @@ +only(['search', 'board_type', 'is_active', 'trashed', 'sort_by', 'sort_direction']); + $boards = $this->boardService->getBoards($filters, 15); + + // HTMX 요청이면 HTML 파셜 반환 + if ($request->header('HX-Request')) { + return view('boards.partials.table', compact('boards')); + } + + // 일반 요청이면 JSON + return response()->json([ + 'success' => true, + 'data' => $boards, + ]); + } + + /** + * 게시판 통계 + */ + public function stats(): JsonResponse + { + $stats = $this->boardService->getBoardStats(); + + return response()->json([ + 'success' => true, + 'data' => $stats, + ]); + } + + /** + * 게시판 상세 조회 + */ + public function show(int $id): JsonResponse + { + $board = $this->boardService->getBoardById($id, true); + + if (! $board) { + return response()->json([ + 'success' => false, + 'message' => '게시판을 찾을 수 없습니다.', + ], 404); + } + + return response()->json([ + 'success' => true, + 'data' => $board, + ]); + } + + /** + * 게시판 생성 + */ + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'board_code' => 'required|string|max:50', + 'name' => 'required|string|max:100', + 'board_type' => 'nullable|string|max:50', + 'description' => 'nullable|string|max:500', + 'editor_type' => 'nullable|string|in:wysiwyg,markdown,text', + 'allow_files' => 'nullable|boolean', + 'max_file_count' => 'nullable|integer|min:0|max:100', + 'max_file_size' => 'nullable|integer|min:0', + 'extra_settings' => 'nullable|array', + 'is_active' => 'nullable|boolean', + ]); + + // 코드 중복 체크 + if ($this->boardService->isCodeExists($validated['board_code'])) { + return response()->json([ + 'success' => false, + 'message' => '이미 사용 중인 게시판 코드입니다.', + ], 422); + } + + $board = $this->boardService->createBoard($validated); + + return response()->json([ + 'success' => true, + 'message' => '게시판이 생성되었습니다.', + 'data' => $board, + ]); + } + + /** + * 게시판 수정 + */ + public function update(Request $request, int $id): JsonResponse + { + $validated = $request->validate([ + 'board_code' => 'required|string|max:50', + 'name' => 'required|string|max:100', + 'board_type' => 'nullable|string|max:50', + 'description' => 'nullable|string|max:500', + 'editor_type' => 'nullable|string|in:wysiwyg,markdown,text', + 'allow_files' => 'nullable|boolean', + 'max_file_count' => 'nullable|integer|min:0|max:100', + 'max_file_size' => 'nullable|integer|min:0', + 'extra_settings' => 'nullable|array', + 'is_active' => 'nullable|boolean', + ]); + + // 코드 중복 체크 (자신 제외) + if ($this->boardService->isCodeExists($validated['board_code'], $id)) { + return response()->json([ + 'success' => false, + 'message' => '이미 사용 중인 게시판 코드입니다.', + ], 422); + } + + $this->boardService->updateBoard($id, $validated); + + return response()->json([ + 'success' => true, + 'message' => '게시판이 수정되었습니다.', + ]); + } + + /** + * 게시판 삭제 (Soft Delete) + */ + public function destroy(int $id): JsonResponse + { + $this->boardService->deleteBoard($id); + + return response()->json([ + 'success' => true, + 'message' => '게시판이 삭제되었습니다.', + ]); + } + + /** + * 게시판 복원 + */ + public function restore(int $id): JsonResponse + { + $this->boardService->restoreBoard($id); + + return response()->json([ + 'success' => true, + 'message' => '게시판이 복원되었습니다.', + ]); + } + + /** + * 게시판 영구 삭제 + */ + public function forceDestroy(int $id): JsonResponse + { + $this->boardService->forceDeleteBoard($id); + + return response()->json([ + 'success' => true, + 'message' => '게시판이 영구 삭제되었습니다.', + ]); + } + + /** + * 게시판 활성/비활성 토글 + */ + public function toggleActive(int $id): JsonResponse + { + $board = $this->boardService->toggleActive($id); + + return response()->json([ + 'success' => true, + 'message' => $board->is_active ? '게시판이 활성화되었습니다.' : '게시판이 비활성화되었습니다.', + 'data' => ['is_active' => $board->is_active], + ]); + } + + // ========================================================================= + // 필드 관리 API + // ========================================================================= + + /** + * 게시판 필드 목록 + */ + public function fields(int $id): JsonResponse + { + $fields = $this->boardService->getBoardFields($id); + + return response()->json([ + 'success' => true, + 'data' => $fields, + ]); + } + + /** + * 게시판 필드 추가 + */ + public function storeField(Request $request, int $id): JsonResponse + { + $validated = $request->validate([ + 'name' => 'required|string|max:100', + 'field_key' => 'required|string|max:50', + 'field_type' => 'required|string|in:text,number,select,date,textarea,checkbox,radio,file', + 'field_meta' => 'nullable|array', + 'is_required' => 'nullable|boolean', + 'sort_order' => 'nullable|integer', + ]); + + $field = $this->boardService->addBoardField($id, $validated); + + return response()->json([ + 'success' => true, + 'message' => '필드가 추가되었습니다.', + 'data' => $field, + ]); + } + + /** + * 게시판 필드 수정 + */ + public function updateField(Request $request, int $id, int $fieldId): JsonResponse + { + $validated = $request->validate([ + 'name' => 'required|string|max:100', + 'field_key' => 'required|string|max:50', + 'field_type' => 'required|string|in:text,number,select,date,textarea,checkbox,radio,file', + 'field_meta' => 'nullable|array', + 'is_required' => 'nullable|boolean', + 'sort_order' => 'nullable|integer', + ]); + + $this->boardService->updateBoardField($fieldId, $validated); + + return response()->json([ + 'success' => true, + 'message' => '필드가 수정되었습니다.', + ]); + } + + /** + * 게시판 필드 삭제 + */ + public function destroyField(int $id, int $fieldId): JsonResponse + { + $this->boardService->deleteBoardField($fieldId); + + return response()->json([ + 'success' => true, + 'message' => '필드가 삭제되었습니다.', + ]); + } + + /** + * 게시판 필드 순서 변경 + */ + public function reorderFields(Request $request, int $id): JsonResponse + { + $validated = $request->validate([ + 'field_ids' => 'required|array', + 'field_ids.*' => 'integer', + ]); + + $this->boardService->reorderBoardFields($id, $validated['field_ids']); + + return response()->json([ + 'success' => true, + 'message' => '필드 순서가 변경되었습니다.', + ]); + } +} diff --git a/app/Http/Controllers/BoardController.php b/app/Http/Controllers/BoardController.php new file mode 100644 index 00000000..e78cd77f --- /dev/null +++ b/app/Http/Controllers/BoardController.php @@ -0,0 +1,45 @@ +boardService->getBoardTypes(); + + return view('boards.index', compact('boardTypes')); + } + + /** + * 게시판 생성 화면 + */ + public function create(): View + { + return view('boards.create'); + } + + /** + * 게시판 수정 화면 + */ + public function edit(int $id): View + { + $board = $this->boardService->getBoardById($id, true); + + if (! $board) { + abort(404, '게시판을 찾을 수 없습니다.'); + } + + return view('boards.edit', compact('board')); + } +} diff --git a/app/Models/Boards/Board.php b/app/Models/Boards/Board.php new file mode 100644 index 00000000..3528c416 --- /dev/null +++ b/app/Models/Boards/Board.php @@ -0,0 +1,141 @@ + '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 + // ========================================================================= + + /** + * 시스템 게시판만 (mng용) + */ + public function scopeSystemOnly(Builder $query): Builder + { + return $query->where('is_system', true); + } + + /** + * 활성 게시판만 + */ + public function scopeActive(Builder $query): Builder + { + return $query->where('is_active', true); + } + + /** + * 게시판 유형으로 필터링 + */ + public function scopeOfType(Builder $query, string $type): Builder + { + return $query->where('board_type', $type); + } + + // ========================================================================= + // Relationships + // ========================================================================= + + public function fields(): HasMany + { + return $this->hasMany(BoardSetting::class, 'board_id') + ->orderBy('sort_order'); + } + + public function creator(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function updater(): BelongsTo + { + return $this->belongsTo(User::class, 'updated_by'); + } + + public function deletedByUser(): BelongsTo + { + return $this->belongsTo(User::class, 'deleted_by'); + } + + // ========================================================================= + // Helper Methods + // ========================================================================= + + /** + * extra_settings에서 특정 키 값 가져오기 + */ + public function getSetting(string $key, $default = null) + { + return data_get($this->extra_settings, $key, $default); + } + + /** + * 시스템 게시판 여부 확인 + */ + public function isSystemBoard(): bool + { + return $this->is_system === true; + } +} diff --git a/app/Models/Boards/BoardSetting.php b/app/Models/Boards/BoardSetting.php new file mode 100644 index 00000000..03c996f5 --- /dev/null +++ b/app/Models/Boards/BoardSetting.php @@ -0,0 +1,59 @@ + 'array', + 'is_required' => 'boolean', + 'sort_order' => 'integer', + ]; + + protected $attributes = [ + 'is_required' => false, + 'sort_order' => 0, + ]; + + public function board(): BelongsTo + { + return $this->belongsTo(Board::class, 'board_id'); + } + + /** + * field_meta에서 특정 키 값 가져오기 + */ + public function getMeta(string $key, $default = null) + { + return data_get($this->field_meta, $key, $default); + } +} diff --git a/app/Services/BoardService.php b/app/Services/BoardService.php new file mode 100644 index 00000000..1e729a18 --- /dev/null +++ b/app/Services/BoardService.php @@ -0,0 +1,271 @@ +systemOnly() + ->withCount('fields') + ->withTrashed(); + + // 검색 필터 + if (! empty($filters['search'])) { + $search = $filters['search']; + $query->where(function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('board_code', 'like', "%{$search}%") + ->orWhere('description', 'like', "%{$search}%"); + }); + } + + // 게시판 유형 필터 + if (! empty($filters['board_type'])) { + $query->where('board_type', $filters['board_type']); + } + + // 활성 상태 필터 + if (isset($filters['is_active']) && $filters['is_active'] !== '') { + $query->where('is_active', $filters['is_active']); + } + + // Soft Delete 필터 + if (isset($filters['trashed'])) { + if ($filters['trashed'] === 'only') { + $query->onlyTrashed(); + } elseif ($filters['trashed'] === 'with') { + $query->withTrashed(); + } + } + + // 정렬 + $sortBy = $filters['sort_by'] ?? 'id'; + $sortDirection = $filters['sort_direction'] ?? 'desc'; + $query->orderBy($sortBy, $sortDirection); + + return $query->paginate($perPage); + } + + /** + * 시스템 게시판 목록 (드롭다운용) + */ + public function getActiveBoardList(): Collection + { + return Board::query() + ->systemOnly() + ->active() + ->orderBy('name') + ->get(['id', 'board_code', 'name', 'board_type']); + } + + /** + * 특정 게시판 조회 + */ + public function getBoardById(int $id, bool $withTrashed = false): ?Board + { + $query = Board::query() + ->systemOnly() + ->with('fields') + ->withCount('fields'); + + if ($withTrashed) { + $query->withTrashed(); + } + + return $query->find($id); + } + + /** + * 게시판 생성 + */ + public function createBoard(array $data): Board + { + // 시스템 게시판 설정 + $data['is_system'] = true; + $data['tenant_id'] = null; + $data['created_by'] = auth()->id(); + + return Board::create($data); + } + + /** + * 게시판 수정 + */ + public function updateBoard(int $id, array $data): bool + { + $board = Board::systemOnly()->findOrFail($id); + + $data['updated_by'] = auth()->id(); + + return $board->update($data); + } + + /** + * 게시판 삭제 (Soft Delete) + */ + public function deleteBoard(int $id): bool + { + $board = Board::systemOnly()->findOrFail($id); + + $board->deleted_by = auth()->id(); + $board->save(); + + return $board->delete(); + } + + /** + * 게시판 복원 + */ + public function restoreBoard(int $id): bool + { + $board = Board::systemOnly()->onlyTrashed()->findOrFail($id); + + $board->deleted_by = null; + + return $board->restore(); + } + + /** + * 게시판 영구 삭제 + */ + public function forceDeleteBoard(int $id): bool + { + $board = Board::systemOnly()->withTrashed()->findOrFail($id); + + // 관련 필드 삭제 + $board->fields()->delete(); + + return $board->forceDelete(); + } + + /** + * 게시판 코드 중복 체크 + */ + public function isCodeExists(string $code, ?int $excludeId = null): bool + { + $query = Board::where('board_code', $code) + ->where('is_system', true); + + if ($excludeId) { + $query->where('id', '!=', $excludeId); + } + + return $query->exists(); + } + + /** + * 게시판 활성/비활성 토글 + */ + public function toggleActive(int $id): Board + { + $board = Board::systemOnly()->findOrFail($id); + + $board->is_active = ! $board->is_active; + $board->updated_by = auth()->id(); + $board->save(); + + return $board; + } + + /** + * 게시판 통계 + */ + public function getBoardStats(): array + { + return [ + 'total' => Board::systemOnly()->count(), + 'active' => Board::systemOnly()->active()->count(), + 'inactive' => Board::systemOnly()->where('is_active', false)->count(), + 'trashed' => Board::systemOnly()->onlyTrashed()->count(), + ]; + } + + // ========================================================================= + // 필드 관리 + // ========================================================================= + + /** + * 게시판 필드 목록 조회 + */ + 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'] = auth()->id(); + + // 기본 정렬 순서 설정 + 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): bool + { + $field = BoardSetting::findOrFail($fieldId); + + $data['updated_by'] = auth()->id(); + + return $field->update($data); + } + + /** + * 게시판 필드 삭제 + */ + public function deleteBoardField(int $fieldId): bool + { + $field = BoardSetting::findOrFail($fieldId); + + 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 getBoardTypes(): array + { + return Board::systemOnly() + ->whereNotNull('board_type') + ->distinct() + ->pluck('board_type') + ->toArray(); + } +} diff --git a/resources/views/boards/create.blade.php b/resources/views/boards/create.blade.php new file mode 100644 index 00000000..2688c84e --- /dev/null +++ b/resources/views/boards/create.blade.php @@ -0,0 +1,182 @@ +@extends('layouts.app') + +@section('title', '게시판 생성') + +@section('content') + +
등록된 커스텀 필드가 없습니다.
+ @endforelse +| ID | +코드 | +게시판명 | +유형 | +필드 수 | +상태 | +생성일 | +액션 | +
|---|---|---|---|---|---|---|---|
| + {{ $board->id }} + | ++ {{ $board->board_code }} + | +
+ {{ $board->name }}
+ @if($board->description)
+ {{ $board->description }}
+ @endif
+ |
+ + @if($board->board_type) + + {{ $board->board_type }} + + @else + - + @endif + | ++ {{ $board->fields_count ?? 0 }}개 + | ++ @if($board->trashed()) + + 삭제됨 + + @elseif($board->is_active) + + @else + + @endif + | ++ {{ $board->created_at->format('Y-m-d') }} + | ++ @if($board->trashed()) + + + + @else + + 수정 + + @endif + | +
| + 게시판이 없습니다. + | +|||||||