- 게시판 템플릿 설정 파일 추가 (config/board_templates.php) - 시스템 템플릿: 공지사항, 1:1문의, FAQ, 팝업공지 - 테넌트 템플릿: 자유게시판, 갤러리, 자료실, 공지사항, Q&A - BoardService 템플릿 관련 메서드 추가 - BoardController 템플릿 API 엔드포인트 추가 - 게시판 생성 UI 3단계 위자드로 개선 - 모든 템플릿 아이콘을 이모지에서 SVG path로 변경 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
528 lines
14 KiB
PHP
528 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Boards\Board;
|
|
use App\Models\Boards\BoardSetting;
|
|
use App\Models\Tenants\Tenant;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class BoardService
|
|
{
|
|
/**
|
|
* 시스템 게시판 목록 조회 (페이지네이션)
|
|
*/
|
|
public function getBoards(array $filters = [], int $perPage = 15): LengthAwarePaginator
|
|
{
|
|
$query = Board::query()
|
|
->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();
|
|
}
|
|
|
|
// =========================================================================
|
|
// 템플릿 관리
|
|
// =========================================================================
|
|
|
|
/**
|
|
* 템플릿 목록 조회
|
|
*/
|
|
public function getTemplates(string $type = 'all'): array
|
|
{
|
|
$templates = config('board_templates', []);
|
|
|
|
if ($type === 'system') {
|
|
return $templates['system'] ?? [];
|
|
}
|
|
|
|
if ($type === 'tenant') {
|
|
return $templates['tenant'] ?? [];
|
|
}
|
|
|
|
return [
|
|
'system' => $templates['system'] ?? [],
|
|
'tenant' => $templates['tenant'] ?? [],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 특정 템플릿 조회
|
|
*/
|
|
public function getTemplate(string $type, string $key): ?array
|
|
{
|
|
$templates = $this->getTemplates($type);
|
|
|
|
return $templates[$key] ?? null;
|
|
}
|
|
|
|
/**
|
|
* 기본 필드 목록 조회
|
|
*/
|
|
public function getBaseFields(): array
|
|
{
|
|
return config('board_templates.base_fields', []);
|
|
}
|
|
|
|
/**
|
|
* 템플릿 기반 게시판 생성
|
|
*/
|
|
public function createBoardFromTemplate(array $data, ?string $templateType = null, ?string $templateKey = null): Board
|
|
{
|
|
return DB::transaction(function () use ($data, $templateType, $templateKey) {
|
|
// 템플릿 설정 적용
|
|
$template = null;
|
|
if ($templateType && $templateKey) {
|
|
$template = $this->getTemplate($templateType, $templateKey);
|
|
if ($template) {
|
|
// 템플릿 기본값 적용 (사용자 입력값 우선)
|
|
$data = array_merge([
|
|
'board_type' => $template['board_type'] ?? null,
|
|
'editor_type' => $template['editor_type'] ?? 'wysiwyg',
|
|
'allow_files' => $template['allow_files'] ?? true,
|
|
'max_file_count' => $template['max_file_count'] ?? 5,
|
|
'max_file_size' => $template['max_file_size'] ?? 20480,
|
|
'extra_settings' => $template['extra_settings'] ?? [],
|
|
], $data);
|
|
}
|
|
}
|
|
|
|
// 시스템/테넌트 구분
|
|
if (isset($data['tenant_id']) && $data['tenant_id']) {
|
|
// 테넌트 게시판
|
|
$data['is_system'] = false;
|
|
} else {
|
|
// 시스템 게시판
|
|
$data['is_system'] = true;
|
|
$data['tenant_id'] = null;
|
|
}
|
|
|
|
$data['created_by'] = auth()->id();
|
|
|
|
// 게시판 생성
|
|
$board = Board::create($data);
|
|
|
|
// 템플릿 기본 필드 생성
|
|
if ($template && ! empty($template['default_fields'])) {
|
|
foreach ($template['default_fields'] as $index => $field) {
|
|
$field['board_id'] = $board->id;
|
|
$field['sort_order'] = $index + 1;
|
|
$field['created_by'] = auth()->id();
|
|
BoardSetting::create($field);
|
|
}
|
|
}
|
|
|
|
return $board->load('fields');
|
|
});
|
|
}
|
|
|
|
// =========================================================================
|
|
// 테넌트 게시판 관리
|
|
// =========================================================================
|
|
|
|
/**
|
|
* 모든 게시판 목록 조회 (시스템 + 테넌트)
|
|
*/
|
|
public function getAllBoards(array $filters = [], int $perPage = 15): LengthAwarePaginator
|
|
{
|
|
$query = Board::query()
|
|
->withCount('fields')
|
|
->withTrashed();
|
|
|
|
// 게시판 유형 필터 (시스템/테넌트)
|
|
if (isset($filters['board_scope'])) {
|
|
if ($filters['board_scope'] === 'system') {
|
|
$query->where('is_system', true);
|
|
} elseif ($filters['board_scope'] === 'tenant') {
|
|
$query->where('is_system', false);
|
|
}
|
|
}
|
|
|
|
// 테넌트 필터
|
|
if (! empty($filters['tenant_id'])) {
|
|
$query->where('tenant_id', $filters['tenant_id']);
|
|
}
|
|
|
|
// 검색 필터
|
|
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 getTenantList(): Collection
|
|
{
|
|
return Tenant::query()
|
|
->active()
|
|
->orderBy('company_name')
|
|
->get(['id', 'code', 'company_name']);
|
|
}
|
|
|
|
/**
|
|
* 테넌트 게시판 코드 중복 체크
|
|
*/
|
|
public function isTenantCodeExists(string $code, int $tenantId, ?int $excludeId = null): bool
|
|
{
|
|
$query = Board::where('board_code', $code)
|
|
->where('tenant_id', $tenantId);
|
|
|
|
if ($excludeId) {
|
|
$query->where('id', '!=', $excludeId);
|
|
}
|
|
|
|
return $query->exists();
|
|
}
|
|
|
|
/**
|
|
* 게시판 상세 조회 (시스템/테넌트 공통)
|
|
*/
|
|
public function getAnyBoardById(int $id, bool $withTrashed = false): ?Board
|
|
{
|
|
$query = Board::query()
|
|
->with('fields')
|
|
->withCount('fields');
|
|
|
|
if ($withTrashed) {
|
|
$query->withTrashed();
|
|
}
|
|
|
|
return $query->find($id);
|
|
}
|
|
|
|
/**
|
|
* 게시판 수정 (시스템/테넌트 공통)
|
|
*/
|
|
public function updateAnyBoard(int $id, array $data): bool
|
|
{
|
|
$board = Board::findOrFail($id);
|
|
|
|
$data['updated_by'] = auth()->id();
|
|
|
|
// tenant_id 변경 방지 (시스템 ↔ 테넌트 전환 불가)
|
|
unset($data['tenant_id'], $data['is_system']);
|
|
|
|
return $board->update($data);
|
|
}
|
|
|
|
/**
|
|
* 게시판 삭제 (시스템/테넌트 공통, Soft Delete)
|
|
*/
|
|
public function deleteAnyBoard(int $id): bool
|
|
{
|
|
$board = Board::findOrFail($id);
|
|
|
|
$board->deleted_by = auth()->id();
|
|
$board->save();
|
|
|
|
return $board->delete();
|
|
}
|
|
|
|
/**
|
|
* 게시판 복원 (시스템/테넌트 공통)
|
|
*/
|
|
public function restoreAnyBoard(int $id): bool
|
|
{
|
|
$board = Board::onlyTrashed()->findOrFail($id);
|
|
|
|
$board->deleted_by = null;
|
|
|
|
return $board->restore();
|
|
}
|
|
|
|
/**
|
|
* 게시판 영구 삭제 (시스템/테넌트 공통)
|
|
*/
|
|
public function forceDeleteAnyBoard(int $id): bool
|
|
{
|
|
$board = Board::withTrashed()->findOrFail($id);
|
|
|
|
// 관련 필드 삭제
|
|
$board->fields()->delete();
|
|
|
|
return $board->forceDelete();
|
|
}
|
|
}
|