- Task, Issue 모델에 is_urgent 필드 추가 - TaskService, IssueService에 toggleUrgent() 메서드 추가 - TaskController, IssueController에 toggleUrgent 엔드포인트 추가 - API 라우트에 toggle-urgent 경로 추가 - 프로젝트 상세 페이지 UI 개선: - 작업/이슈 행에 긴급 토글 버튼(불꽃 아이콘) 추가 - 서브 row(아코디언 내 이슈)에도 긴급 토글 추가 - 서브 row 컬럼을 작업 row와 동일하게 8컬럼으로 정렬 - 진행중 작업의 이슈 아코디언 자동 열기 - 이슈 상태 버튼 항상 테두리 표시
320 lines
8.7 KiB
PHP
320 lines
8.7 KiB
PHP
<?php
|
|
|
|
namespace App\Services\ProjectManagement;
|
|
|
|
use App\Models\Admin\AdminPmIssue;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
|
|
class IssueService
|
|
{
|
|
/**
|
|
* 이슈 목록 조회 (페이지네이션)
|
|
*/
|
|
public function getIssues(array $filters = [], int $perPage = 15): LengthAwarePaginator
|
|
{
|
|
$query = AdminPmIssue::query()
|
|
->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();
|
|
}
|
|
}
|