feat:계약 휴지통 기능 (소프트 삭제/복구/영구삭제)

백엔드:
- destroy를 SoftDelete 방식으로 변경 (deleted_at + deleted_by 기록)
- trashed: 휴지통 목록 조회 API 추가
- restore: 선택 복구 API 추가
- forceDestroy: 영구 삭제 API 추가 (파일+관련 레코드 완전 삭제)
- 라우트 3개 추가 (trashed, restore, force-destroy)

프론트엔드:
- 대시보드에 탭 UI 추가 (계약 목록 / 휴지통)
- 휴지통 탭: 삭제된 계약 목록, 삭제일 표시
- 선택 복구(파란색) / 영구삭제(빨간색) 버튼
- 휴지통 건수 뱃지 표시
- 삭제 시 메시지를 "휴지통으로 이동"으로 변경

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-13 06:45:29 +09:00
parent 156a7430a8
commit 36654d9992
3 changed files with 372 additions and 79 deletions

View File

@@ -220,7 +220,7 @@ public function cancel(Request $request, int $id): JsonResponse
}
/**
* 계약 삭제 (단건/복수)
* 계약 삭제 - 휴지통으로 이동 (SoftDelete)
*/
public function destroy(Request $request): JsonResponse
{
@@ -247,6 +247,95 @@ public function destroy(Request $request): JsonResponse
], 422);
}
$deletedCount = 0;
foreach ($contracts as $contract) {
$contract->update(['deleted_by' => auth()->id()]);
$contract->delete(); // SoftDelete → deleted_at 설정
$deletedCount++;
}
return response()->json([
'success' => true,
'message' => "{$deletedCount}건의 계약이 휴지통으로 이동되었습니다.",
]);
}
/**
* 휴지통 목록
*/
public function trashed(Request $request): JsonResponse
{
$tenantId = session('selected_tenant_id', 1);
$query = EsignContract::onlyTrashed()
->where('tenant_id', $tenantId)
->with(['signers:id,contract_id,name,role,status']);
if ($search = $request->input('search')) {
$query->where(function ($q) use ($search) {
$q->where('title', 'like', "%{$search}%")
->orWhere('contract_code', 'like', "%{$search}%");
});
}
$perPage = $request->input('per_page', 20);
$data = $query->orderBy('deleted_at', 'desc')->paginate($perPage);
return response()->json(['success' => true, 'data' => $data]);
}
/**
* 휴지통에서 복구
*/
public function restore(Request $request): JsonResponse
{
$request->validate([
'ids' => 'required|array|min:1',
'ids.*' => 'required|integer',
]);
$tenantId = session('selected_tenant_id', 1);
$contracts = EsignContract::onlyTrashed()
->where('tenant_id', $tenantId)
->whereIn('id', $request->input('ids'))
->get();
if ($contracts->isEmpty()) {
return response()->json(['success' => false, 'message' => '복구할 계약을 찾을 수 없습니다.'], 404);
}
$restoredCount = 0;
foreach ($contracts as $contract) {
$contract->update(['deleted_by' => null]);
$contract->restore();
$restoredCount++;
}
return response()->json([
'success' => true,
'message' => "{$restoredCount}건의 계약이 복구되었습니다.",
]);
}
/**
* 영구 삭제
*/
public function forceDestroy(Request $request): JsonResponse
{
$request->validate([
'ids' => 'required|array|min:1',
'ids.*' => 'required|integer',
]);
$tenantId = session('selected_tenant_id', 1);
$contracts = EsignContract::onlyTrashed()
->where('tenant_id', $tenantId)
->whereIn('id', $request->input('ids'))
->get();
if ($contracts->isEmpty()) {
return response()->json(['success' => false, 'message' => '삭제할 계약을 찾을 수 없습니다.'], 404);
}
$deletedCount = 0;
foreach ($contracts as $contract) {
// 관련 파일 삭제
@@ -265,17 +354,17 @@ public function destroy(Request $request): JsonResponse
}
}
// 관련 레코드 삭제
// 관련 레코드 영구 삭제
EsignSignField::where('contract_id', $contract->id)->delete();
EsignAuditLog::where('contract_id', $contract->id)->delete();
EsignSigner::withoutGlobalScopes()->where('contract_id', $contract->id)->delete();
$contract->delete();
$contract->forceDelete();
$deletedCount++;
}
return response()->json([
'success' => true,
'message' => "{$deletedCount}건의 계약이 삭제되었습니다.",
'message' => "{$deletedCount}건의 계약이 영구 삭제되었습니다.",
]);
}