diff --git a/app/Http/Controllers/Api/Admin/ProjectManagement/IssueController.php b/app/Http/Controllers/Api/Admin/ProjectManagement/IssueController.php index 5f6d6a8f..330a4a5f 100644 --- a/app/Http/Controllers/Api/Admin/ProjectManagement/IssueController.php +++ b/app/Http/Controllers/Api/Admin/ProjectManagement/IssueController.php @@ -45,16 +45,9 @@ public function index(Request $request): View|JsonResponse /** * 프로젝트별 이슈 목록 */ - public function byProject(int $projectId, Request $request): View|JsonResponse + public function byProject(int $projectId, Request $request): JsonResponse { $issues = $this->issueService->getIssuesByProject($projectId); - $types = AdminPmIssue::getTypes(); - $statuses = AdminPmIssue::getStatuses(); - - // HTMX 요청이면 HTML 파셜 반환 - if ($request->header('HX-Request')) { - return view('project-management.issues.partials.list', compact('issues', 'types', 'statuses')); - } return response()->json([ 'success' => true, @@ -220,6 +213,20 @@ public function changeStatus(Request $request, int $id): JsonResponse ]); } + /** + * 이슈 긴급 토글 + */ + public function toggleUrgent(int $id): JsonResponse + { + $issue = $this->issueService->toggleUrgent($id); + + return response()->json([ + 'success' => true, + 'message' => $issue->is_urgent ? '긴급으로 설정되었습니다.' : '긴급 해제되었습니다.', + 'data' => $issue, + ]); + } + /** * 이슈 일괄 처리 */ diff --git a/app/Http/Controllers/Api/Admin/ProjectManagement/TaskController.php b/app/Http/Controllers/Api/Admin/ProjectManagement/TaskController.php index 241d043b..baeb2c27 100644 --- a/app/Http/Controllers/Api/Admin/ProjectManagement/TaskController.php +++ b/app/Http/Controllers/Api/Admin/ProjectManagement/TaskController.php @@ -42,15 +42,10 @@ public function index(Request $request): View|JsonResponse /** * 프로젝트별 작업 목록 (칸반보드용) */ - public function byProject(int $projectId, Request $request): View|JsonResponse + public function byProject(int $projectId, Request $request): JsonResponse { $tasks = $this->taskService->getTasksByProject($projectId); - // HTMX 요청이면 HTML 파셜 반환 - if ($request->header('HX-Request')) { - return view('project-management.tasks.partials.kanban', compact('tasks')); - } - return response()->json([ 'success' => true, 'data' => $tasks, @@ -175,6 +170,20 @@ public function changeStatus(Request $request, int $id): JsonResponse ]); } + /** + * 작업 긴급 토글 + */ + public function toggleUrgent(int $id): JsonResponse + { + $task = $this->taskService->toggleUrgent($id); + + return response()->json([ + 'success' => true, + 'message' => $task->is_urgent ? '긴급으로 설정되었습니다.' : '긴급 해제되었습니다.', + 'data' => $task, + ]); + } + /** * 작업 순서 변경 (드래그앤드롭) */ diff --git a/app/Models/Admin/AdminPmIssue.php b/app/Models/Admin/AdminPmIssue.php index e273f62a..569d7f60 100644 --- a/app/Models/Admin/AdminPmIssue.php +++ b/app/Models/Admin/AdminPmIssue.php @@ -37,6 +37,7 @@ class AdminPmIssue extends Model 'description', 'type', 'status', + 'is_urgent', 'created_by', 'updated_by', 'deleted_by', @@ -45,6 +46,7 @@ class AdminPmIssue extends Model protected $casts = [ 'project_id' => 'integer', 'task_id' => 'integer', + 'is_urgent' => 'boolean', 'created_by' => 'integer', 'updated_by' => 'integer', 'deleted_by' => 'integer', diff --git a/app/Models/Admin/AdminPmTask.php b/app/Models/Admin/AdminPmTask.php index ddf238ea..43b76fad 100644 --- a/app/Models/Admin/AdminPmTask.php +++ b/app/Models/Admin/AdminPmTask.php @@ -39,6 +39,7 @@ class AdminPmTask extends Model 'description', 'status', 'priority', + 'is_urgent', 'due_date', 'sort_order', 'assignee_id', @@ -49,6 +50,7 @@ class AdminPmTask extends Model protected $casts = [ 'project_id' => 'integer', + 'is_urgent' => 'boolean', 'due_date' => 'date', 'sort_order' => 'integer', 'assignee_id' => 'integer', diff --git a/app/Services/ProjectManagement/IssueService.php b/app/Services/ProjectManagement/IssueService.php index 8728d3ce..946ca79b 100644 --- a/app/Services/ProjectManagement/IssueService.php +++ b/app/Services/ProjectManagement/IssueService.php @@ -176,6 +176,20 @@ public function changeStatus(int $id, string $status): AdminPmIssue 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; + } + /** * 다중 이슈 상태 일괄 변경 */ diff --git a/app/Services/ProjectManagement/TaskService.php b/app/Services/ProjectManagement/TaskService.php index fca55aca..ebc7d45b 100644 --- a/app/Services/ProjectManagement/TaskService.php +++ b/app/Services/ProjectManagement/TaskService.php @@ -82,7 +82,12 @@ public function getTasksByProject(int $projectId): Collection return AdminPmTask::query() ->where('project_id', $projectId) ->with(['assignee', 'issues']) - ->withCount('issues') + ->withCount([ + 'issues', + 'issues as resolved_issues_count' => function ($query) { + $query->whereIn('status', ['resolved', 'closed']); + }, + ]) ->orderBy('sort_order') ->orderBy('id') ->get(); @@ -179,6 +184,20 @@ public function changeStatus(int $id, string $status): AdminPmTask return $task; } + /** + * 작업 긴급 토글 + */ + public function toggleUrgent(int $id): AdminPmTask + { + $task = AdminPmTask::findOrFail($id); + + $task->is_urgent = ! $task->is_urgent; + $task->updated_by = auth()->id(); + $task->save(); + + return $task; + } + /** * 작업 순서 변경 (드래그앤드롭) */ diff --git a/resources/views/project-management/projects/show.blade.php b/resources/views/project-management/projects/show.blade.php index 8069a2a6..f0ba941b 100644 --- a/resources/views/project-management/projects/show.blade.php +++ b/resources/views/project-management/projects/show.blade.php @@ -29,50 +29,49 @@ class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition
- +

진행률

-
-
+
+ +
+ +
- {{ $project->progress }}% + 0% +
+
+ 완료 0% + + 진행 0%

작업 현황

- @php - $taskStats = $project->task_status_counts; - @endphp
- {{ $project->tasks_count ?? 0 }} + 0
- 할일 {{ $taskStats['todo'] ?? 0 }} - 진행 {{ $taskStats['in_progress'] ?? 0 }} - 완료 {{ $taskStats['done'] ?? 0 }} + 할일 0 + 진행 0 + 완료 0

이슈 현황

- @php - $issueStats = $project->issue_status_counts; - @endphp
- {{ $project->issues_count ?? 0 }} + 0
- 열림 {{ $issueStats['open'] ?? 0 }} - 진행 {{ $issueStats['in_progress'] ?? 0 }} - 해결 {{ ($issueStats['resolved'] ?? 0) + ($issueStats['closed'] ?? 0) }} + 열림 0 + 진행 0 + 해결 0
@@ -96,12 +95,12 @@ class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition
@@ -129,11 +128,7 @@ class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition
-
+
@@ -164,11 +159,7 @@ class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg transition te
-
+
@@ -264,9 +255,9 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:rin
@@ -308,6 +299,10 @@ class="bg-red-600 hover:bg-red-700 text-white px-6 py-2 rounded-lg">저장id }}; const csrfToken = '{{ csrf_token() }}'; + // 전역 데이터 저장 (대시보드 업데이트용) + let tasksData = []; + let issuesData = []; + // 탭 전환 function switchTab(tab) { document.querySelectorAll('.tab-btn').forEach(btn => { @@ -323,128 +318,448 @@ function switchTab(tab) { document.getElementById(`content-${tab}`).classList.remove('hidden'); } - // HTMX 응답 처리 - document.body.addEventListener('htmx:afterSwap', function(event) { - const targetId = event.detail.target.id; + // 대시보드 업데이트 + function updateDashboard() { + // 작업 통계 + const taskTotal = tasksData.length; + const taskTodo = tasksData.filter(t => t.status === 'todo').length; + const taskInProgress = tasksData.filter(t => t.status === 'in_progress').length; + const taskDone = tasksData.filter(t => t.status === 'done').length; - if (targetId === 'task-list' || targetId === 'issue-list') { - try { - const response = JSON.parse(event.detail.xhr.response); - if (response.success && response.data) { - if (targetId === 'task-list') { - renderTasks(event.detail.target, response.data); - } else { - renderIssues(event.detail.target, response.data); - } - } - } catch (e) { - // HTML 응답인 경우 그대로 사용 - } + document.getElementById('task-total').textContent = taskTotal; + document.getElementById('task-todo').textContent = taskTodo; + document.getElementById('task-inprogress').textContent = taskInProgress; + document.getElementById('task-done').textContent = taskDone; + + // 이슈 통계 + const issueTotal = issuesData.length; + const issueOpen = issuesData.filter(i => i.status === 'open').length; + const issueInProgress = issuesData.filter(i => i.status === 'in_progress').length; + const issueResolved = issuesData.filter(i => i.status === 'resolved' || i.status === 'closed').length; + + document.getElementById('issue-total').textContent = issueTotal; + document.getElementById('issue-open').textContent = issueOpen; + document.getElementById('issue-inprogress').textContent = issueInProgress; + document.getElementById('issue-resolved').textContent = issueResolved; + + // 진행률 계산 (2색상) + if (taskTotal > 0) { + const donePct = Math.round((taskDone / taskTotal) * 100); + const inProgressPct = Math.round(((taskDone + taskInProgress) / taskTotal) * 100); + + document.getElementById('progress-bar-inprogress').style.width = inProgressPct + '%'; + document.getElementById('progress-bar-done').style.width = donePct + '%'; + document.getElementById('progress-text').textContent = inProgressPct + '%'; // 전체 진행률 표시 + document.getElementById('progress-done-pct').textContent = donePct + '%'; + document.getElementById('progress-inprogress-pct').textContent = (inProgressPct - donePct) + '%'; + } else { + document.getElementById('progress-bar-inprogress').style.width = '0%'; + document.getElementById('progress-bar-done').style.width = '0%'; + document.getElementById('progress-text').textContent = '0%'; + document.getElementById('progress-done-pct').textContent = '0%'; + document.getElementById('progress-inprogress-pct').textContent = '0%'; } + + // 탭 카운트 업데이트 + document.getElementById('tab-tasks').innerHTML = `작업 (${taskTotal})`; + document.getElementById('tab-issues').innerHTML = `이슈 (${issueTotal})`; + } + + // 작업 목록 로드 + async function loadTasks() { + try { + const response = await fetch(`/api/admin/pm/tasks/project/${projectId}`, { + headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': csrfToken } + }); + const result = await response.json(); + if (result.success) { + tasksData = result.data; + renderTasks(document.getElementById('task-list'), result.data); + updateDashboard(); + } + } catch (e) { + console.error('Failed to load tasks:', e); + } + } + + // 이슈 목록 로드 + async function loadIssues() { + try { + const response = await fetch(`/api/admin/pm/issues/project/${projectId}`, { + headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': csrfToken } + }); + const result = await response.json(); + if (result.success) { + issuesData = result.data; + renderIssues(document.getElementById('issue-list'), result.data); + updateDashboard(); + } + } catch (e) { + console.error('Failed to load issues:', e); + } + } + + // 페이지 로드 시 데이터 로드 + document.addEventListener('DOMContentLoaded', function() { + loadTasks(); + loadIssues(); }); - // 작업 목록 렌더링 + // 날짜 포맷 함수 (YYYY-MM-DD) + function formatDate(dateStr) { + if (!dateStr) return ''; + const date = new Date(dateStr); + return date.toISOString().split('T')[0]; + } + + // 우선순위 SVG 아이콘 + const prioritySvg = { + low: '', + medium: '', + high: '' + }; + const priorityLabels = { low: '낮음', medium: '보통', high: '높음' }; + + // 열린 아코디언 상태 저장 + const openAccordions = new Set(); + + // 아코디언 토글 + function toggleTaskIssues(taskId) { + const issueRows = document.querySelectorAll(`.task-issues-${taskId}`); + const icon = document.getElementById(`toggle-icon-${taskId}`); + const isCurrentlyHidden = issueRows[0]?.classList.contains('hidden'); + + issueRows.forEach(row => row.classList.toggle('hidden')); + if (icon) { + icon.classList.toggle('rotate-90'); + } + + // 상태 저장 + if (isCurrentlyHidden) { + openAccordions.add(taskId); + } else { + openAccordions.delete(taskId); + } + } + + // 작업 목록 렌더링 (테이블 형식 + 아코디언) function renderTasks(container, tasks) { if (!tasks || tasks.length === 0) { - container.innerHTML = '
등록된 작업이 없습니다.
'; + container.innerHTML = '
등록된 작업이 없습니다.
'; return; } const statusColors = { - todo: 'bg-gray-100 text-gray-700', - in_progress: 'bg-blue-100 text-blue-700', - done: 'bg-green-100 text-green-700' + todo: 'bg-gray-100 text-gray-600', + in_progress: 'bg-blue-100 text-blue-600', + done: 'bg-green-100 text-green-600' }; const statusLabels = { todo: '할일', in_progress: '진행중', done: '완료' }; - const priorityColors = { low: 'text-gray-400', medium: 'text-yellow-500', high: 'text-red-500' }; - const priorityIcons = { low: '○', medium: '◐', high: '●' }; + const issueTypeLabels = { bug: '버그', feature: '기능', improvement: '개선' }; + const issueStatusColors = { + open: 'bg-red-100 text-red-600', + in_progress: 'bg-yellow-100 text-yellow-600', + resolved: 'bg-green-100 text-green-600', + closed: 'bg-gray-100 text-gray-600' + }; + const issueStatusLabels = { open: '열림', in_progress: '진행중', resolved: '해결됨', closed: '종료' }; + + // 테이블 헤더 + let html = ` + + + + + + + + + + + + + + `; - let html = ''; tasks.forEach(task => { + const issueTotal = task.issues_count || 0; + const issueResolved = task.resolved_issues_count || 0; + const issueProgress = issueTotal > 0 ? Math.round((issueResolved / issueTotal) * 100) : 0; + const isOverdue = task.due_date && new Date(task.due_date) < new Date() && task.status !== 'done'; + const issues = task.issues || []; + const hasIssues = issues.length > 0; + + // 작업 Row html += ` -
- -
-
- ${task.title} - ${statusLabels[task.status]} - ${priorityIcons[task.priority]} +
+ + + + + + + + + `; + + // 이슈 서브 Rows (아코디언) - 8컬럼: 체크박스, 긴급, 작업명, 마감일, 이슈, 상태, 변경, 수정 + issues.forEach(issue => { + html += ` + + + + + + + + + + `; + }); }); + + html += '
긴급작업명마감일이슈상태변경
+ + + + +
+ ${hasIssues ? `` : ''} + ${prioritySvg[task.priority]} + ${task.title}
- ${task.description ? `

${task.description.substring(0, 100)}

` : ''} -
- ${task.due_date ? `${task.d_day_text || task.due_date}` : ''} - ${task.issues_count ? `이슈 ${task.issues_count}개` : ''} +
+ ${task.due_date ? formatDate(task.due_date) : '-'} + + ${issueTotal > 0 ? ` + + + + + ${issueResolved}/${issueTotal} + + ` : '-'} + + ${statusLabels[task.status]} + +
+ + +
- -
- - - -
- `; +
+ +
'; + container.innerHTML = html; + + // 진행중 작업의 아코디언 자동 열기 (최초 로드시만) + if (openAccordions.size === 0) { + tasks.forEach(task => { + if (task.status === 'in_progress' && task.issues && task.issues.length > 0) { + openAccordions.add(task.id); + } + }); + } + + // 열린 아코디언 상태 복원 + openAccordions.forEach(taskId => { + const issueRows = document.querySelectorAll(`.task-issues-${taskId}`); + const icon = document.getElementById(`toggle-icon-${taskId}`); + issueRows.forEach(row => row.classList.remove('hidden')); + if (icon) { + icon.classList.add('rotate-90'); + } + }); + } + + // 이슈 목록 렌더링 (테이블 형식) + function renderIssues(container, issues) { + if (!issues || issues.length === 0) { + container.innerHTML = '
등록된 이슈가 없습니다.
'; + return; + } + + const typeLabels = { bug: '버그', feature: '기능', improvement: '개선' }; + const statusColors = { + open: 'bg-red-100 text-red-600', + in_progress: 'bg-yellow-100 text-yellow-600', + resolved: 'bg-green-100 text-green-600', + closed: 'bg-gray-100 text-gray-600' + }; + const statusLabels = { open: '열림', in_progress: '진행중', resolved: '해결됨', closed: '종료' }; + + // 테이블 헤더 + let html = ` + + + + + + + + + + + + + + `; + + issues.forEach(issue => { + html += ` + + + + + + + + + + `; + }); + + html += '
긴급타입이슈명연결 작업상태변경
+ + + + + ${typeLabels[issue.type] || issue.type} + + ${issue.title} + + ${issue.task ? issue.task.title : '-'} + + ${statusLabels[issue.status]} + +
+ + + +
+
+ +
'; container.innerHTML = html; } - // 이슈 목록 렌더링 - function renderIssues(container, issues) { - if (!issues || issues.length === 0) { - container.innerHTML = '
등록된 이슈가 없습니다.
'; - return; - } - - const typeIcons = { bug: '🐛', feature: '✨', improvement: '💡' }; - const statusColors = { - open: 'bg-red-100 text-red-700', - in_progress: 'bg-yellow-100 text-yellow-700', - resolved: 'bg-green-100 text-green-700', - closed: 'bg-gray-100 text-gray-700' - }; - const statusLabels = { open: 'Open', in_progress: '진행중', resolved: '해결됨', closed: '종료' }; - - let html = ''; - issues.forEach(issue => { - html += ` -
- -
-
- ${typeIcons[issue.type] || '📌'} - ${issue.title} - ${statusLabels[issue.status]} -
- ${issue.description ? `

${issue.description.substring(0, 100)}

` : ''} - ${issue.task ? `

연결: ${issue.task.title}

` : ''} -
-
- - - - -
-
`; + // 작업 긴급 토글 + async function toggleTaskUrgent(taskId) { + await fetch(`/api/admin/pm/tasks/${taskId}/toggle-urgent`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken } }); - container.innerHTML = html; + loadTasks(); + } + + // 이슈 긴급 토글 + async function toggleIssueUrgent(issueId) { + await fetch(`/api/admin/pm/issues/${issueId}/toggle-urgent`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken } + }); + loadIssues(); + } + + // 서브 이슈 긴급 토글 (작업 탭 아코디언 내) + async function toggleSubIssueUrgent(issueId, taskId) { + await fetch(`/api/admin/pm/issues/${issueId}/toggle-urgent`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken } + }); + loadTasks(); // 작업 탭 아코디언 업데이트 + loadIssues(); // 이슈 탭도 동기화 } // 작업 상태 변경 async function changeTaskStatus(taskId, status) { await fetch(`/api/admin/pm/tasks/${taskId}/status`, { - method: 'PUT', + method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken }, body: JSON.stringify({ status }) }); - htmx.trigger(document.body, 'taskRefresh'); + loadTasks(); } - // 이슈 상태 변경 + // 이슈 상태 변경 (이슈 탭) async function changeIssueStatus(issueId, status) { await fetch(`/api/admin/pm/issues/${issueId}/status`, { - method: 'PUT', + method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken }, body: JSON.stringify({ status }) }); - htmx.trigger(document.body, 'issueRefresh'); + + // 이슈가 "진행"으로 변경될 때, 연결된 작업이 "할일"이면 자동으로 "진행중"으로 변경 + if (status === 'in_progress') { + const issue = issuesData.find(i => i.id === issueId); + if (issue && issue.task_id) { + const task = tasksData.find(t => t.id === issue.task_id); + if (task && task.status === 'todo') { + await fetch(`/api/admin/pm/tasks/${issue.task_id}/status`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken }, + body: JSON.stringify({ status: 'in_progress' }) + }); + } + } + } + + loadIssues(); + loadTasks(); // 작업 탭 진행률도 업데이트 + } + + // 서브 이슈 상태 변경 (작업 탭 아코디언 내) + async function changeSubIssueStatus(issueId, status, taskId = null) { + await fetch(`/api/admin/pm/issues/${issueId}/status`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken }, + body: JSON.stringify({ status }) + }); + + // 이슈가 "진행"으로 변경될 때, 연결된 작업이 "할일"이면 자동으로 "진행중"으로 변경 + if (status === 'in_progress' && taskId) { + const task = tasksData.find(t => t.id === taskId); + if (task && task.status === 'todo') { + await fetch(`/api/admin/pm/tasks/${taskId}/status`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken }, + body: JSON.stringify({ status: 'in_progress' }) + }); + } + } + + loadTasks(); // 진행률 즉시 반영 + loadIssues(); // 이슈 탭도 동기화 } // 작업 모달 @@ -496,7 +811,7 @@ function closeTaskModal() { if (result.success) { closeTaskModal(); - htmx.trigger(document.body, 'taskRefresh'); + loadTasks(); } else { alert(result.message || '저장에 실패했습니다.'); } @@ -551,7 +866,7 @@ function closeIssueModal() { if (result.success) { closeIssueModal(); - htmx.trigger(document.body, 'issueRefresh'); + loadIssues(); } else { alert(result.message || '저장에 실패했습니다.'); } @@ -589,7 +904,7 @@ function getSelectedIssueIds() { }); select.value = ''; - htmx.trigger(document.body, 'taskRefresh'); + loadTasks(); } async function handleIssueBulkAction() { @@ -615,7 +930,7 @@ function getSelectedIssueIds() { }); select.value = ''; - htmx.trigger(document.body, 'issueRefresh'); + loadIssues(); } @endpush \ No newline at end of file diff --git a/routes/api.php b/routes/api.php index 54fcfb92..c2a1e66c 100644 --- a/routes/api.php +++ b/routes/api.php @@ -20,10 +20,12 @@ |-------------------------------------------------------------------------- | | HTMX 요청 시 HTML 반환, 일반 요청 시 JSON 반환 -| +| - auth: 기본 인증 확인 +| - hq.member: 본사(HQ) 테넌트 소속 확인 +| - super.admin: 슈퍼관리자 전용 (복구, 영구삭제) */ -Route::middleware(['web', 'auth'])->prefix('admin')->name('api.admin.')->group(function () { +Route::middleware(['web', 'auth', 'hq.member'])->prefix('admin')->name('api.admin.')->group(function () { // 테넌트 관리 API Route::prefix('tenants')->name('tenants.')->group(function () { @@ -37,9 +39,11 @@ Route::put('/{id}', [TenantController::class, 'update'])->name('update'); Route::delete('/{id}', [TenantController::class, 'destroy'])->name('destroy'); - // 추가 액션 - Route::post('/{id}/restore', [TenantController::class, 'restore'])->name('restore'); - Route::delete('/{id}/force', [TenantController::class, 'forceDestroy'])->name('forceDestroy'); + // 슈퍼관리자 전용 액션 + Route::middleware('super.admin')->group(function () { + Route::post('/{id}/restore', [TenantController::class, 'restore'])->name('restore'); + Route::delete('/{id}/force', [TenantController::class, 'forceDestroy'])->name('forceDestroy'); + }); // 모달 관련 API Route::get('/{id}/modal', [TenantController::class, 'modal'])->name('modal'); @@ -66,8 +70,12 @@ Route::get('/{id}', [DepartmentController::class, 'show'])->name('show'); Route::put('/{id}', [DepartmentController::class, 'update'])->name('update'); Route::delete('/{id}', [DepartmentController::class, 'destroy'])->name('destroy'); - Route::post('/{id}/restore', [DepartmentController::class, 'restore'])->name('restore'); - Route::delete('/{id}/force', [DepartmentController::class, 'forceDelete'])->name('forceDelete'); + + // 슈퍼관리자 전용 액션 + Route::middleware('super.admin')->group(function () { + Route::post('/{id}/restore', [DepartmentController::class, 'restore'])->name('restore'); + Route::delete('/{id}/force', [DepartmentController::class, 'forceDelete'])->name('forceDelete'); + }); }); // 사용자 관리 API @@ -78,9 +86,11 @@ Route::put('/{id}', [UserController::class, 'update'])->name('update'); Route::delete('/{id}', [UserController::class, 'destroy'])->name('destroy'); - // 추가 액션 - Route::post('/{id}/restore', [UserController::class, 'restore'])->name('restore'); - Route::delete('/{id}/force', [UserController::class, 'forceDestroy'])->name('forceDestroy'); + // 슈퍼관리자 전용 액션 + Route::middleware('super.admin')->group(function () { + Route::post('/{id}/restore', [UserController::class, 'restore'])->name('restore'); + Route::delete('/{id}/force', [UserController::class, 'forceDestroy'])->name('forceDestroy'); + }); // 모달 관련 API Route::get('/{id}/modal', [UserController::class, 'modal'])->name('modal'); @@ -98,9 +108,13 @@ Route::put('/{id}', [MenuController::class, 'update'])->name('update'); Route::delete('/{id}', [MenuController::class, 'destroy'])->name('destroy'); + // 슈퍼관리자 전용 액션 + Route::middleware('super.admin')->group(function () { + Route::post('/{id}/restore', [MenuController::class, 'restore'])->name('restore'); + Route::delete('/{id}/force', [MenuController::class, 'forceDestroy'])->name('forceDestroy'); + }); + // 추가 액션 - Route::post('/{id}/restore', [MenuController::class, 'restore'])->name('restore'); - Route::delete('/{id}/force', [MenuController::class, 'forceDestroy'])->name('forceDestroy'); Route::post('/{id}/toggle-active', [MenuController::class, 'toggleActive'])->name('toggleActive'); Route::post('/{id}/toggle-hidden', [MenuController::class, 'toggleHidden'])->name('toggleHidden'); }); @@ -126,9 +140,13 @@ Route::put('/{id}', [BoardController::class, 'update'])->name('update'); Route::delete('/{id}', [BoardController::class, 'destroy'])->name('destroy'); + // 슈퍼관리자 전용 액션 + Route::middleware('super.admin')->group(function () { + Route::post('/{id}/restore', [BoardController::class, 'restore'])->name('restore'); + Route::delete('/{id}/force', [BoardController::class, 'forceDestroy'])->name('forceDestroy'); + }); + // 추가 액션 - Route::post('/{id}/restore', [BoardController::class, 'restore'])->name('restore'); - Route::delete('/{id}/force', [BoardController::class, 'forceDestroy'])->name('forceDestroy'); Route::post('/{id}/toggle-active', [BoardController::class, 'toggleActive'])->name('toggleActive'); // 필드 관리 API @@ -203,9 +221,13 @@ Route::put('/{id}', [PmProjectController::class, 'update'])->name('update'); Route::delete('/{id}', [PmProjectController::class, 'destroy'])->name('destroy'); + // 슈퍼관리자 전용 액션 + Route::middleware('super.admin')->group(function () { + Route::post('/{id}/restore', [PmProjectController::class, 'restore'])->name('restore'); + Route::delete('/{id}/force', [PmProjectController::class, 'forceDestroy'])->name('forceDestroy'); + }); + // 추가 액션 - Route::post('/{id}/restore', [PmProjectController::class, 'restore'])->name('restore'); - Route::delete('/{id}/force', [PmProjectController::class, 'forceDestroy'])->name('forceDestroy'); Route::post('/{id}/status', [PmProjectController::class, 'changeStatus'])->name('changeStatus'); Route::post('/{id}/duplicate', [PmProjectController::class, 'duplicate'])->name('duplicate'); }); @@ -223,10 +245,15 @@ Route::put('/{id}', [PmTaskController::class, 'update'])->name('update'); Route::delete('/{id}', [PmTaskController::class, 'destroy'])->name('destroy'); + // 슈퍼관리자 전용 액션 + Route::middleware('super.admin')->group(function () { + Route::post('/{id}/restore', [PmTaskController::class, 'restore'])->name('restore'); + Route::delete('/{id}/force', [PmTaskController::class, 'forceDestroy'])->name('forceDestroy'); + }); + // 추가 액션 - Route::post('/{id}/restore', [PmTaskController::class, 'restore'])->name('restore'); - Route::delete('/{id}/force', [PmTaskController::class, 'forceDestroy'])->name('forceDestroy'); Route::post('/{id}/status', [PmTaskController::class, 'changeStatus'])->name('changeStatus'); + Route::post('/{id}/toggle-urgent', [PmTaskController::class, 'toggleUrgent'])->name('toggleUrgent'); // 프로젝트별 Route::get('/project/{projectId}', [PmTaskController::class, 'byProject'])->name('byProject'); @@ -248,10 +275,15 @@ Route::put('/{id}', [PmIssueController::class, 'update'])->name('update'); Route::delete('/{id}', [PmIssueController::class, 'destroy'])->name('destroy'); + // 슈퍼관리자 전용 액션 + Route::middleware('super.admin')->group(function () { + Route::post('/{id}/restore', [PmIssueController::class, 'restore'])->name('restore'); + Route::delete('/{id}/force', [PmIssueController::class, 'forceDestroy'])->name('forceDestroy'); + }); + // 추가 액션 - Route::post('/{id}/restore', [PmIssueController::class, 'restore'])->name('restore'); - Route::delete('/{id}/force', [PmIssueController::class, 'forceDestroy'])->name('forceDestroy'); Route::post('/{id}/status', [PmIssueController::class, 'changeStatus'])->name('changeStatus'); + Route::post('/{id}/toggle-urgent', [PmIssueController::class, 'toggleUrgent'])->name('toggleUrgent'); // 연관별 Route::get('/project/{projectId}', [PmIssueController::class, 'byProject'])->name('byProject');