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>
This commit is contained in:
195
system3-nonconformance/web/static/js/m/m-common.js
Normal file
195
system3-nonconformance/web/static/js/m/m-common.js
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* m-common.js — TKQC 모바일 공통 JS
|
||||
* 바텀 네비게이션, 바텀시트 엔진, 인증, 뷰포트 가드, 토스트
|
||||
*/
|
||||
|
||||
/* ===== Viewport Guard: 데스크탑이면 리다이렉트 ===== */
|
||||
(function () {
|
||||
if (window.innerWidth > 768) {
|
||||
var page = location.pathname.replace('/m/', '').replace('.html', '');
|
||||
var map = { dashboard: '/issues-dashboard.html', inbox: '/issues-inbox.html', management: '/issues-management.html' };
|
||||
window.location.replace(map[page] || '/issues-dashboard.html');
|
||||
}
|
||||
})();
|
||||
|
||||
/* ===== KST Date Utilities ===== */
|
||||
function getKSTDate(date) {
|
||||
var d = new Date(date);
|
||||
return new Date(d.getTime() + 9 * 60 * 60 * 1000);
|
||||
}
|
||||
function formatKSTDate(date) {
|
||||
return new Date(date).toLocaleDateString('ko-KR', { timeZone: 'Asia/Seoul' });
|
||||
}
|
||||
function formatKSTTime(date) {
|
||||
return new Date(date).toLocaleTimeString('ko-KR', { timeZone: 'Asia/Seoul', hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
function formatKSTDateTime(date) {
|
||||
return new Date(date).toLocaleString('ko-KR', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
function getKSTToday() {
|
||||
var now = new Date();
|
||||
var kst = getKSTDate(now);
|
||||
return new Date(kst.getFullYear(), kst.getMonth(), kst.getDate());
|
||||
}
|
||||
function getTimeAgo(date) {
|
||||
var now = getKSTDate(new Date());
|
||||
var d = getKSTDate(date);
|
||||
var diff = now - d;
|
||||
var mins = Math.floor(diff / 60000);
|
||||
var hours = Math.floor(diff / 3600000);
|
||||
var days = Math.floor(diff / 86400000);
|
||||
if (mins < 1) return '방금 전';
|
||||
if (mins < 60) return mins + '분 전';
|
||||
if (hours < 24) return hours + '시간 전';
|
||||
if (days < 7) return days + '일 전';
|
||||
return formatKSTDate(date);
|
||||
}
|
||||
|
||||
/* ===== Bottom Navigation ===== */
|
||||
function renderBottomNav(activePage) {
|
||||
var nav = document.createElement('nav');
|
||||
nav.className = 'm-bottom-nav';
|
||||
var items = [
|
||||
{ icon: 'fa-chart-line', label: '현황판', href: '/m/dashboard.html', page: 'dashboard' },
|
||||
{ icon: 'fa-inbox', label: '수신함', href: '/m/inbox.html', page: 'inbox' },
|
||||
{ icon: 'fa-tasks', label: '관리함', href: '/m/management.html', page: 'management' },
|
||||
{ icon: 'fa-bullhorn', label: '신고', href: 'https://tkreport.technicalkorea.net', page: 'report', external: true, highlight: true }
|
||||
];
|
||||
items.forEach(function (item) {
|
||||
var a = document.createElement('a');
|
||||
a.href = item.href;
|
||||
a.className = 'm-nav-item';
|
||||
if (item.page === activePage) a.classList.add('active');
|
||||
if (item.highlight) a.classList.add('highlight');
|
||||
if (item.external) { a.target = '_blank'; a.rel = 'noopener'; }
|
||||
a.innerHTML = '<i class="fas ' + item.icon + '"></i><span>' + item.label + '</span>';
|
||||
nav.appendChild(a);
|
||||
});
|
||||
document.body.appendChild(nav);
|
||||
}
|
||||
|
||||
/* ===== Bottom Sheet Engine ===== */
|
||||
var _activeSheets = [];
|
||||
|
||||
function openSheet(sheetId) {
|
||||
var overlay = document.getElementById(sheetId + 'Overlay');
|
||||
var sheet = document.getElementById(sheetId + 'Sheet');
|
||||
if (!overlay || !sheet) return;
|
||||
overlay.classList.add('open');
|
||||
sheet.classList.add('open');
|
||||
document.body.style.overflow = 'hidden';
|
||||
_activeSheets.push(sheetId);
|
||||
}
|
||||
|
||||
function closeSheet(sheetId) {
|
||||
var overlay = document.getElementById(sheetId + 'Overlay');
|
||||
var sheet = document.getElementById(sheetId + 'Sheet');
|
||||
if (!overlay || !sheet) return;
|
||||
overlay.classList.remove('open');
|
||||
sheet.classList.remove('open');
|
||||
_activeSheets = _activeSheets.filter(function (id) { return id !== sheetId; });
|
||||
if (_activeSheets.length === 0) document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
function closeAllSheets() {
|
||||
_activeSheets.slice().forEach(function (id) { closeSheet(id); });
|
||||
}
|
||||
|
||||
// ESC key closes topmost sheet
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && _activeSheets.length) {
|
||||
closeSheet(_activeSheets[_activeSheets.length - 1]);
|
||||
}
|
||||
});
|
||||
|
||||
/* ===== Toast ===== */
|
||||
var _toastTimer = null;
|
||||
function showToast(message, type, duration) {
|
||||
type = type || 'info';
|
||||
duration = duration || 3000;
|
||||
var existing = document.querySelector('.m-toast');
|
||||
if (existing) existing.remove();
|
||||
clearTimeout(_toastTimer);
|
||||
|
||||
var toast = document.createElement('div');
|
||||
toast.className = 'm-toast';
|
||||
if (type !== 'info') toast.classList.add(type);
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
requestAnimationFrame(function () { toast.classList.add('show'); });
|
||||
_toastTimer = setTimeout(function () {
|
||||
toast.classList.remove('show');
|
||||
setTimeout(function () { toast.remove(); }, 300);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
/* ===== Photo Modal ===== */
|
||||
function openPhotoModal(src) {
|
||||
var modal = document.getElementById('photoModal');
|
||||
if (!modal) {
|
||||
modal = document.createElement('div');
|
||||
modal.id = 'photoModal';
|
||||
modal.className = 'm-photo-modal';
|
||||
modal.innerHTML = '<button class="m-photo-modal-close" onclick="closePhotoModal()"><i class="fas fa-times"></i></button><img>';
|
||||
modal.addEventListener('click', function (e) { if (e.target === modal) closePhotoModal(); });
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
modal.querySelector('img').src = src;
|
||||
modal.classList.add('open');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closePhotoModal() {
|
||||
var modal = document.getElementById('photoModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('open');
|
||||
if (!_activeSheets.length) document.body.style.overflow = '';
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Auth Helper ===== */
|
||||
async function mCheckAuth() {
|
||||
var token = TokenManager.getToken();
|
||||
if (!token) {
|
||||
window.location.href = _getLoginUrl();
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
var user = await AuthAPI.getCurrentUser();
|
||||
return user;
|
||||
} catch (e) {
|
||||
TokenManager.removeToken();
|
||||
TokenManager.removeUser();
|
||||
window.location.href = _getLoginUrl();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Loading Overlay ===== */
|
||||
function hideLoading() {
|
||||
var el = document.getElementById('loadingOverlay');
|
||||
if (el) { el.classList.add('hide'); setTimeout(function () { el.remove(); }, 300); }
|
||||
}
|
||||
|
||||
/* ===== Helpers ===== */
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
var div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function getPhotoPaths(issue) {
|
||||
return [issue.photo_path, issue.photo_path2, issue.photo_path3, issue.photo_path4, issue.photo_path5].filter(Boolean);
|
||||
}
|
||||
|
||||
function getCompletionPhotoPaths(issue) {
|
||||
return [issue.completion_photo_path, issue.completion_photo_path2, issue.completion_photo_path3, issue.completion_photo_path4, issue.completion_photo_path5].filter(Boolean);
|
||||
}
|
||||
|
||||
function renderPhotoThumbs(photos) {
|
||||
if (!photos || !photos.length) return '';
|
||||
return '<div class="m-photo-row">' + photos.map(function (p, i) {
|
||||
return '<img src="' + p + '" class="m-photo-thumb" onclick="openPhotoModal(\'' + p + '\')" alt="사진 ' + (i + 1) + '">';
|
||||
}).join('') + '</div>';
|
||||
}
|
||||
Reference in New Issue
Block a user