feat: [approval] 결재관리 삭제 권한 기능 추가

- 관리자/슈퍼관리자 모든 상태 결재 문서 삭제 가능
- 일반 사용자는 기존대로 draft + 본인 기안만 삭제
- 진행 중 문서 삭제 시 휴가 연동 취소 처리
- 삭제 API 403 권한 검증 추가
- 상세 페이지 삭제 버튼 + 2중 확인 다이얼로그
This commit is contained in:
김보곤
2026-03-03 07:35:59 +09:00
parent 420b80e45a
commit 3216bb98bc
4 changed files with 103 additions and 8 deletions

View File

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

View File

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

View File

@@ -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(),
]);
}
}
/**
* 결재 회수 시 연동 처리 (휴가 등)
*/

View File

@@ -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())
<div class="bg-white rounded-lg shadow-sm p-6">
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
<button onclick="copyForRedraft()"
class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition text-sm font-medium">
복사하여 재기안
@@ -220,6 +220,19 @@ class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition
<span class="text-xs text-gray-500 ml-2"> 문서를 복사하여 결재를 작성합니다.</span>
</div>
@endif
{{-- 삭제 (기안자: draft만 / 관리자: 모든 상태) --}}
@if($approval->isDeletableBy(auth()->user()))
<div class="bg-white rounded-lg shadow-sm p-6">
<div class="flex items-center gap-3">
<button onclick="deleteApproval()"
class="bg-red-600 hover:bg-red-700 text-white px-6 py-2 rounded-lg transition text-sm font-medium">
문서 삭제
</button>
<span class="text-xs text-gray-500"> 결재 문서를 삭제합니다. 삭제 복구할 없습니다.</span>
</div>
</div>
@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');
}
}
</script>
@endpush