- Models: AdminPmProject, AdminPmTask, AdminPmIssue - Services: ProjectService, TaskService, IssueService, ImportService - API Controllers: ProjectController, TaskController, IssueController, ImportController - FormRequests: Store/Update/BulkAction 요청 검증 - Views: 대시보드, 프로젝트 CRUD, JSON Import 화면 - Routes: API 42개 + Web 6개 엔드포인트 주요 기능: - 프로젝트/작업/이슈 계층 구조 관리 - 상태 변경, 우선순위, 마감일 추적 - 작업 순서 드래그앤드롭 (reorder API) - JSON Import로 일괄 등록 - Soft Delete 및 복원
300 lines
8.2 KiB
PHP
300 lines
8.2 KiB
PHP
<?php
|
|
|
|
namespace App\Services\ProjectManagement;
|
|
|
|
use App\Models\Admin\AdminPmTask;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class TaskService
|
|
{
|
|
/**
|
|
* 작업 목록 조회 (페이지네이션)
|
|
*/
|
|
public function getTasks(array $filters = [], int $perPage = 15): LengthAwarePaginator
|
|
{
|
|
$query = AdminPmTask::query()
|
|
->with(['project', 'assignee', 'issues'])
|
|
->withCount('issues')
|
|
->withTrashed();
|
|
|
|
// 프로젝트 필터
|
|
if (! empty($filters['project_id'])) {
|
|
$query->where('project_id', $filters['project_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['status'])) {
|
|
$query->where('status', $filters['status']);
|
|
}
|
|
|
|
// 우선순위 필터
|
|
if (! empty($filters['priority'])) {
|
|
$query->where('priority', $filters['priority']);
|
|
}
|
|
|
|
// 담당자 필터
|
|
if (! empty($filters['assignee_id'])) {
|
|
$query->where('assignee_id', $filters['assignee_id']);
|
|
}
|
|
|
|
// 마감 상태 필터
|
|
if (! empty($filters['due_status'])) {
|
|
if ($filters['due_status'] === 'overdue') {
|
|
$query->overdue();
|
|
} elseif ($filters['due_status'] === 'due_soon') {
|
|
$query->dueSoon();
|
|
}
|
|
}
|
|
|
|
// Soft Delete 필터
|
|
if (isset($filters['trashed'])) {
|
|
if ($filters['trashed'] === 'only') {
|
|
$query->onlyTrashed();
|
|
} elseif ($filters['trashed'] === 'with') {
|
|
$query->withTrashed();
|
|
}
|
|
}
|
|
|
|
// 정렬
|
|
$sortBy = $filters['sort_by'] ?? 'sort_order';
|
|
$sortDirection = $filters['sort_direction'] ?? 'asc';
|
|
$query->orderBy($sortBy, $sortDirection)->orderBy('id');
|
|
|
|
return $query->paginate($perPage);
|
|
}
|
|
|
|
/**
|
|
* 프로젝트별 작업 목록 (칸반보드용)
|
|
*/
|
|
public function getTasksByProject(int $projectId): Collection
|
|
{
|
|
return AdminPmTask::query()
|
|
->where('project_id', $projectId)
|
|
->with(['assignee', 'issues'])
|
|
->withCount('issues')
|
|
->orderBy('sort_order')
|
|
->orderBy('id')
|
|
->get();
|
|
}
|
|
|
|
/**
|
|
* 특정 작업 조회
|
|
*/
|
|
public function getTaskById(int $id, bool $withTrashed = false): ?AdminPmTask
|
|
{
|
|
$query = AdminPmTask::query()
|
|
->with(['project', 'assignee', 'issues', 'creator', 'updater'])
|
|
->withCount('issues');
|
|
|
|
if ($withTrashed) {
|
|
$query->withTrashed();
|
|
}
|
|
|
|
return $query->find($id);
|
|
}
|
|
|
|
/**
|
|
* 작업 생성
|
|
*/
|
|
public function createTask(array $data): AdminPmTask
|
|
{
|
|
// 정렬 순서 자동 설정
|
|
if (! isset($data['sort_order'])) {
|
|
$maxOrder = AdminPmTask::where('project_id', $data['project_id'])->max('sort_order') ?? 0;
|
|
$data['sort_order'] = $maxOrder + 1;
|
|
}
|
|
|
|
$data['created_by'] = auth()->id();
|
|
|
|
return AdminPmTask::create($data);
|
|
}
|
|
|
|
/**
|
|
* 작업 수정
|
|
*/
|
|
public function updateTask(int $id, array $data): bool
|
|
{
|
|
$task = AdminPmTask::findOrFail($id);
|
|
|
|
$data['updated_by'] = auth()->id();
|
|
|
|
return $task->update($data);
|
|
}
|
|
|
|
/**
|
|
* 작업 삭제 (Soft Delete)
|
|
*/
|
|
public function deleteTask(int $id): bool
|
|
{
|
|
$task = AdminPmTask::findOrFail($id);
|
|
|
|
$task->deleted_by = auth()->id();
|
|
$task->save();
|
|
|
|
return $task->delete();
|
|
}
|
|
|
|
/**
|
|
* 작업 복원
|
|
*/
|
|
public function restoreTask(int $id): bool
|
|
{
|
|
$task = AdminPmTask::onlyTrashed()->findOrFail($id);
|
|
|
|
$task->deleted_by = null;
|
|
|
|
return $task->restore();
|
|
}
|
|
|
|
/**
|
|
* 작업 영구 삭제
|
|
*/
|
|
public function forceDeleteTask(int $id): bool
|
|
{
|
|
return AdminPmTask::withTrashed()->findOrFail($id)->forceDelete();
|
|
}
|
|
|
|
/**
|
|
* 작업 상태 변경
|
|
*/
|
|
public function changeStatus(int $id, string $status): AdminPmTask
|
|
{
|
|
$task = AdminPmTask::findOrFail($id);
|
|
|
|
$task->status = $status;
|
|
$task->updated_by = auth()->id();
|
|
$task->save();
|
|
|
|
return $task;
|
|
}
|
|
|
|
/**
|
|
* 작업 순서 변경 (드래그앤드롭)
|
|
*/
|
|
public function reorderTasks(int $projectId, array $taskIds): bool
|
|
{
|
|
return DB::transaction(function () use ($projectId, $taskIds) {
|
|
foreach ($taskIds as $order => $taskId) {
|
|
AdminPmTask::where('id', $taskId)
|
|
->where('project_id', $projectId)
|
|
->update(['sort_order' => $order + 1]);
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 다중 작업 상태 일괄 변경
|
|
*/
|
|
public function bulkChangeStatus(array $taskIds, string $status): int
|
|
{
|
|
return AdminPmTask::whereIn('id', $taskIds)
|
|
->update([
|
|
'status' => $status,
|
|
'updated_by' => auth()->id(),
|
|
'updated_at' => now(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 다중 작업 담당자 일괄 변경
|
|
*/
|
|
public function bulkChangeAssignee(array $taskIds, ?int $assigneeId): int
|
|
{
|
|
return AdminPmTask::whereIn('id', $taskIds)
|
|
->update([
|
|
'assignee_id' => $assigneeId,
|
|
'updated_by' => auth()->id(),
|
|
'updated_at' => now(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 다중 작업 우선순위 일괄 변경
|
|
*/
|
|
public function bulkChangePriority(array $taskIds, string $priority): int
|
|
{
|
|
return AdminPmTask::whereIn('id', $taskIds)
|
|
->update([
|
|
'priority' => $priority,
|
|
'updated_by' => auth()->id(),
|
|
'updated_at' => now(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 다중 작업 일괄 삭제
|
|
*/
|
|
public function bulkDelete(array $taskIds): int
|
|
{
|
|
AdminPmTask::whereIn('id', $taskIds)
|
|
->update([
|
|
'deleted_by' => auth()->id(),
|
|
]);
|
|
|
|
return AdminPmTask::whereIn('id', $taskIds)->delete();
|
|
}
|
|
|
|
/**
|
|
* 다중 작업 일괄 복원
|
|
*/
|
|
public function bulkRestore(array $taskIds): int
|
|
{
|
|
return AdminPmTask::onlyTrashed()
|
|
->whereIn('id', $taskIds)
|
|
->update([
|
|
'deleted_by' => null,
|
|
'deleted_at' => null,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 작업 통계 (프로젝트별)
|
|
*/
|
|
public function getTaskStatsByProject(int $projectId): array
|
|
{
|
|
return [
|
|
'total' => AdminPmTask::where('project_id', $projectId)->count(),
|
|
'todo' => AdminPmTask::where('project_id', $projectId)
|
|
->status(AdminPmTask::STATUS_TODO)->count(),
|
|
'in_progress' => AdminPmTask::where('project_id', $projectId)
|
|
->status(AdminPmTask::STATUS_IN_PROGRESS)->count(),
|
|
'done' => AdminPmTask::where('project_id', $projectId)
|
|
->status(AdminPmTask::STATUS_DONE)->count(),
|
|
'overdue' => AdminPmTask::where('project_id', $projectId)->overdue()->count(),
|
|
'due_soon' => AdminPmTask::where('project_id', $projectId)->dueSoon()->count(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 마감 임박/지연 작업 목록
|
|
*/
|
|
public function getUrgentTasks(?int $projectId = null): array
|
|
{
|
|
$overdueQuery = AdminPmTask::overdue()->with('project', 'assignee');
|
|
$dueSoonQuery = AdminPmTask::dueSoon()->with('project', 'assignee');
|
|
|
|
if ($projectId) {
|
|
$overdueQuery->where('project_id', $projectId);
|
|
$dueSoonQuery->where('project_id', $projectId);
|
|
}
|
|
|
|
return [
|
|
'overdue' => $overdueQuery->orderBy('due_date')->get(),
|
|
'due_soon' => $dueSoonQuery->orderBy('due_date')->get(),
|
|
];
|
|
}
|
|
}
|