@@ -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]}
+
+ |
+
+ |
+
+
+ |
+
+
+ ${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]}
+ |
+
+
+
+
+
-
-
-
-
-
-
- `;
+ |
+
+
+ |
+
`;
+
+ // 이슈 서브 Rows (아코디언) - 8컬럼: 체크박스, 긴급, 작업명, 마감일, 이슈, 상태, 변경, 수정
+ issues.forEach(issue => {
+ html += `
+
+ |
+
+
+ |
+
+
+ ${issueTypeLabels[issue.type] || issue.type}
+ ${issue.title}
+
+ |
+ - |
+ - |
+
+ ${issueStatusLabels[issue.status]}
+ |
+
+
+ ${issue.status !== 'open' ? `` : ''}
+ ${issue.status !== 'in_progress' ? `` : ''}
+ ${issue.status !== 'resolved' ? `` : ''}
+ ${issue.status !== 'closed' ? `` : ''}
+
+ |
+
+
+ |
+
`;
+ });
});
+
+ html += '
';
+ 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 = `
+
';
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');