근본 원인: 2026-04-01 보안 패치(f09c86e)에서 system3-api Dockerfile에
USER appuser 추가했으나, named volume(tkqc-package_uploads)이 root 소유로
남아있어 appuser(999)가 /app/uploads 에 쓰기 실패. 하지만 file_service.py
가 except → return None 으로 silent failure 처리해서 system2는 200 OK 로
인식. 결과: 4/1 이후 qc_issues.id=185~191 사진이 전부 photo_path=NULL.
조치:
1. system3 Dockerfile: entrypoint.sh 추가 → 시작 시 chown 후 gosu appuser 강등
(named volume 이 restart 후에도 root로 돌아가는 문제 영구 해결)
2. file_service.py: save_base64_image 실패 시 RuntimeError raise (silent 금지)
3. system2 workIssueController: sendToMProject 실패/예외 시 system 알림 발송
4. 관리함 (desktop + mobile): 이슈 상세/편집 모달에 원본 사진 보충 UI 추가
- 빈 슬롯(photo_path{N}=NULL)에만 자동 채움, 기존 사진 유지
- ManagementUpdateRequest 스키마에 photo/photo2~5 필드 추가
- update_issue_management 엔드포인트에 사진 저장 루프 추가
런타임 chown 으로 immediate data loss 는 이미 차단됨 (09:28 KST).
이 커밋은 재발 방지 + 데이터 복구 UI 제공.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
539 lines
27 KiB
JavaScript
539 lines
27 KiB
JavaScript
/**
|
|
* m-management.js — 관리함 모바일 페이지 로직
|
|
*/
|
|
|
|
var currentUser = null;
|
|
var issues = [];
|
|
var projects = [];
|
|
var filteredIssues = [];
|
|
var currentTab = 'in_progress';
|
|
var currentIssueId = null;
|
|
var rejectIssueId = null;
|
|
|
|
function cleanManagementComment(text) {
|
|
if (!text) return '';
|
|
return text.replace(/\[완료 반려[^\]]*\][^\n]*\n*/g, '').trim();
|
|
}
|
|
|
|
// ===== 초기화 =====
|
|
async function initialize() {
|
|
currentUser = await mCheckAuth();
|
|
if (!currentUser) return;
|
|
|
|
await loadProjects();
|
|
await loadIssues();
|
|
renderBottomNav('management');
|
|
hideLoading();
|
|
}
|
|
|
|
async function loadProjects() {
|
|
try {
|
|
var resp = await fetch(API_BASE_URL + '/projects/', {
|
|
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken() }
|
|
});
|
|
if (resp.ok) {
|
|
projects = await resp.json();
|
|
var sel = document.getElementById('projectFilter');
|
|
sel.innerHTML = '<option value="">전체 프로젝트</option>';
|
|
projects.forEach(function (p) {
|
|
sel.innerHTML += '<option value="' + p.id + '">' + escapeHtml(p.project_name) + '</option>';
|
|
});
|
|
}
|
|
} catch (e) { console.error('프로젝트 로드 실패:', e); }
|
|
}
|
|
|
|
async function loadIssues() {
|
|
try {
|
|
var resp = await fetch(API_BASE_URL + '/issues/admin/all', {
|
|
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken() }
|
|
});
|
|
if (resp.ok) {
|
|
var all = await resp.json();
|
|
var filtered = all.filter(function (i) { return i.review_status === 'in_progress' || i.review_status === 'completed'; });
|
|
|
|
// 프로젝트별 순번
|
|
filtered.sort(function (a, b) { return new Date(a.reviewed_at) - new Date(b.reviewed_at); });
|
|
var groups = {};
|
|
filtered.forEach(function (issue) {
|
|
if (!groups[issue.project_id]) groups[issue.project_id] = [];
|
|
groups[issue.project_id].push(issue);
|
|
});
|
|
Object.keys(groups).forEach(function (pid) {
|
|
groups[pid].forEach(function (issue, idx) { issue.project_sequence_no = idx + 1; });
|
|
});
|
|
|
|
issues = filtered;
|
|
filterIssues();
|
|
}
|
|
} catch (e) { console.error('이슈 로드 실패:', e); }
|
|
}
|
|
|
|
// ===== 탭 전환 =====
|
|
function switchTab(tab) {
|
|
currentTab = tab;
|
|
document.getElementById('tabInProgress').classList.toggle('active', tab === 'in_progress');
|
|
document.getElementById('tabCompleted').classList.toggle('active', tab === 'completed');
|
|
document.getElementById('additionalInfoBtn').style.display = tab === 'in_progress' ? 'flex' : 'none';
|
|
filterIssues();
|
|
}
|
|
|
|
// ===== 통계 =====
|
|
function updateStatistics() {
|
|
var pid = document.getElementById('projectFilter').value;
|
|
var pi = pid ? issues.filter(function (i) { return i.project_id == pid; }) : issues;
|
|
|
|
document.getElementById('totalCount').textContent = pi.length;
|
|
document.getElementById('inProgressCount').textContent = pi.filter(function (i) { return i.review_status === 'in_progress' && !i.completion_requested_at; }).length;
|
|
document.getElementById('pendingCompletionCount').textContent = pi.filter(function (i) { return i.review_status === 'in_progress' && i.completion_requested_at; }).length;
|
|
document.getElementById('completedCount').textContent = pi.filter(function (i) { return i.review_status === 'completed'; }).length;
|
|
}
|
|
|
|
// ===== 필터 =====
|
|
function filterIssues() {
|
|
var pid = document.getElementById('projectFilter').value;
|
|
filteredIssues = issues.filter(function (i) {
|
|
if (i.review_status !== currentTab) return false;
|
|
if (pid && i.project_id != pid) return false;
|
|
return true;
|
|
});
|
|
filteredIssues.sort(function (a, b) { return new Date(b.report_date) - new Date(a.report_date); });
|
|
renderIssues();
|
|
updateStatistics();
|
|
}
|
|
|
|
// ===== 이슈 상태 =====
|
|
function getIssueStatus(issue) {
|
|
if (issue.review_status === 'completed') return 'completed';
|
|
if (issue.completion_requested_at) return 'pending_completion';
|
|
if (issue.expected_completion_date) {
|
|
var diff = (new Date(issue.expected_completion_date) - new Date()) / 86400000;
|
|
if (diff < 0) return 'overdue';
|
|
if (diff <= 3) return 'urgent';
|
|
}
|
|
return 'in_progress';
|
|
}
|
|
|
|
function getStatusBadgeHtml(status) {
|
|
var map = {
|
|
'in_progress': '<span class="m-badge in-progress"><i class="fas fa-cog"></i> 진행 중</span>',
|
|
'urgent': '<span class="m-badge urgent"><i class="fas fa-exclamation-triangle"></i> 긴급</span>',
|
|
'overdue': '<span class="m-badge overdue"><i class="fas fa-clock"></i> 지연됨</span>',
|
|
'pending_completion': '<span class="m-badge pending-completion"><i class="fas fa-hourglass-half"></i> 완료 대기</span>',
|
|
'completed': '<span class="m-badge completed"><i class="fas fa-check-circle"></i> 완료됨</span>'
|
|
};
|
|
return map[status] || map['in_progress'];
|
|
}
|
|
|
|
// ===== 렌더링 =====
|
|
function renderIssues() {
|
|
var container = document.getElementById('issuesList');
|
|
var empty = document.getElementById('emptyState');
|
|
|
|
if (!filteredIssues.length) { container.innerHTML = ''; empty.classList.remove('hidden'); return; }
|
|
empty.classList.add('hidden');
|
|
|
|
// 날짜별 그룹
|
|
var grouped = {};
|
|
var dateObjs = {};
|
|
filteredIssues.forEach(function (issue) {
|
|
var dateToUse = currentTab === 'completed' ? (issue.actual_completion_date || issue.report_date) : issue.report_date;
|
|
var d = new Date(dateToUse);
|
|
var key = d.toLocaleDateString('ko-KR');
|
|
if (!grouped[key]) { grouped[key] = []; dateObjs[key] = d; }
|
|
grouped[key].push(issue);
|
|
});
|
|
|
|
var html = Object.keys(grouped)
|
|
.sort(function (a, b) { return dateObjs[b] - dateObjs[a]; })
|
|
.map(function (dateKey) {
|
|
var issues = grouped[dateKey];
|
|
return '<div class="m-date-group"><div class="m-date-header">' +
|
|
'<i class="fas fa-calendar-alt"></i>' +
|
|
'<span>' + dateKey + '</span>' +
|
|
'<span class="m-date-count">(' + issues.length + '건)</span>' +
|
|
'<span style="font-size:10px;padding:2px 6px;border-radius:8px;background:' +
|
|
(currentTab === 'in_progress' ? '#dbeafe;color:#1d4ed8' : '#dcfce7;color:#15803d') + '">' +
|
|
(currentTab === 'in_progress' ? '업로드일' : '완료일') + '</span>' +
|
|
'</div>' +
|
|
issues.map(function (issue) {
|
|
return currentTab === 'in_progress' ? renderInProgressCard(issue) : renderCompletedCard(issue);
|
|
}).join('') +
|
|
'</div>';
|
|
}).join('');
|
|
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function renderInProgressCard(issue) {
|
|
var project = projects.find(function (p) { return p.id === issue.project_id; });
|
|
var status = getIssueStatus(issue);
|
|
var isPending = status === 'pending_completion';
|
|
var photos = getPhotoPaths(issue);
|
|
|
|
// 관리 필드 표시
|
|
var mgmtHtml = '<div style="margin-top:8px">' +
|
|
'<div class="m-info-row"><i class="fas fa-lightbulb" style="color:#eab308"></i><span style="font-weight:600">해결방안:</span> <span>' + escapeHtml(cleanManagementComment(issue.management_comment) || '-') + '</span></div>' +
|
|
'<div class="m-info-row"><i class="fas fa-building" style="color:#3b82f6"></i><span style="font-weight:600">담당부서:</span> <span>' + getDepartmentText(issue.responsible_department) + '</span></div>' +
|
|
'<div class="m-info-row"><i class="fas fa-user" style="color:#8b5cf6"></i><span style="font-weight:600">담당자:</span> <span>' + escapeHtml(issue.responsible_person || '-') + '</span></div>' +
|
|
'<div class="m-info-row"><i class="fas fa-calendar-alt" style="color:#ef4444"></i><span style="font-weight:600">조치예상일:</span> <span>' + (issue.expected_completion_date ? formatKSTDate(issue.expected_completion_date) : '-') + '</span></div>' +
|
|
'</div>';
|
|
|
|
// 완료 대기 정보
|
|
var completionInfoHtml = '';
|
|
if (isPending) {
|
|
var cPhotos = getCompletionPhotoPaths(issue);
|
|
completionInfoHtml = '<div class="m-completion-info" style="margin-top:8px">' +
|
|
'<div style="font-size:12px;font-weight:600;color:#6d28d9;margin-bottom:4px"><i class="fas fa-check-circle" style="margin-right:4px"></i>완료 신청 정보</div>' +
|
|
(cPhotos.length ? renderPhotoThumbs(cPhotos) : '') +
|
|
'<div style="font-size:12px;color:#6b7280;margin-top:4px">' + escapeHtml(issue.completion_comment || '코멘트 없음') + '</div>' +
|
|
'<div style="font-size:11px;color:#9ca3af;margin-top:2px">신청: ' + formatKSTDateTime(issue.completion_requested_at) + '</div>' +
|
|
'</div>';
|
|
}
|
|
|
|
// 액션 버튼
|
|
var actionHtml = '';
|
|
if (isPending) {
|
|
actionHtml = '<div class="m-action-row">' +
|
|
'<button class="m-action-btn red" onclick="event.stopPropagation();openRejectSheet(' + issue.id + ')"><i class="fas fa-times"></i>반려</button>' +
|
|
'<button class="m-action-btn green" onclick="event.stopPropagation();confirmCompletion(' + issue.id + ')"><i class="fas fa-check-circle"></i>최종확인</button>' +
|
|
'</div>';
|
|
} else {
|
|
actionHtml = '<div class="m-action-row">' +
|
|
'<button class="m-action-btn blue" onclick="event.stopPropagation();openEditMgmtSheet(' + issue.id + ')"><i class="fas fa-edit"></i>편집</button>' +
|
|
'<button class="m-action-btn green" onclick="event.stopPropagation();confirmCompletion(' + issue.id + ')"><i class="fas fa-check"></i>완료처리</button>' +
|
|
'</div>';
|
|
}
|
|
|
|
return '<div class="m-card border-blue">' +
|
|
'<div class="m-card-header">' +
|
|
'<div><span class="m-card-no">No.' + (issue.project_sequence_no || '-') + '</span>' +
|
|
'<span class="m-card-project">' + escapeHtml(project ? project.project_name : '미지정') + '</span></div>' +
|
|
getStatusBadgeHtml(status) +
|
|
'</div>' +
|
|
'<div class="m-card-title">' + escapeHtml(getIssueTitle(issue)) + '</div>' +
|
|
'<div class="m-card-body">' +
|
|
'<div style="font-size:13px;color:#6b7280;line-height:1.5;margin-bottom:6px" class="text-ellipsis-3">' + escapeHtml(getIssueDetail(issue)) + '</div>' +
|
|
'<div style="display:flex;gap:8px;font-size:12px;color:#9ca3af;margin-bottom:6px">' +
|
|
'<span><i class="fas fa-tag" style="margin-right:3px"></i>' + getCategoryText(issue.category || issue.final_category) + '</span>' +
|
|
'<span><i class="fas fa-user" style="margin-right:3px"></i>' + escapeHtml(issue.reporter?.full_name || issue.reporter?.username || '-') + '</span>' +
|
|
'</div>' +
|
|
(photos.length ? renderPhotoThumbs(photos) : '') +
|
|
mgmtHtml +
|
|
completionInfoHtml +
|
|
'</div>' +
|
|
actionHtml +
|
|
'<div class="m-card-footer">' +
|
|
'<span>신고일: ' + formatKSTDate(issue.report_date) + '</span>' +
|
|
'<span>ID: ' + issue.id + '</span>' +
|
|
'</div>' +
|
|
'</div>';
|
|
}
|
|
|
|
function renderCompletedCard(issue) {
|
|
var project = projects.find(function (p) { return p.id === issue.project_id; });
|
|
var completedDate = issue.completed_at ? formatKSTDate(issue.completed_at) : '-';
|
|
|
|
return '<div class="m-card border-green" onclick="openDetailSheet(' + issue.id + ')">' +
|
|
'<div class="m-card-header">' +
|
|
'<div><span class="m-card-no" style="color:#16a34a">No.' + (issue.project_sequence_no || '-') + '</span>' +
|
|
'<span class="m-card-project">' + escapeHtml(project ? project.project_name : '미지정') + '</span></div>' +
|
|
'<span class="m-badge completed"><i class="fas fa-check-circle"></i> 완료</span>' +
|
|
'</div>' +
|
|
'<div class="m-card-title">' + escapeHtml(getIssueTitle(issue)) + '</div>' +
|
|
'<div class="m-card-footer">' +
|
|
'<span>완료일: ' + completedDate + '</span>' +
|
|
'<span style="color:#3b82f6"><i class="fas fa-chevron-right" style="font-size:10px"></i> 상세보기</span>' +
|
|
'</div>' +
|
|
'</div>';
|
|
}
|
|
|
|
// ===== 편집 시트 =====
|
|
function openEditMgmtSheet(issueId) {
|
|
currentIssueId = issueId;
|
|
var issue = issues.find(function (i) { return i.id === issueId; });
|
|
if (!issue) return;
|
|
|
|
// 프로젝트 셀렉트 채우기
|
|
var projSel = document.getElementById('editProject');
|
|
projSel.innerHTML = '<option value="">선택하세요</option>';
|
|
projects.forEach(function (p) {
|
|
projSel.innerHTML += '<option value="' + p.id + '"' + (p.id == issue.project_id ? ' selected' : '') + '>' + escapeHtml(p.project_name || p.job_no) + '</option>';
|
|
});
|
|
projSel.disabled = (issue.review_status === 'completed');
|
|
|
|
document.getElementById('editManagementComment').value = cleanManagementComment(issue.management_comment) || '';
|
|
document.getElementById('editResponsibleDept').value = issue.responsible_department || '';
|
|
document.getElementById('editResponsiblePerson').value = issue.responsible_person || '';
|
|
document.getElementById('editExpectedDate').value = issue.expected_completion_date ? issue.expected_completion_date.split('T')[0] : '';
|
|
|
|
// 원본 사진 보충 UI 초기화
|
|
var slotKeys = ['photo_path', 'photo_path2', 'photo_path3', 'photo_path4', 'photo_path5'];
|
|
var existingPhotos = slotKeys.map(function (k) { return issue[k]; }).filter(function (p) { return p; });
|
|
var emptyCount = 5 - existingPhotos.length;
|
|
|
|
var existingEl = document.getElementById('editExistingPhotos');
|
|
existingEl.innerHTML = existingPhotos.length
|
|
? existingPhotos.map(function (p) {
|
|
return '<img src="' + escapeHtml(p) + '" style="width:52px;height:52px;object-fit:cover;border-radius:6px;border:1px solid #d1d5db" alt="기존 사진">';
|
|
}).join('')
|
|
: '<span style="font-size:12px;color:#9ca3af">기존 사진 없음</span>';
|
|
|
|
var slotInfoEl = document.getElementById('editPhotoSlotInfo');
|
|
slotInfoEl.textContent = emptyCount > 0 ? '(남은 슬롯: ' + emptyCount + '장)' : '(가득 참)';
|
|
|
|
var photoInput = document.getElementById('editPhotoInput');
|
|
photoInput.value = '';
|
|
photoInput.disabled = (emptyCount === 0);
|
|
document.getElementById('editPhotoPreview').innerHTML = '';
|
|
|
|
openSheet('editMgmt');
|
|
}
|
|
|
|
// 파일 input change 시 미리보기 렌더
|
|
function previewEditPhotos(event) {
|
|
var files = event.target.files;
|
|
var preview = document.getElementById('editPhotoPreview');
|
|
preview.innerHTML = '';
|
|
if (!files || !files.length) return;
|
|
|
|
Array.prototype.forEach.call(files, function (file) {
|
|
var reader = new FileReader();
|
|
reader.onload = function (e) {
|
|
var img = document.createElement('img');
|
|
img.src = e.target.result;
|
|
img.style.cssText = 'width:52px;height:52px;object-fit:cover;border-radius:6px;border:2px solid #10b981';
|
|
img.alt = '추가 예정';
|
|
preview.appendChild(img);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}
|
|
|
|
function fileToBase64(file) {
|
|
return new Promise(function (resolve, reject) {
|
|
var reader = new FileReader();
|
|
reader.onload = function (e) { resolve(e.target.result); };
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}
|
|
|
|
async function saveManagementEdit() {
|
|
if (!currentIssueId) return;
|
|
try {
|
|
var updates = {
|
|
management_comment: document.getElementById('editManagementComment').value.trim() || null,
|
|
responsible_department: document.getElementById('editResponsibleDept').value || null,
|
|
responsible_person: document.getElementById('editResponsiblePerson').value.trim() || null,
|
|
expected_completion_date: document.getElementById('editExpectedDate').value ? document.getElementById('editExpectedDate').value + 'T00:00:00' : null
|
|
};
|
|
|
|
// 원본 사진 보충 — 빈 슬롯에만 채움
|
|
var photoInput = document.getElementById('editPhotoInput');
|
|
if (photoInput && photoInput.files && photoInput.files.length > 0) {
|
|
var currentIssue = issues.find(function (i) { return i.id === currentIssueId; });
|
|
if (currentIssue) {
|
|
var slotKeys = ['photo_path', 'photo_path2', 'photo_path3', 'photo_path4', 'photo_path5'];
|
|
var emptySlots = [];
|
|
slotKeys.forEach(function (k, idx) { if (!currentIssue[k]) emptySlots.push(idx + 1); });
|
|
|
|
if (emptySlots.length === 0) {
|
|
showToast('원본 사진 슬롯이 가득 찼습니다', 'warning');
|
|
return;
|
|
}
|
|
|
|
var filesToUpload = Array.prototype.slice.call(photoInput.files, 0, emptySlots.length);
|
|
if (photoInput.files.length > emptySlots.length) {
|
|
showToast('빈 슬롯 ' + emptySlots.length + '장 중 처음 ' + emptySlots.length + '장만 업로드됩니다', 'info');
|
|
}
|
|
|
|
for (var i = 0; i < filesToUpload.length; i++) {
|
|
var base64 = await fileToBase64(filesToUpload[i]);
|
|
var slotNum = emptySlots[i];
|
|
var fieldName = slotNum === 1 ? 'photo' : 'photo' + slotNum;
|
|
updates[fieldName] = base64;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 프로젝트 변경 확인
|
|
var newProjectId = parseInt(document.getElementById('editProject').value);
|
|
var issue = issues.find(function (i) { return i.id === currentIssueId; });
|
|
if (newProjectId && issue && newProjectId !== issue.project_id) {
|
|
// 프로젝트 변경은 /issues/{id} PUT으로 별도 호출
|
|
var projResp = await fetch(API_BASE_URL + '/issues/' + currentIssueId, {
|
|
method: 'PUT',
|
|
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken(), 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ project_id: newProjectId })
|
|
});
|
|
if (!projResp.ok) {
|
|
var projErr = await projResp.json();
|
|
throw new Error(projErr.detail || '프로젝트 변경 실패');
|
|
}
|
|
}
|
|
|
|
var resp = await fetch(API_BASE_URL + '/issues/' + currentIssueId + '/management', {
|
|
method: 'PUT',
|
|
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken(), 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(updates)
|
|
});
|
|
|
|
if (resp.ok) {
|
|
showToast('저장되었습니다.', 'success');
|
|
closeSheet('editMgmt');
|
|
await loadIssues();
|
|
} else {
|
|
var err = await resp.json();
|
|
throw new Error(err.detail || '저장 실패');
|
|
}
|
|
} catch (e) { showToast('오류: ' + e.message, 'error'); }
|
|
}
|
|
|
|
// ===== 완료 처리 =====
|
|
async function confirmCompletion(issueId) {
|
|
if (!confirm('완료 처리하시겠습니까?')) return;
|
|
try {
|
|
var resp = await fetch(API_BASE_URL + '/inbox/' + issueId + '/status', {
|
|
method: 'POST',
|
|
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken(), 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ review_status: 'completed' })
|
|
});
|
|
|
|
if (resp.ok) {
|
|
showToast('완료 처리되었습니다.', 'success');
|
|
await loadIssues();
|
|
} else {
|
|
var err = await resp.json();
|
|
throw new Error(err.detail || '완료 처리 실패');
|
|
}
|
|
} catch (e) { showToast('오류: ' + e.message, 'error'); }
|
|
}
|
|
|
|
// ===== 반려 =====
|
|
function openRejectSheet(issueId) {
|
|
rejectIssueId = issueId;
|
|
document.getElementById('rejectReason').value = '';
|
|
openSheet('reject');
|
|
}
|
|
|
|
async function submitReject() {
|
|
if (!rejectIssueId) return;
|
|
var reason = document.getElementById('rejectReason').value.trim();
|
|
if (!reason) { showToast('반려 사유를 입력해주세요.', 'warning'); return; }
|
|
|
|
try {
|
|
var resp = await fetch(API_BASE_URL + '/issues/' + rejectIssueId + '/reject-completion', {
|
|
method: 'POST',
|
|
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken(), 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ rejection_reason: reason })
|
|
});
|
|
|
|
if (resp.ok) {
|
|
showToast('반려 처리되었습니다.', 'success');
|
|
closeSheet('reject');
|
|
await loadIssues();
|
|
} else {
|
|
var err = await resp.json();
|
|
throw new Error(err.detail || '반려 실패');
|
|
}
|
|
} catch (e) { showToast('오류: ' + e.message, 'error'); }
|
|
}
|
|
|
|
// ===== 추가 정보 =====
|
|
function openAdditionalInfoSheet() {
|
|
var inProgressIssues = issues.filter(function (i) { return i.review_status === 'in_progress'; });
|
|
var sel = document.getElementById('additionalIssueSelect');
|
|
sel.innerHTML = '<option value="">이슈 선택</option>';
|
|
inProgressIssues.forEach(function (i) {
|
|
var p = projects.find(function (pr) { return pr.id === i.project_id; });
|
|
sel.innerHTML += '<option value="' + i.id + '">No.' + (i.project_sequence_no || '-') + ' ' + escapeHtml(getIssueTitle(i)) + '</option>';
|
|
});
|
|
document.getElementById('additionalCauseDept').value = '';
|
|
document.getElementById('additionalCausePerson').value = '';
|
|
document.getElementById('additionalCauseDetail').value = '';
|
|
openSheet('additional');
|
|
}
|
|
|
|
function loadAdditionalInfo() {
|
|
var id = parseInt(document.getElementById('additionalIssueSelect').value);
|
|
if (!id) return;
|
|
var issue = issues.find(function (i) { return i.id === id; });
|
|
if (!issue) return;
|
|
document.getElementById('additionalCauseDept').value = issue.cause_department || '';
|
|
document.getElementById('additionalCausePerson').value = issue.responsible_person_detail || '';
|
|
document.getElementById('additionalCauseDetail').value = issue.cause_detail || '';
|
|
}
|
|
|
|
async function saveAdditionalInfo() {
|
|
var id = parseInt(document.getElementById('additionalIssueSelect').value);
|
|
if (!id) { showToast('이슈를 선택해주세요.', 'warning'); return; }
|
|
|
|
try {
|
|
var resp = await fetch(API_BASE_URL + '/issues/' + id + '/management', {
|
|
method: 'PUT',
|
|
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken(), 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
cause_department: document.getElementById('additionalCauseDept').value || null,
|
|
responsible_person_detail: document.getElementById('additionalCausePerson').value.trim() || null,
|
|
cause_detail: document.getElementById('additionalCauseDetail').value.trim() || null
|
|
})
|
|
});
|
|
|
|
if (resp.ok) {
|
|
showToast('추가 정보가 저장되었습니다.', 'success');
|
|
closeSheet('additional');
|
|
await loadIssues();
|
|
} else {
|
|
var err = await resp.json();
|
|
throw new Error(err.detail || '저장 실패');
|
|
}
|
|
} catch (e) { showToast('오류: ' + e.message, 'error'); }
|
|
}
|
|
|
|
// ===== 완료됨 상세보기 =====
|
|
function openDetailSheet(issueId) {
|
|
var issue = issues.find(function (i) { return i.id === issueId; });
|
|
if (!issue) return;
|
|
var project = projects.find(function (p) { return p.id === issue.project_id; });
|
|
var photos = getPhotoPaths(issue);
|
|
var cPhotos = getCompletionPhotoPaths(issue);
|
|
|
|
document.getElementById('detailSheetTitle').innerHTML =
|
|
'<span style="font-weight:800;color:#16a34a">No.' + (issue.project_sequence_no || '-') + '</span> 상세 정보';
|
|
|
|
document.getElementById('detailSheetBody').innerHTML =
|
|
// 기본 정보
|
|
'<div style="margin-bottom:16px">' +
|
|
'<div style="font-size:14px;font-weight:700;color:#111827;margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid #e5e7eb"><i class="fas fa-info-circle" style="color:#3b82f6;margin-right:6px"></i>기본 정보</div>' +
|
|
'<div class="m-info-row"><span style="font-weight:600">프로젝트:</span> <span>' + escapeHtml(project ? project.project_name : '-') + '</span></div>' +
|
|
'<div class="m-info-row"><span style="font-weight:600">부적합명:</span> <span>' + escapeHtml(getIssueTitle(issue)) + '</span></div>' +
|
|
'<div style="font-size:13px;color:#6b7280;line-height:1.5;margin:6px 0;white-space:pre-wrap">' + escapeHtml(getIssueDetail(issue)) + '</div>' +
|
|
'<div class="m-info-row"><span style="font-weight:600">분류:</span> <span>' + getCategoryText(issue.final_category || issue.category) + '</span></div>' +
|
|
'<div class="m-info-row"><span style="font-weight:600">확인자:</span> <span>' + escapeHtml(getReporterNames(issue)) + '</span></div>' +
|
|
(photos.length ? '<div style="margin-top:6px"><div style="font-size:12px;font-weight:600;color:#6b7280;margin-bottom:4px">업로드 사진</div>' + renderPhotoThumbs(photos) + '</div>' : '') +
|
|
'</div>' +
|
|
|
|
// 관리 정보
|
|
'<div style="margin-bottom:16px">' +
|
|
'<div style="font-size:14px;font-weight:700;color:#111827;margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid #e5e7eb"><i class="fas fa-cogs" style="color:#3b82f6;margin-right:6px"></i>관리 정보</div>' +
|
|
'<div class="m-info-row"><span style="font-weight:600">해결방안:</span> <span>' + escapeHtml(cleanManagementComment(issue.management_comment) || '-') + '</span></div>' +
|
|
'<div class="m-info-row"><span style="font-weight:600">담당부서:</span> <span>' + getDepartmentText(issue.responsible_department) + '</span></div>' +
|
|
'<div class="m-info-row"><span style="font-weight:600">담당자:</span> <span>' + escapeHtml(issue.responsible_person || '-') + '</span></div>' +
|
|
'<div class="m-info-row"><span style="font-weight:600">원인부서:</span> <span>' + getDepartmentText(issue.cause_department) + '</span></div>' +
|
|
'</div>' +
|
|
|
|
// 완료 정보
|
|
'<div>' +
|
|
'<div style="font-size:14px;font-weight:700;color:#111827;margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid #e5e7eb"><i class="fas fa-check-circle" style="color:#22c55e;margin-right:6px"></i>완료 정보</div>' +
|
|
(cPhotos.length ? '<div style="margin-bottom:6px"><div style="font-size:12px;font-weight:600;color:#6b7280;margin-bottom:4px">완료 사진</div>' + renderPhotoThumbs(cPhotos) + '</div>' : '<div class="m-info-row"><span>완료 사진 없음</span></div>') +
|
|
'<div class="m-info-row"><span style="font-weight:600">완료 코멘트:</span> <span>' + escapeHtml(issue.completion_comment || '-') + '</span></div>' +
|
|
(issue.completion_requested_at ? '<div class="m-info-row"><span style="font-weight:600">완료 신청일:</span> <span>' + formatKSTDateTime(issue.completion_requested_at) + '</span></div>' : '') +
|
|
(issue.completed_at ? '<div class="m-info-row"><span style="font-weight:600">최종 완료일:</span> <span>' + formatKSTDateTime(issue.completed_at) + '</span></div>' : '') +
|
|
'</div>';
|
|
|
|
openSheet('detail');
|
|
}
|
|
|
|
// ===== 시작 =====
|
|
document.addEventListener('DOMContentLoaded', initialize);
|