service->getMyDrafts( $request->only(['search', 'status', 'is_urgent', 'date_from', 'date_to']), (int) $request->get('per_page', 15) ); return response()->json($result); } /** * 결재 대기함 */ public function pending(Request $request): JsonResponse { $result = $this->service->getPendingForMe( auth()->id(), $request->only(['search', 'is_urgent', 'date_from', 'date_to']), (int) $request->get('per_page', 15) ); return response()->json($result); } /** * 처리 완료함 */ public function completed(Request $request): JsonResponse { $result = $this->service->getCompletedByMe( auth()->id(), $request->only(['search', 'status', 'date_from', 'date_to']), (int) $request->get('per_page', 15) ); return response()->json($result); } /** * 참조함 */ public function references(Request $request): JsonResponse { $result = $this->service->getReferencesForMe( auth()->id(), $request->only(['search', 'date_from', 'date_to', 'is_read']), (int) $request->get('per_page', 15) ); return response()->json($result); } // ========================================================================= // CRUD // ========================================================================= /** * 상세 조회 */ public function show(int $id): JsonResponse { $approval = $this->service->getApproval($id); return response()->json(['success' => true, 'data' => $approval]); } /** * 생성 (임시저장) */ public function store(Request $request): JsonResponse { $request->validate([ 'form_id' => 'required|exists:approval_forms,id', 'title' => 'required|string|max:200', 'body' => 'nullable|string', 'content' => 'nullable|array', 'is_urgent' => 'boolean', '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()); return response()->json([ 'success' => true, 'message' => '결재 문서가 저장되었습니다.', 'data' => $approval, ], 201); } /** * 수정 */ public function update(Request $request, int $id): JsonResponse { $request->validate([ 'title' => 'sometimes|string|max:200', 'body' => 'nullable|string', 'content' => 'nullable|array', 'is_urgent' => 'boolean', '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 { $approval = $this->service->updateApproval($id, $request->all()); return response()->json([ 'success' => true, 'message' => '결재 문서가 수정되었습니다.', 'data' => $approval, ]); } catch (\InvalidArgumentException $e) { return response()->json([ 'success' => false, 'message' => $e->getMessage(), ], 400); } } /** * 삭제 */ public function destroy(int $id): JsonResponse { try { $user = auth()->user(); $approval = $this->service->getApproval($id); if (! $approval->isDeletableBy($user)) { return response()->json([ 'success' => false, 'message' => '삭제 권한이 없습니다.', ], 403); } $this->service->deleteApproval($id, $user); return response()->json([ 'success' => true, 'message' => '결재 문서가 삭제되었습니다.', ]); } catch (\InvalidArgumentException $e) { return response()->json([ 'success' => false, 'message' => $e->getMessage(), ], 400); } } // ========================================================================= // 워크플로우 // ========================================================================= /** * 상신 */ public function submit(int $id): JsonResponse { try { $approval = $this->service->submit($id); return response()->json([ 'success' => true, 'message' => '결재가 상신되었습니다.', 'data' => $approval, ]); } catch (\InvalidArgumentException $e) { return response()->json([ 'success' => false, 'message' => $e->getMessage(), ], 400); } } /** * 승인 */ public function approve(Request $request, int $id): JsonResponse { try { $approval = $this->service->approve($id, $request->get('comment')); return response()->json([ 'success' => true, 'message' => '승인되었습니다.', 'data' => $approval, ]); } catch (\InvalidArgumentException $e) { return response()->json([ 'success' => false, 'message' => $e->getMessage(), ], 400); } } /** * 반려 */ public function reject(Request $request, int $id): JsonResponse { $request->validate([ 'comment' => 'required|string|max:1000', ]); try { $approval = $this->service->reject($id, $request->get('comment')); return response()->json([ 'success' => true, 'message' => '반려되었습니다.', 'data' => $approval, ]); } catch (\InvalidArgumentException $e) { return response()->json([ 'success' => false, 'message' => $e->getMessage(), ], 400); } } /** * 회수 */ public function cancel(Request $request, int $id): JsonResponse { try { $approval = $this->service->cancel($id, $request->get('recall_reason')); return response()->json([ 'success' => true, 'message' => '결재가 회수되었습니다.', 'data' => $approval, ]); } catch (\InvalidArgumentException $e) { return response()->json([ 'success' => false, 'message' => $e->getMessage(), ], 400); } } /** * 보류 */ public function hold(Request $request, int $id): JsonResponse { $request->validate([ 'comment' => 'required|string|max:1000', ]); try { $approval = $this->service->hold($id, $request->get('comment')); return response()->json([ 'success' => true, 'message' => '보류되었습니다.', 'data' => $approval, ]); } catch (\InvalidArgumentException $e) { return response()->json([ 'success' => false, 'message' => $e->getMessage(), ], 400); } } /** * 보류 해제 */ public function releaseHold(int $id): JsonResponse { try { $approval = $this->service->releaseHold($id); return response()->json([ 'success' => true, 'message' => '보류가 해제되었습니다.', 'data' => $approval, ]); } catch (\InvalidArgumentException $e) { return response()->json([ 'success' => false, 'message' => $e->getMessage(), ], 400); } } /** * 전결 */ public function preDecide(Request $request, int $id): JsonResponse { try { $approval = $this->service->preDecide($id, $request->get('comment')); return response()->json([ 'success' => true, 'message' => '전결 처리되었습니다.', 'data' => $approval, ]); } catch (\InvalidArgumentException $e) { return response()->json([ 'success' => false, 'message' => $e->getMessage(), ], 400); } } /** * 복사 재기안 */ public function copyForRedraft(int $id): JsonResponse { try { $approval = $this->service->copyForRedraft($id); return response()->json([ 'success' => true, 'message' => '문서가 복사되었습니다.', 'data' => $approval, ]); } catch (\InvalidArgumentException $e) { return response()->json([ 'success' => false, 'message' => $e->getMessage(), ], 400); } } /** * 참조 열람 추적 */ public function markAsRead(int $id): JsonResponse { $this->service->markAsRead($id); return response()->json([ 'success' => true, 'message' => '열람 처리되었습니다.', ]); } // ========================================================================= // 유틸 // ========================================================================= /** * 결재선 템플릿 목록 */ public function lines(): JsonResponse { $lines = $this->service->getApprovalLines(); return response()->json(['success' => true, 'data' => $lines]); } /** * 결재선 템플릿 생성 */ public function storeLine(Request $request): JsonResponse { $request->validate([ 'name' => 'required|string|max:100', 'steps' => 'required|array|min:1', 'steps.*.user_id' => 'required|exists:users,id', 'steps.*.step_type' => 'required|in:approval,agreement,reference', 'is_default' => 'boolean', ]); $line = $this->service->createLine($request->all()); return response()->json([ 'success' => true, 'message' => '결재선이 저장되었습니다.', 'data' => $line, ], 201); } /** * 결재선 템플릿 수정 */ public function updateLine(Request $request, int $id): JsonResponse { $request->validate([ 'name' => 'required|string|max:100', 'steps' => 'required|array|min:1', 'steps.*.user_id' => 'required|exists:users,id', 'steps.*.step_type' => 'required|in:approval,agreement,reference', 'is_default' => 'boolean', ]); $line = $this->service->updateLine($id, $request->all()); return response()->json([ 'success' => true, 'message' => '결재선이 수정되었습니다.', 'data' => $line, ]); } /** * 결재선 템플릿 삭제 */ public function destroyLine(int $id): JsonResponse { $this->service->deleteLine($id); return response()->json([ 'success' => true, 'message' => '결재선이 삭제되었습니다.', ]); } /** * 지출결의서 이력 (불러오기용) */ public function expenseHistory(Request $request): JsonResponse { $tenantId = session('selected_tenant_id'); $approvals = \App\Models\Approvals\Approval::where('tenant_id', $tenantId) ->where('drafter_id', auth()->id()) ->whereHas('form', fn ($q) => $q->where('code', 'expense')) ->whereIn('status', ['draft', 'pending', 'approved', 'rejected', 'cancelled']) ->whereNotNull('content') ->orderByDesc('created_at') ->limit(30) ->get(['id', 'title', 'content', 'status', 'created_at']); $data = $approvals->map(fn ($a) => [ 'id' => $a->id, 'title' => $a->title, 'status' => $a->status, 'status_label' => $a->status_label, 'total_amount' => $a->content['total_amount'] ?? 0, 'expense_type' => $a->content['expense_type'] ?? '', 'created_at' => $a->created_at->format('Y-m-d'), 'content' => $a->content, ]); return response()->json(['success' => true, 'data' => $data]); } /** * 양식 목록 */ public function forms(): JsonResponse { $forms = $this->service->getApprovalForms(); return response()->json(['success' => true, 'data' => $forms]); } /** * 미처리 건수 */ public function badgeCounts(): JsonResponse { $counts = $this->service->getBadgeCounts(auth()->id()); 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', 'display_name' => $file->getClientOriginalName(), '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, '파일을 찾을 수 없습니다.'); } }