Files
sam-manage/app/Services/BoardService.php
kent a30410643b fix(board): 게시판 URL tenant_id 처리 및 게시글 수 표시 추가
- Route Model Binding → 수동 조회로 변경 (board_code + tenant_id)
- PostController: resolveBoard() 헬퍼 추가
  - t 파라미터 → 시스템 게시판 → 로그인 회원 tenant 순서
- 사이드바 메뉴 리다이렉트: tenant_id ?? 1 fallback 추가
  - SidebarMenuService와 동일한 로직으로 일관성 확보
- 게시판 목록 테이블에 게시글 수 컬럼 추가
- 모든 posts View에 tenant_id 파라미터 추가

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 01:30:50 +09:00

606 lines
17 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 __construct(
protected MenuService $menuService
) {}
/**
* 시스템 게시판 목록 조회 (페이지네이션)
*/
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();
$board = Board::create($data);
// 메뉴 자동 생성
$this->menuService->createMenuForBoard([
'board_code' => $board->board_code,
'name' => $board->name,
'is_system' => true,
'tenant_id' => null,
]);
return $board;
}
/**
* 게시판 수정
*/
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', []);
}
/**
* 템플릿 기반 게시판 생성
*
* @param bool $createMenu 메뉴 자동 생성 여부 (기본: true)
*/
public function createBoardFromTemplate(array $data, ?string $templateType = null, ?string $templateKey = null, bool $createMenu = true): Board
{
return DB::transaction(function () use ($data, $templateType, $templateKey, $createMenu) {
// 템플릿 설정 적용
$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);
}
}
// 메뉴 자동 생성
if ($createMenu) {
$this->menuService->createMenuForBoard([
'board_code' => $board->board_code,
'name' => $board->name,
'is_system' => $board->is_system,
'tenant_id' => $board->tenant_id,
]);
}
return $board->load('fields');
});
}
// =========================================================================
// 테넌트 게시판 관리
// =========================================================================
/**
* 모든 게시판 목록 조회 (시스템 + 테넌트)
*/
public function getAllBoards(array $filters = [], int $perPage = 15): LengthAwarePaginator
{
$query = Board::query()
->with('tenant:id,code,company_name')
->withCount(['fields', 'posts'])
->withTrashed();
// 헤더에서 선택한 테넌트 기준: 시스템 게시판 + 해당 테넌트 게시판
$selectedTenantId = session('selected_tenant_id');
if ($selectedTenantId && $selectedTenantId !== 'all') {
// 시스템 게시판 + 선택된 테넌트 게시판
$query->where(function ($q) use ($selectedTenantId) {
$q->where('is_system', true)
->orWhere('tenant_id', $selectedTenantId);
});
} else {
// 전체 보기: 시스템 게시판만 (테넌트 게시판은 테넌트 선택 후 표시)
$query->where('is_system', true);
}
// 검색 필터
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);
// 기존 값 저장 (메뉴 업데이트용)
$oldCode = $board->board_code;
$oldName = $board->name;
$data['updated_by'] = auth()->id();
// tenant_id 변경 방지 (시스템 ↔ 테넌트 전환 불가)
unset($data['tenant_id'], $data['is_system']);
$result = $board->update($data);
// board_code 또는 name 변경 시 메뉴도 업데이트
$newCode = $data['board_code'] ?? $oldCode;
$newName = $data['name'] ?? $oldName;
if ($oldCode !== $newCode || $oldName !== $newName) {
$this->menuService->updateMenuForBoard(
$oldCode,
$newCode,
$newName,
$board->is_system,
$board->tenant_id
);
}
return $result;
}
/**
* 게시판 삭제 (시스템/테넌트 공통, Soft Delete)
* - 연결된 메뉴도 함께 Soft Delete
*/
public function deleteAnyBoard(int $id): bool
{
$board = Board::findOrFail($id);
$board->deleted_by = auth()->id();
$board->save();
// 연결된 메뉴도 함께 삭제 (Soft Delete)
$this->menuService->deleteMenuForBoard(
$board->board_code,
$board->is_system,
$board->tenant_id
);
return $board->delete();
}
/**
* 게시판 복원 (시스템/테넌트 공통)
* - 연결된 메뉴도 함께 복원
*/
public function restoreAnyBoard(int $id): bool
{
$board = Board::onlyTrashed()->findOrFail($id);
$board->deleted_by = null;
// 게시판 복원
$result = $board->restore();
// 연결된 메뉴도 복원 (없으면 생성)
if ($result) {
$this->menuService->restoreMenuForBoard(
$board->board_code,
$board->name,
$board->is_system,
$board->tenant_id
);
}
return $result;
}
/**
* 게시판 영구 삭제 (시스템/테넌트 공통)
* - 연결된 메뉴도 함께 영구 삭제
*/
public function forceDeleteAnyBoard(int $id): bool
{
$board = Board::withTrashed()->findOrFail($id);
// 관련 필드 삭제
$board->fields()->delete();
// 연결된 메뉴도 함께 영구 삭제
$this->menuService->deleteMenuForBoard(
$board->board_code,
$board->is_system,
$board->tenant_id,
true // forceDelete
);
return $board->forceDelete();
}
}