Files
tk-factory-services/system3-nonconformance/web/static/js/pages/issues-inbox.js
Hyungi Ahn b3012b8320 feat: AI 서비스 및 AI 어시스턴트 전용 페이지 추가
- ai-service: Ollama 기반 AI 서비스 (분류, 시맨틱 검색, RAG Q&A, 패턴 분석)
- AI 어시스턴트 페이지: 채팅형 Q&A, 시맨틱 검색, 패턴 분석, 분류 테스트
- 권한 시스템에 ai_assistant 페이지 등록 (기본 비활성)
- 기존 페이지에 AI 기능 통합 (대시보드, 수신함, 관리함)
- docker-compose, gateway, nginx 설정 업데이트

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:38:30 +09:00

960 lines
36 KiB
JavaScript

/**
* 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 = '<option value="">전체 프로젝트</option>';
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 `
<div class="issue-card p-6 hover:bg-gray-50 border-l-4 border-blue-500"
data-issue-id="${issue.id}">
<div class="flex items-start justify-between">
<div class="flex-1">
<!-- 상단 정보 -->
<div class="flex items-center justify-between mb-3">
<div class="flex items-center space-x-3">
<span class="badge badge-new">검토 대기</span>
${project ? `<span class="text-sm font-medium text-blue-600">${project.project_name}</span>` : '<span class="text-sm text-gray-400">프로젝트 미지정</span>'}
</div>
<span class="text-xs text-gray-400">ID: ${issue.id}</span>
</div>
<!-- 제목 -->
<h3 class="text-lg font-semibold text-gray-900 mb-3 cursor-pointer hover:text-blue-600 transition-colors" onclick="viewIssueDetail(${issue.id})">${issue.final_description || issue.description}</h3>
<!-- 상세 정보 그리드 -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4 text-sm">
<div class="flex items-center text-gray-600">
<i class="fas fa-user mr-2 text-blue-500"></i>
<span class="font-medium">${issue.reporter?.username || '알 수 없음'}</span>
</div>
<div class="flex items-center text-gray-600">
<i class="fas fa-tag mr-2 text-green-500"></i>
<span>${getCategoryText(issue.category || issue.final_category)}</span>
</div>
${issue.location_info ? `<div class="flex items-center text-gray-600">
<i class="fas fa-map-marker-alt mr-2 text-red-500"></i>
<span>${issue.location_info}</span>
</div>` : ''}
<div class="flex items-center text-gray-600">
<i class="fas fa-camera mr-2 text-purple-500"></i>
<span class="${photoCount > 0 ? 'text-purple-600 font-medium' : ''}">${photoInfo}</span>
</div>
<div class="flex items-center text-gray-600">
<i class="fas fa-clock mr-2 text-orange-500"></i>
<span class="font-medium">${timeAgo}</span>
</div>
</div>
<!-- 업로드 시간 정보 -->
<div class="bg-gray-50 rounded-lg p-3 mb-4">
<div class="flex items-center justify-between text-sm">
<div class="flex items-center text-gray-600">
<i class="fas fa-calendar-alt mr-2"></i>
<span>업로드: <strong>${createdDate} ${createdTime}</strong></span>
</div>
${issue.work_hours > 0 ? `<div class="flex items-center text-gray-600">
<i class="fas fa-hourglass-half mr-2"></i>
<span>공수: <strong>${issue.work_hours}시간</strong></span>
</div>` : ''}
</div>
${issue.detail_notes ? `<div class="mt-2 text-sm text-gray-600">
<i class="fas fa-sticky-note mr-2"></i>
<span class="italic">"${issue.detail_notes}"</span>
</div>` : ''}
</div>
<!-- 사진 미리보기 -->
${photoCount > 0 ? `
<div class="photo-gallery">
${[issue.photo_path, issue.photo_path2, issue.photo_path3, issue.photo_path4, issue.photo_path5]
.filter(Boolean)
.map((path, idx) => `<img src="${path}" class="photo-preview" onclick="openPhotoModal('${path}')" alt="첨부 사진 ${idx + 1}">`)
.join('')}
</div>
` : ''}
<!-- 워크플로우 액션 버튼들 -->
<div class="flex items-center space-x-2 mt-3">
<button onclick="openDisposeModal(${issue.id})"
class="px-3 py-1 bg-red-500 text-white text-sm rounded hover:bg-red-600 transition-colors">
<i class="fas fa-trash mr-1"></i>폐기
</button>
<button onclick="openReviewModal(${issue.id})"
class="px-3 py-1 bg-blue-500 text-white text-sm rounded hover:bg-blue-600 transition-colors">
<i class="fas fa-edit mr-1"></i>검토
</button>
<button onclick="openStatusModal(${issue.id})"
class="px-3 py-1 bg-green-500 text-white text-sm rounded hover:bg-green-600 transition-colors">
<i class="fas fa-check mr-1"></i>확인
</button>
</div>
</div>
</div>
</div>
`;
}).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 = `
<div class="p-4 text-center text-red-500">
<i class="fas fa-exclamation-triangle mr-2"></i>이슈 목록을 불러올 수 없습니다.
</div>
`;
}
}
// 관리함 이슈 목록 표시
function displayManagementIssues(managementIssues) {
const container = document.getElementById('managementIssuesList');
if (managementIssues.length === 0) {
container.innerHTML = `
<div class="p-4 text-center text-gray-500">
<i class="fas fa-inbox mr-2"></i>동일 프로젝트의 관리함 이슈가 없습니다.
</div>
`;
return;
}
container.innerHTML = managementIssues.map(issue => `
<div class="p-3 border-b border-gray-100 hover:bg-gray-50 cursor-pointer"
onclick="selectDuplicateTarget(${issue.id}, this)">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="text-sm font-medium text-gray-900 mb-1">
${issue.description || issue.final_description}
</div>
<div class="flex items-center gap-2 text-xs text-gray-500">
<span class="px-2 py-1 bg-gray-100 rounded">${getCategoryText(issue.category || issue.final_category)}</span>
<span>신고자: ${issue.reporter_name}</span>
${issue.duplicate_count > 0 ? `<span class="text-orange-600">중복 ${issue.duplicate_count}건</span>` : ''}
</div>
</div>
<div class="text-xs text-gray-400">
ID: ${issue.id}
</div>
</div>
</div>
`).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 = `
<div class="space-y-2">
<div><strong>프로젝트:</strong> ${project ? project.project_name : '미지정'}</div>
<div><strong>카테고리:</strong> ${getCategoryText(issue.category || issue.final_category)}</div>
<div><strong>설명:</strong> ${issue.description || issue.final_description}</div>
<div><strong>등록자:</strong> ${issue.reporter?.username || '알 수 없음'}</div>
<div><strong>등록일:</strong> ${new Date(issue.report_date).toLocaleDateString('ko-KR')}</div>
</div>
`;
// 프로젝트 옵션 업데이트
const reviewProjectSelect = document.getElementById('reviewProjectId');
reviewProjectSelect.innerHTML = '<option value="">프로젝트 선택</option>';
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 = '<p class="text-xs text-red-500">AI 서비스를 사용할 수 없습니다</p>';
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 = `
<div class="space-y-1">
<p><strong>분류:</strong> ${categoryMap[cat] || cat} ${confidence ? `(${confidence}%)` : ''}</p>
<p><strong>부서:</strong> ${deptMap[dept] || dept}</p>
<p><strong>심각도:</strong> ${severity}</p>
${summary ? `<p><strong>요약:</strong> ${summary}</p>` : ''}
<button onclick="applyAiClassification('${cat}')"
class="mt-2 px-3 py-1 bg-purple-600 text-white text-xs rounded hover:bg-purple-700">
<i class="fas fa-check mr-1"></i>적용
</button>
</div>
`;
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();