feat: 관리함 통합 수정 모달 및 히스토리 추적 방안 구현

🔧 통합 수정 모달:
- 모든 진행 중 상태에서 '확인' 버튼으로 모달 열기
- 완료 대기 상태에서도 '수정' 버튼으로 동일 모달 사용
- 6열 와이드 모달로 모든 정보 한눈에 표시

📝 모달 구성:
- 왼쪽: 기본 정보 (프로젝트, 부적합명, 상세내용, 원인분류, 업로드 사진)
- 오른쪽: 관리 정보 (해결방안, 담당부서/자, 조치예상일) + 완료 신청 정보

🎯 버튼 시스템 개선:
- 일반 진행 중: 저장 | 확인 | 완료처리
- 완료 대기: 반려 | 수정 | 최종확인
- 모달에서 통합 수정 가능

✏️ 수정 기능:
- 부적합명, 상세내용 직접 수정
- 해결방안, 담당부서/자, 조치예상일 수정
- 모달에서 저장 시 실시간 반영

📋 히스토리 추적 방안 문서화:
- 단일 히스토리 테이블 vs 페이지별 테이블 비교
- 변경 이력 기록 서비스 클래스 설계
- 프론트엔드 히스토리 조회 모달 구현 방안
- 감사 추적, 데이터 복구, 보안 고려사항 포함

🔍 구현 우선순위:
- Phase 1: 기본 히스토리 테이블 + 관리함 이력
- Phase 2: 수신함 이력 + 히스토리 UI
- Phase 3: 데이터 복구 + 감사 보고서

💡 추가 아이디어:
- 변경 승인 워크플로우
- 자동 백업 시스템
- 변경 영향도 분석

Expected Result:
 모든 진행 중 상태에서 통합 수정 모달 사용
 완료 대기 상태 정보 포함 표시
 체계적인 히스토리 추적 방안 수립
 투명하고 추적 가능한 이슈 관리 기반 마련
This commit is contained in:
Hyungi Ahn
2025-10-26 13:11:26 +09:00
parent f5136b5801
commit c680453227
2 changed files with 506 additions and 4 deletions

View File

@@ -786,20 +786,23 @@
<div class="flex space-x-2">
${isPendingCompletion ? `
<!-- 완료 대기 상태 버튼들 -->
<button onclick="editIssue(${issue.id})" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
<i class="fas fa-edit mr-1"></i>수정
</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="openIssueEditModal(${issue.id})" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
<i class="fas fa-edit 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>확인
<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="openIssueEditModal(${issue.id})" class="px-4 py-2 bg-purple-500 text-white rounded-lg hover:bg-purple-600 transition-colors">
<i class="fas fa-eye 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>
@@ -1745,6 +1748,198 @@
}
}
// 이슈 수정 모달 열기 (모든 진행 중 상태에서 사용)
function openIssueEditModal(issueId) {
const issue = issues.find(i => i.id === issueId);
if (!issue) return;
const project = projects.find(p => p.id === issue.project_id);
const isPendingCompletion = issue.completion_requested_at;
// 모달 내용 생성
const modalContent = `
<div class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center" id="issueEditModal">
<div class="bg-white rounded-xl shadow-xl max-w-6xl 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-edit text-blue-500 mr-2"></i>
이슈 수정 - No.${issue.project_sequence_no || '-'}
</h3>
<button onclick="closeIssueEditModal()" 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-3">기본 정보</h4>
<div class="space-y-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">프로젝트</label>
<input type="text" value="${project ? project.project_name : '-'}" class="w-full px-3 py-2 bg-gray-100 border border-gray-300 rounded-lg text-sm" readonly>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">부적합명</label>
<input type="text" id="edit-issue-title-${issue.id}" value="${getIssueTitle(issue)}" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">상세내용</label>
<textarea id="edit-issue-detail-${issue.id}" rows="4" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm resize-none">${getIssueDetail(issue)}</textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">원인분류</label>
<input type="text" value="${getCategoryText(issue.final_category || issue.category)}" class="w-full px-3 py-2 bg-gray-100 border border-gray-300 rounded-lg text-sm" readonly>
</div>
</div>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-semibold text-gray-800 mb-3">업로드 사진</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 class="space-y-4">
<div class="bg-green-50 p-4 rounded-lg">
<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>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">담당부서</label>
<select id="edit-department-${issue.id}" 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">
${getDepartmentOptions().map(opt =>
`<option value="${opt.value}" ${opt.value === (issue.responsible_department || '') ? 'selected' : ''}>${opt.text}</option>`
).join('')}
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">담당자</label>
<input type="text" id="edit-person-${issue.id}" 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" placeholder="담당자 이름" value="${issue.responsible_person || ''}">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">조치예상일</label>
<input type="date" id="edit-date-${issue.id}" 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" value="${issue.expected_completion_date ? issue.expected_completion_date.split('T')[0] : ''}">
</div>
</div>
</div>
${isPendingCompletion ? `
<div class="bg-purple-50 p-4 rounded-lg">
<h4 class="font-semibold text-purple-800 mb-3">완료 신청 정보</h4>
<div class="space-y-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">완료 사진</label>
${issue.completion_photo_path ? `
<div class="mt-1">
<img src="${issue.completion_photo_path}" class="w-32 h-32 object-cover rounded-lg cursor-pointer border-2 border-purple-200" onclick="openPhotoModal('${issue.completion_photo_path}')" alt="완료 사진">
</div>
` : '<p class="text-sm text-gray-500 mt-1">완료 사진 없음</p>'}
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">완료 코멘트</label>
<p class="text-sm text-gray-700 p-2 bg-white rounded border">${issue.completion_comment || '코멘트 없음'}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">신청일시</label>
<p class="text-sm text-gray-700">${new Date(issue.completion_requested_at).toLocaleString('ko-KR')}</p>
</div>
</div>
</div>
` : ''}
</div>
</div>
<!-- 버튼 -->
<div class="flex justify-end space-x-3">
<button onclick="closeIssueEditModal()" class="px-6 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors">
취소
</button>
<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>
${isPendingCompletion ? `
<button onclick="finalConfirmCompletion(${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>
` : ''}
</div>
</div>
</div>
</div>
`;
// 모달을 body에 추가
document.body.insertAdjacentHTML('beforeend', modalContent);
}
// 이슈 수정 모달 닫기
function closeIssueEditModal() {
const modal = document.getElementById('issueEditModal');
if (modal) {
modal.remove();
}
}
// 모달에서 이슈 저장
async function saveIssueFromModal(issueId) {
const title = document.getElementById(`edit-issue-title-${issueId}`).value.trim();
const detail = document.getElementById(`edit-issue-detail-${issueId}`).value.trim();
const solution = document.getElementById(`edit-solution-${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;
if (!title) {
alert('부적합명을 입력해주세요.');
return;
}
const combinedDescription = title + (detail ? '\\n' + detail : '');
try {
const response = await fetch(\`/api/management/\${issueId}\`, {
method: 'PUT',
headers: {
'Authorization': \`Bearer \${localStorage.getItem('access_token')}\`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
final_description: combinedDescription,
solution: solution || null,
responsible_department: department || null,
responsible_person: person || null,
expected_completion_date: date || null
})
});
if (response.ok) {
alert('이슈가 성공적으로 저장되었습니다.');
closeIssueEditModal();
loadManagementData(); // 페이지 새로고침
} else {
const error = await response.json();
alert(\`저장 실패: \${error.detail || '알 수 없는 오류'}\`);
}
} catch (error) {
console.error('저장 오류:', error);
alert('저장 중 오류가 발생했습니다.');
}
}
// 완료 대기 상태 관련 함수들
function editIssue(issueId) {
// 수정 모드로 전환 (완료 대기 상태를 해제)