withCount(['tasks', 'issues']) ->withTrashed(); // 검색 필터 if (! empty($filters['search'])) { $search = $filters['search']; $query->where(function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") ->orWhere('description', 'like', "%{$search}%"); }); } // 상태 필터 if (! empty($filters['status'])) { $query->where('status', $filters['status']); } // 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 getActiveProjects(): Collection { return AdminPmProject::query() ->active() ->orderBy('name') ->get(['id', 'name', 'status']); } /** * 특정 프로젝트 조회 */ public function getProjectById(int $id, bool $withTrashed = false): ?AdminPmProject { $query = AdminPmProject::query() ->with(['tasks' => function ($q) { $q->orderBy('sort_order')->orderBy('id'); }, 'issues' => function ($q) { $q->latest(); }]) ->withCount(['tasks', 'issues']); if ($withTrashed) { $query->withTrashed(); } return $query->find($id); } /** * 프로젝트 생성 */ public function createProject(array $data): AdminPmProject { $data['created_by'] = auth()->id(); return AdminPmProject::create($data); } /** * 프로젝트 수정 */ public function updateProject(int $id, array $data): bool { $project = AdminPmProject::findOrFail($id); $data['updated_by'] = auth()->id(); return $project->update($data); } /** * 프로젝트 삭제 (Soft Delete) */ public function deleteProject(int $id): bool { $project = AdminPmProject::findOrFail($id); $project->deleted_by = auth()->id(); $project->save(); return $project->delete(); } /** * 프로젝트 복원 */ public function restoreProject(int $id): bool { $project = AdminPmProject::onlyTrashed()->findOrFail($id); $project->deleted_by = null; return $project->restore(); } /** * 프로젝트 영구 삭제 */ public function forceDeleteProject(int $id): bool { $project = AdminPmProject::withTrashed()->findOrFail($id); // 관련 작업/이슈 삭제 (cascade 설정됨) return $project->forceDelete(); } /** * 프로젝트 통계 */ public function getProjectStats(): array { return [ 'total' => AdminPmProject::count(), 'active' => AdminPmProject::where('status', AdminPmProject::STATUS_ACTIVE)->count(), 'completed' => AdminPmProject::where('status', AdminPmProject::STATUS_COMPLETED)->count(), 'on_hold' => AdminPmProject::where('status', AdminPmProject::STATUS_ON_HOLD)->count(), 'trashed' => AdminPmProject::onlyTrashed()->count(), ]; } /** * 대시보드용 프로젝트 요약 */ public function getDashboardSummary(): array { $today = now()->format('Y-m-d'); $activeProjects = AdminPmProject::active() ->withCount(['tasks', 'issues']) ->with(['tasks' => function ($q) { $q->select('id', 'project_id', 'status'); }, 'issues' => function ($q) { $q->whereIn('status', [AdminPmIssue::STATUS_OPEN, AdminPmIssue::STATUS_IN_PROGRESS]); }, 'dailyLogs' => function ($q) use ($today) { $q->where('log_date', $today)->with('entries'); }]) ->get(); $taskStats = [ 'total' => AdminPmTask::count(), 'todo' => AdminPmTask::status(AdminPmTask::STATUS_TODO)->count(), 'in_progress' => AdminPmTask::status(AdminPmTask::STATUS_IN_PROGRESS)->count(), 'done' => AdminPmTask::status(AdminPmTask::STATUS_DONE)->count(), 'overdue' => AdminPmTask::overdue()->count(), 'due_soon' => AdminPmTask::dueSoon()->count(), ]; $issueStats = [ '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(), ]; return [ 'projects' => $activeProjects, 'project_stats' => $this->getProjectStats(), 'task_stats' => $taskStats, 'issue_stats' => $issueStats, ]; } /** * 프로젝트 상태 변경 */ public function changeStatus(int $id, string $status): AdminPmProject { $project = AdminPmProject::findOrFail($id); $project->status = $status; $project->updated_by = auth()->id(); $project->save(); return $project; } /** * 프로젝트 복제 */ public function duplicateProject(int $id, ?string $newName = null): AdminPmProject { $original = AdminPmProject::with(['tasks.issues'])->findOrFail($id); $newProject = $original->replicate(); $newProject->name = $newName ?? $original->name.' (복사본)'; $newProject->status = AdminPmProject::STATUS_ACTIVE; $newProject->created_by = auth()->id(); $newProject->updated_by = null; $newProject->deleted_by = null; $newProject->created_at = now(); $newProject->updated_at = now(); $newProject->save(); // 작업 및 이슈 복제 (task_id 매핑 유지) foreach ($original->tasks as $task) { $newTask = $task->replicate(); $newTask->project_id = $newProject->id; $newTask->status = AdminPmTask::STATUS_TODO; $newTask->created_by = auth()->id(); $newTask->updated_by = null; $newTask->deleted_by = null; $newTask->created_at = now(); $newTask->updated_at = now(); $newTask->save(); // 해당 작업의 이슈들 복제 foreach ($task->issues as $issue) { $newIssue = $issue->replicate(); $newIssue->project_id = $newProject->id; $newIssue->task_id = $newTask->id; $newIssue->status = AdminPmIssue::STATUS_OPEN; $newIssue->created_by = auth()->id(); $newIssue->updated_by = null; $newIssue->deleted_by = null; $newIssue->created_at = now(); $newIssue->updated_at = now(); $newIssue->save(); } } return $newProject->load('tasks.issues'); } }