feat: [pm] 프로젝트 진행 관리 시스템 구현

- 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 및 복원
This commit is contained in:
2025-11-28 08:49:30 +09:00
parent fe902472c1
commit e2475d0d9f
30 changed files with 5131 additions and 1 deletions

View File

@@ -1,8 +1,13 @@
<?php
use App\Http\Controllers\Api\Admin\BoardController;
use App\Http\Controllers\Api\Admin\DepartmentController;
use App\Http\Controllers\Api\Admin\MenuController;
use App\Http\Controllers\Api\Admin\PermissionController;
use App\Http\Controllers\Api\Admin\ProjectManagement\ImportController as PmImportController;
use App\Http\Controllers\Api\Admin\ProjectManagement\IssueController as PmIssueController;
use App\Http\Controllers\Api\Admin\ProjectManagement\ProjectController as PmProjectController;
use App\Http\Controllers\Api\Admin\ProjectManagement\TaskController as PmTaskController;
use App\Http\Controllers\Api\Admin\RoleController;
use App\Http\Controllers\Api\Admin\RolePermissionController;
use App\Http\Controllers\Api\Admin\TenantController;
@@ -109,6 +114,31 @@
Route::delete('/{id}', [PermissionController::class, 'destroy'])->name('destroy');
});
// 시스템 게시판 관리 API
Route::prefix('boards')->name('boards.')->group(function () {
// 고정 경로는 먼저 정의
Route::get('/stats', [BoardController::class, 'stats'])->name('stats');
// 기본 CRUD
Route::get('/', [BoardController::class, 'index'])->name('index');
Route::post('/', [BoardController::class, 'store'])->name('store');
Route::get('/{id}', [BoardController::class, 'show'])->name('show');
Route::put('/{id}', [BoardController::class, 'update'])->name('update');
Route::delete('/{id}', [BoardController::class, 'destroy'])->name('destroy');
// 추가 액션
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
Route::get('/{id}/fields', [BoardController::class, 'fields'])->name('fields');
Route::post('/{id}/fields', [BoardController::class, 'storeField'])->name('storeField');
Route::put('/{id}/fields/{fieldId}', [BoardController::class, 'updateField'])->name('updateField');
Route::delete('/{id}/fields/{fieldId}', [BoardController::class, 'destroyField'])->name('destroyField');
Route::post('/{id}/fields/reorder', [BoardController::class, 'reorderFields'])->name('reorderFields');
});
// 역할 권한 관리 API
Route::prefix('role-permissions')->name('role-permissions.')->group(function () {
Route::get('/matrix', [RolePermissionController::class, 'getMatrix'])->name('matrix');
@@ -151,4 +181,89 @@
Route::get('/', [\App\Http\Controllers\Api\Admin\ArchivedRecordController::class, 'index'])->name('index');
Route::get('/{id}', [\App\Http\Controllers\Api\Admin\ArchivedRecordController::class, 'show'])->name('show');
});
/*
|--------------------------------------------------------------------------
| 프로젝트 관리 API
|--------------------------------------------------------------------------
*/
Route::prefix('pm')->name('pm.')->group(function () {
// 프로젝트 관리 API
Route::prefix('projects')->name('projects.')->group(function () {
// 고정 경로
Route::get('/stats', [PmProjectController::class, 'stats'])->name('stats');
Route::get('/dashboard', [PmProjectController::class, 'dashboard'])->name('dashboard');
Route::get('/dropdown', [PmProjectController::class, 'dropdown'])->name('dropdown');
// 기본 CRUD
Route::get('/', [PmProjectController::class, 'index'])->name('index');
Route::post('/', [PmProjectController::class, 'store'])->name('store');
Route::get('/{id}', [PmProjectController::class, 'show'])->name('show');
Route::put('/{id}', [PmProjectController::class, 'update'])->name('update');
Route::delete('/{id}', [PmProjectController::class, 'destroy'])->name('destroy');
// 추가 액션
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');
});
// 작업 관리 API
Route::prefix('tasks')->name('tasks.')->group(function () {
// 고정 경로
Route::get('/urgent', [PmTaskController::class, 'urgent'])->name('urgent');
Route::post('/bulk', [PmTaskController::class, 'bulk'])->name('bulk');
// 기본 CRUD
Route::get('/', [PmTaskController::class, 'index'])->name('index');
Route::post('/', [PmTaskController::class, 'store'])->name('store');
Route::get('/{id}', [PmTaskController::class, 'show'])->name('show');
Route::put('/{id}', [PmTaskController::class, 'update'])->name('update');
Route::delete('/{id}', [PmTaskController::class, 'destroy'])->name('destroy');
// 추가 액션
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::get('/project/{projectId}', [PmTaskController::class, 'byProject'])->name('byProject');
Route::post('/project/{projectId}/reorder', [PmTaskController::class, 'reorder'])->name('reorder');
Route::get('/project/{projectId}/stats', [PmTaskController::class, 'stats'])->name('stats');
});
// 이슈 관리 API
Route::prefix('issues')->name('issues.')->group(function () {
// 고정 경로
Route::get('/stats', [PmIssueController::class, 'stats'])->name('stats');
Route::get('/open', [PmIssueController::class, 'open'])->name('open');
Route::post('/bulk', [PmIssueController::class, 'bulk'])->name('bulk');
// 기본 CRUD
Route::get('/', [PmIssueController::class, 'index'])->name('index');
Route::post('/', [PmIssueController::class, 'store'])->name('store');
Route::get('/{id}', [PmIssueController::class, 'show'])->name('show');
Route::put('/{id}', [PmIssueController::class, 'update'])->name('update');
Route::delete('/{id}', [PmIssueController::class, 'destroy'])->name('destroy');
// 추가 액션
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::get('/project/{projectId}', [PmIssueController::class, 'byProject'])->name('byProject');
Route::get('/task/{taskId}', [PmIssueController::class, 'byTask'])->name('byTask');
});
// JSON Import API
Route::prefix('import')->name('import.')->group(function () {
Route::get('/template', [PmImportController::class, 'template'])->name('template');
Route::post('/validate', [PmImportController::class, 'validate'])->name('validate');
Route::post('/', [PmImportController::class, 'import'])->name('import');
Route::post('/project/{projectId}/tasks', [PmImportController::class, 'importTasks'])->name('importTasks');
});
});
});