feat: [pm] 이슈에 팀/담당자/고객사 필드 추가
- DB 마이그레이션: 하이브리드 FK + 문자열 필드 방식 - Model: fillable, casts, relationships, accessor 추가 - FormRequest: validation rules 추가 (Store/Update) - ImportService: JSON import 시 새 필드 처리 - UI: 이슈 모달에 입력 필드 추가 - UI: 작업 탭 아코디언에 고객사·부서·담당자 표시 - 이슈 저장 후 작업 탭 즉시 갱신
This commit is contained in:
@@ -36,6 +36,17 @@ public function rules(): array
|
||||
'tasks.*.issues.*.description' => 'nullable|string',
|
||||
'tasks.*.issues.*.type' => 'nullable|in:bug,feature,improvement',
|
||||
'tasks.*.issues.*.status' => 'nullable|in:open,in_progress,resolved,closed',
|
||||
// 일정 관련
|
||||
'tasks.*.issues.*.start_date' => 'nullable|date',
|
||||
'tasks.*.issues.*.due_date' => 'nullable|date|after_or_equal:tasks.*.issues.*.start_date',
|
||||
'tasks.*.issues.*.estimated_hours' => 'nullable|integer|min:0',
|
||||
'tasks.*.issues.*.is_urgent' => 'nullable|boolean',
|
||||
// 팀/담당자/고객사 (하이브리드)
|
||||
'tasks.*.issues.*.department_id' => 'nullable|integer|exists:departments,id',
|
||||
'tasks.*.issues.*.team' => 'nullable|string|max:100',
|
||||
'tasks.*.issues.*.assignee_id' => 'nullable|integer|exists:users,id',
|
||||
'tasks.*.issues.*.assignee_name' => 'nullable|string|max:100',
|
||||
'tasks.*.issues.*.client' => 'nullable|string|max:100',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -48,4 +59,4 @@ public function messages(): array
|
||||
'tasks.*.issues.*.title.required' => '각 이슈의 title은 필수입니다.',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,12 @@ public function rules(): array
|
||||
'start_date' => 'nullable|date',
|
||||
'due_date' => 'nullable|date|after_or_equal:start_date',
|
||||
'estimated_hours' => 'nullable|integer|min:0|max:9999',
|
||||
// 팀/담당자/고객사 (하이브리드)
|
||||
'department_id' => 'nullable|integer|exists:departments,id',
|
||||
'team' => 'nullable|string|max:100',
|
||||
'assignee_id' => 'nullable|integer|exists:users,id',
|
||||
'assignee_name' => 'nullable|string|max:100',
|
||||
'client' => 'nullable|string|max:100',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -48,6 +54,11 @@ public function attributes(): array
|
||||
'start_date' => '시작일',
|
||||
'due_date' => '마감일',
|
||||
'estimated_hours' => '예상 시간',
|
||||
'department_id' => '부서',
|
||||
'team' => '팀/부서명',
|
||||
'assignee_id' => '담당자',
|
||||
'assignee_name' => '담당자명',
|
||||
'client' => '고객사',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,12 @@ public function rules(): array
|
||||
'start_date' => 'nullable|date',
|
||||
'due_date' => 'nullable|date|after_or_equal:start_date',
|
||||
'estimated_hours' => 'nullable|integer|min:0|max:9999',
|
||||
// 팀/담당자/고객사 (하이브리드)
|
||||
'department_id' => 'nullable|integer|exists:departments,id',
|
||||
'team' => 'nullable|string|max:100',
|
||||
'assignee_id' => 'nullable|integer|exists:users,id',
|
||||
'assignee_name' => 'nullable|string|max:100',
|
||||
'client' => 'nullable|string|max:100',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -48,6 +54,11 @@ public function attributes(): array
|
||||
'start_date' => '시작일',
|
||||
'due_date' => '마감일',
|
||||
'estimated_hours' => '예상 시간',
|
||||
'department_id' => '부서',
|
||||
'team' => '팀/부서명',
|
||||
'assignee_id' => '담당자',
|
||||
'assignee_name' => '담당자명',
|
||||
'client' => '고객사',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\Admin;
|
||||
|
||||
use App\Models\Department;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@@ -41,6 +42,12 @@ class AdminPmIssue extends Model
|
||||
'due_date',
|
||||
'estimated_hours',
|
||||
'is_urgent',
|
||||
// 팀/담당자/고객사 (하이브리드)
|
||||
'department_id',
|
||||
'team',
|
||||
'assignee_id',
|
||||
'assignee_name',
|
||||
'client',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
@@ -53,6 +60,8 @@ class AdminPmIssue extends Model
|
||||
'due_date' => 'date',
|
||||
'estimated_hours' => 'integer',
|
||||
'is_urgent' => 'boolean',
|
||||
'department_id' => 'integer',
|
||||
'assignee_id' => 'integer',
|
||||
'created_by' => 'integer',
|
||||
'updated_by' => 'integer',
|
||||
'deleted_by' => 'integer',
|
||||
@@ -159,6 +168,46 @@ public function updater(): BelongsTo
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계: 부서 (연동 시)
|
||||
*/
|
||||
public function department(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Department::class, 'department_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 관계: 담당자 (연동 시)
|
||||
*/
|
||||
public function assignee(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'assignee_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 팀/부서 표시명 (FK 우선, 없으면 문자열)
|
||||
*/
|
||||
public function getTeamDisplayAttribute(): ?string
|
||||
{
|
||||
if ($this->department_id && $this->department) {
|
||||
return $this->department->name;
|
||||
}
|
||||
|
||||
return $this->team;
|
||||
}
|
||||
|
||||
/**
|
||||
* 담당자 표시명 (FK 우선, 없으면 문자열)
|
||||
*/
|
||||
public function getAssigneeDisplayAttribute(): ?string
|
||||
{
|
||||
if ($this->assignee_id && $this->assignee) {
|
||||
return $this->assignee->name;
|
||||
}
|
||||
|
||||
return $this->assignee_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 타입 아이콘
|
||||
*/
|
||||
|
||||
@@ -60,6 +60,16 @@ public function importFromJson(array $data): array
|
||||
'description' => $issueData['description'] ?? null,
|
||||
'type' => $issueData['type'] ?? AdminPmIssue::TYPE_BUG,
|
||||
'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']++;
|
||||
@@ -109,6 +119,16 @@ public function importTasksToProject(int $projectId, array $tasks): array
|
||||
'description' => $issueData['description'] ?? null,
|
||||
'type' => $issueData['type'] ?? AdminPmIssue::TYPE_BUG,
|
||||
'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']++;
|
||||
@@ -143,8 +163,19 @@ public function getSampleTemplate(): array
|
||||
[
|
||||
'title' => '이슈 1',
|
||||
'description' => '이슈 설명',
|
||||
'type' => 'bug',
|
||||
'type' => 'feature',
|
||||
'status' => 'open',
|
||||
'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' => '경동기업',
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -188,4 +219,4 @@ public function validateJsonStructure(array $data): array
|
||||
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,6 +302,25 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:rin
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 팀/담당자/고객사 -->
|
||||
<div class="grid grid-cols-3 gap-4 mb-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">팀/부서</label>
|
||||
<input type="text" name="team" id="issueTeam" placeholder="예: 개발팀"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">담당자</label>
|
||||
<input type="text" name="assignee_name" id="issueAssigneeName" placeholder="예: 홍길동"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">고객사</label>
|
||||
<input type="text" name="client" id="issueClient" placeholder="예: 경동기업"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3">
|
||||
<button type="button" onclick="closeIssueModal()"
|
||||
class="px-4 py-2 text-gray-600 hover:text-gray-800">취소</button>
|
||||
@@ -575,7 +594,9 @@ function renderTasks(container, tasks) {
|
||||
<td class="py-1 text-center text-xs ${issue.due_date && new Date(issue.due_date) < new Date() && issue.status !== 'resolved' && issue.status !== 'closed' ? 'text-red-500 font-medium' : 'text-gray-500'}">
|
||||
${issue.due_date ? formatDate(issue.due_date) : '-'}
|
||||
</td>
|
||||
<td class="py-1 text-center text-xs text-gray-400">-</td>
|
||||
<td class="py-1 text-center text-xs text-gray-500">
|
||||
${[issue.client, issue.team, issue.assignee_name].filter(Boolean).join(' · ') || '-'}
|
||||
</td>
|
||||
<td class="py-1 text-center">
|
||||
<span class="px-1.5 py-0.5 text-xs rounded ${issueStatusColors[issue.status]}">${issueStatusLabels[issue.status]}</span>
|
||||
</td>
|
||||
@@ -896,6 +917,10 @@ function closeIssueModal() {
|
||||
document.getElementById('issueStartDate').value = issue.start_date ? formatDate(issue.start_date) : '';
|
||||
document.getElementById('issueDueDate').value = issue.due_date ? formatDate(issue.due_date) : '';
|
||||
document.getElementById('issueEstimatedHours').value = issue.estimated_hours || '';
|
||||
// 팀/담당자/고객사
|
||||
document.getElementById('issueTeam').value = issue.team || '';
|
||||
document.getElementById('issueAssigneeName').value = issue.assignee_name || '';
|
||||
document.getElementById('issueClient').value = issue.client || '';
|
||||
document.getElementById('issueModal').classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
@@ -920,6 +945,7 @@ function closeIssueModal() {
|
||||
if (result.success) {
|
||||
closeIssueModal();
|
||||
loadIssues();
|
||||
loadTasks(); // 작업 탭 아코디언도 업데이트
|
||||
} else {
|
||||
alert(result.message || '저장에 실패했습니다.');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user