feat: 게시판 템플릿 기반 생성 기능 및 SVG 아이콘 적용
- 게시판 템플릿 설정 파일 추가 (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>
This commit is contained in:
@@ -281,4 +281,140 @@ public function reorderFields(Request $request, int $id): JsonResponse
|
||||
'message' => '필드 순서가 변경되었습니다.',
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 템플릿 API
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 템플릿 목록 조회
|
||||
*/
|
||||
public function templates(Request $request): JsonResponse
|
||||
{
|
||||
$type = $request->get('type', 'all'); // system, tenant, all
|
||||
|
||||
$templates = $this->boardService->getTemplates($type);
|
||||
$baseFields = $this->boardService->getBaseFields();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'templates' => $templates,
|
||||
'base_fields' => $baseFields,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 템플릿 상세 조회
|
||||
*/
|
||||
public function templateDetail(string $type, string $key): JsonResponse
|
||||
{
|
||||
$template = $this->boardService->getTemplate($type, $key);
|
||||
|
||||
if (! $template) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '템플릿을 찾을 수 없습니다.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $template,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 기반 게시판 생성
|
||||
*/
|
||||
public function storeFromTemplate(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'board_code' => 'required|string|max:50',
|
||||
'name' => 'required|string|max:100',
|
||||
'template_type' => 'nullable|string|in:system,tenant',
|
||||
'template_key' => 'nullable|string|max:50',
|
||||
'tenant_id' => 'nullable|integer|exists:tenants,id',
|
||||
'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 (! empty($validated['tenant_id'])) {
|
||||
// 테넌트 게시판
|
||||
if ($this->boardService->isTenantCodeExists($validated['board_code'], $validated['tenant_id'])) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '해당 테넌트에서 이미 사용 중인 게시판 코드입니다.',
|
||||
], 422);
|
||||
}
|
||||
} else {
|
||||
// 시스템 게시판
|
||||
if ($this->boardService->isCodeExists($validated['board_code'])) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '이미 사용 중인 게시판 코드입니다.',
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
|
||||
$board = $this->boardService->createBoardFromTemplate(
|
||||
$validated,
|
||||
$validated['template_type'] ?? null,
|
||||
$validated['template_key'] ?? null
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '게시판이 생성되었습니다.',
|
||||
'data' => $board,
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 테넌트 관련 API
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 테넌트 목록 조회 (드롭다운용)
|
||||
*/
|
||||
public function tenants(): JsonResponse
|
||||
{
|
||||
$tenants = $this->boardService->getTenantList();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $tenants,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 게시판 코드 중복 체크
|
||||
*/
|
||||
public function checkTenantCode(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'code' => 'required|string|max:50',
|
||||
'tenant_id' => 'required|integer|exists:tenants,id',
|
||||
'exclude_id' => 'nullable|integer',
|
||||
]);
|
||||
|
||||
$exists = $this->boardService->isTenantCodeExists(
|
||||
$validated['code'],
|
||||
$validated['tenant_id'],
|
||||
$validated['exclude_id'] ?? null
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => ['exists' => $exists],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
|
||||
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
|
||||
{
|
||||
@@ -268,4 +270,258 @@ public function getBoardTypes(): array
|
||||
->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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user