- 이미지 압축: 삼성/네이버 브라우저 호환, URL.createObjectURL 방식으로 메모리 절감, 대용량 PNG/HEIC 처리, blob 유효성 검증, 순차 압축으로 모바일 OOM 방지 - HEIC/HEIF 지원: pillow-heif 서버사이드 변환, Pillow 12.2.0 업그레이드 - 조치 페이지: '조치 완료 저장' 단일 버튼으로 단순화 - 재조치 흐름: 관리자 재조치 요청 시 이전 조치 이력을 번호 카드로 순차 표시 - 신고 순번: 전체 기준 ROW_NUMBER(oldest=1) 순번 표시, 삭제 gap 제거 - 모바일 탭바: position:fixed 적용으로 nav 하단 흰 여백 제거 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
114 lines
4.4 KiB
JavaScript
114 lines
4.4 KiB
JavaScript
const Auth = (() => {
|
|
const KEY_TOKEN = 'ev_token';
|
|
const KEY_ROLE = 'ev_role';
|
|
const KEY_NAME = 'ev_name';
|
|
const KEY_ID = 'ev_uid';
|
|
|
|
function save(token, role, name, id) {
|
|
localStorage.setItem(KEY_TOKEN, token);
|
|
localStorage.setItem(KEY_ROLE, role);
|
|
localStorage.setItem(KEY_NAME, name);
|
|
localStorage.setItem(KEY_ID, id);
|
|
}
|
|
function token() { return localStorage.getItem(KEY_TOKEN); }
|
|
function role() { return localStorage.getItem(KEY_ROLE); }
|
|
function name() { return localStorage.getItem(KEY_NAME); }
|
|
function uid() { return localStorage.getItem(KEY_ID); }
|
|
|
|
function logout() {
|
|
[KEY_TOKEN, KEY_ROLE, KEY_NAME, KEY_ID].forEach(k => localStorage.removeItem(k));
|
|
location.href = '/pages/login.html';
|
|
}
|
|
|
|
function require(allowedRoles) {
|
|
if (!token()) {
|
|
// 로그인 후 원래 페이지로 돌아올 수 있도록 현재 URL 저장
|
|
if (location.pathname !== '/pages/login.html') {
|
|
sessionStorage.setItem('ev_redirect', location.pathname + location.search);
|
|
}
|
|
logout(); return false;
|
|
}
|
|
if (allowedRoles && !allowedRoles.includes(role())) {
|
|
alert('접근 권한이 없습니다. (현재 역할: ' + (role() || '없음') + ')');
|
|
logout();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function renderNav(el) {
|
|
if (!el) return;
|
|
el.innerHTML = `
|
|
<button class="nav-hamburger" onclick="Auth.toggleMobileNav()" aria-label="메뉴">☰</button>
|
|
<span class="nav-user">
|
|
<span>${name()} <small style="color:var(--accent)">[${role()}]</small></span>
|
|
<a onclick="Auth.logout()">로그아웃</a>
|
|
</span>`;
|
|
|
|
// 모바일 오버레이 삽입 (중복 방지)
|
|
if (!document.getElementById('mobileNavOverlay')) {
|
|
const ov = document.createElement('div');
|
|
ov.id = 'mobileNavOverlay';
|
|
ov.className = 'mobile-nav-overlay';
|
|
ov.addEventListener('click', closeMobileNav);
|
|
document.body.appendChild(ov);
|
|
}
|
|
|
|
// 사이드바 링크 클릭 시 드로어 닫기 + 하단 로그아웃 주입 (모바일용)
|
|
setTimeout(() => {
|
|
document.querySelectorAll('.sidebar a').forEach(a => {
|
|
a.addEventListener('click', closeMobileNav);
|
|
});
|
|
const sidebar = document.querySelector('.sidebar');
|
|
if (sidebar && !sidebar.querySelector('.sidebar-user-footer')) {
|
|
const footer = document.createElement('div');
|
|
footer.className = 'sidebar-user-footer';
|
|
footer.innerHTML = `
|
|
<div style="border-top:1px solid rgba(255,255,255,.12);margin:12px 0 4px;"></div>
|
|
<div style="padding:6px 20px;font-size:11px;color:rgba(255,255,255,.4);">${name()} <span style="color:var(--accent)">[${role()}]</span></div>
|
|
<a onclick="Auth.logout()" style="cursor:pointer;color:rgba(255,255,255,.6);">🚪 로그아웃</a>`;
|
|
sidebar.appendChild(footer);
|
|
}
|
|
}, 0);
|
|
}
|
|
|
|
function toggleMobileNav() {
|
|
const sidebar = document.querySelector('.sidebar');
|
|
const overlay = document.getElementById('mobileNavOverlay');
|
|
if (!sidebar) return;
|
|
const opening = !sidebar.classList.contains('mobile-open');
|
|
sidebar.classList.toggle('mobile-open', opening);
|
|
if (overlay) overlay.classList.toggle('show', opening);
|
|
}
|
|
|
|
function closeMobileNav() {
|
|
var sb = document.querySelector('.sidebar');
|
|
if (sb) sb.classList.remove('mobile-open');
|
|
var ov = document.getElementById('mobileNavOverlay');
|
|
if (ov) ov.classList.remove('show');
|
|
}
|
|
|
|
function statusBadge(status) {
|
|
const map = {
|
|
pending_approval: '승인대기', pending: '접수', in_progress: '처리중',
|
|
done: '완료', waiting: '부품대기', revisit: '재방문', closed: '상황종료',
|
|
registered: '등록', reviewing: '검토중', developing: '개발중',
|
|
deployed: '배포완료', observer: '옵저버',
|
|
};
|
|
return `<span class="badge s-${status}">${map[status] || status}</span>`;
|
|
}
|
|
|
|
function costStatusBadge(s) {
|
|
const map = { pending:'미처리', billed:'청구완료', waived:'면제', settled:'정산완료' };
|
|
return `<span class="badge s-cost-${s}">${map[s] || s}</span>`;
|
|
}
|
|
|
|
function fmtDt(dt) {
|
|
if (!dt) return '-';
|
|
return new Date(dt).toLocaleString('ko-KR', {year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'});
|
|
}
|
|
|
|
return { save, token, role, name, uid, logout, require, renderNav,
|
|
toggleMobileNav, closeMobileNav, statusBadge, costStatusBadge, fmtDt };
|
|
})();
|