- 오늘의 활동을 3컬럼 칸반 레이아웃으로 변경 (예정/진행중/완료) - 담당자별 항목 그룹핑 적용 - 인라인 상태 변경 버튼 추가 (hover 시 표시) - 담당자별 다중 항목 편집 모달 구현 - 담당자 이름 공통 입력 - 항목별 textarea, 상태 버튼, 삭제 버튼 - 항목 추가/삭제 기능 - Promise.all로 일괄 저장 - 인라인 삭제 기능 추가 - 라우트 경로 수정 (pm.daily-logs.index → daily-logs.index)
260 lines
7.9 KiB
PHP
260 lines
7.9 KiB
PHP
<?php
|
|
|
|
namespace App\Services\ProjectManagement;
|
|
|
|
use App\Models\Admin\AdminPmIssue;
|
|
use App\Models\Admin\AdminPmProject;
|
|
use App\Models\Admin\AdminPmTask;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
|
|
class ProjectService
|
|
{
|
|
/**
|
|
* 프로젝트 목록 조회 (페이지네이션)
|
|
*/
|
|
public function getProjects(array $filters = [], int $perPage = 15): LengthAwarePaginator
|
|
{
|
|
$query = AdminPmProject::query()
|
|
->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');
|
|
}
|
|
}
|