From 3216bb98bce86d4e2166834f5e62e590a372b3dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Tue, 3 Mar 2026 07:35:59 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[approval]=20=EA=B2=B0=EC=9E=AC?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EC=82=AD=EC=A0=9C=20=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 관리자/슈퍼관리자 모든 상태 결재 문서 삭제 가능 - 일반 사용자는 기존대로 draft + 본인 기안만 삭제 - 진행 중 문서 삭제 시 휴가 연동 취소 처리 - 삭제 API 403 권한 검증 추가 - 상세 페이지 삭제 버튼 + 2중 확인 다이얼로그 --- .../Api/Admin/ApprovalApiController.php | 12 ++++- app/Models/Approvals/Approval.php | 14 ++++++ app/Services/ApprovalService.php | 36 +++++++++++--- resources/views/approvals/show.blade.php | 49 ++++++++++++++++++- 4 files changed, 103 insertions(+), 8 deletions(-) diff --git a/app/Http/Controllers/Api/Admin/ApprovalApiController.php b/app/Http/Controllers/Api/Admin/ApprovalApiController.php index f4da88b3..b55c369f 100644 --- a/app/Http/Controllers/Api/Admin/ApprovalApiController.php +++ b/app/Http/Controllers/Api/Admin/ApprovalApiController.php @@ -146,7 +146,17 @@ public function update(Request $request, int $id): JsonResponse public function destroy(int $id): JsonResponse { try { - $this->service->deleteApproval($id); + $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, diff --git a/app/Models/Approvals/Approval.php b/app/Models/Approvals/Approval.php index 361868a8..af5b5053 100644 --- a/app/Models/Approvals/Approval.php +++ b/app/Models/Approvals/Approval.php @@ -214,6 +214,20 @@ public function isDeletable(): bool return $this->status === self::STATUS_DRAFT; } + public function isDeletableBy(?User $user = null): bool + { + if (! $user) { + return $this->isDeletable(); + } + + if ($user->isAdmin()) { + return true; + } + + return $this->status === self::STATUS_DRAFT + && $this->drafter_id === $user->id; + } + public function getStatusLabelAttribute(): string { return match ($this->status) { diff --git a/app/Services/ApprovalService.php b/app/Services/ApprovalService.php index e95fb54c..032ac1eb 100644 --- a/app/Services/ApprovalService.php +++ b/app/Services/ApprovalService.php @@ -175,18 +175,24 @@ public function updateApproval(int $id, array $data): Approval } /** - * 삭제 (draft만) + * 삭제 (일반: draft만 / 관리자: 모든 상태) */ - public function deleteApproval(int $id): bool + public function deleteApproval(int $id, ?User $user = null): bool { - $approval = Approval::findOrFail($id); + $approval = Approval::with('form')->findOrFail($id); + $user = $user ?? auth()->user(); - if (! $approval->isDeletable()) { - throw new \InvalidArgumentException('삭제할 수 없는 상태입니다.'); + if (! $approval->isDeletableBy($user)) { + throw new \InvalidArgumentException('삭제 권한이 없습니다.'); + } + + // 진행 중/보류 문서 삭제 시 연동 후처리 (휴가 등) + if (in_array($approval->status, [Approval::STATUS_PENDING, Approval::STATUS_ON_HOLD])) { + $this->handleApprovalDeleted($approval); } $approval->steps()->delete(); - $approval->update(['deleted_by' => auth()->id()]); + $approval->update(['deleted_by' => $user->id]); return $approval->delete(); } @@ -738,6 +744,24 @@ private function handleApprovalRejected(Approval $approval, string $comment): vo } } + /** + * 결재 삭제 시 연동 처리 (휴가 등) + */ + private function handleApprovalDeleted(Approval $approval): void + { + if (! $approval->form || $approval->form->code !== 'leave') { + return; + } + + $leave = \App\Models\HR\Leave::where('approval_id', $approval->id)->first(); + if ($leave && in_array($leave->status, ['pending', 'approved'])) { + $leave->update([ + 'status' => 'cancelled', + 'updated_by' => auth()->id(), + ]); + } + } + /** * 결재 회수 시 연동 처리 (휴가 등) */ diff --git a/resources/views/approvals/show.blade.php b/resources/views/approvals/show.blade.php index ad04de94..82dce2fb 100644 --- a/resources/views/approvals/show.blade.php +++ b/resources/views/approvals/show.blade.php @@ -212,7 +212,7 @@ class="bg-yellow-500 hover:bg-yellow-600 text-white px-6 py-2 rounded-lg transit {{-- 복사 재기안 (완료/반려/회수 상태에서 기안자만) --}} @if($approval->isCopyable() && $approval->drafter_id === auth()->id()) -
+
@endif + + {{-- 삭제 (기안자: draft만 / 관리자: 모든 상태) --}} + @if($approval->isDeletableBy(auth()->user())) +
+
+ + 이 결재 문서를 삭제합니다. 삭제 후 복구할 수 없습니다. +
+
+ @endif @endsection @push('scripts') @@ -402,5 +415,39 @@ class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition showToast('서버 오류가 발생했습니다.', 'error'); } } + +async function deleteApproval() { + const status = '{{ $approval->status }}'; + const isActive = ['pending', 'on_hold'].includes(status); + + if (isActive) { + if (!confirm('이 문서는 현재 진행 중입니다.\n삭제하면 연관된 휴가 등의 처리도 취소됩니다.\n정말 삭제하시겠습니까?')) return; + } + + if (!confirm('결재 문서를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.')) return; + + try { + const response = await fetch('/api/admin/approvals/{{ $approval->id }}', { + method: 'DELETE', + headers: { + 'X-CSRF-TOKEN': '{{ csrf_token() }}', + 'Accept': 'application/json', + }, + }); + + const data = await response.json(); + + if (data.success) { + showToast(data.message, 'success'); + setTimeout(() => { + location.href = '{{ route("approvals.drafts") }}'; + }, 500); + } else { + showToast(data.message || '삭제에 실패했습니다.', 'error'); + } + } catch (e) { + showToast('서버 오류가 발생했습니다.', 'error'); + } +} @endpush