feat: 수신함 완료 사진 업로드 및 최종 부적합 사항 DB 구조 구현
📸 Completion Photo Upload: - 수신함에서 '완료됨' 상태 선택 시 완료 사진 업로드 기능 추가 (1장 제한) - Base64 인코딩으로 사진 업로드 및 미리보기 기능 - 완료 상태 변경 시 actual_completion_date 자동 설정 🗄️ Final Issue DB Structure: - 최종 부적합 사항을 위한 포괄적인 DB 스키마 설계 및 구현 - 프로젝트별 순번 (project_sequence_no) 자동 생성 시스템 📋 New Database Fields: - completion_photo_path: 완료 사진 경로 - solution: 해결방안 (관리함에서 입력) - responsible_department: 담당부서 (department_type ENUM) - responsible_person: 담당자 (VARCHAR 100) - expected_completion_date: 조치 예상일 (DATE) - actual_completion_date: 완료 확인일 (DATE, 자동 설정) - cause_department: 원인부서 (department_type ENUM) - management_comment: ISSUE에 대한 의견 (TEXT) - final_description: 최종 내용 (수정본 또는 원본) - final_category: 최종 카테고리 (수정본 또는 원본) 🔧 Backend Implementation: - Issue 모델에 11개 새 필드 추가 - IssueStatusUpdateRequest에 completion_photo 필드 추가 - ManagementUpdateRequest 스키마 신규 생성 - update_issue_status API에 완료 사진 처리 로직 추가 - generate_project_sequence_no() 함수로 프로젝트별 순번 자동 생성 🎨 Frontend Implementation: - 상태 결정 모달에 완료 사진 업로드 섹션 추가 - 완료 상태 선택 시에만 사진 업로드 UI 표시 - 파일 크기 제한 (5MB), 이미지 파일 검증 - 사진 미리보기 및 Base64 변환 처리 - 완료 사진 없이 완료 처리 시 확인 다이얼로그 🚀 User Experience: - 직관적인 완료 사진 업로드 워크플로우 - 실시간 사진 미리보기로 업로드 확인 가능 - 완료 처리 시 자동으로 완료 확인일 기록 - 프로젝트별 순번으로 체계적인 이슈 관리 🔍 Database Migration: - 016_add_management_fields.sql 마이그레이션 성공적으로 실행 - 멱등성 보장 및 기존 데이터 보존 - 인덱스 최적화 (project_sequence, responsible_department, expected_completion) - 기존 이슈들의 final_description/final_category 자동 초기화 Expected Result: ✅ 수신함에서 완료 상태 선택 시 완료 사진 업로드 가능 ✅ 완료 처리 시 완료 확인일 자동 기록 ✅ 프로젝트별 순번으로 체계적인 이슈 번호 관리 ✅ 관리함에서 사용할 모든 필드 준비 완료 ✅ 최종 부적합 사항 리포트 생성을 위한 DB 구조 완성
This commit is contained in:
@@ -411,11 +411,11 @@
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">상태 선택</label>
|
||||
<div class="space-y-2">
|
||||
<label class="flex items-center">
|
||||
<input type="radio" name="finalStatus" value="in_progress" class="mr-2">
|
||||
<input type="radio" name="finalStatus" value="in_progress" class="mr-2" onchange="toggleCompletionPhotoSection()">
|
||||
<span class="text-sm">🔄 진행 중 (관리함으로 이동)</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="radio" name="finalStatus" value="completed" class="mr-2">
|
||||
<input type="radio" name="finalStatus" value="completed" class="mr-2" onchange="toggleCompletionPhotoSection()">
|
||||
<span class="text-sm">✅ 완료됨 (관리함으로 이동)</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -427,6 +427,21 @@
|
||||
placeholder="처리 내용이나 특이사항을 입력하세요..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 완료 사진 업로드 (완료 상태 선택 시에만 표시) -->
|
||||
<div id="completionPhotoSection" class="hidden">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
<i class="fas fa-camera text-green-500 mr-1"></i>완료 사진 (1장)
|
||||
</label>
|
||||
<input type="file" id="completionPhotoInput" accept="image/*"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
onchange="handleCompletionPhotoSelect(event)">
|
||||
<div id="completionPhotoPreview" class="mt-2 hidden">
|
||||
<img id="completionPhotoImg" src="" alt="완료 사진 미리보기"
|
||||
class="w-full max-h-40 object-cover rounded-lg border">
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-1">완료 상태로 변경 시 완료 사진을 업로드할 수 있습니다.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button onclick="closeStatusModal()" class="px-4 py-2 text-gray-600 hover:text-gray-800">
|
||||
취소
|
||||
@@ -1210,6 +1225,62 @@
|
||||
function closeStatusModal() {
|
||||
currentIssueId = null;
|
||||
document.getElementById('statusModal').classList.add('hidden');
|
||||
// 완료 사진 관련 초기화
|
||||
document.getElementById('completionPhotoSection').classList.add('hidden');
|
||||
document.getElementById('completionPhotoInput').value = '';
|
||||
document.getElementById('completionPhotoPreview').classList.add('hidden');
|
||||
completionPhotoBase64 = null;
|
||||
}
|
||||
|
||||
// 완료 사진 섹션 토글
|
||||
function toggleCompletionPhotoSection() {
|
||||
const selectedStatus = document.querySelector('input[name="finalStatus"]:checked');
|
||||
const photoSection = document.getElementById('completionPhotoSection');
|
||||
|
||||
if (selectedStatus && selectedStatus.value === 'completed') {
|
||||
photoSection.classList.remove('hidden');
|
||||
} else {
|
||||
photoSection.classList.add('hidden');
|
||||
// 완료 사진 초기화
|
||||
document.getElementById('completionPhotoInput').value = '';
|
||||
document.getElementById('completionPhotoPreview').classList.add('hidden');
|
||||
completionPhotoBase64 = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 완료 사진 선택 처리
|
||||
let completionPhotoBase64 = null;
|
||||
function handleCompletionPhotoSelect(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) {
|
||||
completionPhotoBase64 = null;
|
||||
document.getElementById('completionPhotoPreview').classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// 파일 크기 체크 (5MB 제한)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
alert('파일 크기는 5MB 이하여야 합니다.');
|
||||
event.target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 이미지 파일인지 확인
|
||||
if (!file.type.startsWith('image/')) {
|
||||
alert('이미지 파일만 업로드 가능합니다.');
|
||||
event.target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
completionPhotoBase64 = e.target.result.split(',')[1]; // Base64 부분만 추출
|
||||
|
||||
// 미리보기 표시
|
||||
document.getElementById('completionPhotoImg').src = e.target.result;
|
||||
document.getElementById('completionPhotoPreview').classList.remove('hidden');
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
// 상태 변경 확인
|
||||
@@ -1225,17 +1296,31 @@
|
||||
const reviewStatus = selectedStatus.value;
|
||||
const notes = document.getElementById('statusNotes').value.trim();
|
||||
|
||||
// 완료 상태인데 완료 사진이 없으면 확인
|
||||
if (reviewStatus === 'completed' && !completionPhotoBase64) {
|
||||
if (!confirm('완료 사진을 업로드하지 않고 완료 처리하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const requestBody = {
|
||||
review_status: reviewStatus,
|
||||
notes: notes || null
|
||||
};
|
||||
|
||||
// 완료 사진이 있으면 추가
|
||||
if (reviewStatus === 'completed' && completionPhotoBase64) {
|
||||
requestBody.completion_photo = completionPhotoBase64;
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/inbox/${currentIssueId}/status`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
review_status: reviewStatus,
|
||||
notes: notes || null
|
||||
})
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
|
||||
Reference in New Issue
Block a user