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>
This commit is contained in:
@@ -22,7 +22,7 @@ public function index(Request $request): View|JsonResponse
|
||||
\Log::info('Board request all:', $request->all());
|
||||
\Log::info('Board request query:', $request->query());
|
||||
|
||||
$filters = $request->only(['search', 'board_type', 'tenant_id', 'is_active', 'trashed', 'sort_by', 'sort_direction']);
|
||||
$filters = $request->only(['search', 'board_type', 'is_active', 'trashed', 'sort_by', 'sort_direction']);
|
||||
|
||||
\Log::info('Board filters:', $filters);
|
||||
|
||||
|
||||
@@ -17,9 +17,8 @@ public function __construct(
|
||||
public function index(): View
|
||||
{
|
||||
$boardTypes = $this->boardService->getBoardTypes();
|
||||
$tenants = $this->boardService->getTenantList();
|
||||
|
||||
return view('boards.index', compact('boardTypes', 'tenants'));
|
||||
return view('boards.index', compact('boardTypes'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,11 +18,48 @@ public function __construct(
|
||||
private readonly PostService $postService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* board_code + tenant_id로 게시판 조회
|
||||
* - t 파라미터가 있으면 해당 테넌트의 게시판
|
||||
* - 없으면 시스템 게시판 우선, 그 다음 로그인 회원의 테넌트 게시판
|
||||
*/
|
||||
private function resolveBoard(string $boardCode, Request $request): Board
|
||||
{
|
||||
$tenantId = $request->query('t');
|
||||
|
||||
// t 파라미터가 있으면 해당 테넌트의 게시판 조회
|
||||
if ($tenantId) {
|
||||
return Board::where('board_code', $boardCode)
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('is_active', true)
|
||||
->firstOrFail();
|
||||
}
|
||||
|
||||
// t 파라미터가 없으면: 시스템 게시판 우선, 그 다음 로그인 회원의 테넌트 게시판
|
||||
$board = Board::where('board_code', $boardCode)
|
||||
->whereNull('tenant_id') // 시스템 게시판
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
if ($board) {
|
||||
return $board;
|
||||
}
|
||||
|
||||
// 시스템 게시판이 없으면 로그인 회원의 테넌트 게시판
|
||||
$userTenantId = auth()->user()?->tenant_id;
|
||||
|
||||
return Board::where('board_code', $boardCode)
|
||||
->where('tenant_id', $userTenantId)
|
||||
->where('is_active', true)
|
||||
->firstOrFail();
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 목록
|
||||
*/
|
||||
public function index(Board $board, Request $request): View
|
||||
public function index(string $boardCode, Request $request): View
|
||||
{
|
||||
$board = $this->resolveBoard($boardCode, $request);
|
||||
$filters = $request->only(['search', 'is_notice']);
|
||||
$posts = $this->postService->getPosts($board->id, $filters, 15);
|
||||
$notices = $this->postService->getNotices($board->id, 5);
|
||||
@@ -34,8 +71,9 @@ public function index(Board $board, Request $request): View
|
||||
/**
|
||||
* 게시글 작성 폼
|
||||
*/
|
||||
public function create(Board $board): View
|
||||
public function create(string $boardCode, Request $request): View
|
||||
{
|
||||
$board = $this->resolveBoard($boardCode, $request);
|
||||
$fields = $board->fields;
|
||||
|
||||
return view('posts.create', compact('board', 'fields'));
|
||||
@@ -44,8 +82,9 @@ public function create(Board $board): View
|
||||
/**
|
||||
* 게시글 저장
|
||||
*/
|
||||
public function store(Board $board, Request $request): RedirectResponse
|
||||
public function store(string $boardCode, Request $request): RedirectResponse
|
||||
{
|
||||
$board = $this->resolveBoard($boardCode, $request);
|
||||
$validated = $request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'content' => 'nullable|string',
|
||||
@@ -79,21 +118,23 @@ public function store(Board $board, Request $request): RedirectResponse
|
||||
} catch (\Exception $e) {
|
||||
// 파일 업로드 실패해도 게시글은 저장됨 - 경고 메시지만 표시
|
||||
return redirect()
|
||||
->route('boards.posts.show', [$board, $post])
|
||||
->route('boards.posts.show', [$board->board_code, $post, 't' => $board->tenant_id])
|
||||
->with('warning', '게시글이 작성되었으나 일부 파일 업로드에 실패했습니다: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('boards.posts.show', [$board, $post])
|
||||
->route('boards.posts.show', [$board->board_code, $post, 't' => $board->tenant_id])
|
||||
->with('success', '게시글이 작성되었습니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 상세
|
||||
*/
|
||||
public function show(Board $board, Post $post): View|RedirectResponse
|
||||
public function show(string $boardCode, Post $post, Request $request): View|RedirectResponse
|
||||
{
|
||||
$board = $this->resolveBoard($boardCode, $request);
|
||||
|
||||
// 게시판 일치 확인
|
||||
if ($post->board_id !== $board->id) {
|
||||
abort(404);
|
||||
@@ -119,8 +160,10 @@ public function show(Board $board, Post $post): View|RedirectResponse
|
||||
/**
|
||||
* 게시글 수정 폼
|
||||
*/
|
||||
public function edit(Board $board, Post $post): View|RedirectResponse
|
||||
public function edit(string $boardCode, Post $post, Request $request): View|RedirectResponse
|
||||
{
|
||||
$board = $this->resolveBoard($boardCode, $request);
|
||||
|
||||
// 게시판 일치 확인
|
||||
if ($post->board_id !== $board->id) {
|
||||
abort(404);
|
||||
@@ -140,8 +183,10 @@ public function edit(Board $board, Post $post): View|RedirectResponse
|
||||
/**
|
||||
* 게시글 수정 저장
|
||||
*/
|
||||
public function update(Board $board, Post $post, Request $request): RedirectResponse
|
||||
public function update(string $boardCode, Post $post, Request $request): RedirectResponse
|
||||
{
|
||||
$board = $this->resolveBoard($boardCode, $request);
|
||||
|
||||
// 게시판 일치 확인
|
||||
if ($post->board_id !== $board->id) {
|
||||
abort(404);
|
||||
@@ -184,21 +229,23 @@ public function update(Board $board, Post $post, Request $request): RedirectResp
|
||||
$this->postService->uploadFiles($post, $request->file('files'));
|
||||
} catch (\Exception $e) {
|
||||
return redirect()
|
||||
->route('boards.posts.show', [$board, $post])
|
||||
->route('boards.posts.show', [$board->board_code, $post, 't' => $board->tenant_id])
|
||||
->with('warning', '게시글이 수정되었으나 일부 파일 업로드에 실패했습니다: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('boards.posts.show', [$board, $post])
|
||||
->route('boards.posts.show', [$board->board_code, $post, 't' => $board->tenant_id])
|
||||
->with('success', '게시글이 수정되었습니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시글 삭제
|
||||
*/
|
||||
public function destroy(Board $board, Post $post): RedirectResponse
|
||||
public function destroy(string $boardCode, Post $post, Request $request): RedirectResponse
|
||||
{
|
||||
$board = $this->resolveBoard($boardCode, $request);
|
||||
|
||||
// 게시판 일치 확인
|
||||
if ($post->board_id !== $board->id) {
|
||||
abort(404);
|
||||
@@ -215,7 +262,7 @@ public function destroy(Board $board, Post $post): RedirectResponse
|
||||
$this->postService->deletePost($post);
|
||||
|
||||
return redirect()
|
||||
->route('boards.posts.index', $board)
|
||||
->route('boards.posts.index', [$board->board_code, 't' => $board->tenant_id])
|
||||
->with('success', '게시글이 삭제되었습니다.');
|
||||
}
|
||||
|
||||
@@ -226,8 +273,10 @@ public function destroy(Board $board, Post $post): RedirectResponse
|
||||
/**
|
||||
* 파일 업로드 (AJAX)
|
||||
*/
|
||||
public function uploadFiles(Board $board, Post $post, Request $request): JsonResponse
|
||||
public function uploadFiles(string $boardCode, Post $post, Request $request): JsonResponse
|
||||
{
|
||||
$board = $this->resolveBoard($boardCode, $request);
|
||||
|
||||
// 게시판 일치 확인
|
||||
if ($post->board_id !== $board->id) {
|
||||
return response()->json(['success' => false, 'message' => '게시글을 찾을 수 없습니다.'], 404);
|
||||
@@ -269,8 +318,10 @@ public function uploadFiles(Board $board, Post $post, Request $request): JsonRes
|
||||
/**
|
||||
* 파일 다운로드
|
||||
*/
|
||||
public function downloadFile(Board $board, Post $post, int $fileId): BinaryFileResponse|StreamedResponse|RedirectResponse
|
||||
public function downloadFile(string $boardCode, Post $post, int $fileId, Request $request): BinaryFileResponse|StreamedResponse|RedirectResponse
|
||||
{
|
||||
$board = $this->resolveBoard($boardCode, $request);
|
||||
|
||||
// 게시판 일치 확인
|
||||
if ($post->board_id !== $board->id) {
|
||||
abort(404);
|
||||
@@ -287,8 +338,10 @@ public function downloadFile(Board $board, Post $post, int $fileId): BinaryFileR
|
||||
/**
|
||||
* 파일 미리보기 (이미지 인라인 표시)
|
||||
*/
|
||||
public function previewFile(Board $board, Post $post, int $fileId): BinaryFileResponse|StreamedResponse|RedirectResponse
|
||||
public function previewFile(string $boardCode, Post $post, int $fileId, Request $request): BinaryFileResponse|StreamedResponse|RedirectResponse
|
||||
{
|
||||
$board = $this->resolveBoard($boardCode, $request);
|
||||
|
||||
// 게시판 일치 확인
|
||||
if ($post->board_id !== $board->id) {
|
||||
abort(404);
|
||||
@@ -305,8 +358,10 @@ public function previewFile(Board $board, Post $post, int $fileId): BinaryFileRe
|
||||
/**
|
||||
* 파일 삭제 (AJAX)
|
||||
*/
|
||||
public function deleteFile(Board $board, Post $post, int $fileId): JsonResponse
|
||||
public function deleteFile(string $boardCode, Post $post, int $fileId, Request $request): JsonResponse
|
||||
{
|
||||
$board = $this->resolveBoard($boardCode, $request);
|
||||
|
||||
// 게시판 일치 확인
|
||||
if ($post->board_id !== $board->id) {
|
||||
return response()->json(['success' => false, 'message' => '게시글을 찾을 수 없습니다.'], 404);
|
||||
|
||||
@@ -403,7 +403,7 @@ public function getAllBoards(array $filters = [], int $perPage = 15): LengthAwar
|
||||
{
|
||||
$query = Board::query()
|
||||
->with('tenant:id,code,company_name')
|
||||
->withCount('fields')
|
||||
->withCount(['fields', 'posts'])
|
||||
->withTrashed();
|
||||
|
||||
// 헤더에서 선택한 테넌트 기준: 시스템 게시판 + 해당 테넌트 게시판
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Boards\Board;
|
||||
use App\Models\Commons\Menu;
|
||||
use App\Models\Tenants\Department;
|
||||
use App\Models\User;
|
||||
@@ -160,12 +161,73 @@ public function isMenuActive(Menu $menu): bool
|
||||
if ($menu->url) {
|
||||
$currentPath = '/'.ltrim(request()->path(), '/');
|
||||
|
||||
// /boards/{id}/posts 패턴인 경우 게시판 기반 메뉴 매칭
|
||||
$boardMenuUrl = $this->getBoardMenuUrl();
|
||||
if ($boardMenuUrl) {
|
||||
return $menu->url === $boardMenuUrl;
|
||||
}
|
||||
|
||||
return $currentPath === $menu->url || str_starts_with($currentPath, $menu->url.'/');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 /boards/{code}/posts 경로인 경우 해당 게시판의 메뉴 URL 반환
|
||||
*/
|
||||
private function getBoardMenuUrl(): ?string
|
||||
{
|
||||
static $cachedUrl = null;
|
||||
static $calculated = false;
|
||||
|
||||
if ($calculated) {
|
||||
return $cachedUrl;
|
||||
}
|
||||
|
||||
$calculated = true;
|
||||
$currentPath = request()->path();
|
||||
|
||||
// /boards/{code}/posts 패턴 확인 (code는 영문자로 시작하는 문자열)
|
||||
if (! preg_match('#^boards/([a-zA-Z][a-zA-Z0-9_-]*)(/posts.*)?$#', $currentPath, $matches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$boardCode = $matches[1];
|
||||
|
||||
// 게시판 조회 (board_code로)
|
||||
$board = Board::withoutGlobalScopes()->where('board_code', $boardCode)->first();
|
||||
if (! $board) {
|
||||
// 게시판 없으면 게시판 관리 메뉴
|
||||
$cachedUrl = '/boards';
|
||||
|
||||
return $cachedUrl;
|
||||
}
|
||||
|
||||
// 시스템 게시판: /customer-center/{board_code}
|
||||
// 테넌트 게시판: /boards/{board_code}
|
||||
if ($board->is_system) {
|
||||
$expectedUrl = '/customer-center/'.$board->board_code;
|
||||
} else {
|
||||
$expectedUrl = '/boards/'.$board->board_code;
|
||||
}
|
||||
|
||||
// 해당 URL의 메뉴가 존재하는지 확인
|
||||
$menuExists = Menu::withoutGlobalScopes()
|
||||
->where('url', $expectedUrl)
|
||||
->where('is_active', true)
|
||||
->exists();
|
||||
|
||||
if ($menuExists) {
|
||||
$cachedUrl = $expectedUrl;
|
||||
} else {
|
||||
// 메뉴 없으면 게시판 관리 메뉴
|
||||
$cachedUrl = '/boards';
|
||||
}
|
||||
|
||||
return $cachedUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 또는 자식 메뉴가 활성 상태인지 확인
|
||||
*/
|
||||
|
||||
@@ -22,16 +22,6 @@
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<!-- 테넌트 선택 (전체=시스템 게시판, 선택=해당 테넌트 게시판) -->
|
||||
<div class="w-full sm:w-48">
|
||||
<select name="tenant_id" id="tenantSelect" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">시스템 게시판</option>
|
||||
@foreach($tenants as $tenant)
|
||||
<option value="{{ $tenant->id }}">{{ $tenant->company_name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 게시판 유형 필터 -->
|
||||
<div class="w-full sm:w-40">
|
||||
<select name="board_type" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
@@ -87,7 +77,6 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
const csrfToken = '{{ csrf_token() }}';
|
||||
const boardTable = document.getElementById('board-table');
|
||||
const filterForm = document.getElementById('filterForm');
|
||||
const tenantSelect = document.getElementById('tenantSelect');
|
||||
|
||||
// 테이블 로드 함수
|
||||
function loadTable() {
|
||||
@@ -115,11 +104,6 @@ function loadTable() {
|
||||
loadTable();
|
||||
});
|
||||
|
||||
// 테넌트 선택 변경 시 자동 검색
|
||||
tenantSelect.addEventListener('change', function() {
|
||||
loadTable();
|
||||
});
|
||||
|
||||
// 삭제 확인
|
||||
window.confirmDelete = function(id, name) {
|
||||
showDeleteConfirm(name, () => {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">게시판명</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">유형</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">필드 수</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">게시글 수</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">상태</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">생성일</th>
|
||||
<th class="px-3 py-2 text-center text-sm font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap">액션</th>
|
||||
@@ -18,7 +19,7 @@
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@forelse($boards as $board)
|
||||
<tr class="{{ $board->trashed() ? 'bg-red-50 opacity-60' : 'hover:bg-gray-50 cursor-pointer' }}"
|
||||
@unless($board->trashed()) onclick="window.location='{{ route('boards.posts.index', $board->id) }}'" @endunless>
|
||||
@unless($board->trashed()) onclick="window.location='{{ route('boards.posts.index', ['boardCode' => $board->board_code, 't' => $board->tenant_id]) }}'" @endunless>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{{ $board->id }}
|
||||
</td>
|
||||
@@ -59,9 +60,12 @@
|
||||
<span class="text-gray-400">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-center">
|
||||
{{ $board->fields_count ?? 0 }}개
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-center">
|
||||
<span class="px-2 py-1 bg-blue-50 text-blue-700 rounded-full">{{ number_format($board->posts_count ?? 0) }}건</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
@if($board->trashed())
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
|
||||
@@ -103,7 +107,7 @@ class="text-red-600 hover:text-red-900">삭제</button>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="9" class="px-6 py-12 text-center text-gray-500">
|
||||
<td colspan="10" class="px-6 py-12 text-center text-gray-500">
|
||||
게시판이 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@forelse($boards as $board)
|
||||
<tr class="hover:bg-gray-50 cursor-pointer" onclick="window.location='{{ route('boards.posts.index', $board->id) }}'">
|
||||
<tr class="hover:bg-gray-50 cursor-pointer" onclick="window.location='{{ route('boards.posts.index', ['boardCode' => $board->board_code]) }}'">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{{ $board->id }}
|
||||
</td>
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
<h1 class="text-2xl font-bold text-gray-800">글쓰기</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">{{ $board->name }}</p>
|
||||
</div>
|
||||
<a href="{{ route('boards.posts.index', $board) }}" class="text-gray-600 hover:text-gray-900">
|
||||
<a href="{{ route('boards.posts.index', ['boardCode' => $board->board_code, 't' => $board->tenant_id]) }}" class="text-gray-600 hover:text-gray-900">
|
||||
← 목록으로
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 작성 폼 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<form action="{{ route('boards.posts.store', $board) }}" method="POST" enctype="multipart/form-data">
|
||||
<form action="{{ route('boards.posts.store', ['boardCode' => $board->board_code, 't' => $board->tenant_id]) }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
|
||||
<!-- 제목 -->
|
||||
@@ -179,7 +179,7 @@ class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
||||
|
||||
<!-- 버튼 -->
|
||||
<div class="flex justify-end gap-3">
|
||||
<a href="{{ route('boards.posts.index', $board) }}"
|
||||
<a href="{{ route('boards.posts.index', ['boardCode' => $board->board_code, 't' => $board->tenant_id]) }}"
|
||||
class="px-6 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition">
|
||||
취소
|
||||
</a>
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
<h1 class="text-2xl font-bold text-gray-800">게시글 수정</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">{{ $board->name }}</p>
|
||||
</div>
|
||||
<a href="{{ route('boards.posts.show', [$board, $post]) }}" class="text-gray-600 hover:text-gray-900">
|
||||
<a href="{{ route('boards.posts.show', ['boardCode' => $board->board_code, 'post' => $post, 't' => $board->tenant_id]) }}" class="text-gray-600 hover:text-gray-900">
|
||||
← 돌아가기
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 수정 폼 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<form action="{{ route('boards.posts.update', [$board, $post]) }}" method="POST" enctype="multipart/form-data">
|
||||
<form action="{{ route('boards.posts.update', ['boardCode' => $board->board_code, 'post' => $post, 't' => $board->tenant_id]) }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
@@ -219,7 +219,7 @@ class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
||||
|
||||
<!-- 버튼 -->
|
||||
<div class="flex justify-end gap-3">
|
||||
<a href="{{ route('boards.posts.show', [$board, $post]) }}"
|
||||
<a href="{{ route('boards.posts.show', ['boardCode' => $board->board_code, 'post' => $post, 't' => $board->tenant_id]) }}"
|
||||
class="px-6 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition">
|
||||
취소
|
||||
</a>
|
||||
@@ -336,7 +336,7 @@ function formatFileSize(bytes) {
|
||||
|
||||
function deleteFile(fileId) {
|
||||
showConfirm('이 파일을 삭제하시겠습니까?', () => {
|
||||
const url = '{{ route("boards.posts.files.delete", [$board, $post, ":fileId"]) }}'.replace(':fileId', fileId);
|
||||
const url = '{{ route("boards.posts.files.delete", ["boardCode" => $board->board_code, "post" => $post, "fileId" => ":fileId", "t" => $board->tenant_id]) }}'.replace(':fileId', fileId);
|
||||
|
||||
fetch(url, {
|
||||
method: 'DELETE',
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="{{ route('boards.posts.create', $board) }}"
|
||||
<a href="{{ route('boards.posts.create', ['boardCode' => $board->board_code, 't' => $board->tenant_id]) }}"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
||||
@@ -51,7 +51,8 @@ class="px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg text-sm
|
||||
|
||||
<!-- 검색 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 mb-6">
|
||||
<form action="{{ route('boards.posts.index', $board) }}" method="GET" class="flex items-center gap-4">
|
||||
<form action="{{ route('boards.posts.index', ['boardCode' => $board->board_code, 't' => $board->tenant_id]) }}" method="GET" class="flex items-center gap-4">
|
||||
<input type="hidden" name="t" value="{{ $board->tenant_id }}">
|
||||
<div class="flex-1">
|
||||
<input type="text" name="search" value="{{ $filters['search'] ?? '' }}"
|
||||
placeholder="제목, 내용 검색..."
|
||||
@@ -61,7 +62,7 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
검색
|
||||
</button>
|
||||
@if(!empty($filters['search']))
|
||||
<a href="{{ route('boards.posts.index', $board) }}" class="px-4 py-2 text-gray-500 hover:text-gray-700">
|
||||
<a href="{{ route('boards.posts.index', ['boardCode' => $board->board_code, 't' => $board->tenant_id]) }}" class="px-4 py-2 text-gray-500 hover:text-gray-700">
|
||||
초기화
|
||||
</a>
|
||||
@endif
|
||||
@@ -93,7 +94,7 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<a href="{{ route('boards.posts.show', [$board, $post]) }}"
|
||||
<a href="{{ route('boards.posts.show', ['boardCode' => $board->board_code, 'post' => $post, 't' => $board->tenant_id]) }}"
|
||||
class="text-gray-900 hover:text-blue-600 font-medium">
|
||||
@if($post->is_secret)
|
||||
<svg class="w-4 h-4 inline-block text-gray-400 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -120,7 +121,7 @@ class="text-gray-900 hover:text-blue-600 font-medium">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
<p>게시글이 없습니다.</p>
|
||||
<a href="{{ route('boards.posts.create', $board) }}" class="mt-2 inline-block text-blue-600 hover:text-blue-800">
|
||||
<a href="{{ route('boards.posts.create', ['boardCode' => $board->board_code, 't' => $board->tenant_id]) }}" class="mt-2 inline-block text-blue-600 hover:text-blue-800">
|
||||
첫 게시글 작성하기
|
||||
</a>
|
||||
</td>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
{{ $post->title }}
|
||||
</h1>
|
||||
</div>
|
||||
<a href="{{ route('boards.posts.index', $board) }}" class="text-gray-600 hover:text-gray-900">
|
||||
<a href="{{ route('boards.posts.index', ['boardCode' => $board->board_code, 't' => $board->tenant_id]) }}" class="text-gray-600 hover:text-gray-900">
|
||||
← 목록으로
|
||||
</a>
|
||||
</div>
|
||||
@@ -47,11 +47,11 @@
|
||||
</div>
|
||||
@if($post->user_id === auth()->id() || auth()->user()->hasRole(['admin', 'super-admin']))
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="{{ route('boards.posts.edit', [$board, $post]) }}"
|
||||
<a href="{{ route('boards.posts.edit', ['boardCode' => $board->board_code, 'post' => $post, 't' => $board->tenant_id]) }}"
|
||||
class="px-3 py-1 text-sm text-blue-600 hover:text-blue-800 border border-blue-300 rounded hover:bg-blue-50 transition">
|
||||
수정
|
||||
</a>
|
||||
<form id="deletePostForm" action="{{ route('boards.posts.destroy', [$board, $post]) }}" method="POST">
|
||||
<form id="deletePostForm" action="{{ route('boards.posts.destroy', ['boardCode' => $board->board_code, 'post' => $post, 't' => $board->tenant_id]) }}" method="POST">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="button" onclick="confirmDeletePost()"
|
||||
@@ -96,10 +96,10 @@ function confirmDeletePost() {
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<div class="space-y-4">
|
||||
@foreach($imageFiles as $file)
|
||||
<a href="{{ route('boards.posts.files.preview', [$board, $post, $file->id]) }}"
|
||||
<a href="{{ route('boards.posts.files.preview', ['boardCode' => $board->board_code, 'post' => $post, 'fileId' => $file->id, 't' => $board->tenant_id]) }}"
|
||||
target="_blank"
|
||||
class="block overflow-hidden rounded-lg border border-gray-200 hover:border-blue-400 transition">
|
||||
<img src="{{ route('boards.posts.files.preview', [$board, $post, $file->id]) }}"
|
||||
<img src="{{ route('boards.posts.files.preview', ['boardCode' => $board->board_code, 'post' => $post, 'fileId' => $file->id, 't' => $board->tenant_id]) }}"
|
||||
alt="{{ $file->display_name ?? $file->original_name }}"
|
||||
class="w-full h-auto">
|
||||
</a>
|
||||
@@ -133,7 +133,7 @@ class="w-full h-auto">
|
||||
<span class="text-xs text-gray-400 ml-2">({{ $file->getFormattedSize() }})</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ route('boards.posts.files.download', [$board, $post, $file->id]) }}"
|
||||
<a href="{{ route('boards.posts.files.download', ['boardCode' => $board->board_code, 'post' => $post, 'fileId' => $file->id, 't' => $board->tenant_id]) }}"
|
||||
class="px-3 py-1 text-sm text-blue-600 hover:text-blue-800 border border-blue-300 rounded hover:bg-blue-50 transition">
|
||||
다운로드
|
||||
</a>
|
||||
@@ -148,7 +148,7 @@ class="px-3 py-1 text-sm text-blue-600 hover:text-blue-800 border border-blue-30
|
||||
<div class="mt-6 bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
<div class="divide-y divide-gray-200">
|
||||
@if($adjacent['prev'])
|
||||
<a href="{{ route('boards.posts.show', [$board, $adjacent['prev']->id]) }}"
|
||||
<a href="{{ route('boards.posts.show', ['boardCode' => $board->board_code, 'post' => $adjacent['prev']->id, 't' => $board->tenant_id]) }}"
|
||||
class="block px-6 py-4 hover:bg-gray-50 transition">
|
||||
<div class="flex items-center">
|
||||
<span class="text-sm text-gray-500 w-20">이전글</span>
|
||||
@@ -157,7 +157,7 @@ class="block px-6 py-4 hover:bg-gray-50 transition">
|
||||
</a>
|
||||
@endif
|
||||
@if($adjacent['next'])
|
||||
<a href="{{ route('boards.posts.show', [$board, $adjacent['next']->id]) }}"
|
||||
<a href="{{ route('boards.posts.show', ['boardCode' => $board->board_code, 'post' => $adjacent['next']->id, 't' => $board->tenant_id]) }}"
|
||||
class="block px-6 py-4 hover:bg-gray-50 transition">
|
||||
<div class="flex items-center">
|
||||
<span class="text-sm text-gray-500 w-20">다음글</span>
|
||||
@@ -170,7 +170,7 @@ class="block px-6 py-4 hover:bg-gray-50 transition">
|
||||
|
||||
<!-- 목록 버튼 -->
|
||||
<div class="mt-6 flex justify-center">
|
||||
<a href="{{ route('boards.posts.index', $board) }}"
|
||||
<a href="{{ route('boards.posts.index', ['boardCode' => $board->board_code, 't' => $board->tenant_id]) }}"
|
||||
class="px-6 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition">
|
||||
목록
|
||||
</a>
|
||||
|
||||
@@ -108,14 +108,14 @@
|
||||
// 고객센터 (활성화된 시스템 게시판 목록)
|
||||
Route::get('/customer-center', [CustomerCenterController::class, 'index'])->name('customer-center.index');
|
||||
|
||||
// 고객센터 게시판 리다이렉트 (board_code → boards/{id}/posts)
|
||||
// 고객센터 게시판 리다이렉트 (board_code → boards/{code}/posts)
|
||||
Route::get('/customer-center/{code}', function (string $code) {
|
||||
$board = \App\Models\Boards\Board::where('board_code', $code)
|
||||
->whereNull('tenant_id')
|
||||
->where('is_active', true)
|
||||
->firstOrFail();
|
||||
|
||||
return redirect()->route('boards.posts.index', $board->id);
|
||||
return redirect()->route('boards.posts.index', $board->board_code);
|
||||
})->name('customer-center.board');
|
||||
|
||||
// 시스템 게시판 관리 (Blade 화면만)
|
||||
@@ -124,25 +124,42 @@
|
||||
Route::get('/create', [BoardController::class, 'create'])->name('create');
|
||||
Route::get('/{id}/edit', [BoardController::class, 'edit'])->name('edit');
|
||||
|
||||
// 테넌트 게시판 리다이렉트 (board_code → boards/{id}/posts)
|
||||
// 사이드바 메뉴에서 접근 시 리다이렉트 (board_code → boards/{boardCode}/posts?t=)
|
||||
// 좌측 메뉴는 로그인 회원의 테넌트 게시판 사용
|
||||
Route::get('/{code}', function (string $code) {
|
||||
// 숫자만 있는 경우 (기존 라우트와 충돌 방지)
|
||||
if (is_numeric($code)) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$tenantId = session('selected_tenant_id');
|
||||
// 로그인 회원의 tenant_id 사용 (사이드바 메뉴용)
|
||||
// SidebarMenuService와 동일하게 tenant_id가 null이면 1 사용
|
||||
$userTenantId = auth()->user()?->tenant_id ?? 1;
|
||||
|
||||
// 시스템 게시판 우선, 그 다음 로그인 회원의 테넌트 게시판
|
||||
$board = \App\Models\Boards\Board::where('board_code', $code)
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereNull('tenant_id')
|
||||
->where('is_active', true)
|
||||
->firstOrFail();
|
||||
->first();
|
||||
|
||||
return redirect()->route('boards.posts.index', $board->id);
|
||||
if (! $board) {
|
||||
$board = \App\Models\Boards\Board::where('board_code', $code)
|
||||
->where('tenant_id', $userTenantId)
|
||||
->where('is_active', true)
|
||||
->firstOrFail();
|
||||
}
|
||||
|
||||
// t 파라미터 포함하여 리다이렉트
|
||||
return redirect()->route('boards.posts.index', [
|
||||
'boardCode' => $board->board_code,
|
||||
't' => $board->tenant_id,
|
||||
]);
|
||||
})->name('tenant-board')->where('code', '[a-zA-Z][a-zA-Z0-9_-]*');
|
||||
|
||||
// 게시글 CRUD (board 하위 중첩 라우트)
|
||||
Route::prefix('{board}/posts')->name('posts.')->group(function () {
|
||||
// board_code + tenant_id(query param 't')로 게시판 조회
|
||||
// tenant_id 없으면 로그인 회원의 tenant_id 사용
|
||||
Route::prefix('{boardCode}/posts')->name('posts.')->group(function () {
|
||||
Route::get('/', [PostController::class, 'index'])->name('index');
|
||||
Route::get('/create', [PostController::class, 'create'])->name('create');
|
||||
Route::post('/', [PostController::class, 'store'])->name('store');
|
||||
@@ -158,7 +175,7 @@
|
||||
Route::post('/', [PostController::class, 'uploadFiles'])->name('upload');
|
||||
Route::delete('/{fileId}', [PostController::class, 'deleteFile'])->name('delete');
|
||||
});
|
||||
});
|
||||
})->where('boardCode', '[a-zA-Z][a-zA-Z0-9_-]*');
|
||||
});
|
||||
|
||||
// 역할 권한 관리 (Blade 화면만)
|
||||
|
||||
Reference in New Issue
Block a user