with(['project', 'task', 'creator']) ->withTrashed(); // 프로젝트 필터 if (! empty($filters['project_id'])) { $query->where('project_id', $filters['project_id']); } // 작업 필터 if (! empty($filters['task_id'])) { $query->where('task_id', $filters['task_id']); } // 검색 필터 if (! empty($filters['search'])) { $search = $filters['search']; $query->where(function ($q) use ($search) { $q->where('title', 'like', "%{$search}%") ->orWhere('description', 'like', "%{$search}%"); }); } // 타입 필터 if (! empty($filters['type'])) { $query->where('type', $filters['type']); } // 상태 필터 if (! empty($filters['status'])) { $query->where('status', $filters['status']); } // 열린 이슈만 필터 if (! empty($filters['open_only']) && $filters['open_only']) { $query->open(); } // Soft Delete 필터 if (isset($filters['trashed'])) { if ($filters['trashed'] === 'only') { $query->onlyTrashed(); } elseif ($filters['trashed'] === 'with') { $query->withTrashed(); } } // 정렬 $sortBy = $filters['sort_by'] ?? 'id'; $sortDirection = $filters['sort_direction'] ?? 'desc'; $query->orderBy($sortBy, $sortDirection); return $query->paginate($perPage); } /** * 프로젝트별 이슈 목록 */ public function getIssuesByProject(int $projectId): Collection { return AdminPmIssue::query() ->where('project_id', $projectId) ->with(['task', 'creator']) ->latest() ->get(); } /** * 작업별 이슈 목록 */ public function getIssuesByTask(int $taskId): Collection { return AdminPmIssue::query() ->where('task_id', $taskId) ->with(['creator']) ->latest() ->get(); } /** * 특정 이슈 조회 */ public function getIssueById(int $id, bool $withTrashed = false): ?AdminPmIssue { $query = AdminPmIssue::query() ->with(['project', 'task', 'creator', 'updater']); if ($withTrashed) { $query->withTrashed(); } return $query->find($id); } /** * 이슈 생성 */ public function createIssue(array $data): AdminPmIssue { $data['created_by'] = auth()->id(); return AdminPmIssue::create($data); } /** * 이슈 수정 */ public function updateIssue(int $id, array $data): bool { $issue = AdminPmIssue::findOrFail($id); $data['updated_by'] = auth()->id(); return $issue->update($data); } /** * 이슈 삭제 (Soft Delete) */ public function deleteIssue(int $id): bool { $issue = AdminPmIssue::findOrFail($id); $issue->deleted_by = auth()->id(); $issue->save(); return $issue->delete(); } /** * 이슈 복원 */ public function restoreIssue(int $id): bool { $issue = AdminPmIssue::onlyTrashed()->findOrFail($id); $issue->deleted_by = null; return $issue->restore(); } /** * 이슈 영구 삭제 */ public function forceDeleteIssue(int $id): bool { return AdminPmIssue::withTrashed()->findOrFail($id)->forceDelete(); } /** * 이슈 상태 변경 */ public function changeStatus(int $id, string $status): AdminPmIssue { $issue = AdminPmIssue::findOrFail($id); $issue->status = $status; $issue->updated_by = auth()->id(); $issue->save(); return $issue; } /** * 이슈 긴급 토글 */ public function toggleUrgent(int $id): AdminPmIssue { $issue = AdminPmIssue::findOrFail($id); $issue->is_urgent = ! $issue->is_urgent; $issue->updated_by = auth()->id(); $issue->save(); return $issue; } /** * 다중 이슈 상태 일괄 변경 */ public function bulkChangeStatus(array $issueIds, string $status): int { return AdminPmIssue::whereIn('id', $issueIds) ->update([ 'status' => $status, 'updated_by' => auth()->id(), 'updated_at' => now(), ]); } /** * 다중 이슈 타입 일괄 변경 */ public function bulkChangeType(array $issueIds, string $type): int { return AdminPmIssue::whereIn('id', $issueIds) ->update([ 'type' => $type, 'updated_by' => auth()->id(), 'updated_at' => now(), ]); } /** * 다중 이슈 작업 연결 일괄 변경 */ public function bulkLinkToTask(array $issueIds, ?int $taskId): int { return AdminPmIssue::whereIn('id', $issueIds) ->update([ 'task_id' => $taskId, 'updated_by' => auth()->id(), 'updated_at' => now(), ]); } /** * 다중 이슈 일괄 삭제 */ public function bulkDelete(array $issueIds): int { AdminPmIssue::whereIn('id', $issueIds) ->update([ 'deleted_by' => auth()->id(), ]); return AdminPmIssue::whereIn('id', $issueIds)->delete(); } /** * 다중 이슈 일괄 복원 */ public function bulkRestore(array $issueIds): int { return AdminPmIssue::onlyTrashed() ->whereIn('id', $issueIds) ->update([ 'deleted_by' => null, 'deleted_at' => null, ]); } /** * 이슈 통계 (프로젝트별) */ public function getIssueStatsByProject(int $projectId): array { return [ 'total' => AdminPmIssue::where('project_id', $projectId)->count(), 'open' => AdminPmIssue::where('project_id', $projectId) ->status(AdminPmIssue::STATUS_OPEN)->count(), 'in_progress' => AdminPmIssue::where('project_id', $projectId) ->status(AdminPmIssue::STATUS_IN_PROGRESS)->count(), 'resolved' => AdminPmIssue::where('project_id', $projectId) ->status(AdminPmIssue::STATUS_RESOLVED)->count(), 'closed' => AdminPmIssue::where('project_id', $projectId) ->status(AdminPmIssue::STATUS_CLOSED)->count(), 'by_type' => [ 'bug' => AdminPmIssue::where('project_id', $projectId) ->type(AdminPmIssue::TYPE_BUG)->count(), 'feature' => AdminPmIssue::where('project_id', $projectId) ->type(AdminPmIssue::TYPE_FEATURE)->count(), 'improvement' => AdminPmIssue::where('project_id', $projectId) ->type(AdminPmIssue::TYPE_IMPROVEMENT)->count(), ], ]; } /** * 전체 이슈 통계 */ public function getIssueStats(): array { return [ 'total' => AdminPmIssue::count(), 'open' => AdminPmIssue::status(AdminPmIssue::STATUS_OPEN)->count(), 'in_progress' => AdminPmIssue::status(AdminPmIssue::STATUS_IN_PROGRESS)->count(), 'resolved' => AdminPmIssue::status(AdminPmIssue::STATUS_RESOLVED)->count(), 'closed' => AdminPmIssue::status(AdminPmIssue::STATUS_CLOSED)->count(), 'trashed' => AdminPmIssue::onlyTrashed()->count(), 'by_type' => [ 'bug' => AdminPmIssue::type(AdminPmIssue::TYPE_BUG)->count(), 'feature' => AdminPmIssue::type(AdminPmIssue::TYPE_FEATURE)->count(), 'improvement' => AdminPmIssue::type(AdminPmIssue::TYPE_IMPROVEMENT)->count(), ], ]; } /** * 열린 이슈 목록 (대시보드용) */ public function getOpenIssues(?int $projectId = null, int $limit = 10): Collection { $query = AdminPmIssue::open() ->with(['project', 'task', 'creator']) ->latest(); if ($projectId) { $query->where('project_id', $projectId); } return $query->limit($limit)->get(); } }