feat: [approval] 지출결의서 첨부파일 업로드/다운로드 기능 추가

- 첨부파일 업로드 API (GCS 연동, 20MB 제한)
- 첨부파일 삭제/다운로드 API 추가
- 지출결의서 폼에 드래그&드롭 멀티 파일 업로드 UI 추가
- ApprovalService에 linkAttachments 메서드 추가 (is_temp 플래그 관리)
- show 페이지에 첨부파일 목록 표시 및 다운로드 링크
- 지출부서 기본값 '본사', 로그인 사용자 이름 자동입력, 제목 필드 제거
This commit is contained in:
김보곤
2026-03-04 20:07:49 +09:00
parent b791b7d764
commit 622fb92a92
7 changed files with 348 additions and 13 deletions

View File

@@ -3,9 +3,13 @@
namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use App\Models\Boards\File;
use App\Services\ApprovalService;
use App\Services\GoogleCloudStorageService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class ApprovalApiController extends Controller
{
@@ -100,6 +104,8 @@ public function store(Request $request): JsonResponse
'steps' => 'nullable|array',
'steps.*.user_id' => 'required_with:steps|exists:users,id',
'steps.*.step_type' => 'required_with:steps|in:approval,agreement,reference',
'attachment_file_ids' => 'nullable|array',
'attachment_file_ids.*' => 'integer',
]);
$approval = $this->service->createApproval($request->all());
@@ -124,6 +130,8 @@ public function update(Request $request, int $id): JsonResponse
'steps' => 'nullable|array',
'steps.*.user_id' => 'required_with:steps|exists:users,id',
'steps.*.step_type' => 'required_with:steps|in:approval,agreement,reference',
'attachment_file_ids' => 'nullable|array',
'attachment_file_ids.*' => 'integer',
]);
try {
@@ -455,4 +463,94 @@ public function badgeCounts(): JsonResponse
return response()->json(['success' => true, 'data' => $counts]);
}
// =========================================================================
// 첨부파일
// =========================================================================
/**
* 첨부파일 업로드
*/
public function uploadFile(Request $request, GoogleCloudStorageService $gcs): JsonResponse
{
$request->validate([
'file' => 'required|file|max:20480',
]);
$file = $request->file('file');
$tenantId = session('selected_tenant_id');
$storedName = Str::random(40).'.'.$file->getClientOriginalExtension();
$storagePath = "approvals/{$tenantId}/{$storedName}";
Storage::disk('tenant')->put($storagePath, file_get_contents($file));
$gcsUri = null;
$gcsObjectName = null;
if ($gcs->isAvailable()) {
$gcsObjectName = $storagePath;
$gcsUri = $gcs->upload($file->getRealPath(), $gcsObjectName);
}
$fileRecord = File::create([
'tenant_id' => $tenantId,
'document_type' => 'approval_attachment',
'original_name' => $file->getClientOriginalName(),
'stored_name' => $storedName,
'file_path' => $storagePath,
'mime_type' => $file->getMimeType(),
'file_size' => $file->getSize(),
'file_type' => strtolower($file->getClientOriginalExtension()),
'gcs_object_name' => $gcsObjectName,
'gcs_uri' => $gcsUri,
'is_temp' => true,
'uploaded_by' => auth()->id(),
'created_by' => auth()->id(),
]);
return response()->json([
'success' => true,
'data' => [
'id' => $fileRecord->id,
'name' => $fileRecord->original_name,
'size' => $fileRecord->file_size,
'mime_type' => $fileRecord->mime_type,
],
]);
}
/**
* 첨부파일 삭제
*/
public function deleteFile(int $fileId): JsonResponse
{
$file = File::where('id', $fileId)
->where('uploaded_by', auth()->id())
->first();
if (! $file) {
return response()->json(['success' => false, 'message' => '파일을 찾을 수 없습니다.'], 404);
}
if ($file->existsInStorage()) {
Storage::disk('tenant')->delete($file->file_path);
}
$file->forceDelete();
return response()->json(['success' => true, 'message' => '파일이 삭제되었습니다.']);
}
/**
* 첨부파일 다운로드
*/
public function downloadFile(int $fileId)
{
$file = File::findOrFail($fileId);
if (Storage::disk('tenant')->exists($file->file_path)) {
return Storage::disk('tenant')->download($file->file_path, $file->original_name);
}
abort(404, '파일을 찾을 수 없습니다.');
}
}