feat: [pm] 이슈 일정 UI 및 정렬 기능 추가

- 이슈 모달에 시작일/마감일/예상시간 입력 필드 추가
- 작업 탭 아코디언 서브 이슈에 마감일 표시 및 지연 강조
- 이슈 정렬: 마감일 → 상태 순 (이슈탭 + 작업탭 아코디언)
This commit is contained in:
2025-12-02 17:49:15 +09:00
parent 20354557ed
commit 8b88224be9
4 changed files with 73 additions and 2 deletions

View File

@@ -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' => '예상 시간',
];
}

View File

@@ -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' => '예상 시간',
];
}

View File

@@ -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',

View File

@@ -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');
}
}