refactor: 완료 반려 필드 분리 및 데이터 구조 개선

- backend: completion_rejection_reason 등 전용 필드 추가
- 기존 management_comment에 섞여있던 완료 반려 내용 분리
- 현황판: 완료 반려 내역 별도 카드로 표시
- 관리함: 해결방안에 완료 반려 내용 제외하여 표시
- DB 마이그레이션: completion_rejected_at, completion_rejected_by_id, completion_rejection_reason 필드 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2025-11-08 11:56:09 +09:00
parent d1ed53cbd7
commit 2fc7d4bc2c
5 changed files with 1937 additions and 82 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -425,6 +425,13 @@
let currentIssueId = null;
let currentTab = 'in_progress'; // 기본값: 진행 중
// 완료 반려 패턴 제거 (해결방안 표시용)
function cleanManagementComment(text) {
if (!text) return '';
// 기존 데이터에서 완료 반려 패턴 제거
return text.replace(/\[완료 반려[^\]]*\][^\n]*\n*/g, '').trim();
}
// API 로드 후 초기화 함수
async function initializeManagement() {
const token = localStorage.getItem('access_token');
@@ -782,6 +789,9 @@
<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="confirmDeleteIssue(${issue.id})" class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors">
<i class="fas fa-trash 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 mr-1"></i>완료처리
</button>
@@ -852,9 +862,9 @@
<div class="space-y-4">
<div>
<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>
<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>
<textarea id="management_comment_${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' : ''}>${cleanManagementComment(issue.management_comment)}</textarea>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
@@ -987,11 +997,11 @@
관리 정보
</h4>
<div class="space-y-2 text-sm">
<div><span class="font-medium text-blue-700">해결방안:</span> <span class="text-blue-900">${issue.solution || '-'}</span></div>
<div><span class="font-medium text-blue-700">해결방안 (확정):</span> <span class="text-blue-900">${cleanManagementComment(issue.management_comment) || '-'}</span></div>
<div><span class="font-medium text-blue-700">담당부서:</span> <span class="text-blue-900">${getDepartmentText(issue.responsible_department) || '-'}</span></div>
<div><span class="font-medium text-blue-700">담당자:</span> <span class="text-blue-900">${issue.responsible_person || '-'}</span></div>
<div><span class="font-medium text-blue-700">원인부서:</span> <span class="text-blue-900">${getDepartmentText(issue.cause_department) || '-'}</span></div>
<div><span class="font-medium text-blue-700">관리 코멘트:</span> <span class="text-blue-900">${issue.management_comment || '-'}</span></div>
<div><span class="font-medium text-blue-700">관리 코멘트:</span> <span class="text-blue-900">${cleanManagementComment(issue.management_comment) || '-'}</span></div>
</div>
</div>
@@ -1277,7 +1287,7 @@
try {
// 편집된 필드들의 값 수집
const updates = {};
const fields = ['solution', 'responsible_department', 'responsible_person', 'expected_completion_date', 'cause_department', 'management_comment'];
const fields = ['management_comment', 'responsible_department', 'responsible_person', 'expected_completion_date', 'cause_department'];
fields.forEach(field => {
const element = document.getElementById(`${field}_${issueId}`);
@@ -1406,8 +1416,8 @@
<div class="space-y-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">해결방안</label>
<textarea id="modal_solution" rows="3" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">${issue.solution || ''}</textarea>
<label class="block text-sm font-medium text-gray-700 mb-1">해결방안 (확정)</label>
<textarea id="modal_management_comment" rows="3" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="확정된 해결 방안을 입력하세요...">${cleanManagementComment(issue.management_comment)}</textarea>
</div>
<div>
@@ -1440,7 +1450,7 @@
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">의견</label>
<textarea id="modal_management_comment" rows="3" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">${issue.management_comment || ''}</textarea>
<textarea id="modal_management_comment" rows="3" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">${cleanManagementComment(issue.management_comment)}</textarea>
</div>
<div>
@@ -1465,7 +1475,7 @@
try {
// 편집된 필드들의 값 수집
const updates = {};
const fields = ['solution', 'responsible_department', 'responsible_person', 'expected_completion_date', 'cause_department', 'management_comment'];
const fields = ['management_comment', 'responsible_department', 'responsible_person', 'expected_completion_date', 'cause_department'];
fields.forEach(field => {
const element = document.getElementById(`modal_${field}`);
@@ -1872,8 +1882,8 @@
<h4 class="font-semibold text-green-800 mb-3">관리 정보</h4>
<div class="space-y-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">해결방안</label>
<textarea id="edit-solution-${issue.id}" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 text-sm resize-none" placeholder="해결 방안을 입력하세요...">${issue.solution || ''}</textarea>
<label class="block text-sm font-medium text-gray-700 mb-1">해결방안 (확정)</label>
<textarea id="edit-management-comment-${issue.id}" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 text-sm resize-none" placeholder="확정된 해결 방안을 입력하세요...">${cleanManagementComment(issue.management_comment)}</textarea>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
@@ -1903,7 +1913,7 @@
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">관리 코멘트</label>
<textarea id="edit-management-comment-${issue.id}" rows="2" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 text-sm resize-none" placeholder="관리 코멘트를 입력하세요...">${issue.management_comment || ''}</textarea>
<textarea id="edit-management-comment-${issue.id}" rows="2" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 text-sm resize-none" placeholder="관리 코멘트를 입력하세요...">${cleanManagementComment(issue.management_comment)}</textarea>
</div>
</div>
</div>
@@ -1964,6 +1974,9 @@
<button onclick="saveIssueFromModal(${issue.id})" class="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
<i class="fas fa-save mr-2"></i>저장
</button>
<button onclick="confirmDeleteIssue(${issue.id})" class="px-6 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors">
<i class="fas fa-trash mr-2"></i>삭제
</button>
<button onclick="saveAndCompleteIssue(${issue.id})" 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>
@@ -2016,12 +2029,11 @@
const title = document.getElementById(`edit-issue-title-${issueId}`).value.trim();
const detail = document.getElementById(`edit-issue-detail-${issueId}`).value.trim();
const category = document.getElementById(`edit-category-${issueId}`).value;
const solution = document.getElementById(`edit-solution-${issueId}`).value.trim();
const managementComment = document.getElementById(`edit-management-comment-${issueId}`).value.trim();
const department = document.getElementById(`edit-department-${issueId}`).value;
const person = document.getElementById(`edit-person-${issueId}`).value.trim();
const date = document.getElementById(`edit-date-${issueId}`).value;
const causeDepartment = document.getElementById(`edit-cause-department-${issueId}`).value;
const managementComment = document.getElementById(`edit-management-comment-${issueId}`).value.trim();
// 완료 신청 정보 (완료 대기 상태일 때만)
const completionCommentElement = document.getElementById(`edit-completion-comment-${issueId}`);
@@ -2064,16 +2076,15 @@
}
const combinedDescription = title + (detail ? '\n' + detail : '');
const requestBody = {
final_description: combinedDescription,
final_category: category,
solution: solution || null,
management_comment: managementComment || null,
responsible_department: department || null,
responsible_person: person || null,
expected_completion_date: date || null,
cause_department: causeDepartment || null,
management_comment: managementComment || null
cause_department: causeDepartment || null
};
// 완료 신청 정보가 있으면 추가
@@ -2267,7 +2278,7 @@
<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> ${cleanManagementComment(issue.management_comment) || '-'}</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>
@@ -2344,12 +2355,11 @@
const title = document.getElementById(`edit-issue-title-${issueId}`).value.trim();
const detail = document.getElementById(`edit-issue-detail-${issueId}`).value.trim();
const category = document.getElementById(`edit-category-${issueId}`).value;
const solution = document.getElementById(`edit-solution-${issueId}`).value.trim();
const managementComment = document.getElementById(`edit-management-comment-${issueId}`).value.trim();
const department = document.getElementById(`edit-department-${issueId}`).value;
const person = document.getElementById(`edit-person-${issueId}`).value.trim();
const date = document.getElementById(`edit-date-${issueId}`).value;
const causeDepartment = document.getElementById(`edit-cause-department-${issueId}`).value;
const managementComment = document.getElementById(`edit-management-comment-${issueId}`).value.trim();
// 완료 신청 정보 (완료 대기 상태일 때만)
const completionCommentElement = document.getElementById(`edit-completion-comment-${issueId}`);
@@ -2392,16 +2402,15 @@
}
const combinedDescription = title + (detail ? '\n' + detail : '');
const requestBody = {
final_description: combinedDescription,
final_category: category,
solution: solution || null,
management_comment: managementComment || null,
responsible_department: department || null,
responsible_person: person || null,
expected_completion_date: date || null,
cause_department: causeDepartment || null,
management_comment: managementComment || null,
review_status: 'completed' // 완료 상태로 변경
};
@@ -2466,6 +2475,75 @@
alert('완료 처리 중 오류가 발생했습니다.');
}
}
// 삭제 확인 다이얼로그
function confirmDeleteIssue(issueId) {
const modal = document.createElement('div');
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[60]';
modal.onclick = (e) => {
if (e.target === modal) modal.remove();
};
modal.innerHTML = `
<div class="bg-white rounded-lg p-6 w-96 max-w-md mx-4">
<div class="text-center mb-4">
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4">
<i class="fas fa-exclamation-triangle text-red-600 text-xl"></i>
</div>
<h3 class="text-lg font-semibold mb-2">부적합 삭제</h3>
<p class="text-sm text-gray-600">
이 부적합 사항을 삭제하시겠습니까?<br>
<strong class="text-red-600">삭제된 데이터는 로그로 보관되지만 복구할 수 없습니다.</strong>
</p>
</div>
<div class="flex gap-2">
<button onclick="this.closest('.fixed').remove()"
class="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
취소
</button>
<button onclick="handleDeleteIssueFromManagement(${issueId})"
class="flex-1 px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors">
<i class="fas fa-trash mr-1"></i>삭제
</button>
</div>
</div>
`;
document.body.appendChild(modal);
}
// 삭제 처리 함수
async function handleDeleteIssueFromManagement(issueId) {
try {
const response = await fetch(`/api/issues/${issueId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
alert('부적합이 삭제되었습니다.\n삭제 로그가 기록되었습니다.');
// 모달들 닫기
const deleteModal = document.querySelector('.fixed');
if (deleteModal) deleteModal.remove();
closeIssueEditModal();
// 페이지 새로고침
initializeManagement();
} else {
const error = await response.json();
alert(`삭제 실패: ${error.detail || '알 수 없는 오류'}`);
}
} catch (error) {
console.error('삭제 오류:', error);
alert('삭제 중 오류가 발생했습니다: ' + error.message);
}
}
</script>
<!-- 추가 정보 입력 모달 -->