Files
AriWeb-Suite/modules/webmaker/public/webmail.js
T

66 lines
5.5 KiB
JavaScript

'use strict';
/* ============================================================================
* 웹메일 화면 스크립트 (v1.3.0) — /webmail/<사이트ID>/
* - 로그인: POST /api/webmail/<id>/login {id, password} → 사이트 관리자 토큰 (localStorage 'wm_webmail_<id>')
* - 상태 : 모든 응답에 status {level, message, canMail} 가 들어 있어 맨 위 띠에 표시한다.
* level 이 ready 가 아니면(운영자 꺼짐·DNS 미완료·게이트웨이 미연결) "메일 쓰기" 를 막는다.
* - 메일함: GET /api/webmail/<id>/box?folder=inbox|sent, 보내기: POST /api/webmail/<id>/send
* ========================================================================== */
const $ = (id) => document.getElementById(id);
const SITE = (location.pathname.match(/\/webmail\/([a-f0-9]{32})/) || [])[1];
const KEY = 'wm_webmail_' + SITE;
let token = ''; try { token = localStorage.getItem(KEY) || ''; } catch (_) {}
let folder = 'inbox'; let status = null;
const dt = (s) => (s ? new Date(s).toLocaleString(uiLocale(), { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '');
/** API 호출. 401 이면 로그인 화면으로 */
async function api(p, opt = {}) {
const headers = { 'Content-Type': 'application/json' }; if (token) headers.Authorization = 'Bearer ' + token;
const r = await fetch(`/api/webmail/${SITE}${p}`, { ...opt, headers });
const b = await r.json().catch(() => ({}));
if (b.status) showStatus(b.status); if (b.site) $('hSite').textContent = b.site.title || '';
if (r.status === 401) { showLogin(); throw new Error(b.error || '로그인이 필요합니다'); }
if (!r.ok) throw new Error(b.error || `HTTP ${r.status}`);
return b;
}
/** 상태 띠: ready 는 초록, 운영자 꺼짐은 빨강, 나머지(DNS·게이트웨이)는 노랑 */
function showStatus(st) {
status = st; const b = $('banner'); b.classList.remove('hidden'); b.className = 'banner ' + (st.level === 'ready' ? 'ok' : st.level === 'off' ? 'off' : '');
b.textContent = st.message; $('btnCompose').disabled = !st.canMail; $('btnCompose').title = st.canMail ? '' : st.message;
}
function showLogin() { $('loginBox').classList.remove('hidden'); $('mailBox').classList.add('hidden'); $('btnLogout').classList.add('hidden'); $('btnCompose').classList.add('hidden'); $('hAddr').textContent = ''; }
$('lGo').onclick = async () => {
$('lMsg').textContent = '확인 중…';
try { const r = await api('/login', { method: 'POST', body: JSON.stringify({ id: $('lId').value.trim(), password: $('lPw').value }) }); token = r.token; try { localStorage.setItem(KEY, token); } catch (_) {} $('lPw').value = ''; $('lMsg').textContent = ''; openBox(); }
catch (e) { $('lMsg').textContent = e.message; }
};
$('lPw').addEventListener('keydown', (e) => { if (e.key === 'Enter') $('lGo').click(); });
$('btnLogout').onclick = () => { token = ''; try { localStorage.removeItem(KEY); } catch (_) {} showLogin(); };
document.querySelectorAll('#folders button').forEach((b) => (b.onclick = () => { folder = b.dataset.folder; document.querySelectorAll('#folders button').forEach((x) => x.classList.toggle('active', x === b)); openBox(); }));
/** 메일함 열기: 목록을 그리고 첫 화면 상태를 맞춘다 */
async function openBox() {
let r; try { r = await api(`/box?folder=${folder}`); } catch (_) { return; }
$('loginBox').classList.add('hidden'); $('mailBox').classList.remove('hidden'); $('btnLogout').classList.remove('hidden'); $('btnCompose').classList.remove('hidden'); $('hAddr').textContent = r.address || '';
const list = $('list'); list.innerHTML = '';
for (const m of r.messages) {
const it = document.createElement('div'); it.className = 'item' + (m.read ? '' : ' unread');
const b = document.createElement('b'); b.textContent = m.subject || '(제목 없음)'; const sub = document.createElement('div'); sub.className = 'sub'; sub.textContent = `${folder === 'sent' ? '받는 사람 ' + (m.to || '') : (m.from || '')} · ${dt(m.at)}`;
it.appendChild(b); it.appendChild(sub); it.onclick = () => showMessage(m); list.appendChild(it);
}
if (!r.messages.length) { const p = document.createElement('p'); p.className = 'muted small'; p.style.padding = '12px'; p.textContent = status && !status.canMail ? '메일 수발신이 제한된 상태라 메일함이 비어 있습니다. 위 안내를 확인하세요.' : '메일이 없습니다.'; list.appendChild(p); }
}
function showMessage(m) {
const box = $('read'); box.innerHTML = ''; const h = document.createElement('h3'); h.textContent = m.subject || '(제목 없음)'; const meta = document.createElement('p'); meta.className = 'muted small'; meta.textContent = `보낸 사람 ${m.from || ''} · 받는 사람 ${m.to || ''} · ${dt(m.at)}`; const pre = document.createElement('pre'); pre.textContent = m.text || '';
box.appendChild(h); box.appendChild(meta); box.appendChild(pre);
}
$('btnCompose').onclick = () => { $('cMsg').textContent = ''; $('composeDlg').showModal(); };
$('cCancel').onclick = () => $('composeDlg').close();
$('cSend').onclick = async () => { $('cMsg').textContent = '보내는 중…'; try { await api('/send', { method: 'POST', body: JSON.stringify({ to: $('cTo').value.trim(), subject: $('cSubject').value, text: $('cText').value }) }); $('composeDlg').close(); folder = 'sent'; openBox(); } catch (e) { $('cMsg').textContent = e.message; } };
(async () => {
if (!SITE) { document.body.innerHTML = '<p style="padding:40px">잘못된 주소입니다.</p>'; return; }
try { await api('/info'); } catch (_) {}
if (token) openBox(); else showLogin();
})();