/** * issues-dashboard.js — 부적합 현황판 페이지 스크립트 */ let currentUser = null; let allIssues = []; let projects = []; let filteredIssues = []; // 애니메이션 함수들 function animateHeaderAppearance() { const header = document.getElementById('commonHeader'); if (header) { header.classList.add('header-fade-in'); } } function animateContentAppearance() { const content = document.querySelector('.content-fade-in'); if (content) { content.classList.add('visible'); } } // 페이지 초기화 async function initializeDashboard() { try { // 인증 확인 currentUser = await window.authManager.checkAuth(); if (!currentUser) { showLoginScreen(); return; } // 페이지 권한 확인 window.pagePermissionManager.setUser(currentUser); await window.pagePermissionManager.loadPagePermissions(); if (!window.pagePermissionManager.canAccessPage('issues_dashboard')) { alert('현황판 접근 권한이 없습니다.'); window.location.href = '/'; return; } // 공통 헤더 초기화 if (window.commonHeader) { await window.commonHeader.init(currentUser, 'issues_dashboard'); setTimeout(() => animateHeaderAppearance(), 100); } // 데이터 로드 await Promise.all([ loadProjects(), loadInProgressIssues() ]); updateDashboard(); hideLoadingScreen(); } catch (error) { console.error('대시보드 초기화 실패:', error); alert('대시보드를 불러오는데 실패했습니다.'); hideLoadingScreen(); } } // 로딩 스크린 관리 function hideLoadingScreen() { document.getElementById('loadingScreen').style.display = 'none'; } function showLoginScreen() { document.getElementById('loadingScreen').style.display = 'none'; document.getElementById('loginScreen').classList.remove('hidden'); } // 데이터 로드 함수들 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(); } else { throw new Error('프로젝트 목록을 불러올 수 없습니다.'); } } catch (error) { console.error('프로젝트 로드 실패:', error); } } async function loadInProgressIssues() { try { const response = await fetch('/api/issues/admin/all', { headers: { 'Authorization': `Bearer ${TokenManager.getToken()}`, 'Content-Type': 'application/json' } }); if (response.ok) { const allData = await response.json(); // 진행 중 상태만 필터링 allIssues = allData.filter(issue => issue.review_status === 'in_progress'); filteredIssues = [...allIssues]; } else { throw new Error('부적합 목록을 불러올 수 없습니다.'); } } 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); }); } // 대시보드 업데이트 function updateDashboard() { updateStatistics(); updateProjectCards(); } // 통계 업데이트 function updateStatistics() { const today = new Date().toDateString(); // 오늘 신규 (오늘 수신함에서 진행중으로 넘어온 것들) const todayIssues = allIssues.filter(issue => issue.reviewed_at && new Date(issue.reviewed_at).toDateString() === today ); // 완료 대기 (완료 신청이 된 것들) const pendingCompletionIssues = allIssues.filter(issue => issue.completion_requested_at && issue.review_status === 'in_progress' ); // 지연 중 (마감일이 지난 것들) const overdueIssues = allIssues.filter(issue => { if (!issue.expected_completion_date) return false; const expectedDate = new Date(issue.expected_completion_date); const now = new Date(); return expectedDate < now; // 마감일 지남 }); document.getElementById('totalInProgress').textContent = allIssues.length; document.getElementById('todayNew').textContent = todayIssues.length; document.getElementById('pendingCompletion').textContent = pendingCompletionIssues.length; document.getElementById('overdue').textContent = overdueIssues.length; } // 이슈 카드 업데이트 (관리함 스타일 - 날짜별 그룹화) function updateProjectCards() { const container = document.getElementById('projectDashboard'); const emptyState = document.getElementById('emptyState'); if (filteredIssues.length === 0) { container.innerHTML = ''; emptyState.classList.remove('hidden'); return; } emptyState.classList.add('hidden'); // 날짜별로 그룹화 (관리함 진입일 기준) const groupedByDate = {}; const dateObjects = {}; // 정렬용 Date 객체 저장 filteredIssues.forEach(issue => { // reviewed_at이 있으면 관리함 진입일, 없으면 report_date 사용 const dateToUse = issue.reviewed_at || issue.report_date; const dateObj = new Date(dateToUse); const dateKey = dateObj.toLocaleDateString('ko-KR'); if (!groupedByDate[dateKey]) { groupedByDate[dateKey] = []; dateObjects[dateKey] = dateObj; } groupedByDate[dateKey].push(issue); }); // 날짜별 그룹 생성 const dateGroups = Object.keys(groupedByDate) .sort((a, b) => dateObjects[b] - dateObjects[a]) // 최신순 .map(dateKey => { const issues = groupedByDate[dateKey]; const formattedDate = dateObjects[dateKey].toLocaleDateString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit' }).replace(/\./g, '. ').trim(); return `
${formattedDate} (${issues.length}건) 관리함 진입일
${issues.map(issue => createIssueCard(issue)).join('')}
`; }).join(''); container.innerHTML = dateGroups; } // getIssueTitle, getIssueDetail은 issue-helpers.js에서 제공됨 // 이슈 카드 생성 (관리함 진행 중 스타일, 읽기 전용) function createIssueCard(issue) { const project = projects.find(p => p.id === issue.project_id); const projectName = project ? project.project_name : '미지정'; // getDepartmentText, getCategoryText는 issue-helpers.js에서 제공됨 // 완료 반려 내용 포맷팅 const formatRejectionContent = (issue) => { // 1. 새 필드에서 확인 if (issue.completion_rejection_reason) { const rejectedAt = issue.completion_rejected_at ? new Date(issue.completion_rejected_at).toLocaleString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : ''; return rejectedAt ? `[${rejectedAt}] ${issue.completion_rejection_reason}` : issue.completion_rejection_reason; } // 2. 기존 데이터에서 패턴 추출 (마이그레이션 전 데이터용) if (issue.management_comment) { const rejectionPattern = /\[완료 반려[^\]]*\][^\n]*/g; const rejections = issue.management_comment.match(rejectionPattern); return rejections ? rejections.join('\n') : ''; } return ''; }; // solution 파싱 및 카드 형식으로 표시 const parseSolutionOpinions = (solution, issue) => { let html = ''; // 1. 수신함/관리함 내용 표시 - 항상 표시 let rawManagementContent = issue.management_comment || issue.final_description || ''; // 기존 데이터에서 완료 반려 패턴 제거 (마이그레이션용) rawManagementContent = rawManagementContent.replace(/\[완료 반려[^\]]*\][^\n]*\n*/g, '').trim(); // 기본 텍스트들 필터링 const defaultTexts = [ '중복작업 신고용', '상세 내용 없음', '자재 누락', '설계 오류', '반입 불량', '검사 누락', '기타', '부적합명', '상세내용', '상세 내용' ]; const filteredLines = rawManagementContent.split('\n').filter(line => { const trimmed = line.trim(); if (!trimmed) return false; if (defaultTexts.includes(trimmed)) return false; return true; }); const managementContent = filteredLines.join('\n').trim(); const displayContent = managementContent ? managementContent : '확정된 해결 방안 없음'; const contentStyle = managementContent ? 'text-red-700' : 'text-red-400 italic'; html += `
${displayContent}
`; // 2. 해결 방안 의견들 표시 if (!solution || solution.trim() === '') { return html; } // 구분선으로 의견들을 분리 const opinions = solution.split(/─{30,}/); html += opinions.map((opinion, opinionIndex) => { const trimmed = opinion.trim(); if (!trimmed) return ''; // [작성자] (날짜시간) 패턴 매칭 const headerMatch = trimmed.match(/^\[([^\]]+)\]\s*\(([^)]+)\)/); if (headerMatch) { const author = headerMatch[1]; const datetime = headerMatch[2]; // 댓글과 본문 분리 const lines = trimmed.substring(headerMatch[0].length).trim().split('\n'); let mainContent = ''; let comments = []; let currentCommentIndex = -1; for (const line of lines) { if (line.match(/^\s*└/)) { const commentMatch = line.match(/└\s*\[([^\]]+)\]\s*\(([^)]+)\):\s*(.+)/); if (commentMatch) { comments.push({ author: commentMatch[1], datetime: commentMatch[2], content: commentMatch[3], replies: [] }); currentCommentIndex = comments.length - 1; } } else if (line.match(/^\s*↳/)) { const replyMatch = line.match(/↳\s*\[([^\]]+)\]\s*\(([^)]+)\):\s*(.+)/); if (replyMatch && currentCommentIndex >= 0) { comments[currentCommentIndex].replies.push({ author: replyMatch[1], datetime: replyMatch[2], content: replyMatch[3] }); } } else { mainContent += (mainContent ? '\n' : '') + line; } } // 색상 스킴 const colorSchemes = [ 'bg-gradient-to-r from-green-50 to-emerald-50 border-green-300', 'bg-gradient-to-r from-blue-50 to-cyan-50 border-blue-300', 'bg-gradient-to-r from-purple-50 to-pink-50 border-purple-300', 'bg-gradient-to-r from-orange-50 to-yellow-50 border-orange-300', 'bg-gradient-to-r from-indigo-50 to-violet-50 border-indigo-300' ]; const colorScheme = colorSchemes[opinionIndex % colorSchemes.length]; const isOwnOpinion = currentUser && (author === currentUser.full_name || author === currentUser.username); return `
${author.charAt(0)}
${author}
${datetime}
${isOwnOpinion ? ` ` : ''}
${mainContent}
${comments.length > 0 ? `
` : ''}
`; } else { return `
${trimmed}
`; } }).join(''); return html; }; // 날짜 포맷팅 const formatKSTDate = (dateStr) => { if (!dateStr) return '-'; const date = new Date(dateStr); return date.toLocaleDateString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit' }); }; // 상태 체크 함수들 const getIssueStatus = () => { if (issue.review_status === 'completed') return 'completed'; if (issue.completion_requested_at) return 'pending_completion'; if (issue.expected_completion_date) { const expectedDate = new Date(issue.expected_completion_date); const now = new Date(); const diffDays = (expectedDate - now) / (1000 * 60 * 60 * 24); if (diffDays < 0) return 'overdue'; if (diffDays <= 3) return 'urgent'; } return 'in_progress'; }; const getStatusConfig = (status) => { const configs = { 'in_progress': { text: '진행 중', bgColor: 'bg-gradient-to-r from-blue-500 to-blue-600', icon: 'fas fa-cog fa-spin', dotColor: 'bg-white' }, 'urgent': { text: '긴급', bgColor: 'bg-gradient-to-r from-orange-500 to-orange-600', icon: 'fas fa-exclamation-triangle', dotColor: 'bg-white' }, 'overdue': { text: '지연됨', bgColor: 'bg-gradient-to-r from-red-500 to-red-600', icon: 'fas fa-clock', dotColor: 'bg-white' }, 'pending_completion': { text: '완료 대기', bgColor: 'bg-gradient-to-r from-purple-500 to-purple-600', icon: 'fas fa-hourglass-half', dotColor: 'bg-white' }, 'completed': { text: '완료됨', bgColor: 'bg-gradient-to-r from-green-500 to-green-600', icon: 'fas fa-check-circle', dotColor: 'bg-white' } }; return configs[status] || configs['in_progress']; }; const currentStatus = getIssueStatus(); const statusConfig = getStatusConfig(currentStatus); return `
No.${issue.project_sequence_no || '-'}
${projectName}

${getIssueTitle(issue)}

${statusConfig.text}
발생: ${formatKSTDate(issue.report_date)}
${getCategoryText(issue.category || issue.final_category)}
상세 내용
${getIssueDetail(issue)}
신고자 & 담당 & 마감
신고자
${issue.reporter?.full_name || issue.reporter?.username || '-'}
담당자
${issue.responsible_person || '-'}
마감
${formatKSTDate(issue.expected_completion_date)}
${currentStatus === 'in_progress' || currentStatus === 'urgent' || currentStatus === 'overdue' ? `
` : ''}
해결 방안
${parseSolutionOpinions(issue.solution, issue)}
이미지
${(() => { const photos = [ issue.photo_path, issue.photo_path2, issue.photo_path3, issue.photo_path4, issue.photo_path5 ].filter(p => p); if (photos.length === 0) { return `
`; } return photos.map((path, idx) => `
부적합 사진 ${idx + 1}
`).join(''); })()}
${(() => { const rejection = formatRejectionContent(issue); if (!rejection) return ''; return `
완료 반려 내역
${rejection}
`; })()} ${currentStatus === 'pending_completion' ? `
` : ''}
`; } // openPhotoModal, closePhotoModal은 photo-modal.js에서 제공됨 // ESC 키로 모달 닫기 document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { closePhotoModal(); closeRejectionModal(); closeOpinionModal(); closeCompletionRequestModal(); closeCommentModal(); closeEditOpinionModal(); closeReplyModal(); closeEditCommentModal(); closeEditReplyModal(); } }); // 날짜 그룹 토글 기능 function toggleDateGroup(dateKey) { const content = document.getElementById(`content-${dateKey}`); const chevron = document.getElementById(`chevron-${dateKey}`); if (content.style.display === 'none') { content.style.display = 'block'; chevron.classList.remove('fa-chevron-right'); chevron.classList.add('fa-chevron-down'); } else { content.style.display = 'none'; chevron.classList.remove('fa-chevron-down'); chevron.classList.add('fa-chevron-right'); } } // 필터 및 정렬 함수들 function filterByProject() { const projectId = document.getElementById('projectFilter').value; if (projectId) { filteredIssues = allIssues.filter(issue => issue.project_id == projectId); } else { filteredIssues = [...allIssues]; } updateProjectCards(); } // getDepartmentText는 issue-helpers.js에서 제공됨 function viewIssueDetail(issueId) { window.location.href = `/issues-management.html#issue-${issueId}`; } function refreshDashboard() { // 현재 선택된 프로젝트 기준으로 다시 로드 const selectedProject = document.getElementById('projectFilter').value; if (selectedProject) { filterByProject(); } else { initializeDashboard(); } } // 로그인 처리 document.getElementById('loginForm').addEventListener('submit', async (e) => { e.preventDefault(); const username = document.getElementById('username').value; const password = document.getElementById('password').value; try { const user = await window.authManager.login(username, password); if (user) { document.getElementById('loginScreen').classList.add('hidden'); await initializeDashboard(); } } catch (error) { alert('로그인에 실패했습니다.'); } }); // 페이지 로드 시 초기화 document.addEventListener('DOMContentLoaded', () => { // AuthManager 로드 대기 const checkAuthManager = () => { if (window.authManager) { initializeDashboard(); } else { setTimeout(checkAuthManager, 100); } }; checkAuthManager(); }); // ===== 두 번째 스크립트 블록 (API 로드 및 추가 기능) ===== function initializeDashboardApp() { console.log('API 스크립트 로드 완료 (issues-dashboard.html)'); } // 완료 신청 관련 함수들 let selectedCompletionIssueId = null; let completionPhotoBase64 = null; function openCompletionRequestModal(issueId) { selectedCompletionIssueId = issueId; document.getElementById('completionRequestModal').classList.remove('hidden'); // 폼 초기화 document.getElementById('completionRequestForm').reset(); document.getElementById('photoPreview').classList.add('hidden'); document.getElementById('photoUploadArea').classList.remove('hidden'); completionPhotoBase64 = null; } function closeCompletionRequestModal() { selectedCompletionIssueId = null; completionPhotoBase64 = null; document.getElementById('completionRequestModal').classList.add('hidden'); } // 완료 신청 반려 관련 함수들 let selectedRejectionIssueId = null; function openRejectionModal(issueId) { selectedRejectionIssueId = issueId; document.getElementById('rejectionModal').classList.remove('hidden'); document.getElementById('rejectionReason').value = ''; document.getElementById('rejectionReason').focus(); } function closeRejectionModal() { selectedRejectionIssueId = null; document.getElementById('rejectionModal').classList.add('hidden'); } async function submitRejection(event) { event.preventDefault(); if (!selectedRejectionIssueId) { alert('이슈 ID가 없습니다.'); return; } const rejectionReason = document.getElementById('rejectionReason').value.trim(); if (!rejectionReason) { alert('반려 사유를 입력해주세요.'); return; } try { const response = await fetch(`/api/issues/${selectedRejectionIssueId}/reject-completion`, { method: 'POST', headers: { 'Authorization': `Bearer ${TokenManager.getToken()}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ rejection_reason: rejectionReason }) }); if (response.ok) { alert('완료 신청이 반려되었습니다.'); closeRejectionModal(); await initializeDashboard(); } else { const error = await response.json(); alert(`반려 처리 실패: ${error.detail || '알 수 없는 오류'}`); } } catch (error) { console.error('반려 처리 오류:', error); alert('반려 처리 중 오류가 발생했습니다: ' + error.message); } } // 댓글 토글 기능 function toggleComments(issueId, opinionIndex) { const commentsDiv = document.getElementById(`comments-${issueId}-${opinionIndex}`); const chevron = document.getElementById(`comment-chevron-${issueId}-${opinionIndex}`); if (commentsDiv.classList.contains('hidden')) { commentsDiv.classList.remove('hidden'); chevron.classList.add('fa-rotate-180'); } else { commentsDiv.classList.add('hidden'); chevron.classList.remove('fa-rotate-180'); } } // 의견 제시 모달 관련 let selectedOpinionIssueId = null; function openOpinionModal(issueId) { selectedOpinionIssueId = issueId; document.getElementById('opinionModal').classList.remove('hidden'); document.getElementById('opinionText').value = ''; document.getElementById('opinionText').focus(); } function closeOpinionModal() { selectedOpinionIssueId = null; document.getElementById('opinionModal').classList.add('hidden'); } // 댓글 추가 모달 관련 let selectedCommentIssueId = null; let selectedCommentOpinionIndex = null; function openCommentModal(issueId, opinionIndex) { selectedCommentIssueId = issueId; selectedCommentOpinionIndex = opinionIndex; document.getElementById('commentModal').classList.remove('hidden'); document.getElementById('commentText').value = ''; document.getElementById('commentText').focus(); } function closeCommentModal() { selectedCommentIssueId = null; selectedCommentOpinionIndex = null; document.getElementById('commentModal').classList.add('hidden'); } // 로그 기록 함수 async function logModification(issueId, action, details) { try { const issueResponse = await fetch(`/api/issues/${issueId}`, { headers: { 'Authorization': `Bearer ${TokenManager.getToken()}` } }); if (issueResponse.ok) { const issue = await issueResponse.json(); const modificationLog = issue.modification_log || []; modificationLog.push({ timestamp: new Date().toISOString(), user: currentUser.full_name || currentUser.username, user_id: currentUser.id, action: action, details: details }); return modificationLog; } } catch (error) { console.error('로그 기록 오류:', error); } return null; } async function submitComment(event) { event.preventDefault(); if (!selectedCommentIssueId || selectedCommentOpinionIndex === null) { alert('대상 의견이 선택되지 않았습니다.'); return; } const commentText = document.getElementById('commentText').value.trim(); if (!commentText) { alert('댓글을 입력해주세요.'); return; } try { const issueResponse = await fetch(`/api/issues/${selectedCommentIssueId}`, { headers: { 'Authorization': `Bearer ${TokenManager.getToken()}` } }); if (!issueResponse.ok) { throw new Error('이슈 정보를 가져올 수 없습니다.'); } const issue = await issueResponse.json(); const opinions = issue.solution ? issue.solution.split(/─{30,}/) : []; if (selectedCommentOpinionIndex >= opinions.length) { throw new Error('잘못된 의견 인덱스입니다.'); } const now = new Date(); const dateStr = now.toLocaleString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); const newComment = ` └ [${currentUser.full_name || currentUser.username}] (${dateStr}): ${commentText}`; opinions[selectedCommentOpinionIndex] = opinions[selectedCommentOpinionIndex].trim() + '\n' + newComment; const updatedSolution = opinions.join('\n' + '─'.repeat(50) + '\n'); const response = await fetch(`/api/issues/${selectedCommentIssueId}/management`, { method: 'PUT', headers: { 'Authorization': `Bearer ${TokenManager.getToken()}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ solution: updatedSolution }) }); if (response.ok) { await logModification(selectedCommentIssueId, 'comment_added', { opinion_index: selectedCommentOpinionIndex, comment: commentText }); alert('댓글이 추가되었습니다.'); closeCommentModal(); await initializeDashboard(); } else { const error = await response.json(); alert(`댓글 추가 실패: ${error.detail || '알 수 없는 오류'}`); } } catch (error) { console.error('댓글 추가 오류:', error); alert('댓글 추가 중 오류가 발생했습니다: ' + error.message); } } // 답글(대댓글) 관련 let selectedReplyIssueId = null; let selectedReplyOpinionIndex = null; let selectedReplyCommentIndex = null; function openReplyModal(issueId, opinionIndex, commentIndex) { selectedReplyIssueId = issueId; selectedReplyOpinionIndex = opinionIndex; selectedReplyCommentIndex = commentIndex; document.getElementById('replyModal').classList.remove('hidden'); document.getElementById('replyText').value = ''; document.getElementById('replyText').focus(); } function closeReplyModal() { selectedReplyIssueId = null; selectedReplyOpinionIndex = null; selectedReplyCommentIndex = null; document.getElementById('replyModal').classList.add('hidden'); } async function submitReply(event) { event.preventDefault(); if (!selectedReplyIssueId || selectedReplyOpinionIndex === null || selectedReplyCommentIndex === null) { alert('대상 댓글이 선택되지 않았습니다.'); return; } const replyText = document.getElementById('replyText').value.trim(); if (!replyText) { alert('답글을 입력해주세요.'); return; } try { const issueResponse = await fetch(`/api/issues/${selectedReplyIssueId}`, { headers: { 'Authorization': `Bearer ${TokenManager.getToken()}` } }); if (!issueResponse.ok) { throw new Error('이슈 정보를 가져올 수 없습니다.'); } const issue = await issueResponse.json(); const opinions = issue.solution ? issue.solution.split(/─{30,}/) : []; if (selectedReplyOpinionIndex >= opinions.length) { throw new Error('잘못된 의견 인덱스입니다.'); } const now = new Date(); const dateStr = now.toLocaleString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); const lines = opinions[selectedReplyOpinionIndex].trim().split('\n'); const newReply = ` ↳ [${currentUser.full_name || currentUser.username}] (${dateStr}): ${replyText}`; let commentCount = -1; let insertIndex = -1; for (let i = 0; i < lines.length; i++) { if (lines[i].match(/^\s*└/)) { commentCount++; if (commentCount === selectedReplyCommentIndex) { insertIndex = i + 1; while (insertIndex < lines.length && lines[insertIndex].match(/^\s*↳/)) { insertIndex++; } break; } } } if (insertIndex >= 0) { lines.splice(insertIndex, 0, newReply); opinions[selectedReplyOpinionIndex] = lines.join('\n'); } const updatedSolution = opinions.join('\n' + '─'.repeat(50) + '\n'); const response = await fetch(`/api/issues/${selectedReplyIssueId}/management`, { method: 'PUT', headers: { 'Authorization': `Bearer ${TokenManager.getToken()}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ solution: updatedSolution }) }); if (response.ok) { await logModification(selectedReplyIssueId, 'reply_added', { opinion_index: selectedReplyOpinionIndex, comment_index: selectedReplyCommentIndex, reply: replyText }); alert('답글이 추가되었습니다.'); closeReplyModal(); await initializeDashboard(); } else { const error = await response.json(); alert(`답글 추가 실패: ${error.detail || '알 수 없는 오류'}`); } } catch (error) { console.error('답글 추가 오류:', error); alert('답글 추가 중 오류가 발생했습니다: ' + error.message); } } // 의견 수정 관련 let selectedEditIssueId = null; let selectedEditOpinionIndex = null; async function editOpinion(issueId, opinionIndex) { selectedEditIssueId = issueId; selectedEditOpinionIndex = opinionIndex; try { const issueResponse = await fetch(`/api/issues/${issueId}`, { headers: { 'Authorization': `Bearer ${TokenManager.getToken()}` } }); if (!issueResponse.ok) throw new Error('이슈 정보를 가져올 수 없습니다.'); const issue = await issueResponse.json(); const opinions = issue.solution ? issue.solution.split(/─{30,}/) : []; if (opinionIndex >= opinions.length) throw new Error('잘못된 의견 인덱스입니다.'); const opinion = opinions[opinionIndex].trim(); const headerMatch = opinion.match(/^\[([^\]]+)\]\s*\(([^)]+)\)/); if (headerMatch) { const lines = opinion.substring(headerMatch[0].length).trim().split('\n'); let mainContent = ''; for (const line of lines) { if (!line.match(/^\s*[└├]/)) { mainContent += (mainContent ? '\n' : '') + line; } } document.getElementById('editOpinionText').value = mainContent; document.getElementById('editOpinionModal').classList.remove('hidden'); document.getElementById('editOpinionText').focus(); } } catch (error) { console.error('의견 수정 준비 오류:', error); alert('의견을 불러오는 중 오류가 발생했습니다: ' + error.message); } } function closeEditOpinionModal() { selectedEditIssueId = null; selectedEditOpinionIndex = null; document.getElementById('editOpinionModal').classList.add('hidden'); } async function submitEditOpinion(event) { event.preventDefault(); if (!selectedEditIssueId || selectedEditOpinionIndex === null) { alert('대상 의견이 선택되지 않았습니다.'); return; } const newText = document.getElementById('editOpinionText').value.trim(); if (!newText) { alert('의견 내용을 입력해주세요.'); return; } try { const issueResponse = await fetch(`/api/issues/${selectedEditIssueId}`, { headers: { 'Authorization': `Bearer ${TokenManager.getToken()}` } }); if (!issueResponse.ok) throw new Error('이슈 정보를 가져올 수 없습니다.'); const issue = await issueResponse.json(); const opinions = issue.solution ? issue.solution.split(/─{30,}/) : []; if (selectedEditOpinionIndex >= opinions.length) throw new Error('잘못된 의견 인덱스입니다.'); const opinion = opinions[selectedEditOpinionIndex].trim(); const headerMatch = opinion.match(/^\[([^\]]+)\]\s*\(([^)]+)\)/); if (headerMatch) { const lines = opinion.substring(headerMatch[0].length).trim().split('\n'); let comments = []; for (const line of lines) { if (line.match(/^\s*[└├]/)) { comments.push(line); } } opinions[selectedEditOpinionIndex] = headerMatch[0] + '\n' + newText + (comments.length > 0 ? '\n' + comments.join('\n') : ''); } const updatedSolution = opinions.join('\n' + '─'.repeat(50) + '\n'); const response = await fetch(`/api/issues/${selectedEditIssueId}/management`, { method: 'PUT', headers: { 'Authorization': `Bearer ${TokenManager.getToken()}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ solution: updatedSolution }) }); if (response.ok) { alert('의견이 수정되었습니다.'); closeEditOpinionModal(); await initializeDashboard(); } else { const error = await response.json(); alert(`의견 수정 실패: ${error.detail || '알 수 없는 오류'}`); } } catch (error) { console.error('의견 수정 오류:', error); alert('의견 수정 중 오류가 발생했습니다: ' + error.message); } } // 의견 삭제 async function deleteOpinion(issueId, opinionIndex) { if (!confirm('이 의견을 삭제하시겠습니까? (댓글도 함께 삭제됩니다)')) return; try { const issueResponse = await fetch(`/api/issues/${issueId}`, { headers: { 'Authorization': `Bearer ${TokenManager.getToken()}` } }); if (!issueResponse.ok) throw new Error('이슈 정보를 가져올 수 없습니다.'); const issue = await issueResponse.json(); const opinions = issue.solution ? issue.solution.split(/─{30,}/) : []; if (opinionIndex >= opinions.length) throw new Error('잘못된 의견 인덱스입니다.'); const deletedOpinion = opinions[opinionIndex]; opinions.splice(opinionIndex, 1); const updatedSolution = opinions.length > 0 ? opinions.join('\n' + '─'.repeat(50) + '\n') : ''; const response = await fetch(`/api/issues/${issueId}/management`, { method: 'PUT', headers: { 'Authorization': `Bearer ${TokenManager.getToken()}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ solution: updatedSolution || null }) }); if (response.ok) { await logModification(issueId, 'opinion_deleted', { opinion_index: opinionIndex, deleted_content: deletedOpinion }); alert('의견이 삭제되었습니다.'); await initializeDashboard(); } else { const error = await response.json(); alert(`의견 삭제 실패: ${error.detail || '알 수 없는 오류'}`); } } catch (error) { console.error('의견 삭제 오류:', error); alert('의견 삭제 중 오류가 발생했습니다: ' + error.message); } } // 댓글 수정 let selectedEditCommentIssueId = null; let selectedEditCommentOpinionIndex = null; let selectedEditCommentIndex = null; async function editComment(issueId, opinionIndex, commentIndex) { selectedEditCommentIssueId = issueId; selectedEditCommentOpinionIndex = opinionIndex; selectedEditCommentIndex = commentIndex; try { const issueResponse = await fetch(`/api/issues/${issueId}`, { headers: { 'Authorization': `Bearer ${TokenManager.getToken()}` } }); if (!issueResponse.ok) throw new Error('이슈 정보를 가져올 수 없습니다.'); const issue = await issueResponse.json(); const opinions = issue.solution ? issue.solution.split(/─{30,}/) : []; const lines = opinions[opinionIndex].trim().split('\n'); let commentCount = -1; for (const line of lines) { if (line.match(/^\s*└/)) { commentCount++; if (commentCount === commentIndex) { const match = line.match(/└\s*\[([^\]]+)\]\s*\(([^)]+)\):\s*(.+)/); if (match) { document.getElementById('editCommentText').value = match[3]; document.getElementById('editCommentModal').classList.remove('hidden'); document.getElementById('editCommentText').focus(); } break; } } } } catch (error) { console.error('댓글 수정 준비 오류:', error); alert('댓글을 불러오는 중 오류가 발생했습니다: ' + error.message); } } function closeEditCommentModal() { selectedEditCommentIssueId = null; selectedEditCommentOpinionIndex = null; selectedEditCommentIndex = null; document.getElementById('editCommentModal').classList.add('hidden'); } async function submitEditComment(event) { event.preventDefault(); const newText = document.getElementById('editCommentText').value.trim(); if (!newText) { alert('댓글 내용을 입력해주세요.'); return; } try { const issueResponse = await fetch(`/api/issues/${selectedEditCommentIssueId}`, { headers: { 'Authorization': `Bearer ${TokenManager.getToken()}` } }); if (!issueResponse.ok) throw new Error('이슈 정보를 가져올 수 없습니다.'); const issue = await issueResponse.json(); const opinions = issue.solution ? issue.solution.split(/─{30,}/) : []; const lines = opinions[selectedEditCommentOpinionIndex].trim().split('\n'); let commentCount = -1; for (let i = 0; i < lines.length; i++) { if (lines[i].match(/^\s*└/)) { commentCount++; if (commentCount === selectedEditCommentIndex) { const match = lines[i].match(/└\s*\[([^\]]+)\]\s*\(([^)]+)\):/); if (match) { lines[i] = ` └ [${match[1]}] (${match[2]}): ${newText}`; } break; } } } opinions[selectedEditCommentOpinionIndex] = lines.join('\n'); const updatedSolution = opinions.join('\n' + '─'.repeat(50) + '\n'); const response = await fetch(`/api/issues/${selectedEditCommentIssueId}/management`, { method: 'PUT', headers: { 'Authorization': `Bearer ${TokenManager.getToken()}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ solution: updatedSolution }) }); if (response.ok) { await logModification(selectedEditCommentIssueId, 'comment_edited', { opinion_index: selectedEditCommentOpinionIndex, comment_index: selectedEditCommentIndex, new_content: newText }); alert('댓글이 수정되었습니다.'); closeEditCommentModal(); await initializeDashboard(); } else { const error = await response.json(); alert(`댓글 수정 실패: ${error.detail || '알 수 없는 오류'}`); } } catch (error) { console.error('댓글 수정 오류:', error); alert('댓글 수정 중 오류가 발생했습니다: ' + error.message); } } // 댓글 삭제 async function deleteComment(issueId, opinionIndex, commentIndex) { if (!confirm('이 댓글을 삭제하시겠습니까? (답글도 함께 삭제됩니다)')) return; try { const issueResponse = await fetch(`/api/issues/${issueId}`, { headers: { 'Authorization': `Bearer ${TokenManager.getToken()}` } }); if (!issueResponse.ok) throw new Error('이슈 정보를 가져올 수 없습니다.'); const issue = await issueResponse.json(); const opinions = issue.solution ? issue.solution.split(/─{30,}/) : []; const lines = opinions[opinionIndex].trim().split('\n'); let commentCount = -1; let deleteStart = -1; let deleteEnd = -1; for (let i = 0; i < lines.length; i++) { if (lines[i].match(/^\s*└/)) { commentCount++; if (commentCount === commentIndex) { deleteStart = i; deleteEnd = i + 1; while (deleteEnd < lines.length && lines[deleteEnd].match(/^\s*↳/)) { deleteEnd++; } break; } } } if (deleteStart >= 0) { lines.splice(deleteStart, deleteEnd - deleteStart); opinions[opinionIndex] = lines.join('\n'); } const updatedSolution = opinions.join('\n' + '─'.repeat(50) + '\n'); const response = await fetch(`/api/issues/${issueId}/management`, { method: 'PUT', headers: { 'Authorization': `Bearer ${TokenManager.getToken()}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ solution: updatedSolution }) }); if (response.ok) { await logModification(issueId, 'comment_deleted', { opinion_index: opinionIndex, comment_index: commentIndex }); alert('댓글이 삭제되었습니다.'); await initializeDashboard(); } else { const error = await response.json(); alert(`댓글 삭제 실패: ${error.detail || '알 수 없는 오류'}`); } } catch (error) { console.error('댓글 삭제 오류:', error); alert('댓글 삭제 중 오류가 발생했습니다: ' + error.message); } } // 대댓글(답글) 수정 let selectedEditReplyIssueId = null; let selectedEditReplyOpinionIndex = null; let selectedEditReplyCommentIndex = null; let selectedEditReplyIndex = null; async function editReply(issueId, opinionIndex, commentIndex, replyIndex) { selectedEditReplyIssueId = issueId; selectedEditReplyOpinionIndex = opinionIndex; selectedEditReplyCommentIndex = commentIndex; selectedEditReplyIndex = replyIndex; try { const issueResponse = await fetch(`/api/issues/${issueId}`, { headers: { 'Authorization': `Bearer ${TokenManager.getToken()}` } }); if (!issueResponse.ok) throw new Error('이슈 정보를 가져올 수 없습니다.'); const issue = await issueResponse.json(); const opinions = issue.solution ? issue.solution.split(/─{30,}/) : []; const lines = opinions[opinionIndex].trim().split('\n'); let commentCount = -1; let replyCount = -1; for (const line of lines) { if (line.match(/^\s*└/)) { commentCount++; replyCount = -1; } else if (line.match(/^\s*↳/) && commentCount === commentIndex) { replyCount++; if (replyCount === replyIndex) { const match = line.match(/↳\s*\[([^\]]+)\]\s*\(([^)]+)\):\s*(.+)/); if (match) { document.getElementById('editReplyText').value = match[3]; document.getElementById('editReplyModal').classList.remove('hidden'); document.getElementById('editReplyText').focus(); } break; } } } } catch (error) { console.error('답글 수정 준비 오류:', error); alert('답글을 불러오는 중 오류가 발생했습니다: ' + error.message); } } function closeEditReplyModal() { selectedEditReplyIssueId = null; selectedEditReplyOpinionIndex = null; selectedEditReplyCommentIndex = null; selectedEditReplyIndex = null; document.getElementById('editReplyModal').classList.add('hidden'); } async function submitEditReply(event) { event.preventDefault(); const newText = document.getElementById('editReplyText').value.trim(); if (!newText) { alert('답글 내용을 입력해주세요.'); return; } try { const issueResponse = await fetch(`/api/issues/${selectedEditReplyIssueId}`, { headers: { 'Authorization': `Bearer ${TokenManager.getToken()}` } }); if (!issueResponse.ok) throw new Error('이슈 정보를 가져올 수 없습니다.'); const issue = await issueResponse.json(); const opinions = issue.solution ? issue.solution.split(/─{30,}/) : []; const lines = opinions[selectedEditReplyOpinionIndex].trim().split('\n'); let commentCount = -1; let replyCount = -1; for (let i = 0; i < lines.length; i++) { if (lines[i].match(/^\s*└/)) { commentCount++; replyCount = -1; } else if (lines[i].match(/^\s*↳/) && commentCount === selectedEditReplyCommentIndex) { replyCount++; if (replyCount === selectedEditReplyIndex) { const match = lines[i].match(/↳\s*\[([^\]]+)\]\s*\(([^)]+)\):/); if (match) { lines[i] = ` ↳ [${match[1]}] (${match[2]}): ${newText}`; } break; } } } opinions[selectedEditReplyOpinionIndex] = lines.join('\n'); const updatedSolution = opinions.join('\n' + '─'.repeat(50) + '\n'); const response = await fetch(`/api/issues/${selectedEditReplyIssueId}/management`, { method: 'PUT', headers: { 'Authorization': `Bearer ${TokenManager.getToken()}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ solution: updatedSolution }) }); if (response.ok) { await logModification(selectedEditReplyIssueId, 'reply_edited', { opinion_index: selectedEditReplyOpinionIndex, comment_index: selectedEditReplyCommentIndex, reply_index: selectedEditReplyIndex, new_content: newText }); alert('답글이 수정되었습니다.'); closeEditReplyModal(); await initializeDashboard(); } else { const error = await response.json(); alert(`답글 수정 실패: ${error.detail || '알 수 없는 오류'}`); } } catch (error) { console.error('답글 수정 오류:', error); alert('답글 수정 중 오류가 발생했습니다: ' + error.message); } } // 대댓글(답글) 삭제 async function deleteReply(issueId, opinionIndex, commentIndex, replyIndex) { if (!confirm('이 답글을 삭제하시겠습니까?')) return; try { const issueResponse = await fetch(`/api/issues/${issueId}`, { headers: { 'Authorization': `Bearer ${TokenManager.getToken()}` } }); if (!issueResponse.ok) throw new Error('이슈 정보를 가져올 수 없습니다.'); const issue = await issueResponse.json(); const opinions = issue.solution ? issue.solution.split(/─{30,}/) : []; const lines = opinions[opinionIndex].trim().split('\n'); let commentCount = -1; let replyCount = -1; let deleteIndex = -1; for (let i = 0; i < lines.length; i++) { if (lines[i].match(/^\s*└/)) { commentCount++; replyCount = -1; } else if (lines[i].match(/^\s*↳/) && commentCount === commentIndex) { replyCount++; if (replyCount === replyIndex) { deleteIndex = i; break; } } } if (deleteIndex >= 0) { lines.splice(deleteIndex, 1); opinions[opinionIndex] = lines.join('\n'); } const updatedSolution = opinions.join('\n' + '─'.repeat(50) + '\n'); const response = await fetch(`/api/issues/${issueId}/management`, { method: 'PUT', headers: { 'Authorization': `Bearer ${TokenManager.getToken()}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ solution: updatedSolution }) }); if (response.ok) { await logModification(issueId, 'reply_deleted', { opinion_index: opinionIndex, comment_index: commentIndex, reply_index: replyIndex }); alert('답글이 삭제되었습니다.'); await initializeDashboard(); } else { const error = await response.json(); alert(`답글 삭제 실패: ${error.detail || '알 수 없는 오류'}`); } } catch (error) { console.error('답글 삭제 오류:', error); alert('답글 삭제 중 오류가 발생했습니다: ' + error.message); } } async function submitOpinion(event) { event.preventDefault(); if (!selectedOpinionIssueId) { alert('이슈 ID가 없습니다.'); return; } const opinionText = document.getElementById('opinionText').value.trim(); if (!opinionText) { alert('의견을 입력해주세요.'); return; } try { const issueResponse = await fetch(`/api/issues/${selectedOpinionIssueId}`, { headers: { 'Authorization': `Bearer ${TokenManager.getToken()}` } }); if (!issueResponse.ok) throw new Error('이슈 정보를 가져올 수 없습니다.'); const issue = await issueResponse.json(); const now = new Date(); const dateStr = now.toLocaleString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); const newOpinion = `[${currentUser.full_name || currentUser.username}] (${dateStr})\n${opinionText}`; const updatedSolution = issue.solution ? `${newOpinion}\n${'─'.repeat(50)}\n${issue.solution}` : newOpinion; const response = await fetch(`/api/issues/${selectedOpinionIssueId}/management`, { method: 'PUT', headers: { 'Authorization': `Bearer ${TokenManager.getToken()}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ solution: updatedSolution }) }); if (response.ok) { alert('의견이 추가되었습니다.'); closeOpinionModal(); await initializeDashboard(); } else { const error = await response.json(); alert(`의견 추가 실패: ${error.detail || '알 수 없는 오류'}`); } } catch (error) { console.error('의견 추가 오류:', error); alert('의견 추가 중 오류가 발생했습니다: ' + error.message); } } function handleCompletionPhotoUpload(event) { const file = event.target.files[0]; if (!file) return; if (file.size > 5 * 1024 * 1024) { alert('파일 크기는 5MB 이하여야 합니다.'); return; } if (!file.type.startsWith('image/')) { alert('이미지 파일만 업로드 가능합니다.'); return; } const reader = new FileReader(); reader.onload = function(e) { completionPhotoBase64 = e.target.result; document.getElementById('previewImage').src = e.target.result; document.getElementById('photoUploadArea').classList.add('hidden'); document.getElementById('photoPreview').classList.remove('hidden'); }; reader.readAsDataURL(file); } // 완료 신청 폼 제출 처리 document.addEventListener('DOMContentLoaded', function() { const completionForm = document.getElementById('completionRequestForm'); if (completionForm) { completionForm.addEventListener('submit', async function(e) { e.preventDefault(); if (!selectedCompletionIssueId) { alert('선택된 이슈가 없습니다.'); return; } if (!completionPhotoBase64) { alert('완료 사진을 업로드해주세요.'); return; } const comment = document.getElementById('completionComment').value.trim(); try { const response = await fetch(`/api/issues/${selectedCompletionIssueId}/completion-request`, { method: 'POST', headers: { 'Authorization': `Bearer ${TokenManager.getToken()}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ completion_photo: completionPhotoBase64, completion_comment: comment }) }); if (response.ok) { alert('완료 신청이 성공적으로 제출되었습니다.'); closeCompletionRequestModal(); refreshDashboard(); } else { const error = await response.json(); alert(`완료 신청 실패: ${error.detail || '알 수 없는 오류'}`); } } catch (error) { console.error('완료 신청 오류:', error); alert('완료 신청 중 오류가 발생했습니다.'); } }); } }); // AI 시맨틱 검색 async function aiSemanticSearch() { const query = document.getElementById('aiSearchQuery')?.value?.trim(); if (!query || typeof AiAPI === 'undefined') return; const loading = document.getElementById('aiSearchLoading'); const results = document.getElementById('aiSearchResults'); if (loading) loading.classList.remove('hidden'); if (results) { results.classList.add('hidden'); results.innerHTML = ''; } const data = await AiAPI.searchSimilar(query, 8); if (loading) loading.classList.add('hidden'); if (!data.available || !data.results || data.results.length === 0) { results.innerHTML = '

검색 결과가 없습니다

'; results.classList.remove('hidden'); return; } results.innerHTML = data.results.map(r => { const meta = r.metadata || {}; const similarity = Math.round((r.similarity || 0) * 100); const issueId = meta.issue_id || r.id.replace('issue_', ''); const doc = (r.document || '').substring(0, 100); const cat = meta.category || ''; const status = meta.review_status || ''; return `
${similarity}%
No.${issueId} ${cat ? `${cat}` : ''} ${status ? `${status}` : ''}

${doc}

`; }).join(''); results.classList.remove('hidden'); } // RAG Q&A async function aiAskQuestion() { const question = document.getElementById('aiQaQuestion')?.value?.trim(); if (!question || typeof AiAPI === 'undefined') return; const loading = document.getElementById('aiQaLoading'); const result = document.getElementById('aiQaResult'); const answer = document.getElementById('aiQaAnswer'); const sources = document.getElementById('aiQaSources'); if (loading) loading.classList.remove('hidden'); if (result) result.classList.add('hidden'); const projectId = document.getElementById('projectFilter')?.value || null; const data = await AiAPI.askQuestion(question, projectId ? parseInt(projectId) : null); if (loading) loading.classList.add('hidden'); if (!data.available) { if (answer) answer.textContent = 'AI 서비스를 사용할 수 없습니다'; if (result) result.classList.remove('hidden'); return; } if (answer) answer.textContent = data.answer || ''; if (sources && data.sources) { const refs = data.sources.slice(0, 5).map(s => `No.${s.id}(${s.similarity}%)` ).join(', '); sources.textContent = refs ? `참고: ${refs}` : ''; } if (result) result.classList.remove('hidden'); } // AI 이슈 상세 모달 async function showAiIssueModal(issueId) { const modal = document.getElementById('aiIssueModal'); const title = document.getElementById('aiIssueModalTitle'); const body = document.getElementById('aiIssueModalBody'); if (!modal || !body) return; title.textContent = `부적합 No.${issueId}`; body.innerHTML = '
로딩 중...
'; modal.classList.remove('hidden'); try { const token = typeof TokenManager !== 'undefined' ? TokenManager.getToken() : null; const headers = token ? { 'Authorization': `Bearer ${token}` } : {}; const res = await fetch(`/api/issues/${issueId}`, { headers }); if (!res.ok) throw new Error('fetch failed'); const issue = await res.json(); const categoryText = typeof getCategoryText === 'function' ? getCategoryText(issue.category || issue.final_category) : (issue.category || issue.final_category || '-'); const statusText = typeof getStatusText === 'function' ? getStatusText(issue.review_status) : (issue.review_status || '-'); const deptText = typeof getDepartmentText === 'function' ? getDepartmentText(issue.responsible_department) : (issue.responsible_department || '-'); body.innerHTML = `
${categoryText} ${statusText} ${deptText} ${issue.report_date ? `${issue.report_date}` : ''}
${issue.description ? `
설명:

${issue.description}

` : ''} ${issue.detail_notes ? `
상세:

${issue.detail_notes}

` : ''} ${issue.final_description ? `
최종 판정:

${issue.final_description}

` : ''} ${issue.solution ? `
해결방안:

${issue.solution}

` : ''} ${issue.cause_detail ? `
원인:

${issue.cause_detail}

` : ''} ${issue.management_comment ? `
관리 의견:

${issue.management_comment}

` : ''}
관리함에서 보기 →
`; } catch (e) { body.innerHTML = `

이슈를 불러올 수 없습니다

관리함에서 보기 →`; } } // 초기화 initializeDashboardApp();