feat: 관리함 진행 중 페이지 완료 대기 상태 관리 시스템 구현

🎯 상태별 UI 개선:
- 진행 중: 파란색 (일반 상태)
- 긴급: 주황색 (마감 3일 이내)
- 지연됨: 빨간색 (마감일 초과)
- 완료 대기: 보라색 (완료 신청 후)

🔒 완료 대기 상태 제한:
- 모든 입력 필드 비활성화 (readonly/disabled)
- 상세 내용 수정 버튼 → '완료 대기 중' 표시
- 저장 버튼 제거
- 회색 배경으로 비활성화 표시

📋 완료 대기 상태 정보 표시:
- 완료 신청 정보 섹션 추가
- 완료 사진, 코멘트, 신청일시 표시
- 보라색 테마로 구분

🔧 3단계 버튼 시스템:
1. 수정: 완료 대기 상태 해제 → 수정 모드 전환
2. 반려: 완료 신청 반려 + 사유 입력
3. 확인: 모든 정보 확인 모달 → 최종 완료 처리

📊 완료 확인 모달:
- 기본 정보 (프로젝트, 부적합명, 상세내용, 원인분류)
- 관리 정보 (해결방안, 담당부서/자, 조치예상일)
- 완료 신청 정보 (완료 사진, 코멘트, 신청일시)
- 업로드 사진 (원본 사진 1, 2)
- 최종 확인 버튼

🔄 API 엔드포인트 (구현 예정):
- POST /api/issues/{id}/reset-completion (수정 모드 전환)
- POST /api/issues/{id}/reject-completion (반려 처리)
- POST /api/issues/{id}/final-completion (최종 완료)

💡 사용자 경험:
- 상태별 색상 코딩으로 직관적 구분
- 완료 대기 시 수정 불가 명확 표시
- 모든 정보 한눈에 확인 가능한 모달
- 단계별 승인 프로세스

Expected Result:
 완료 대기 상태 시각적 구분
 수정 기능 적절한 제한
 체계적인 완료 승인 프로세스
 관리자 친화적 인터페이스
This commit is contained in:
Hyungi Ahn
2025-10-26 13:06:13 +09:00
parent 919bc82ca1
commit 63bdf4e689

View File

@@ -708,6 +708,44 @@
// 진행 중 카드 생성 // 진행 중 카드 생성
function createInProgressRow(issue, project) { function createInProgressRow(issue, project) {
// 상태 판별
const isPendingCompletion = issue.completion_requested_at;
const isOverdue = issue.expected_completion_date && new Date(issue.expected_completion_date) < new Date();
const isUrgent = issue.expected_completion_date &&
(new Date(issue.expected_completion_date) - new Date()) / (1000 * 60 * 60 * 24) <= 3 &&
!isOverdue;
// 상태 설정
let statusConfig = {
text: '진행 중',
bgColor: 'bg-gradient-to-r from-blue-500 to-blue-600',
icon: 'fas fa-cog fa-spin',
dotColor: 'bg-white'
};
if (isPendingCompletion) {
statusConfig = {
text: '완료 대기',
bgColor: 'bg-gradient-to-r from-purple-500 to-purple-600',
icon: 'fas fa-hourglass-half',
dotColor: 'bg-white'
};
} else if (isOverdue) {
statusConfig = {
text: '지연됨',
bgColor: 'bg-gradient-to-r from-red-500 to-red-600',
icon: 'fas fa-clock',
dotColor: 'bg-white'
};
} else if (isUrgent) {
statusConfig = {
text: '긴급',
bgColor: 'bg-gradient-to-r from-orange-500 to-orange-600',
icon: 'fas fa-exclamation-triangle',
dotColor: 'bg-white'
};
}
return ` return `
<div class="issue-card bg-white border border-gray-200 rounded-xl p-6 mb-4 shadow-sm hover:shadow-md transition-shadow" data-issue-id="${issue.id}"> <div class="issue-card bg-white border border-gray-200 rounded-xl p-6 mb-4 shadow-sm hover:shadow-md transition-shadow" data-issue-id="${issue.id}">
<!-- 카드 헤더 --> <!-- 카드 헤더 -->
@@ -719,18 +757,38 @@
<div class="w-2 h-2 bg-blue-500 rounded-full animate-pulse"></div> <div class="w-2 h-2 bg-blue-500 rounded-full animate-pulse"></div>
</div> </div>
<span class="text-sm text-gray-600">${project ? project.project_name : '프로젝트 미지정'}</span> <span class="text-sm text-gray-600">${project ? project.project_name : '프로젝트 미지정'}</span>
<!-- 상태 표시 -->
<div class="flex items-center space-x-2 ${statusConfig.bgColor} text-white px-3 py-1 rounded-full shadow-sm">
<div class="w-1.5 h-1.5 ${statusConfig.dotColor} rounded-full animate-pulse"></div>
<span class="text-xs font-bold">${statusConfig.text}</span>
<i class="${statusConfig.icon} text-xs"></i>
</div>
</div> </div>
<div class="bg-blue-50 px-3 py-2 rounded-lg border-l-4 border-blue-400"> <div class="bg-blue-50 px-3 py-2 rounded-lg border-l-4 border-blue-400">
<h3 class="text-lg font-bold text-blue-900">${getIssueTitle(issue)}</h3> <h3 class="text-lg font-bold text-blue-900">${getIssueTitle(issue)}</h3>
</div> </div>
</div> </div>
<div class="flex space-x-2"> <div class="flex space-x-2">
<button onclick="saveIssueChanges(${issue.id})" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"> ${isPendingCompletion ? `
<i class="fas fa-save mr-1"></i>저장 <!-- 완료 대기 상태 버튼들 -->
</button> <button onclick="editIssue(${issue.id})" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
<button onclick="completeIssue(${issue.id})" class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors"> <i class="fas fa-edit mr-1"></i>수정
<i class="fas fa-check mr-1"></i>완료처리 </button>
</button> <button onclick="rejectCompletion(${issue.id})" class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors">
<i class="fas fa-times mr-1"></i>반려
</button>
<button onclick="confirmCompletion(${issue.id})" class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
<i class="fas fa-check-circle mr-1"></i>확인
</button>
` : `
<!-- 일반 진행 중 상태 버튼들 -->
<button onclick="saveIssueChanges(${issue.id})" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
<i class="fas fa-save mr-1"></i>저장
</button>
<button onclick="completeIssue(${issue.id})" class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
<i class="fas fa-check mr-1"></i>완료처리
</button>
`}
</div> </div>
</div> </div>
@@ -744,9 +802,15 @@
<i class="fas fa-align-left text-gray-500 mr-2"></i> <i class="fas fa-align-left text-gray-500 mr-2"></i>
<label class="text-sm font-medium text-gray-700">상세 내용</label> <label class="text-sm font-medium text-gray-700">상세 내용</label>
</div> </div>
<button onclick="toggleDetailEdit(${issue.id})" class="text-xs text-blue-600 hover:text-blue-800 px-2 py-1 rounded border border-blue-200 hover:bg-blue-50 transition-colors"> ${!isPendingCompletion ? `
<i class="fas fa-edit mr-1"></i>수정 <button onclick="toggleDetailEdit(${issue.id})" class="text-xs text-blue-600 hover:text-blue-800 px-2 py-1 rounded border border-blue-200 hover:bg-blue-50 transition-colors">
</button> <i class="fas fa-edit mr-1"></i>수정
</button>
` : `
<span class="text-xs text-gray-400 px-2 py-1 rounded border border-gray-200 bg-gray-50">
<i class="fas fa-lock mr-1"></i>완료 대기 중
</span>
`}
</div> </div>
<div id="detail-display-${issue.id}" class="p-3 bg-gray-50 rounded-lg border border-gray-200 min-h-[80px]"> <div id="detail-display-${issue.id}" class="p-3 bg-gray-50 rounded-lg border border-gray-200 min-h-[80px]">
<div class="text-gray-600 text-sm leading-relaxed italic"> <div class="text-gray-600 text-sm leading-relaxed italic">
@@ -793,7 +857,7 @@
<label class="block text-sm font-medium text-gray-700 mb-2"> <label class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-lightbulb text-yellow-500 mr-1"></i>해결방안 <i class="fas fa-lightbulb text-yellow-500 mr-1"></i>해결방안
</label> </label>
<textarea id="solution_${issue.id}" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none" placeholder="해결 방안을 입력하세요...">${issue.solution || ''}</textarea> <textarea id="solution_${issue.id}" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none ${isPendingCompletion ? 'bg-gray-100 cursor-not-allowed' : ''}" placeholder="해결 방안을 입력하세요..." ${isPendingCompletion ? 'readonly' : ''}>${issue.solution || ''}</textarea>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
@@ -801,7 +865,7 @@
<label class="block text-sm font-medium text-gray-700 mb-2"> <label class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-building text-blue-500 mr-1"></i>담당부서 <i class="fas fa-building text-blue-500 mr-1"></i>담당부서
</label> </label>
<select id="responsible_department_${issue.id}" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> <select id="responsible_department_${issue.id}" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${isPendingCompletion ? 'bg-gray-100 cursor-not-allowed' : ''}" ${isPendingCompletion ? 'disabled' : ''}>
${getDepartmentOptions().map(opt => ${getDepartmentOptions().map(opt =>
`<option value="${opt.value}" ${opt.value === (issue.responsible_department || '') ? 'selected' : ''}>${opt.text}</option>` `<option value="${opt.value}" ${opt.value === (issue.responsible_department || '') ? 'selected' : ''}>${opt.text}</option>`
).join('')} ).join('')}
@@ -812,7 +876,7 @@
<label class="block text-sm font-medium text-gray-700 mb-2"> <label class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-user text-purple-500 mr-1"></i>담당자 <i class="fas fa-user text-purple-500 mr-1"></i>담당자
</label> </label>
<input type="text" id="responsible_person_${issue.id}" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="담당자 이름" value="${issue.responsible_person || ''}"> <input type="text" id="responsible_person_${issue.id}" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${isPendingCompletion ? 'bg-gray-100 cursor-not-allowed' : ''}" placeholder="담당자 이름" value="${issue.responsible_person || ''}" ${isPendingCompletion ? 'readonly' : ''}>
</div> </div>
</div> </div>
@@ -820,16 +884,43 @@
<label class="block text-sm font-medium text-gray-700 mb-2"> <label class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-calendar-alt text-red-500 mr-1"></i>조치 예상일 <i class="fas fa-calendar-alt text-red-500 mr-1"></i>조치 예상일
</label> </label>
<input type="date" id="expected_completion_date_${issue.id}" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" value="${issue.expected_completion_date ? issue.expected_completion_date.split('T')[0] : ''}"> <input type="date" id="expected_completion_date_${issue.id}" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${isPendingCompletion ? 'bg-gray-100 cursor-not-allowed' : ''}" value="${issue.expected_completion_date ? issue.expected_completion_date.split('T')[0] : ''}" ${isPendingCompletion ? 'readonly' : ''}>
</div> </div>
<!-- 완료 대기 상태일 때 완료 정보 표시 -->
${isPendingCompletion ? `
<div class="mt-4 p-4 bg-purple-50 rounded-lg border border-purple-200">
<h4 class="text-sm font-semibold text-purple-800 mb-3 flex items-center">
<i class="fas fa-check-circle mr-2"></i>완료 신청 정보
</h4>
<div class="space-y-3">
<div>
<label class="text-xs text-purple-600 font-medium">완료 사진</label>
${issue.completion_photo_path ? `
<div class="mt-1">
<img src="${issue.completion_photo_path}" class="w-24 h-24 object-cover rounded-lg cursor-pointer border-2 border-purple-200 hover:border-purple-400 transition-colors" onclick="openPhotoModal('${issue.completion_photo_path}')" alt="완료 사진">
</div>
` : '<p class="text-xs text-gray-500 mt-1">완료 사진 없음</p>'}
</div>
<div>
<label class="text-xs text-purple-600 font-medium">완료 코멘트</label>
<p class="text-sm text-gray-700 mt-1 p-2 bg-white rounded border">${issue.completion_comment || '코멘트 없음'}</p>
</div>
<div>
<label class="text-xs text-purple-600 font-medium">신청일시</label>
<p class="text-sm text-gray-700 mt-1">${new Date(issue.completion_requested_at).toLocaleString('ko-KR')}</p>
</div>
</div>
</div>
` : ''}
<!-- 진행 상태 표시 --> <!-- 진행 상태 표시 -->
<div class="mt-4 p-3 bg-blue-50 rounded-lg"> <div class="mt-4 p-3 ${isPendingCompletion ? 'bg-purple-50' : 'bg-blue-50'} rounded-lg">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="text-sm text-blue-700 font-medium"> <span class="text-sm ${isPendingCompletion ? 'text-purple-700' : 'text-blue-700'} font-medium">
<i class="fas fa-clock mr-1"></i>진행 중 <i class="${statusConfig.icon} mr-1"></i>${statusConfig.text}
</span> </span>
<span class="text-xs text-blue-600"> <span class="text-xs ${isPendingCompletion ? 'text-purple-600' : 'text-blue-600'}">
신고일: ${new Date(issue.report_date).toLocaleDateString('ko-KR')} 신고일: ${new Date(issue.report_date).toLocaleDateString('ko-KR')}
</span> </span>
</div> </div>
@@ -1638,6 +1729,216 @@
alert('저장 중 오류가 발생했습니다.'); alert('저장 중 오류가 발생했습니다.');
} }
} }
// 완료 대기 상태 관련 함수들
function editIssue(issueId) {
// 수정 모드로 전환 (완료 대기 상태를 해제)
if (confirm('완료 대기 상태를 해제하고 수정 모드로 전환하시겠습니까?')) {
// 완료 신청 정보 초기화 API 호출
resetCompletionRequest(issueId);
}
}
function rejectCompletion(issueId) {
const reason = prompt('반려 사유를 입력하세요:');
if (reason && reason.trim()) {
// 반려 처리 API 호출
rejectCompletionRequest(issueId, reason.trim());
}
}
function confirmCompletion(issueId) {
// 모든 정보 확인 모달 열기
openCompletionConfirmModal(issueId);
}
// 완료 신청 초기화 (수정 모드로 전환)
async function resetCompletionRequest(issueId) {
try {
const response = await fetch(`/api/issues/${issueId}/reset-completion`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
alert('완료 대기 상태가 해제되었습니다. 수정이 가능합니다.');
loadManagementData(); // 페이지 새로고침
} else {
const error = await response.json();
alert(`상태 변경 실패: ${error.detail || '알 수 없는 오류'}`);
}
} catch (error) {
console.error('상태 변경 오류:', error);
alert('상태 변경 중 오류가 발생했습니다.');
}
}
// 완료 신청 반려
async function rejectCompletionRequest(issueId, reason) {
try {
const response = await fetch(`/api/issues/${issueId}/reject-completion`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
rejection_reason: reason
})
});
if (response.ok) {
alert('완료 신청이 반려되었습니다.');
loadManagementData(); // 페이지 새로고침
} else {
const error = await response.json();
alert(`반려 처리 실패: ${error.detail || '알 수 없는 오류'}`);
}
} catch (error) {
console.error('반려 처리 오류:', error);
alert('반려 처리 중 오류가 발생했습니다.');
}
}
// 완료 확인 모달 열기
function openCompletionConfirmModal(issueId) {
const issue = issues.find(i => i.id === issueId);
if (!issue) return;
const project = projects.find(p => p.id === issue.project_id);
// 모달 내용 생성
const modalContent = `
<div class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center" id="completionConfirmModal">
<div class="bg-white rounded-xl shadow-xl max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<div class="p-6">
<!-- 모달 헤더 -->
<div class="flex items-center justify-between mb-6">
<h3 class="text-xl font-semibold text-gray-900">
<i class="fas fa-check-circle text-green-500 mr-2"></i>
완료 확인 - No.${issue.project_sequence_no || '-'}
</h3>
<button onclick="closeCompletionConfirmModal()" class="text-gray-400 hover:text-gray-600 transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<!-- 이슈 정보 -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- 기본 정보 -->
<div class="space-y-4">
<div class="bg-blue-50 p-4 rounded-lg">
<h4 class="font-semibold text-blue-800 mb-2">기본 정보</h4>
<div class="space-y-2 text-sm">
<div><span class="font-medium">프로젝트:</span> ${project ? project.project_name : '-'}</div>
<div><span class="font-medium">부적합명:</span> ${getIssueTitle(issue)}</div>
<div><span class="font-medium">상세내용:</span> ${getIssueDetail(issue)}</div>
<div><span class="font-medium">원인분류:</span> ${getCategoryText(issue.final_category || issue.category)}</div>
</div>
</div>
<div class="bg-green-50 p-4 rounded-lg">
<h4 class="font-semibold text-green-800 mb-2">관리 정보</h4>
<div class="space-y-2 text-sm">
<div><span class="font-medium">해결방안:</span> ${issue.solution || '-'}</div>
<div><span class="font-medium">담당부서:</span> ${issue.responsible_department || '-'}</div>
<div><span class="font-medium">담당자:</span> ${issue.responsible_person || '-'}</div>
<div><span class="font-medium">조치예상일:</span> ${issue.expected_completion_date ? new Date(issue.expected_completion_date).toLocaleDateString('ko-KR') : '-'}</div>
</div>
</div>
</div>
<!-- 완료 정보 -->
<div class="space-y-4">
<div class="bg-purple-50 p-4 rounded-lg">
<h4 class="font-semibold text-purple-800 mb-2">완료 신청 정보</h4>
<div class="space-y-3">
<div>
<span class="font-medium text-sm">완료 사진:</span>
${issue.completion_photo_path ? `
<div class="mt-2">
<img src="${issue.completion_photo_path}" class="w-32 h-32 object-cover rounded-lg border-2 border-purple-200 cursor-pointer" onclick="openPhotoModal('${issue.completion_photo_path}')" alt="완료 사진">
</div>
` : '<p class="text-sm text-gray-500 mt-1">완료 사진 없음</p>'}
</div>
<div>
<span class="font-medium text-sm">완료 코멘트:</span>
<p class="text-sm text-gray-700 mt-1 p-2 bg-white rounded border">${issue.completion_comment || '코멘트 없음'}</p>
</div>
<div>
<span class="font-medium text-sm">신청일시:</span>
<p class="text-sm text-gray-700 mt-1">${new Date(issue.completion_requested_at).toLocaleString('ko-KR')}</p>
</div>
</div>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-semibold text-gray-800 mb-2">업로드 사진</h4>
<div class="flex gap-2">
${issue.photo_path ? `<img src="${issue.photo_path}" class="w-20 h-20 object-cover rounded-lg cursor-pointer border-2 border-gray-200" onclick="openPhotoModal('${issue.photo_path}')" alt="업로드 사진 1">` : '<div class="w-20 h-20 bg-gray-100 rounded-lg flex items-center justify-center text-gray-400 text-xs">사진 없음</div>'}
${issue.photo_path2 ? `<img src="${issue.photo_path2}" class="w-20 h-20 object-cover rounded-lg cursor-pointer border-2 border-gray-200" onclick="openPhotoModal('${issue.photo_path2}')" alt="업로드 사진 2">` : '<div class="w-20 h-20 bg-gray-100 rounded-lg flex items-center justify-center text-gray-400 text-xs">사진 없음</div>'}
</div>
</div>
</div>
</div>
<!-- 버튼 -->
<div class="flex justify-end space-x-3">
<button onclick="closeCompletionConfirmModal()" class="px-6 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors">
취소
</button>
<button onclick="finalConfirmCompletion(${issueId})" class="px-6 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
<i class="fas fa-check-circle mr-2"></i>최종 확인
</button>
</div>
</div>
</div>
</div>
`;
// 모달을 body에 추가
document.body.insertAdjacentHTML('beforeend', modalContent);
}
// 완료 확인 모달 닫기
function closeCompletionConfirmModal() {
const modal = document.getElementById('completionConfirmModal');
if (modal) {
modal.remove();
}
}
// 최종 완료 확인
async function finalConfirmCompletion(issueId) {
if (!confirm('이 부적합을 최종 완료 처리하시겠습니까?\n완료 처리 후에는 수정할 수 없습니다.')) {
return;
}
try {
const response = await fetch(`/api/issues/${issueId}/final-completion`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
alert('부적합이 최종 완료 처리되었습니다.');
closeCompletionConfirmModal();
loadManagementData(); // 페이지 새로고침
} else {
const error = await response.json();
alert(`완료 처리 실패: ${error.detail || '알 수 없는 오류'}`);
}
} catch (error) {
console.error('완료 처리 오류:', error);
alert('완료 처리 중 오류가 발생했습니다.');
}
}
</script> </script>
<!-- 추가 정보 입력 모달 --> <!-- 추가 정보 입력 모달 -->