Files
tk-factory-services/system3-nonconformance/web/static/js/m/m-inbox.js
Hyungi Ahn 9b81a52283 feat(system3): TKQC 모바일 전용 페이지 구현 및 데스크탑 관리함 반응형 개선
- 모바일 전용 페이지 신규: /m/dashboard, /m/inbox, /m/management
- 공통 모바일 CSS/JS: m-common.css, m-common.js (바텀시트, 바텀네비, 터치 최적화)
- nginx.conf에 /m/ location 블록 추가
- 데스크탑 HTML에 모바일 뷰포트 리다이렉트 추가 (<=768px)
- 데스크탑 관리함 카드 헤더 반응형 레이아웃 (flex-wrap, 1280px 브레이크포인트)
- collapse-content overflow:hidden → overflow:visible 수정 (내용 잘림 해결)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 13:34:52 +09:00

353 lines
16 KiB
JavaScript

/**
* m-inbox.js — 수신함 모바일 페이지 로직
*/
var currentUser = null;
var issues = [];
var projects = [];
var filteredIssues = [];
var currentIssueId = null;
var statusPhotoBase64 = null;
// ===== 초기화 =====
async function initialize() {
currentUser = await mCheckAuth();
if (!currentUser) return;
await loadProjects();
await loadIssues();
renderBottomNav('inbox');
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 pid = document.getElementById('projectFilter').value;
var url = API_BASE_URL + '/inbox/' + (pid ? '?project_id=' + pid : '');
var resp = await fetch(url, {
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken() }
});
if (resp.ok) {
issues = await resp.json();
filterIssues();
await loadStatistics();
}
} catch (e) { console.error('수신함 로드 실패:', e); }
}
async function loadStatistics() {
try {
var todayStart = getKSTToday();
var todayNewCount = issues.filter(function (i) {
var d = getKSTDate(new Date(i.report_date));
return new Date(d.getFullYear(), d.getMonth(), d.getDate()) >= todayStart;
}).length;
var todayProcessedCount = 0;
try {
var resp = await fetch(API_BASE_URL + '/inbox/statistics', {
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken() }
});
if (resp.ok) { var s = await resp.json(); todayProcessedCount = s.today_processed || 0; }
} catch (e) {}
var unresolvedCount = issues.filter(function (i) {
var d = getKSTDate(new Date(i.report_date));
return new Date(d.getFullYear(), d.getMonth(), d.getDate()) < todayStart;
}).length;
document.getElementById('todayNewCount').textContent = todayNewCount;
document.getElementById('todayProcessedCount').textContent = todayProcessedCount;
document.getElementById('unresolvedCount').textContent = unresolvedCount;
} catch (e) { console.error('통계 로드 오류:', e); }
}
function filterIssues() {
var pid = document.getElementById('projectFilter').value;
filteredIssues = pid ? issues.filter(function (i) { return i.project_id == pid; }) : issues.slice();
filteredIssues.sort(function (a, b) { return new Date(b.report_date) - new Date(a.report_date); });
renderIssues();
}
// ===== 렌더링 =====
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');
container.innerHTML = filteredIssues.map(function (issue) {
var project = projects.find(function (p) { return p.id === issue.project_id; });
var photos = getPhotoPaths(issue);
var photoCount = photos.length;
return '<div class="m-card border-blue">' +
'<div class="m-card-header">' +
'<div><span class="m-badge review"><i class="fas fa-clock"></i> 검토 대기</span>' +
(project ? '<span class="m-card-project">' + escapeHtml(project.project_name) + '</span>' : '') +
'</div>' +
'<span style="font-size:11px;color:#9ca3af">ID: ' + issue.id + '</span>' +
'</div>' +
'<div class="m-card-title text-ellipsis-3">' + escapeHtml(issue.final_description || issue.description) + '</div>' +
'<div class="m-card-body">' +
'<div style="display:flex;gap:12px;font-size:12px;color:#6b7280;margin-bottom:8px;flex-wrap:wrap">' +
'<span><i class="fas fa-user" style="color:#3b82f6;margin-right:3px"></i>' + escapeHtml(issue.reporter?.username || '알 수 없음') + '</span>' +
'<span><i class="fas fa-tag" style="color:#22c55e;margin-right:3px"></i>' + getCategoryText(issue.category || issue.final_category) + '</span>' +
'<span><i class="fas fa-camera" style="color:#8b5cf6;margin-right:3px"></i>' + (photoCount > 0 ? photoCount + '장' : '없음') + '</span>' +
'<span><i class="fas fa-clock" style="color:#f59e0b;margin-right:3px"></i>' + getTimeAgo(issue.report_date) + '</span>' +
'</div>' +
(photos.length ? renderPhotoThumbs(photos) : '') +
(issue.detail_notes ? '<div style="font-size:12px;color:#6b7280;margin-top:6px;font-style:italic">"' + escapeHtml(issue.detail_notes) + '"</div>' : '') +
'</div>' +
'<div class="m-action-row">' +
'<button class="m-action-btn red" onclick="openDisposeSheet(' + issue.id + ')"><i class="fas fa-trash"></i>폐기</button>' +
'<button class="m-action-btn blue" onclick="openReviewSheet(' + issue.id + ')"><i class="fas fa-edit"></i>검토</button>' +
'<button class="m-action-btn green" onclick="openStatusSheet(' + issue.id + ')"><i class="fas fa-check"></i>확인</button>' +
'</div>' +
'</div>';
}).join('');
}
// ===== 폐기 =====
function openDisposeSheet(issueId) {
currentIssueId = issueId;
document.getElementById('disposalReason').value = 'duplicate';
document.getElementById('customReason').value = '';
document.getElementById('customReasonDiv').classList.add('hidden');
document.getElementById('selectedDuplicateId').value = '';
toggleDisposalFields();
openSheet('dispose');
loadManagementIssues();
}
function toggleDisposalFields() {
var reason = document.getElementById('disposalReason').value;
document.getElementById('customReasonDiv').classList.toggle('hidden', reason !== 'custom');
document.getElementById('duplicateDiv').classList.toggle('hidden', reason !== 'duplicate');
if (reason === 'duplicate') loadManagementIssues();
}
async function loadManagementIssues() {
var issue = issues.find(function (i) { return i.id === currentIssueId; });
var pid = issue ? issue.project_id : null;
try {
var resp = await fetch(API_BASE_URL + '/inbox/management-issues' + (pid ? '?project_id=' + pid : ''), {
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken() }
});
if (!resp.ok) throw new Error('로드 실패');
var list = await resp.json();
var container = document.getElementById('managementIssuesList');
if (!list.length) {
container.innerHTML = '<div style="padding:16px;text-align:center;color:#9ca3af;font-size:13px">동일 프로젝트의 관리함 이슈가 없습니다.</div>';
return;
}
container.innerHTML = list.map(function (mi) {
return '<div style="padding:10px 12px;border-bottom:1px solid #f3f4f6;font-size:13px" onclick="selectDuplicate(' + mi.id + ',this)">' +
'<div style="font-weight:500;color:#111827;margin-bottom:2px">' + escapeHtml(mi.description || mi.final_description) + '</div>' +
'<div style="display:flex;gap:6px;color:#9ca3af;font-size:11px">' +
'<span>' + getCategoryText(mi.category || mi.final_category) + '</span>' +
'<span>신고자: ' + escapeHtml(mi.reporter_name) + '</span>' +
'<span>ID: ' + mi.id + '</span>' +
'</div></div>';
}).join('');
} catch (e) {
document.getElementById('managementIssuesList').innerHTML = '<div style="padding:16px;text-align:center;color:#ef4444;font-size:13px">목록 로드 실패</div>';
}
}
function selectDuplicate(id, el) {
var items = document.getElementById('managementIssuesList').children;
for (var i = 0; i < items.length; i++) items[i].style.background = '';
el.style.background = '#eff6ff';
document.getElementById('selectedDuplicateId').value = id;
}
async function confirmDispose() {
if (!currentIssueId) return;
var reason = document.getElementById('disposalReason').value;
var customReason = document.getElementById('customReason').value;
var duplicateId = document.getElementById('selectedDuplicateId').value;
if (reason === 'custom' && !customReason.trim()) { showToast('폐기 사유를 입력해주세요.', 'warning'); return; }
if (reason === 'duplicate' && !duplicateId) { showToast('중복 대상을 선택해주세요.', 'warning'); return; }
try {
var body = { disposal_reason: reason, custom_disposal_reason: reason === 'custom' ? customReason : null };
if (reason === 'duplicate' && duplicateId) body.duplicate_of_issue_id = parseInt(duplicateId);
var resp = await fetch(API_BASE_URL + '/inbox/' + currentIssueId + '/dispose', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken(), 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (resp.ok) {
showToast('폐기 처리되었습니다.', 'success');
closeSheet('dispose');
await loadIssues();
} else {
var err = await resp.json();
throw new Error(err.detail || '폐기 실패');
}
} catch (e) { showToast('오류: ' + e.message, 'error'); }
}
// ===== 검토 =====
function openReviewSheet(issueId) {
currentIssueId = 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; });
// 원본 정보
document.getElementById('originalInfo').innerHTML =
'<div style="margin-bottom:4px"><strong>프로젝트:</strong> ' + (project ? escapeHtml(project.project_name) : '미지정') + '</div>' +
'<div style="margin-bottom:4px"><strong>신고자:</strong> ' + escapeHtml(issue.reporter?.username || '알 수 없음') + '</div>' +
'<div><strong>등록일:</strong> ' + formatKSTDate(issue.report_date) + '</div>';
// 프로젝트 select
var sel = document.getElementById('reviewProjectId');
sel.innerHTML = '<option value="">프로젝트 선택</option>';
projects.forEach(function (p) {
sel.innerHTML += '<option value="' + p.id + '"' + (p.id === issue.project_id ? ' selected' : '') + '>' + escapeHtml(p.project_name) + '</option>';
});
document.getElementById('reviewCategory').value = issue.category || issue.final_category || 'etc';
var desc = issue.description || issue.final_description || '';
var lines = desc.split('\n');
document.getElementById('reviewTitle').value = lines[0] || '';
document.getElementById('reviewDescription').value = lines.slice(1).join('\n') || desc;
openSheet('review');
}
async function saveReview() {
if (!currentIssueId) return;
var projectId = document.getElementById('reviewProjectId').value;
var category = document.getElementById('reviewCategory').value;
var title = document.getElementById('reviewTitle').value.trim();
var description = document.getElementById('reviewDescription').value.trim();
if (!title) { showToast('부적합명을 입력해주세요.', 'warning'); return; }
var combined = title + (description ? '\n' + description : '');
try {
var resp = await fetch(API_BASE_URL + '/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: combined
})
});
if (resp.ok) {
showToast('검토가 완료되었습니다.', 'success');
closeSheet('review');
await loadIssues();
} else {
var err = await resp.json();
throw new Error(err.detail || '검토 실패');
}
} catch (e) { showToast('오류: ' + e.message, 'error'); }
}
// ===== 확인 (상태 결정) =====
function openStatusSheet(issueId) {
currentIssueId = issueId;
document.querySelectorAll('input[name="finalStatus"]').forEach(function (r) { r.checked = false; });
document.querySelectorAll('.m-radio-item').forEach(function (el) { el.classList.remove('selected'); });
document.getElementById('completionSection').classList.add('hidden');
statusPhotoBase64 = null;
document.getElementById('statusPhotoInput').value = '';
document.getElementById('statusPhotoPreview').classList.add('hidden');
document.getElementById('solutionInput').value = '';
document.getElementById('responsibleDepartmentInput').value = '';
document.getElementById('responsiblePersonInput').value = '';
openSheet('status');
}
function selectStatus(value) {
document.querySelectorAll('.m-radio-item').forEach(function (el) { el.classList.remove('selected'); });
var radio = document.querySelector('input[name="finalStatus"][value="' + value + '"]');
if (radio) { radio.checked = true; radio.closest('.m-radio-item').classList.add('selected'); }
document.getElementById('completionSection').classList.toggle('hidden', value !== 'completed');
}
function handleStatusPhoto(event) {
var file = event.target.files[0];
if (!file) return;
if (file.size > 5 * 1024 * 1024) { showToast('5MB 이하 파일만 가능합니다.', 'warning'); event.target.value = ''; return; }
var reader = new FileReader();
reader.onload = function (e) {
statusPhotoBase64 = e.target.result.split(',')[1];
var preview = document.getElementById('statusPhotoPreview');
preview.src = e.target.result;
preview.classList.remove('hidden');
};
reader.readAsDataURL(file);
}
async function confirmStatus() {
if (!currentIssueId) return;
var selected = document.querySelector('input[name="finalStatus"]:checked');
if (!selected) { showToast('상태를 선택해주세요.', 'warning'); return; }
var reviewStatus = selected.value;
var body = { review_status: reviewStatus };
if (reviewStatus === 'completed') {
var solution = document.getElementById('solutionInput').value.trim();
var dept = document.getElementById('responsibleDepartmentInput').value;
var person = document.getElementById('responsiblePersonInput').value.trim();
if (solution) body.solution = solution;
if (dept) body.responsible_department = dept;
if (person) body.responsible_person = person;
if (statusPhotoBase64) body.completion_photo = statusPhotoBase64;
}
try {
var resp = await fetch(API_BASE_URL + '/inbox/' + currentIssueId + '/status', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken(), 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (resp.ok) {
showToast('상태가 변경되었습니다.', 'success');
closeSheet('status');
await loadIssues();
} else {
var err = await resp.json();
throw new Error(err.detail || '상태 변경 실패');
}
} catch (e) { showToast('오류: ' + e.message, 'error'); }
}
// ===== 시작 =====
document.addEventListener('DOMContentLoaded', initialize);