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:
2025-12-01 23:27:37 +09:00
parent d992a19735
commit 817690f544
5 changed files with 1187 additions and 37 deletions

View File

@@ -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],
]);
}
}

View File

@@ -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();
}
}