feat: 관리함 진행중 페이지에 추가 정보 입력 기능 구현

🎯 관리함 진행중 페이지 추가 정보 입력 시스템:

📊 DB 구조 확장:
- responsible_person_detail: 해당자 상세 정보 (VARCHAR 200)
- cause_detail: 원인 상세 정보 (TEXT)
- additional_info_updated_at: 추가 정보 입력 시간
- additional_info_updated_by_id: 추가 정보 입력자 ID
- 018_add_additional_info_fields.sql 마이그레이션 실행 완료

🔧 백엔드 API:
- /api/management/{issue_id}/additional-info (PUT): 추가 정보 업데이트
- /api/management/{issue_id}/additional-info (GET): 추가 정보 조회
- AdditionalInfoUpdateRequest 스키마 추가
- management.py 라우터 생성 및 등록

🎨 프론트엔드 UI:
- 진행중 탭 상단에 '추가 정보 입력' 버튼 추가
- 완료됨 탭에서는 버튼 자동 숨김
- 세련된 모달 디자인 (오렌지 테마)
- 원인부서 드롭다운 (생산/품질/구매/설계/영업)
- 해당자 상세 입력 필드
- 원인 상세 텍스트 영역

💡 핵심 특징:
- 모든 필드 선택사항 (NULL 허용)
- 기록용 정보로 외부 노출 없음
- 기존 데이터 자동 로드 및 수정 가능
- 입력 시간/입력자 자동 추적
- 진행중 상태 이슈만 대상

🔐 권한 관리:
- issues_management 페이지 권한 필요
- 진행중 상태 이슈만 수정 가능
- 사용자별 입력 이력 추적

🎯 사용 시나리오:
1. 관리함 > 진행중 탭 접근
2. '추가 정보 입력' 버튼 클릭
3. 원인부서, 해당자, 원인상세 입력
4. 저장 후 내부 기록으로 보관

Expected Result:
 관리함에서 상세한 원인 정보 기록 가능
 체계적인 이슈 추적 및 분석 기반 마련
 선택적 정보 입력으로 유연한 운영
 깔끔한 UI로 사용자 경험 향상
This commit is contained in:
Hyungi Ahn
2025-10-26 11:39:30 +09:00
parent b45cfd96bc
commit 61f5720af3
10 changed files with 409 additions and 12 deletions

View File

@@ -247,17 +247,27 @@
</select>
</div>
<!-- 상태 탭 -->
<div class="flex space-x-1 bg-gray-100 p-1 rounded-lg max-w-md">
<button id="inProgressTab"
class="flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors duration-200 bg-blue-500 text-white"
onclick="switchTab('in_progress')">
<i class="fas fa-cog mr-2"></i>진행 중
</button>
<button id="completedTab"
class="flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors duration-200 text-gray-600 hover:text-gray-900"
onclick="switchTab('completed')">
<i class="fas fa-check-circle mr-2"></i>완료됨
<!-- 상태 탭 및 추가 정보 버튼 -->
<div class="flex items-center justify-between">
<div class="flex space-x-1 bg-gray-100 p-1 rounded-lg max-w-md">
<button id="inProgressTab"
class="flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors duration-200 bg-blue-500 text-white"
onclick="switchTab('in_progress')">
<i class="fas fa-cog mr-2"></i>진행 중
</button>
<button id="completedTab"
class="flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors duration-200 text-gray-600 hover:text-gray-900"
onclick="switchTab('completed')">
<i class="fas fa-check-circle mr-2"></i>완료됨
</button>
</div>
<!-- 추가 정보 입력 버튼 (진행 중 탭에서만 표시) -->
<button id="additionalInfoBtn"
class="px-4 py-2 bg-orange-500 text-white text-sm font-medium rounded-lg hover:bg-orange-600 transition-colors duration-200 shadow-sm"
onclick="openAdditionalInfoModal()"
style="display: none;">
<i class="fas fa-plus-circle mr-2"></i>추가 정보 입력
</button>
</div>
@@ -518,13 +528,18 @@
// 탭 버튼 스타일 업데이트
const inProgressTab = document.getElementById('inProgressTab');
const completedTab = document.getElementById('completedTab');
const additionalInfoBtn = document.getElementById('additionalInfoBtn');
if (tab === 'in_progress') {
inProgressTab.className = 'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors duration-200 bg-blue-500 text-white';
completedTab.className = 'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors duration-200 text-gray-600 hover:text-gray-900';
// 진행 중 탭에서만 추가 정보 버튼 표시
additionalInfoBtn.style.display = 'block';
} else {
inProgressTab.className = 'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors duration-200 text-gray-600 hover:text-gray-900';
completedTab.className = 'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors duration-200 bg-green-500 text-white';
// 완료됨 탭에서는 추가 정보 버튼 숨김
additionalInfoBtn.style.display = 'none';
}
filterIssues(); // 이미 updateStatistics()가 포함됨
@@ -1392,6 +1407,181 @@
console.error('❌ API 스크립트 로드 실패');
};
document.head.appendChild(script);
// 추가 정보 모달 관련 함수들
let selectedIssueId = null;
function openAdditionalInfoModal() {
// 진행 중 탭에서 선택된 이슈가 있는지 확인
const inProgressIssues = allIssues.filter(issue => issue.review_status === 'in_progress');
if (inProgressIssues.length === 0) {
alert('진행 중인 부적합이 없습니다.');
return;
}
// 첫 번째 진행 중 이슈를 기본 선택 (추후 개선 가능)
selectedIssueId = inProgressIssues[0].id;
// 기존 데이터 로드
loadAdditionalInfo(selectedIssueId);
document.getElementById('additionalInfoModal').classList.remove('hidden');
}
function closeAdditionalInfoModal() {
document.getElementById('additionalInfoModal').classList.add('hidden');
selectedIssueId = null;
// 폼 초기화
document.getElementById('additionalInfoForm').reset();
}
async function loadAdditionalInfo(issueId) {
try {
const response = await fetch(`/api/management/${issueId}/additional-info`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
// 폼에 기존 데이터 채우기
document.getElementById('causeDepartment').value = data.cause_department || '';
document.getElementById('responsiblePersonDetail').value = data.responsible_person_detail || '';
document.getElementById('causeDetail').value = data.cause_detail || '';
}
} catch (error) {
console.error('추가 정보 로드 실패:', error);
}
}
// 추가 정보 폼 제출 처리
document.getElementById('additionalInfoForm').addEventListener('submit', async function(e) {
e.preventDefault();
if (!selectedIssueId) {
alert('선택된 부적합이 없습니다.');
return;
}
const formData = {
cause_department: document.getElementById('causeDepartment').value || null,
responsible_person_detail: document.getElementById('responsiblePersonDetail').value || null,
cause_detail: document.getElementById('causeDetail').value || null
};
try {
const response = await fetch(`/api/management/${selectedIssueId}/additional-info`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
if (response.ok) {
const result = await response.json();
alert('추가 정보가 성공적으로 저장되었습니다.');
closeAdditionalInfoModal();
// 목록 새로고침
loadIssues();
} else {
const error = await response.json();
alert(`저장 실패: ${error.detail || '알 수 없는 오류'}`);
}
} catch (error) {
console.error('추가 정보 저장 실패:', error);
alert('저장 중 오류가 발생했습니다.');
}
});
</script>
<!-- 추가 정보 입력 모달 -->
<div id="additionalInfoModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50 flex items-center justify-center">
<div class="bg-white rounded-xl shadow-xl max-w-md 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-lg font-semibold text-gray-900">
<i class="fas fa-info-circle text-orange-500 mr-2"></i>
추가 정보 입력
</h3>
<button onclick="closeAdditionalInfoModal()" class="text-gray-400 hover:text-gray-600 transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<!-- 모달 내용 -->
<form id="additionalInfoForm" class="space-y-4">
<!-- 원인부서 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-building text-gray-500 mr-1"></i>
원인부서
</label>
<select id="causeDepartment" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500">
<option value="">선택하세요</option>
<option value="production">생산</option>
<option value="quality">품질</option>
<option value="purchasing">구매</option>
<option value="design">설계</option>
<option value="sales">영업</option>
</select>
</div>
<!-- 해당자 상세 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-user text-gray-500 mr-1"></i>
해당자 상세
</label>
<input type="text" id="responsiblePersonDetail"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
placeholder="해당자 이름, 직책 등 상세 정보">
</div>
<!-- 원인 상세 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-clipboard-list text-gray-500 mr-1"></i>
원인 상세
</label>
<textarea id="causeDetail" rows="4"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500 resize-none"
placeholder="원인에 대한 상세한 설명을 입력하세요"></textarea>
</div>
<!-- 안내 메시지 -->
<div class="bg-orange-50 border border-orange-200 rounded-lg p-3">
<div class="flex items-start">
<i class="fas fa-info-circle text-orange-500 mt-0.5 mr-2"></i>
<div class="text-sm text-orange-700">
<p class="font-medium mb-1">기록용 정보</p>
<p>이 정보는 내부 기록용으로만 사용되며, 모든 필드는 선택사항입니다.</p>
</div>
</div>
</div>
<!-- 버튼 -->
<div class="flex space-x-3 pt-4">
<button type="button" onclick="closeAdditionalInfoModal()"
class="flex-1 px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors">
취소
</button>
<button type="submit"
class="flex-1 px-4 py-2 bg-orange-500 text-white rounded-lg hover:bg-orange-600 transition-colors">
<i class="fas fa-save mr-2"></i>저장
</button>
</div>
</form>
</div>
</div>
</div>
</body>
</html>