- 모바일 전용 페이지 신규: /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>
353 lines
16 KiB
JavaScript
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);
|