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:
2025-12-28 01:30:50 +09:00
parent 5b8ecf02ab
commit a30410643b
13 changed files with 192 additions and 70 deletions

View File

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

View File

@@ -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'));
}
/**

View File

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

View File

@@ -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();
// 헤더에서 선택한 테넌트 기준: 시스템 게시판 + 해당 테넌트 게시판

View File

@@ -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;
}
/**
* 메뉴 또는 자식 메뉴가 활성 상태인지 확인
*/

View File

@@ -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, () => {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">
&larr; 목록으로
</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>

View File

@@ -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">
&larr; 돌아가기
</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',

View File

@@ -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>

View File

@@ -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">
&larr; 목록으로
</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>

View File

@@ -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 화면만)