- 모바일 전용 페이지 신규: /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>
196 lines
7.0 KiB
JavaScript
196 lines
7.0 KiB
JavaScript
/**
|
|
* 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>';
|
|
}
|