feat: [pm] 이슈 일정 UI 및 정렬 기능 추가
- 이슈 모달에 시작일/마감일/예상시간 입력 필드 추가 - 작업 탭 아코디언 서브 이슈에 마감일 표시 및 지연 강조 - 이슈 정렬: 마감일 → 상태 순 (이슈탭 + 작업탭 아코디언)
This commit is contained in:
@@ -27,6 +27,9 @@ public function rules(): array
|
||||
'description' => 'nullable|string|max:5000',
|
||||
'type' => 'nullable|in:'.implode(',', array_keys(AdminPmIssue::getTypes())),
|
||||
'status' => 'nullable|in:'.implode(',', array_keys(AdminPmIssue::getStatuses())),
|
||||
'start_date' => 'nullable|date',
|
||||
'due_date' => 'nullable|date|after_or_equal:start_date',
|
||||
'estimated_hours' => 'nullable|integer|min:0|max:9999',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -42,6 +45,9 @@ public function attributes(): array
|
||||
'description' => '이슈 설명',
|
||||
'type' => '타입',
|
||||
'status' => '상태',
|
||||
'start_date' => '시작일',
|
||||
'due_date' => '마감일',
|
||||
'estimated_hours' => '예상 시간',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,9 @@ public function rules(): array
|
||||
'description' => 'nullable|string|max:5000',
|
||||
'type' => 'sometimes|in:'.implode(',', array_keys(AdminPmIssue::getTypes())),
|
||||
'status' => 'sometimes|in:'.implode(',', array_keys(AdminPmIssue::getStatuses())),
|
||||
'start_date' => 'nullable|date',
|
||||
'due_date' => 'nullable|date|after_or_equal:start_date',
|
||||
'estimated_hours' => 'nullable|integer|min:0|max:9999',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -42,6 +45,9 @@ public function attributes(): array
|
||||
'description' => '이슈 설명',
|
||||
'type' => '타입',
|
||||
'status' => '상태',
|
||||
'start_date' => '시작일',
|
||||
'due_date' => '마감일',
|
||||
'estimated_hours' => '예상 시간',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,9 @@ class AdminPmIssue extends Model
|
||||
'description',
|
||||
'type',
|
||||
'status',
|
||||
'start_date',
|
||||
'due_date',
|
||||
'estimated_hours',
|
||||
'is_urgent',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
@@ -46,6 +49,9 @@ class AdminPmIssue extends Model
|
||||
protected $casts = [
|
||||
'project_id' => 'integer',
|
||||
'task_id' => 'integer',
|
||||
'start_date' => 'date',
|
||||
'due_date' => 'date',
|
||||
'estimated_hours' => 'integer',
|
||||
'is_urgent' => 'boolean',
|
||||
'created_by' => 'integer',
|
||||
'updated_by' => 'integer',
|
||||
|
||||
@@ -283,6 +283,25 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:rin
|
||||
</select>
|
||||
</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="date" name="start_date" id="issueStartDate"
|
||||
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="date" name="due_date" id="issueDueDate"
|
||||
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="number" name="estimated_hours" id="issueEstimatedHours" min="0" max="9999"
|
||||
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>
|
||||
@@ -486,7 +505,7 @@ function renderTasks(container, tasks) {
|
||||
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 issues = sortIssues(task.issues || []);
|
||||
const hasIssues = issues.length > 0;
|
||||
|
||||
// 작업 Row
|
||||
@@ -553,7 +572,9 @@ function renderTasks(container, tasks) {
|
||||
<span class="text-sm ${issue.is_urgent ? 'text-red-600' : 'text-gray-700'}">${issue.title}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-1 text-center text-xs text-gray-400">-</td>
|
||||
<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">
|
||||
<span class="px-1.5 py-0.5 text-xs rounded ${issueStatusColors[issue.status]}">${issueStatusLabels[issue.status]}</span>
|
||||
@@ -598,6 +619,24 @@ function renderTasks(container, tasks) {
|
||||
});
|
||||
}
|
||||
|
||||
// 이슈 정렬 함수 (마감일 → 상태)
|
||||
function sortIssues(issues) {
|
||||
const statusPriority = { open: 0, in_progress: 1, resolved: 2, closed: 3 };
|
||||
return [...issues].sort((a, b) => {
|
||||
// 1. 마감일 기준 정렬 (null은 맨 뒤)
|
||||
if (a.due_date && b.due_date) {
|
||||
const diff = new Date(a.due_date) - new Date(b.due_date);
|
||||
if (diff !== 0) return diff;
|
||||
} else if (a.due_date && !b.due_date) {
|
||||
return -1;
|
||||
} else if (!a.due_date && b.due_date) {
|
||||
return 1;
|
||||
}
|
||||
// 2. 마감일이 같거나 없으면 상태로 정렬
|
||||
return (statusPriority[a.status] || 99) - (statusPriority[b.status] || 99);
|
||||
});
|
||||
}
|
||||
|
||||
// 이슈 목록 렌더링 (테이블 형식)
|
||||
function renderIssues(container, issues) {
|
||||
if (!issues || issues.length === 0) {
|
||||
@@ -605,6 +644,9 @@ function renderIssues(container, issues) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 정렬 적용
|
||||
issues = sortIssues(issues);
|
||||
|
||||
const typeLabels = { bug: '버그', feature: '기능', improvement: '개선' };
|
||||
const statusColors = {
|
||||
open: 'bg-red-100 text-red-600',
|
||||
@@ -624,6 +666,8 @@ function renderIssues(container, issues) {
|
||||
<th class="w-16 px-2 py-2 text-center font-medium">타입</th>
|
||||
<th class="px-2 py-2 text-left font-medium">이슈명</th>
|
||||
<th class="w-32 px-2 py-2 text-left font-medium">연결 작업</th>
|
||||
<th class="w-24 px-2 py-2 text-center font-medium">시작일</th>
|
||||
<th class="w-24 px-2 py-2 text-center font-medium">마감일</th>
|
||||
<th class="w-16 px-2 py-2 text-center font-medium">상태</th>
|
||||
<th class="w-40 px-2 py-2 text-center font-medium">변경</th>
|
||||
<th class="w-12 px-2 py-2"></th>
|
||||
@@ -651,6 +695,12 @@ function renderIssues(container, issues) {
|
||||
<td class="px-2 py-1.5 text-xs text-gray-500 truncate max-w-[120px]" title="${issue.task?.title || ''}">
|
||||
${issue.task ? issue.task.title : '-'}
|
||||
</td>
|
||||
<td class="px-2 py-1.5 text-center text-xs text-gray-500">
|
||||
${issue.start_date ? formatDate(issue.start_date) : '-'}
|
||||
</td>
|
||||
<td class="px-2 py-1.5 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="px-2 py-1.5 text-center">
|
||||
<span class="px-1.5 py-0.5 text-xs rounded ${statusColors[issue.status]}">${statusLabels[issue.status]}</span>
|
||||
</td>
|
||||
@@ -843,6 +893,9 @@ function closeIssueModal() {
|
||||
document.getElementById('issueType').value = issue.type;
|
||||
document.getElementById('issueStatus').value = issue.status;
|
||||
document.getElementById('issueTaskId').value = issue.task_id || '';
|
||||
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('issueModal').classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user