diff --git a/app/Http/Controllers/Api/Admin/ProjectManagement/ImportController.php b/app/Http/Controllers/Api/Admin/ProjectManagement/ImportController.php index eec46488..71a3a628 100644 --- a/app/Http/Controllers/Api/Admin/ProjectManagement/ImportController.php +++ b/app/Http/Controllers/Api/Admin/ProjectManagement/ImportController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Api\Admin\ProjectManagement; use App\Http\Controllers\Controller; +use App\Http\Requests\ProjectManagement\ImportIssuesRequest; use App\Http\Requests\ProjectManagement\ImportProjectRequest; use App\Services\ProjectManagement\ImportService; use Illuminate\Http\JsonResponse; @@ -75,6 +76,27 @@ public function importTasks(Request $request, int $projectId): JsonResponse } } + /** + * 기존 작업에 이슈만 추가 + */ + public function importIssues(ImportIssuesRequest $request, int $taskId): JsonResponse + { + try { + $result = $this->importService->importIssuesToTask($taskId, $request->validated()['issues']); + + return response()->json([ + 'success' => true, + 'message' => "이슈가 추가되었습니다. ({$result['issues_count']}개)", + 'data' => $result, + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => '가져오기 실패: '.$e->getMessage(), + ], 500); + } + } + /** * JSON 구조 사전 검증 */ diff --git a/app/Http/Requests/ProjectManagement/ImportIssuesRequest.php b/app/Http/Requests/ProjectManagement/ImportIssuesRequest.php new file mode 100644 index 00000000..d560fca1 --- /dev/null +++ b/app/Http/Requests/ProjectManagement/ImportIssuesRequest.php @@ -0,0 +1,44 @@ + 'required|array|min:1', + 'issues.*.title' => 'required|string|max:255', + 'issues.*.description' => 'nullable|string', + 'issues.*.type' => 'nullable|in:bug,feature,improvement', + 'issues.*.status' => 'nullable|in:open,in_progress,resolved,closed', + // 일정 관련 + 'issues.*.start_date' => 'nullable|date', + 'issues.*.due_date' => 'nullable|date|after_or_equal:issues.*.start_date', + 'issues.*.estimated_hours' => 'nullable|integer|min:0', + 'issues.*.is_urgent' => 'nullable|boolean', + // 팀/담당자/고객사 (하이브리드) + 'issues.*.department_id' => 'nullable|integer|exists:departments,id', + 'issues.*.team' => 'nullable|string|max:100', + 'issues.*.assignee_id' => 'nullable|integer|exists:users,id', + 'issues.*.assignee_name' => 'nullable|string|max:100', + 'issues.*.client' => 'nullable|string|max:100', + ]; + } + + public function messages(): array + { + return [ + 'issues.required' => 'issues 배열은 필수입니다.', + 'issues.min' => '최소 1개 이상의 이슈가 필요합니다.', + 'issues.*.title.required' => '각 이슈의 title은 필수입니다.', + ]; + } +} diff --git a/app/Http/Requests/ProjectManagement/ImportProjectRequest.php b/app/Http/Requests/ProjectManagement/ImportProjectRequest.php index cbd4e4fa..8477fbcd 100644 --- a/app/Http/Requests/ProjectManagement/ImportProjectRequest.php +++ b/app/Http/Requests/ProjectManagement/ImportProjectRequest.php @@ -29,6 +29,9 @@ public function rules(): array 'tasks.*.status' => 'nullable|in:todo,in_progress,done', 'tasks.*.priority' => 'nullable|in:low,medium,high', 'tasks.*.due_date' => 'nullable|date', + 'tasks.*.is_urgent' => 'nullable|boolean', + 'tasks.*.assignee_id' => 'nullable|integer|exists:users,id', + 'tasks.*.assignee_name' => 'nullable|string|max:100', // 작업별 이슈 목록 'tasks.*.issues' => 'nullable|array', diff --git a/app/Services/ProjectManagement/ImportService.php b/app/Services/ProjectManagement/ImportService.php index 1dc5497f..63cef483 100644 --- a/app/Services/ProjectManagement/ImportService.php +++ b/app/Services/ProjectManagement/ImportService.php @@ -139,6 +139,48 @@ public function importTasksToProject(int $projectId, array $tasks): array }); } + /** + * 기존 작업에 이슈만 추가 + */ + public function importIssuesToTask(int $taskId, array $issues): array + { + return DB::transaction(function () use ($taskId, $issues) { + $task = AdminPmTask::findOrFail($taskId); + + $result = [ + 'task_id' => $taskId, + 'task_title' => $task->title, + 'project_id' => $task->project_id, + 'issues_count' => 0, + ]; + + foreach ($issues as $issueData) { + AdminPmIssue::create([ + 'project_id' => $task->project_id, + 'task_id' => $task->id, + 'title' => $issueData['title'], + 'description' => $issueData['description'] ?? null, + 'type' => $issueData['type'] ?? AdminPmIssue::TYPE_FEATURE, + 'status' => $issueData['status'] ?? AdminPmIssue::STATUS_OPEN, + 'start_date' => $issueData['start_date'] ?? null, + 'due_date' => $issueData['due_date'] ?? null, + 'estimated_hours' => $issueData['estimated_hours'] ?? null, + 'is_urgent' => $issueData['is_urgent'] ?? false, + // 팀/담당자/고객사 (하이브리드) + 'department_id' => $issueData['department_id'] ?? null, + 'team' => $issueData['team'] ?? null, + 'assignee_id' => $issueData['assignee_id'] ?? null, + 'assignee_name' => $issueData['assignee_name'] ?? null, + 'client' => $issueData['client'] ?? null, + 'created_by' => auth()->id(), + ]); + $result['issues_count']++; + } + + return $result; + }); + } + /** * JSON 샘플 템플릿 반환 */ diff --git a/ms1-issues.json b/ms1-issues.json new file mode 100644 index 00000000..21362735 --- /dev/null +++ b/ms1-issues.json @@ -0,0 +1,15 @@ +{ + "issues": [ + {"title": "견적관리", "description": "공수: 1.75일", "type": "feature", "start_date": "2025-11-27", "due_date": "2025-11-28", "team": "디자인팀"}, + {"title": "기준정보-견적수식", "description": "공수: 0.3일", "type": "feature", "start_date": "2025-11-28", "due_date": "2025-12-01", "team": "디자인팀"}, + {"title": "수주관리", "description": "공수: 3.25일", "type": "feature", "start_date": "2025-12-01", "due_date": "2025-12-04", "team": "디자인팀"}, + {"title": "생산관리", "description": "공수: 1.95일", "type": "feature", "start_date": "2025-12-04", "due_date": "2025-12-08", "team": "디자인팀"}, + {"title": "기준정보-공정", "description": "공수: 1.45일", "type": "feature", "start_date": "2025-12-08", "due_date": "2025-12-10", "team": "디자인팀"}, + {"title": "출하관리", "description": "공수: 2.25일", "type": "feature", "start_date": "2025-12-10", "due_date": "2025-12-12", "team": "디자인팀"}, + {"title": "거래처관리", "description": "공수: 1.1일", "type": "feature", "start_date": "2025-12-15", "due_date": "2025-12-16", "team": "디자인팀"}, + {"title": "품질관리", "description": "공수: 3.5일", "type": "feature", "start_date": "2025-12-16", "due_date": "2025-12-19", "team": "디자인팀"}, + {"title": "자재관리", "description": "공수: 3.3일", "type": "feature", "start_date": "2025-12-19", "due_date": "2025-12-24", "team": "디자인팀"}, + {"title": "단가관리", "description": "공수: 1일", "type": "feature", "start_date": "2025-12-24", "due_date": "2025-12-24", "team": "디자인팀"}, + {"title": "회계관리", "description": "공수: 1일", "type": "feature", "start_date": "2025-12-26", "due_date": "2025-12-26", "team": "디자인팀"} + ] +} diff --git a/resources/views/project-management/import.blade.php b/resources/views/project-management/import.blade.php index b1dff301..fc773b7a 100644 --- a/resources/views/project-management/import.blade.php +++ b/resources/views/project-management/import.blade.php @@ -24,7 +24,7 @@ {{-- Import 모드 선택 --}}

Import 모드

-
+
+
- {{-- 기존 프로젝트 선택 (existing 모드) --}} + {{-- 기존 프로젝트 선택 (existing, task_issues 모드) --}} + + {{-- 작업 선택 (task_issues 모드) --}} +
{{-- JSON 입력 --}} @@ -116,27 +131,28 @@ class="flex-1 px-4 py-2.5 text-sm font-medium text-white bg-indigo-600 rounded-l

새 프로젝트 생성

{
   "project": {
-    "name": "프로젝트명 (필수)",
-    "description": "설명",
-    "status": "active|completed|on_hold",
+    "name": "SAM 시스템 개발",
+    "description": "프로젝트 설명입니다",
+    "status": "active",
     "start_date": "2025-01-01",
     "end_date": "2025-03-31"
   },
   "tasks": [
     {
-      "title": "작업 제목 (필수)",
-      "description": "작업 설명",
-      "status": "todo|in_progress|done",
-      "priority": "low|medium|high",
+      "title": "API 개발",
+      "description": "REST API 엔드포인트 구현",
+      "status": "in_progress",
+      "priority": "high",
       "is_urgent": false,
       "due_date": "2025-01-15",
       "assignee_id": null,
+      "assignee_name": "김개발",
       "issues": [
         {
-          "title": "이슈 제목 (필수)",
-          "description": "이슈 설명",
-          "type": "bug|feature|improvement",
-          "status": "open|in_progress|resolved|closed",
+          "title": "사용자 인증 구현",
+          "description": "JWT 기반 인증 시스템",
+          "type": "feature",
+          "status": "open",
           "start_date": "2025-01-01",
           "due_date": "2025-01-15",
           "estimated_hours": 8,
@@ -145,7 +161,7 @@ class="flex-1 px-4 py-2.5 text-sm font-medium text-white bg-indigo-600 rounded-l
           "team": "개발팀",
           "assignee_id": null,
           "assignee_name": "홍길동",
-          "client": "고객사명"
+          "client": "내부"
         }
       ]
     }
@@ -163,6 +179,30 @@ class="flex-1 px-4 py-2.5 text-sm font-medium text-white bg-indigo-600 rounded-l
     }
   ]
 }
+ +

기존 작업에 이슈 추가

+
{
+  "issues": [
+    {
+      "title": "Phase 1: 견적관리",
+      "description": "공수: 1.75일",
+      "type": "feature",
+      "status": "open",
+      "start_date": "2024-11-27",
+      "due_date": "2024-11-28",
+      "estimated_hours": 14,
+      "team": "개발팀"
+    },
+    {
+      "title": "Phase 2: 기준정보-견적수식",
+      "description": "공수: 0.3일",
+      "type": "feature",
+      "status": "open",
+      "start_date": "2024-11-28",
+      "due_date": "2024-12-01"
+    }
+  ]
+}
@@ -249,12 +289,58 @@ class="flex-1 px-4 py-2.5 text-sm font-medium text-white bg-indigo-600 rounded-l function toggleImportMode() { const mode = document.querySelector('input[name="import_mode"]:checked').value; const existingSelect = document.getElementById('existingProjectSelect'); + const taskSelect = document.getElementById('existingTaskSelect'); - if (mode === 'existing') { + if (mode === 'existing' || mode === 'task_issues') { existingSelect.classList.remove('hidden'); } else { existingSelect.classList.add('hidden'); } + + if (mode === 'task_issues') { + taskSelect.classList.remove('hidden'); + loadTasks(); // 작업 목록 로드 + } else { + taskSelect.classList.add('hidden'); + } + } + + // 프로젝트별 작업 목록 로드 + async function loadTasks() { + const mode = document.querySelector('input[name="import_mode"]:checked').value; + if (mode !== 'task_issues') return; + + const projectId = document.getElementById('targetProject').value; + const taskSelect = document.getElementById('targetTask'); + + if (!projectId) { + taskSelect.innerHTML = ''; + return; + } + + try { + const response = await fetch(`/api/admin/pm/tasks/project/${projectId}`, { + headers: { + 'Accept': 'application/json', + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, + }, + }); + + if (!response.ok) throw new Error('작업 목록 로드 실패'); + + const result = await response.json(); + const tasks = result.data || []; + + taskSelect.innerHTML = ''; + tasks.forEach(task => { + const option = document.createElement('option'); + option.value = task.id; + option.textContent = task.title; + taskSelect.appendChild(option); + }); + } catch (e) { + showError('작업 목록 로드 실패: ' + e.message); + } } // 샘플 템플릿 불러오기 @@ -265,6 +351,22 @@ function loadTemplate() { if (mode === 'existing') { // 기존 프로젝트 모드에서는 tasks만 표시 template = { tasks: sampleTemplate.tasks }; + } else if (mode === 'task_issues') { + // 기존 작업에 이슈 추가 모드에서는 issues만 표시 + template = { + issues: [ + { + title: "이슈 제목", + description: "이슈 설명", + type: "feature", + status: "open", + start_date: new Date().toISOString().split('T')[0], + due_date: new Date(Date.now() + 7*24*60*60*1000).toISOString().split('T')[0], + estimated_hours: 8, + team: "개발팀" + } + ] + }; } document.getElementById('jsonInput').value = JSON.stringify(template, null, 2); @@ -387,6 +489,13 @@ function loadFile(event) { return; } url = `/api/admin/pm/import/project/${projectId}/tasks`; + } else if (mode === 'task_issues') { + const taskId = document.getElementById('targetTask').value; + if (!taskId) { + showError('대상 작업을 선택해주세요.'); + return; + } + url = `/api/admin/pm/import/task/${taskId}/issues`; } try { @@ -415,17 +524,30 @@ function loadFile(event) { const result = await response.json(); if (result.success) { - showResult('success', result.message, [ - `프로젝트: ${result.data.project_name || result.data.project_id}`, - `작업: ${result.data.tasks_count}개`, - `이슈: ${result.data.issues_count}개`, - ]); + let resultItems = []; + if (mode === 'task_issues') { + resultItems = [ + `작업: ${result.data.task_title}`, + `이슈: ${result.data.issues_count}개 추가`, + ]; + } else { + resultItems = [ + `프로젝트: ${result.data.project_name || result.data.project_id}`, + `작업: ${result.data.tasks_count}개`, + `이슈: ${result.data.issues_count}개`, + ]; + } + showResult('success', result.message, resultItems); showToast('Import 완료!', 'success'); // 새 프로젝트면 상세 페이지로 이동 버튼 표시 if (mode === 'new' && result.data.project_id) { appendViewButton(result.data.project_id); } + // 작업 이슈 추가 모드면 프로젝트 상세 페이지로 이동 버튼 표시 + if (mode === 'task_issues' && result.data.project_id) { + appendViewButton(result.data.project_id); + } } else { showResult('error', 'Import 실패', [result.message]); } diff --git a/routes/api.php b/routes/api.php index 468a4330..e0c5c29a 100644 --- a/routes/api.php +++ b/routes/api.php @@ -351,6 +351,7 @@ 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'); + Route::post('/task/{taskId}/issues', [PmImportController::class, 'importIssues'])->name('importIssues'); }); });