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 및 복원
2025-11-28 08:49:30 +09:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Services\ProjectManagement;
|
|
|
|
|
|
|
|
|
|
use App\Models\Admin\AdminPmIssue;
|
|
|
|
|
use App\Models\Admin\AdminPmProject;
|
|
|
|
|
use App\Models\Admin\AdminPmTask;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
|
|
|
|
class ImportService
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* JSON 데이터로 프로젝트, 작업, 이슈 일괄 생성
|
|
|
|
|
*/
|
|
|
|
|
public function importFromJson(array $data): array
|
|
|
|
|
{
|
|
|
|
|
return DB::transaction(function () use ($data) {
|
|
|
|
|
$result = [
|
|
|
|
|
'project' => null,
|
|
|
|
|
'tasks_count' => 0,
|
|
|
|
|
'issues_count' => 0,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// 1. 프로젝트 생성
|
|
|
|
|
$projectData = $data['project'];
|
|
|
|
|
$project = AdminPmProject::create([
|
|
|
|
|
'name' => $projectData['name'],
|
|
|
|
|
'description' => $projectData['description'] ?? null,
|
|
|
|
|
'status' => $projectData['status'] ?? AdminPmProject::STATUS_ACTIVE,
|
|
|
|
|
'start_date' => $projectData['start_date'] ?? null,
|
|
|
|
|
'end_date' => $projectData['end_date'] ?? null,
|
|
|
|
|
'created_by' => auth()->id(),
|
|
|
|
|
]);
|
|
|
|
|
$result['project'] = $project;
|
|
|
|
|
|
|
|
|
|
// 2. 작업 생성
|
|
|
|
|
$tasks = $data['tasks'] ?? [];
|
|
|
|
|
$sortOrder = 1;
|
|
|
|
|
|
|
|
|
|
foreach ($tasks as $taskData) {
|
|
|
|
|
$task = AdminPmTask::create([
|
|
|
|
|
'project_id' => $project->id,
|
|
|
|
|
'title' => $taskData['title'],
|
|
|
|
|
'description' => $taskData['description'] ?? null,
|
|
|
|
|
'status' => $taskData['status'] ?? AdminPmTask::STATUS_TODO,
|
|
|
|
|
'priority' => $taskData['priority'] ?? AdminPmTask::PRIORITY_MEDIUM,
|
|
|
|
|
'due_date' => $taskData['due_date'] ?? null,
|
|
|
|
|
'sort_order' => $sortOrder++,
|
|
|
|
|
'created_by' => auth()->id(),
|
|
|
|
|
]);
|
|
|
|
|
$result['tasks_count']++;
|
|
|
|
|
|
|
|
|
|
// 3. 작업별 이슈 생성
|
|
|
|
|
$issues = $taskData['issues'] ?? [];
|
|
|
|
|
foreach ($issues as $issueData) {
|
|
|
|
|
AdminPmIssue::create([
|
|
|
|
|
'project_id' => $project->id,
|
|
|
|
|
'task_id' => $task->id,
|
|
|
|
|
'title' => $issueData['title'],
|
|
|
|
|
'description' => $issueData['description'] ?? null,
|
|
|
|
|
'type' => $issueData['type'] ?? AdminPmIssue::TYPE_BUG,
|
|
|
|
|
'status' => $issueData['status'] ?? AdminPmIssue::STATUS_OPEN,
|
2025-12-02 19:10:15 +09:00
|
|
|
'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,
|
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 및 복원
2025-11-28 08:49:30 +09:00
|
|
|
'created_by' => auth()->id(),
|
|
|
|
|
]);
|
|
|
|
|
$result['issues_count']++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 기존 프로젝트에 작업/이슈 추가
|
|
|
|
|
*/
|
|
|
|
|
public function importTasksToProject(int $projectId, array $tasks): array
|
|
|
|
|
{
|
|
|
|
|
return DB::transaction(function () use ($projectId, $tasks) {
|
|
|
|
|
$project = AdminPmProject::findOrFail($projectId);
|
|
|
|
|
|
|
|
|
|
$result = [
|
|
|
|
|
'project_id' => $projectId,
|
|
|
|
|
'tasks_count' => 0,
|
|
|
|
|
'issues_count' => 0,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$maxSortOrder = AdminPmTask::where('project_id', $projectId)->max('sort_order') ?? 0;
|
|
|
|
|
$sortOrder = $maxSortOrder + 1;
|
|
|
|
|
|
|
|
|
|
foreach ($tasks as $taskData) {
|
|
|
|
|
$task = AdminPmTask::create([
|
|
|
|
|
'project_id' => $project->id,
|
|
|
|
|
'title' => $taskData['title'],
|
|
|
|
|
'description' => $taskData['description'] ?? null,
|
|
|
|
|
'status' => $taskData['status'] ?? AdminPmTask::STATUS_TODO,
|
|
|
|
|
'priority' => $taskData['priority'] ?? AdminPmTask::PRIORITY_MEDIUM,
|
|
|
|
|
'due_date' => $taskData['due_date'] ?? null,
|
|
|
|
|
'sort_order' => $sortOrder++,
|
|
|
|
|
'created_by' => auth()->id(),
|
|
|
|
|
]);
|
|
|
|
|
$result['tasks_count']++;
|
|
|
|
|
|
|
|
|
|
$issues = $taskData['issues'] ?? [];
|
|
|
|
|
foreach ($issues as $issueData) {
|
|
|
|
|
AdminPmIssue::create([
|
|
|
|
|
'project_id' => $project->id,
|
|
|
|
|
'task_id' => $task->id,
|
|
|
|
|
'title' => $issueData['title'],
|
|
|
|
|
'description' => $issueData['description'] ?? null,
|
|
|
|
|
'type' => $issueData['type'] ?? AdminPmIssue::TYPE_BUG,
|
|
|
|
|
'status' => $issueData['status'] ?? AdminPmIssue::STATUS_OPEN,
|
2025-12-02 19:10:15 +09:00
|
|
|
'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,
|
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 및 복원
2025-11-28 08:49:30 +09:00
|
|
|
'created_by' => auth()->id(),
|
|
|
|
|
]);
|
|
|
|
|
$result['issues_count']++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-09 16:39:52 +09:00
|
|
|
/**
|
|
|
|
|
* 기존 작업에 이슈만 추가
|
|
|
|
|
*/
|
|
|
|
|
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;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
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 및 복원
2025-11-28 08:49:30 +09:00
|
|
|
/**
|
|
|
|
|
* JSON 샘플 템플릿 반환
|
|
|
|
|
*/
|
|
|
|
|
public function getSampleTemplate(): array
|
|
|
|
|
{
|
|
|
|
|
return [
|
|
|
|
|
'project' => [
|
|
|
|
|
'name' => '프로젝트명 (필수)',
|
|
|
|
|
'description' => '프로젝트 설명 (선택)',
|
|
|
|
|
'status' => 'active',
|
|
|
|
|
'start_date' => date('Y-m-d'),
|
|
|
|
|
'end_date' => date('Y-m-d', strtotime('+3 months')),
|
|
|
|
|
],
|
|
|
|
|
'tasks' => [
|
|
|
|
|
[
|
|
|
|
|
'title' => '작업 1 (필수)',
|
|
|
|
|
'description' => '작업 설명 (선택)',
|
|
|
|
|
'status' => 'todo',
|
|
|
|
|
'priority' => 'medium',
|
|
|
|
|
'due_date' => date('Y-m-d', strtotime('+1 week')),
|
|
|
|
|
'issues' => [
|
|
|
|
|
[
|
|
|
|
|
'title' => '이슈 1',
|
|
|
|
|
'description' => '이슈 설명',
|
2025-12-02 19:10:15 +09:00
|
|
|
'type' => 'feature',
|
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 및 복원
2025-11-28 08:49:30 +09:00
|
|
|
'status' => 'open',
|
2025-12-02 19:10:15 +09:00
|
|
|
'start_date' => date('Y-m-d'),
|
|
|
|
|
'due_date' => date('Y-m-d', strtotime('+1 week')),
|
|
|
|
|
'estimated_hours' => 8,
|
|
|
|
|
'is_urgent' => false,
|
|
|
|
|
// 방법 1: FK 연동 (DB에 존재하는 ID)
|
|
|
|
|
'department_id' => null,
|
|
|
|
|
'assignee_id' => null,
|
|
|
|
|
// 방법 2: 문자열 직접 입력 (연동 없이)
|
|
|
|
|
'team' => '개발팀',
|
|
|
|
|
'assignee_name' => '홍길동',
|
|
|
|
|
'client' => '경동기업',
|
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 및 복원
2025-11-28 08:49:30 +09:00
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'title' => '작업 2',
|
|
|
|
|
'status' => 'todo',
|
|
|
|
|
'priority' => 'high',
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* JSON 유효성 검사 (FormRequest 전에 미리 체크)
|
|
|
|
|
*/
|
|
|
|
|
public function validateJsonStructure(array $data): array
|
|
|
|
|
{
|
|
|
|
|
$errors = [];
|
|
|
|
|
|
|
|
|
|
if (! isset($data['project'])) {
|
|
|
|
|
$errors[] = 'project 객체가 필요합니다.';
|
|
|
|
|
} elseif (! isset($data['project']['name']) || empty($data['project']['name'])) {
|
|
|
|
|
$errors[] = 'project.name은 필수입니다.';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isset($data['tasks']) && is_array($data['tasks'])) {
|
|
|
|
|
foreach ($data['tasks'] as $index => $task) {
|
|
|
|
|
if (! isset($task['title']) || empty($task['title'])) {
|
|
|
|
|
$errors[] = "tasks[{$index}].title은 필수입니다.";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isset($task['issues']) && is_array($task['issues'])) {
|
|
|
|
|
foreach ($task['issues'] as $issueIndex => $issue) {
|
|
|
|
|
if (! isset($issue['title']) || empty($issue['title'])) {
|
|
|
|
|
$errors[] = "tasks[{$index}].issues[{$issueIndex}].title은 필수입니다.";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $errors;
|
|
|
|
|
}
|
2025-12-02 19:10:15 +09:00
|
|
|
}
|