/** * issues-inbox.js — 수신함 페이지 스크립트 */ let currentUser = null; let issues = []; let projects = []; let filteredIssues = []; // 한국 시간(KST) 유틸리티 함수 function getKSTDate(date) { const utcDate = new Date(date); // UTC + 9시간 = KST return new Date(utcDate.getTime() + (9 * 60 * 60 * 1000)); } function formatKSTDate(date) { const kstDate = getKSTDate(date); return kstDate.toLocaleDateString('ko-KR', { timeZone: 'Asia/Seoul' }); } function formatKSTTime(date) { const kstDate = getKSTDate(date); return kstDate.toLocaleTimeString('ko-KR', { timeZone: 'Asia/Seoul', hour: '2-digit', minute: '2-digit' }); } function getKSTToday() { const now = new Date(); const kstNow = getKSTDate(now); return new Date(kstNow.getFullYear(), kstNow.getMonth(), kstNow.getDate()); } // 애니메이션 함수들 function animateHeaderAppearance() { // 헤더 요소 찾기 (공통 헤더가 생성한 요소) const headerElement = document.querySelector('header') || document.querySelector('[class*="header"]') || document.querySelector('nav'); if (headerElement) { headerElement.classList.add('header-fade-in'); setTimeout(() => { headerElement.classList.add('visible'); // 헤더 애니메이션 완료 후 본문 애니메이션 setTimeout(() => { animateContentAppearance(); }, 200); }, 50); } else { // 헤더를 찾지 못했으면 바로 본문 애니메이션 animateContentAppearance(); } } // 본문 컨텐츠 애니메이션 function animateContentAppearance() { // 모든 content-fade-in 요소들을 순차적으로 애니메이션 const contentElements = document.querySelectorAll('.content-fade-in'); contentElements.forEach((element, index) => { setTimeout(() => { element.classList.add('visible'); }, index * 100); // 100ms씩 지연 }); } // API 로드 후 초기화 함수 async function initializeInbox() { console.log('수신함 초기화 시작'); const token = TokenManager.getToken(); if (!token) { window.location.href = '/index.html'; return; } try { const user = await AuthAPI.getCurrentUser(); currentUser = user; localStorage.setItem('currentUser', JSON.stringify(user)); // 공통 헤더 초기화 await window.commonHeader.init(user, 'issues_inbox'); // 헤더 초기화 후 부드러운 애니메이션 시작 setTimeout(() => { animateHeaderAppearance(); }, 100); // 페이지 접근 권한 체크 setTimeout(() => { if (typeof canAccessPage === 'function') { const hasAccess = canAccessPage('issues_inbox'); if (!hasAccess) { alert('수신함 페이지에 접근할 권한이 없습니다.'); window.location.href = '/index.html'; return; } } }, 500); // 데이터 로드 await loadProjects(); await loadIssues(); // loadIssues()에서 이미 loadStatistics() 호출함 } catch (error) { console.error('수신함 초기화 실패:', error); // 401 Unauthorized 에러인 경우만 로그아웃 처리 if (error.message && (error.message.includes('401') || error.message.includes('Unauthorized') || error.message.includes('Not authenticated'))) { TokenManager.removeToken(); TokenManager.removeUser(); window.location.href = '/index.html'; } else { // 다른 에러는 사용자에게 알리고 계속 진행 alert('일부 데이터를 불러오는데 실패했습니다. 새로고침 후 다시 시도해주세요.'); // 공통 헤더만이라도 초기화 try { const user = JSON.parse(localStorage.getItem('currentUser') || '{}'); if (user.id) { await window.commonHeader.init(user, 'issues_inbox'); // 에러 상황에서도 애니메이션 적용 setTimeout(() => { animateHeaderAppearance(); }, 100); } } catch (headerError) { console.error('공통 헤더 초기화 실패:', headerError); } } } } // 프로젝트 로드 async function loadProjects() { try { const apiUrl = window.API_BASE_URL || '/api'; const response = await fetch(`${apiUrl}/projects/`, { headers: { 'Authorization': `Bearer ${TokenManager.getToken()}`, 'Content-Type': 'application/json' } }); if (response.ok) { projects = await response.json(); updateProjectFilter(); } } catch (error) { console.error('프로젝트 로드 실패:', error); } } // 프로젝트 필터 업데이트 function updateProjectFilter() { const projectFilter = document.getElementById('projectFilter'); projectFilter.innerHTML = ''; projects.forEach(project => { const option = document.createElement('option'); option.value = project.id; option.textContent = project.project_name; projectFilter.appendChild(option); }); } // 수신함 부적합 목록 로드 (실제 API 연동) async function loadIssues() { showLoading(true); try { const projectId = document.getElementById('projectFilter').value; let url = '/api/inbox/'; // 프로젝트 필터 적용 if (projectId) { url += `?project_id=${projectId}`; } const response = await fetch(url, { headers: { 'Authorization': `Bearer ${TokenManager.getToken()}`, 'Content-Type': 'application/json' } }); if (response.ok) { issues = await response.json(); filterIssues(); await loadStatistics(); } else { throw new Error('수신함 목록을 불러올 수 없습니다.'); } } catch (error) { console.error('수신함 로드 실패:', error); showError('수신함 목록을 불러오는데 실패했습니다.'); } finally { showLoading(false); } } // 신고 필터링 function filterIssues() { const projectFilter = document.getElementById('projectFilter').value; filteredIssues = issues.filter(issue => { // 프로젝트 필터 if (projectFilter && issue.project_id != projectFilter) return false; return true; }); sortIssues(); displayIssues(); } // 신고 정렬 function sortIssues() { const sortOrder = document.getElementById('sortOrder').value; filteredIssues.sort((a, b) => { switch (sortOrder) { case 'newest': return new Date(b.report_date) - new Date(a.report_date); case 'oldest': return new Date(a.report_date) - new Date(b.report_date); case 'priority': const priorityOrder = { 'high': 3, 'medium': 2, 'low': 1 }; return (priorityOrder[b.priority] || 1) - (priorityOrder[a.priority] || 1); default: return new Date(b.report_date) - new Date(a.report_date); } }); } // 부적합 목록 표시 function displayIssues() { const container = document.getElementById('issuesList'); const emptyState = document.getElementById('emptyState'); if (filteredIssues.length === 0) { container.innerHTML = ''; emptyState.classList.remove('hidden'); return; } emptyState.classList.add('hidden'); container.innerHTML = filteredIssues.map(issue => { const project = projects.find(p => p.id === issue.project_id); const reportDate = new Date(issue.report_date); const createdDate = formatKSTDate(reportDate); const createdTime = formatKSTTime(reportDate); const timeAgo = getTimeAgo(reportDate); // 사진 정보 처리 const photoCount = [issue.photo_path, issue.photo_path2, issue.photo_path3, issue.photo_path4, issue.photo_path5].filter(Boolean).length; const photoInfo = photoCount > 0 ? `사진 ${photoCount}장` : '사진 없음'; return `
검토 대기 ${project ? `${project.project_name}` : '프로젝트 미지정'}
ID: ${issue.id}

${issue.final_description || issue.description}

${issue.reporter?.username || '알 수 없음'}
${getCategoryText(issue.category || issue.final_category)}
${issue.location_info ? `
${issue.location_info}
` : ''}
${photoInfo}
${timeAgo}
업로드: ${createdDate} ${createdTime}
${issue.work_hours > 0 ? `
공수: ${issue.work_hours}시간
` : ''}
${issue.detail_notes ? `
"${issue.detail_notes}"
` : ''}
${photoCount > 0 ? ` ` : ''}
`; }).join(''); } // 통계 로드 (새로운 기준) async function loadStatistics() { try { // 현재 수신함 이슈들을 기반으로 통계 계산 (KST 기준) const todayStart = getKSTToday(); // 금일 신규: 오늘 올라온 목록 숫자 (확인된 것 포함) - KST 기준 const todayNewCount = issues.filter(issue => { const reportDate = getKSTDate(new Date(issue.report_date)); const reportDateOnly = new Date(reportDate.getFullYear(), reportDate.getMonth(), reportDate.getDate()); return reportDateOnly >= todayStart; }).length; // 금일 처리: 오늘 처리된 건수 (API에서 가져와야 함) let todayProcessedCount = 0; try { const processedResponse = await fetch('/api/inbox/statistics', { headers: { 'Authorization': `Bearer ${TokenManager.getToken()}`, 'Content-Type': 'application/json' } }); if (processedResponse.ok) { const stats = await processedResponse.json(); todayProcessedCount = stats.today_processed || 0; } } catch (e) { console.log('처리된 건수 조회 실패:', e); } // 미해결: 오늘꺼 제외한 남아있는 것들 - KST 기준 const unresolvedCount = issues.filter(issue => { const reportDate = getKSTDate(new Date(issue.report_date)); const reportDateOnly = new Date(reportDate.getFullYear(), reportDate.getMonth(), reportDate.getDate()); return reportDateOnly < todayStart; }).length; // 통계 업데이트 document.getElementById('todayNewCount').textContent = todayNewCount; document.getElementById('todayProcessedCount').textContent = todayProcessedCount; document.getElementById('unresolvedCount').textContent = unresolvedCount; } catch (error) { console.error('통계 로드 오류:', error); // 오류 시 기본값 설정 document.getElementById('todayNewCount').textContent = '0'; document.getElementById('todayProcessedCount').textContent = '0'; document.getElementById('unresolvedCount').textContent = '0'; } } // 새로고침 function refreshInbox() { loadIssues(); } // 신고 상세 보기 function viewIssueDetail(issueId) { window.location.href = `/issue-view.html#detail-${issueId}`; } // openPhotoModal, closePhotoModal, handleEscKey는 photo-modal.js에서 제공됨 // ===== 워크플로우 모달 관련 함수들 ===== let currentIssueId = null; // 폐기 모달 열기 function openDisposeModal(issueId) { currentIssueId = issueId; document.getElementById('disposalReason').value = 'duplicate'; document.getElementById('customReason').value = ''; document.getElementById('customReasonDiv').classList.add('hidden'); document.getElementById('selectedDuplicateId').value = ''; document.getElementById('disposeModal').classList.remove('hidden'); // 중복 선택 영역 표시 (기본값이 duplicate이므로) toggleDuplicateSelection(); } // 폐기 모달 닫기 function closeDisposeModal() { currentIssueId = null; document.getElementById('disposeModal').classList.add('hidden'); } // 사용자 정의 사유 토글 function toggleCustomReason() { const reason = document.getElementById('disposalReason').value; const customDiv = document.getElementById('customReasonDiv'); if (reason === 'custom') { customDiv.classList.remove('hidden'); } else { customDiv.classList.add('hidden'); } } // 중복 대상 선택 토글 function toggleDuplicateSelection() { const reason = document.getElementById('disposalReason').value; const duplicateDiv = document.getElementById('duplicateSelectionDiv'); if (reason === 'duplicate') { duplicateDiv.classList.remove('hidden'); loadManagementIssues(); } else { duplicateDiv.classList.add('hidden'); document.getElementById('selectedDuplicateId').value = ''; } } // 관리함 이슈 목록 로드 async function loadManagementIssues() { const currentIssue = issues.find(issue => issue.id === currentIssueId); const projectId = currentIssue ? currentIssue.project_id : null; try { const response = await fetch(`/api/inbox/management-issues${projectId ? `?project_id=${projectId}` : ''}`, { headers: { 'Authorization': `Bearer ${TokenManager.getToken()}` } }); if (!response.ok) { throw new Error('관리함 이슈 목록을 불러올 수 없습니다.'); } const managementIssues = await response.json(); displayManagementIssues(managementIssues); } catch (error) { console.error('관리함 이슈 로드 오류:', error); document.getElementById('managementIssuesList').innerHTML = `
이슈 목록을 불러올 수 없습니다.
`; } } // 관리함 이슈 목록 표시 function displayManagementIssues(managementIssues) { const container = document.getElementById('managementIssuesList'); if (managementIssues.length === 0) { container.innerHTML = `
동일 프로젝트의 관리함 이슈가 없습니다.
`; return; } container.innerHTML = managementIssues.map(issue => `
${issue.description || issue.final_description}
${getCategoryText(issue.category || issue.final_category)} 신고자: ${issue.reporter_name} ${issue.duplicate_count > 0 ? `중복 ${issue.duplicate_count}건` : ''}
ID: ${issue.id}
`).join(''); } // 중복 대상 선택 function selectDuplicateTarget(issueId, element) { // 이전 선택 해제 document.querySelectorAll('#managementIssuesList > div').forEach(div => { div.classList.remove('bg-blue-50', 'border-blue-200'); }); // 현재 선택 표시 element.classList.add('bg-blue-50', 'border-blue-200'); document.getElementById('selectedDuplicateId').value = issueId; } // 폐기 확인 async function confirmDispose() { if (!currentIssueId) return; const disposalReason = document.getElementById('disposalReason').value; const customReason = document.getElementById('customReason').value; const duplicateId = document.getElementById('selectedDuplicateId').value; // 사용자 정의 사유 검증 if (disposalReason === 'custom' && !customReason.trim()) { alert('사용자 정의 폐기 사유를 입력해주세요.'); return; } // 중복 대상 선택 검증 if (disposalReason === 'duplicate' && !duplicateId) { alert('중복 대상을 선택해주세요.'); return; } try { const requestBody = { disposal_reason: disposalReason, custom_disposal_reason: disposalReason === 'custom' ? customReason : null }; // 중복 처리인 경우 대상 ID 추가 if (disposalReason === 'duplicate' && duplicateId) { requestBody.duplicate_of_issue_id = parseInt(duplicateId); } const response = await fetch(`/api/inbox/${currentIssueId}/dispose`, { method: 'POST', headers: { 'Authorization': `Bearer ${TokenManager.getToken()}`, 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (response.ok) { const result = await response.json(); const message = disposalReason === 'duplicate' ? '중복 신고가 처리되었습니다.\n신고자 정보가 원본 이슈에 추가되었습니다.' : `부적합이 성공적으로 폐기되었습니다.\n사유: ${getDisposalReasonText(disposalReason)}`; alert(message); closeDisposeModal(); await loadIssues(); // 목록 새로고침 } else { const error = await response.json(); throw new Error(error.detail || '폐기 처리에 실패했습니다.'); } } catch (error) { console.error('폐기 처리 오류:', error); alert('폐기 처리 중 오류가 발생했습니다: ' + error.message); } } // 검토 모달 열기 async function openReviewModal(issueId) { currentIssueId = issueId; // 현재 부적합 정보 찾기 const issue = issues.find(i => i.id === issueId); if (!issue) return; // 원본 정보 표시 const originalInfo = document.getElementById('originalInfo'); const project = projects.find(p => p.id === issue.project_id); originalInfo.innerHTML = `
프로젝트: ${project ? project.project_name : '미지정'}
카테고리: ${getCategoryText(issue.category || issue.final_category)}
설명: ${issue.description || issue.final_description}
등록자: ${issue.reporter?.username || '알 수 없음'}
등록일: ${new Date(issue.report_date).toLocaleDateString('ko-KR')}
`; // 프로젝트 옵션 업데이트 const reviewProjectSelect = document.getElementById('reviewProjectId'); reviewProjectSelect.innerHTML = ''; projects.forEach(project => { const option = document.createElement('option'); option.value = project.id; option.textContent = project.project_name; if (project.id === issue.project_id) { option.selected = true; } reviewProjectSelect.appendChild(option); }); // 현재 값들로 폼 초기화 (최신 내용 우선 사용) document.getElementById('reviewCategory').value = issue.category || issue.final_category; // 최신 description을 title과 description으로 분리 (첫 번째 줄을 title로 사용) const currentDescription = issue.description || issue.final_description; const lines = currentDescription.split('\n'); document.getElementById('reviewTitle').value = lines[0] || ''; document.getElementById('reviewDescription').value = lines.slice(1).join('\n') || currentDescription; document.getElementById('reviewModal').classList.remove('hidden'); } // 검토 모달 닫기 function closeReviewModal() { currentIssueId = null; document.getElementById('reviewModal').classList.add('hidden'); } // 검토 저장 async function saveReview() { if (!currentIssueId) return; const projectId = document.getElementById('reviewProjectId').value; const category = document.getElementById('reviewCategory').value; const title = document.getElementById('reviewTitle').value.trim(); const description = document.getElementById('reviewDescription').value.trim(); if (!title) { alert('부적합명을 입력해주세요.'); return; } // 부적합명과 상세 내용을 합쳐서 저장 (첫 번째 줄에 제목, 나머지는 상세 내용) const combinedDescription = title + (description ? '\n' + description : ''); try { const response = await fetch(`/api/inbox/${currentIssueId}/review`, { method: 'POST', headers: { 'Authorization': `Bearer ${TokenManager.getToken()}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ project_id: projectId ? parseInt(projectId) : null, category: category, description: combinedDescription }) }); if (response.ok) { const result = await response.json(); alert(`검토가 완료되었습니다.\n수정된 항목: ${result.modifications_count}개`); closeReviewModal(); await loadIssues(); // 목록 새로고침 } else { const error = await response.json(); throw new Error(error.detail || '검토 처리에 실패했습니다.'); } } catch (error) { console.error('검토 처리 오류:', error); alert('검토 처리 중 오류가 발생했습니다: ' + error.message); } } // 상태 모달 열기 function openStatusModal(issueId) { currentIssueId = issueId; // 라디오 버튼 초기화 document.querySelectorAll('input[name="finalStatus"]').forEach(radio => { radio.checked = false; }); document.getElementById('statusModal').classList.remove('hidden'); } // 상태 모달 닫기 function closeStatusModal() { currentIssueId = null; document.getElementById('statusModal').classList.add('hidden'); // 완료 관련 필드 초기화 document.getElementById('completionSection').classList.add('hidden'); document.getElementById('completionPhotoInput').value = ''; document.getElementById('completionPhotoPreview').classList.add('hidden'); document.getElementById('solutionInput').value = ''; document.getElementById('responsibleDepartmentInput').value = ''; document.getElementById('responsiblePersonInput').value = ''; completionPhotoBase64 = null; } // 완료 섹션 토글 function toggleCompletionPhotoSection() { const selectedStatus = document.querySelector('input[name="finalStatus"]:checked'); const completionSection = document.getElementById('completionSection'); if (selectedStatus && selectedStatus.value === 'completed') { completionSection.classList.remove('hidden'); } else { completionSection.classList.add('hidden'); // 완료 관련 필드 초기화 document.getElementById('completionPhotoInput').value = ''; document.getElementById('completionPhotoPreview').classList.add('hidden'); document.getElementById('solutionInput').value = ''; document.getElementById('responsibleDepartmentInput').value = ''; document.getElementById('responsiblePersonInput').value = ''; 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); } // 상태 변경 확인 async function confirmStatus() { if (!currentIssueId) return; const selectedStatus = document.querySelector('input[name="finalStatus"]:checked'); if (!selectedStatus) { alert('상태를 선택해주세요.'); return; } const reviewStatus = selectedStatus.value; try { const requestBody = { review_status: reviewStatus }; // 완료 상태일 때 추가 정보 수집 if (reviewStatus === 'completed') { // 완료 사진 if (completionPhotoBase64) { requestBody.completion_photo = completionPhotoBase64; } // 해결방안 const solution = document.getElementById('solutionInput').value.trim(); if (solution) { requestBody.solution = solution; } // 담당부서 const responsibleDepartment = document.getElementById('responsibleDepartmentInput').value; if (responsibleDepartment) { requestBody.responsible_department = responsibleDepartment; } // 담당자 const responsiblePerson = document.getElementById('responsiblePersonInput').value.trim(); if (responsiblePerson) { requestBody.responsible_person = responsiblePerson; } } const response = await fetch(`/api/inbox/${currentIssueId}/status`, { method: 'POST', headers: { 'Authorization': `Bearer ${TokenManager.getToken()}`, 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (response.ok) { const result = await response.json(); alert(`상태가 성공적으로 변경되었습니다.\n${result.destination}으로 이동됩니다.`); closeStatusModal(); await loadIssues(); // 목록 새로고침 } else { const error = await response.json(); throw new Error(error.detail || '상태 변경에 실패했습니다.'); } } catch (error) { console.error('상태 변경 오류:', error); alert('상태 변경 중 오류가 발생했습니다: ' + error.message); } } // getStatusBadgeClass, getStatusText, getCategoryText, getDisposalReasonText는 // issue-helpers.js에서 제공됨 function getTimeAgo(date) { const now = getKSTDate(new Date()); const kstDate = getKSTDate(date); const diffMs = now - kstDate; const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return '방금 전'; if (diffMins < 60) return `${diffMins}분 전`; if (diffHours < 24) return `${diffHours}시간 전`; if (diffDays < 7) return `${diffDays}일 전`; return formatKSTDate(date); } function showLoading(show) { const overlay = document.getElementById('loadingOverlay'); if (show) { overlay.classList.add('active'); } else { overlay.classList.remove('active'); } } function showError(message) { alert(message); } // AI 분류 추천 async function aiClassifyCurrentIssue() { if (!currentIssueId || typeof AiAPI === 'undefined') return; const issue = issues.find(i => i.id === currentIssueId); if (!issue) return; const btn = document.getElementById('aiClassifyBtn'); const loading = document.getElementById('aiClassifyLoading'); const result = document.getElementById('aiClassifyResult'); if (btn) btn.disabled = true; if (loading) loading.classList.remove('hidden'); if (result) result.classList.add('hidden'); // RAG 강화 분류 사용 (과거 사례 참고) const classifyFn = AiAPI.classifyWithRAG || AiAPI.classifyIssue; const data = await classifyFn( issue.description || issue.final_description || '', issue.detail_notes || '' ); if (loading) loading.classList.add('hidden'); if (btn) btn.disabled = false; if (!data.available) { if (result) { result.innerHTML = '

AI 서비스를 사용할 수 없습니다

'; result.classList.remove('hidden'); } return; } const categoryMap = { 'material_missing': '자재 누락', 'design_error': '설계 오류', 'incoming_defect': '반입 불량', 'inspection_miss': '검사 누락', }; const deptMap = { 'production': '생산', 'quality': '품질', 'purchasing': '구매', 'design': '설계', 'sales': '영업', }; const cat = data.category || ''; const dept = data.responsible_department || ''; const severity = data.severity || ''; const summary = data.summary || ''; const confidence = data.category_confidence ? Math.round(data.category_confidence * 100) : ''; result.innerHTML = `

분류: ${categoryMap[cat] || cat} ${confidence ? `(${confidence}%)` : ''}

부서: ${deptMap[dept] || dept}

심각도: ${severity}

${summary ? `

요약: ${summary}

` : ''}
`; result.classList.remove('hidden'); } function applyAiClassification(category) { const reviewCategory = document.getElementById('reviewCategory'); if (reviewCategory && category) { reviewCategory.value = category; } if (window.showToast) { window.showToast('AI 추천이 적용되었습니다', 'success'); } } // 초기화 (api.js는 HTML에서 로드됨) initializeInbox();