Files
sam-manage/app/Http/Controllers/PostController.php
kent da159cc46e feat(boards): 게시글 파일 시스템 개선 및 이미지 미리보기 추가
- Morph map에 Post, Department 모델 등록 (ClassMorphViolationException 해결)
- 파일 저장 방식을 API 스타일로 변경 (document_id + document_type)
- 파일 미리보기 라우트 및 메서드 추가 (previewFile)
- 게시글 상세 페이지에서 이미지 첨부파일을 본문 상단에 풀 너비로 표시
- 비이미지 첨부파일은 하단에 다운로드 목록으로 분리

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 17:08:53 +09:00

329 lines
11 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Boards\Board;
use App\Models\Boards\Post;
use App\Services\PostService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
class PostController extends Controller
{
public function __construct(
private readonly PostService $postService
) {}
/**
* 게시글 목록
*/
public function index(Board $board, Request $request): View
{
$filters = $request->only(['search', 'is_notice']);
$posts = $this->postService->getPosts($board->id, $filters, 15);
$notices = $this->postService->getNotices($board->id, 5);
$stats = $this->postService->getBoardPostStats($board->id);
return view('posts.index', compact('board', 'posts', 'notices', 'stats', 'filters'));
}
/**
* 게시글 작성 폼
*/
public function create(Board $board): View
{
$fields = $board->fields;
return view('posts.create', compact('board', 'fields'));
}
/**
* 게시글 저장
*/
public function store(Board $board, Request $request): RedirectResponse
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'nullable|string',
'is_notice' => 'nullable|boolean',
'is_secret' => 'nullable|boolean',
'files' => 'nullable|array',
'files.*' => 'file|max:'.($board->max_file_size),
]);
// 커스텀 필드 값
$customFields = $request->input('custom_fields', []);
// 커스텀 필드 유효성 검사
foreach ($board->fields as $field) {
if ($field->is_required && empty($customFields[$field->field_key])) {
return back()
->withInput()
->withErrors([$field->field_key => "{$field->name}은(는) 필수입니다."]);
}
}
$validated['is_notice'] = $request->boolean('is_notice');
$validated['is_secret'] = $request->boolean('is_secret');
$post = $this->postService->createPost($board, $validated, $customFields);
// 파일 업로드 처리
if ($request->hasFile('files') && $board->allow_files) {
try {
$this->postService->uploadFiles($post, $request->file('files'));
} catch (\Exception $e) {
// 파일 업로드 실패해도 게시글은 저장됨 - 경고 메시지만 표시
return redirect()
->route('boards.posts.show', [$board, $post])
->with('warning', '게시글이 작성되었으나 일부 파일 업로드에 실패했습니다: '.$e->getMessage());
}
}
return redirect()
->route('boards.posts.show', [$board, $post])
->with('success', '게시글이 작성되었습니다.');
}
/**
* 게시글 상세
*/
public function show(Board $board, Post $post): View|RedirectResponse
{
// 게시판 일치 확인
if ($post->board_id !== $board->id) {
abort(404);
}
// 비밀글 접근 권한 확인
if (! $post->canAccess(auth()->user())) {
return back()->with('error', '비밀글은 작성자만 볼 수 있습니다.');
}
// 조회수 증가
$this->postService->incrementViews($post);
// 이전/다음 글
$adjacent = $this->postService->getAdjacentPosts($post);
// 커스텀 필드 값
$customFieldValues = $post->getCustomFieldsArray();
return view('posts.show', compact('board', 'post', 'adjacent', 'customFieldValues'));
}
/**
* 게시글 수정 폼
*/
public function edit(Board $board, Post $post): View|RedirectResponse
{
// 게시판 일치 확인
if ($post->board_id !== $board->id) {
abort(404);
}
// 수정 권한 확인 (작성자 또는 관리자)
if ($post->user_id !== auth()->id() && ! auth()->user()->hasRole(['admin', 'super-admin'])) {
return back()->with('error', '수정 권한이 없습니다.');
}
$fields = $board->fields;
$customFieldValues = $post->getCustomFieldsArray();
return view('posts.edit', compact('board', 'post', 'fields', 'customFieldValues'));
}
/**
* 게시글 수정 저장
*/
public function update(Board $board, Post $post, Request $request): RedirectResponse
{
// 게시판 일치 확인
if ($post->board_id !== $board->id) {
abort(404);
}
// 수정 권한 확인
if ($post->user_id !== auth()->id() && ! auth()->user()->hasRole(['admin', 'super-admin'])) {
return back()->with('error', '수정 권한이 없습니다.');
}
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'nullable|string',
'is_notice' => 'nullable|boolean',
'is_secret' => 'nullable|boolean',
'files' => 'nullable|array',
'files.*' => 'file|max:'.($board->max_file_size),
]);
// 커스텀 필드 값
$customFields = $request->input('custom_fields', []);
// 커스텀 필드 유효성 검사
foreach ($board->fields as $field) {
if ($field->is_required && empty($customFields[$field->field_key])) {
return back()
->withInput()
->withErrors([$field->field_key => "{$field->name}은(는) 필수입니다."]);
}
}
$validated['is_notice'] = $request->boolean('is_notice');
$validated['is_secret'] = $request->boolean('is_secret');
$this->postService->updatePost($post, $validated, $customFields);
// 파일 업로드 처리
if ($request->hasFile('files') && $board->allow_files) {
try {
$this->postService->uploadFiles($post, $request->file('files'));
} catch (\Exception $e) {
return redirect()
->route('boards.posts.show', [$board, $post])
->with('warning', '게시글이 수정되었으나 일부 파일 업로드에 실패했습니다: '.$e->getMessage());
}
}
return redirect()
->route('boards.posts.show', [$board, $post])
->with('success', '게시글이 수정되었습니다.');
}
/**
* 게시글 삭제
*/
public function destroy(Board $board, Post $post): RedirectResponse
{
// 게시판 일치 확인
if ($post->board_id !== $board->id) {
abort(404);
}
// 삭제 권한 확인
if ($post->user_id !== auth()->id() && ! auth()->user()->hasRole(['admin', 'super-admin'])) {
return back()->with('error', '삭제 권한이 없습니다.');
}
// 첨부 파일 삭제
$this->postService->deleteAllFiles($post);
$this->postService->deletePost($post);
return redirect()
->route('boards.posts.index', $board)
->with('success', '게시글이 삭제되었습니다.');
}
// =========================================================================
// File Management
// =========================================================================
/**
* 파일 업로드 (AJAX)
*/
public function uploadFiles(Board $board, Post $post, Request $request): JsonResponse
{
// 게시판 일치 확인
if ($post->board_id !== $board->id) {
return response()->json(['success' => false, 'message' => '게시글을 찾을 수 없습니다.'], 404);
}
// 수정 권한 확인
if ($post->user_id !== auth()->id() && ! auth()->user()->hasRole(['admin', 'super-admin'])) {
return response()->json(['success' => false, 'message' => '권한이 없습니다.'], 403);
}
// 파일 첨부 허용 여부
if (! $board->allow_files) {
return response()->json(['success' => false, 'message' => '이 게시판은 파일 첨부가 허용되지 않습니다.'], 400);
}
$request->validate([
'files' => 'required|array',
'files.*' => 'file|max:'.($board->max_file_size),
]);
try {
$uploadedFiles = $this->postService->uploadFiles($post, $request->file('files'));
return response()->json([
'success' => true,
'message' => count($uploadedFiles).'개의 파일이 업로드되었습니다.',
'files' => collect($uploadedFiles)->map(fn ($file) => [
'id' => $file->id,
'name' => $file->display_name ?? $file->original_name,
'size' => $file->getFormattedSize(),
'type' => $file->file_type,
]),
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => $e->getMessage()], 400);
}
}
/**
* 파일 다운로드
*/
public function downloadFile(Board $board, Post $post, int $fileId): BinaryFileResponse|StreamedResponse|RedirectResponse
{
// 게시판 일치 확인
if ($post->board_id !== $board->id) {
abort(404);
}
// 비밀글 접근 권한 확인
if (! $post->canAccess(auth()->user())) {
return back()->with('error', '파일에 접근할 수 없습니다.');
}
return $this->postService->downloadFile($post, $fileId);
}
/**
* 파일 미리보기 (이미지 인라인 표시)
*/
public function previewFile(Board $board, Post $post, int $fileId): BinaryFileResponse|StreamedResponse|RedirectResponse
{
// 게시판 일치 확인
if ($post->board_id !== $board->id) {
abort(404);
}
// 비밀글 접근 권한 확인
if (! $post->canAccess(auth()->user())) {
return back()->with('error', '파일에 접근할 수 없습니다.');
}
return $this->postService->previewFile($post, $fileId);
}
/**
* 파일 삭제 (AJAX)
*/
public function deleteFile(Board $board, Post $post, int $fileId): JsonResponse
{
// 게시판 일치 확인
if ($post->board_id !== $board->id) {
return response()->json(['success' => false, 'message' => '게시글을 찾을 수 없습니다.'], 404);
}
// 삭제 권한 확인 (작성자 또는 관리자)
if ($post->user_id !== auth()->id() && ! auth()->user()->hasRole(['admin', 'super-admin'])) {
return response()->json(['success' => false, 'message' => '권한이 없습니다.'], 403);
}
try {
$this->postService->deleteFile($post, $fileId);
return response()->json(['success' => true, 'message' => '파일이 삭제되었습니다.']);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => $e->getMessage()], 400);
}
}
}