v.2.6.1
This commit is contained in:
+253
-1
@@ -3418,6 +3418,7 @@
|
||||
<option value="telegram">Telegram</option>
|
||||
<option value="discord">Discord</option>
|
||||
<option value="whatsapp">WhatsApp</option>
|
||||
<option value="email">Email</option>
|
||||
</select>
|
||||
|
||||
<div id="tg-status-bar" style="display:flex;align-items:center;gap:8px;margin-bottom:10px;padding:8px 10px;border-radius:8px;background:#f8f9fa;font-size:12px">
|
||||
@@ -3432,6 +3433,10 @@
|
||||
<div id="wa-status-dot" style="width:8px;height:8px;border-radius:50%;background:#ccc;flex-shrink:0"></div>
|
||||
<span id="wa-status-text" style="color:var(--muted)">미설정</span>
|
||||
</div>
|
||||
<div id="em-status-bar" style="display:none;align-items:center;gap:8px;margin-bottom:10px;padding:8px 10px;border-radius:8px;background:#f8f9fa;font-size:12px">
|
||||
<div id="em-status-dot" style="width:8px;height:8px;border-radius:50%;background:#ccc;flex-shrink:0"></div>
|
||||
<span id="em-status-text" style="color:var(--muted)">미설정</span>
|
||||
</div>
|
||||
|
||||
<div id="channel-form-telegram">
|
||||
<label style="display:block;font-size:12px;color:var(--muted);margin-bottom:6px">Bot Token</label>
|
||||
@@ -3480,6 +3485,60 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="channel-form-email" style="display:none">
|
||||
<div id="email-accounts-list" style="margin-bottom:10px"></div>
|
||||
<div style="border:1px dashed var(--line);border-radius:8px;padding:10px;margin-top:6px">
|
||||
<div style="font-size:12px;font-weight:600;margin-bottom:6px">계정 추가 / 수정</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px">
|
||||
<div>
|
||||
<label style="display:block;font-size:11px;color:var(--muted)">계정 ID</label>
|
||||
<input id="em-id" type="text" placeholder="applecherry" style="width:100%;border:1px solid var(--line);border-radius:8px;padding:6px;font-size:12px;font-family:'IBM Plex Mono',monospace;box-sizing:border-box" />
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;font-size:11px;color:var(--muted)">이메일 주소</label>
|
||||
<input id="em-username" type="text" placeholder="user@example.com" style="width:100%;border:1px solid var(--line);border-radius:8px;padding:6px;font-size:12px;font-family:'IBM Plex Mono',monospace;box-sizing:border-box" />
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;font-size:11px;color:var(--muted)">비밀번호</label>
|
||||
<input id="em-password" type="password" placeholder="••••••••" style="width:100%;border:1px solid var(--line);border-radius:8px;padding:6px;font-size:12px;font-family:'IBM Plex Mono',monospace;box-sizing:border-box" />
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;font-size:11px;color:var(--muted)">IMAP 서버</label>
|
||||
<input id="em-imap-host" type="text" placeholder="imap.example.com" style="width:100%;border:1px solid var(--line);border-radius:8px;padding:6px;font-size:12px;font-family:'IBM Plex Mono',monospace;box-sizing:border-box" />
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;font-size:11px;color:var(--muted)">IMAP 포트</label>
|
||||
<input id="em-imap-port" type="number" value="993" style="width:100%;border:1px solid var(--line);border-radius:8px;padding:6px;font-size:12px;font-family:'IBM Plex Mono',monospace;box-sizing:border-box" />
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;font-size:11px;color:var(--muted)">SMTP 서버</label>
|
||||
<input id="em-smtp-host" type="text" placeholder="smtp.example.com" style="width:100%;border:1px solid var(--line);border-radius:8px;padding:6px;font-size:12px;font-family:'IBM Plex Mono',monospace;box-sizing:border-box" />
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;font-size:11px;color:var(--muted)">SMTP 포트</label>
|
||||
<input id="em-smtp-port" type="number" value="587" style="width:100%;border:1px solid var(--line);border-radius:8px;padding:6px;font-size:12px;font-family:'IBM Plex Mono',monospace;box-sizing:border-box" />
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;justify-content:end;gap:4px">
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:11px;cursor:pointer"><input type="checkbox" id="em-imap-tls" checked /> IMAP TLS</label>
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:11px;cursor:pointer"><input type="checkbox" id="em-smtp-secure" /> SMTP SSL</label>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;margin-top:8px">
|
||||
<button class="btn btn-sm" onclick="saveEmailAccount()" style="background:var(--brand);color:#fff;border:1px solid var(--brand);padding:5px 12px;font-size:12px">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:14px;padding-top:10px;border-top:1px solid var(--line)">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between">
|
||||
<div style="font-size:13px;font-weight:600">허트비트 알림</div>
|
||||
<label style="display:flex;align-items:center;gap:8px;font-size:12px;cursor:pointer">
|
||||
<input type="checkbox" id="settings-hb-notify" checked onchange="saveHbNotifyConfig()" />
|
||||
<span id="settings-hb-notify-label">알림 받기</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:8px;margin-top:12px">
|
||||
<button class="btn btn-sm" id="channel-test-btn" onclick="testSelectedChannel()" style="background:#fff;border:1px solid var(--line);color:var(--text)">테스트</button>
|
||||
<button class="btn btn-sm" onclick="saveSelectedChannelSettings()" style="background:var(--brand);color:#fff;border:1px solid var(--brand)">저장</button>
|
||||
@@ -3513,6 +3572,14 @@
|
||||
4. Add a test recipient number in E.164 format.<br>
|
||||
5. Click <b>Test</b>, <b>Save</b>, then <b>Send Test</b>.
|
||||
</div>
|
||||
<div id="channel-guide-email" style="display:none;font-size:12px;color:var(--muted);line-height:1.8">
|
||||
<b>Email 계정 설정</b><br>
|
||||
1. IMAP/SMTP 서버 주소와 포트를 입력하세요.<br>
|
||||
2. 일반적인 포트: IMAP=993(TLS), SMTP=587(STARTTLS) 또는 465(SSL).<br>
|
||||
3. Gmail은 앱 비밀번호 사용 (imap.gmail.com / smtp.gmail.com).<br>
|
||||
4. 저장 후 <b>테스트</b> 버튼으로 IMAP 연결을 확인하세요.<br>
|
||||
5. 여러 계정을 추가하면 email_list에서 모든 계정을 순회합니다.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -7305,18 +7372,21 @@ function onChannelTypeChange() {
|
||||
telegram: 'channel-form-telegram',
|
||||
discord: 'channel-form-discord',
|
||||
whatsapp: 'channel-form-whatsapp',
|
||||
email: 'channel-form-email',
|
||||
};
|
||||
const guideMap = {
|
||||
telegram: 'channel-guide-telegram',
|
||||
discord: 'channel-guide-discord',
|
||||
whatsapp: 'channel-guide-whatsapp',
|
||||
email: 'channel-guide-email',
|
||||
};
|
||||
const statusMap = {
|
||||
telegram: 'tg-status-bar',
|
||||
discord: 'dc-status-bar',
|
||||
whatsapp: 'wa-status-bar',
|
||||
email: 'em-status-bar',
|
||||
};
|
||||
['telegram', 'discord', 'whatsapp'].forEach((k) => {
|
||||
['telegram', 'discord', 'whatsapp', 'email'].forEach((k) => {
|
||||
const form = document.getElementById(formMap[k]);
|
||||
const guide = document.getElementById(guideMap[k]);
|
||||
const status = document.getElementById(statusMap[k]);
|
||||
@@ -7324,6 +7394,13 @@ function onChannelTypeChange() {
|
||||
if (guide) guide.style.display = (k === ch) ? 'block' : 'none';
|
||||
if (status) status.style.display = (k === ch) ? 'flex' : 'none';
|
||||
});
|
||||
// Email: hide test/send-test buttons (email has per-account test)
|
||||
const testBtn = document.getElementById('channel-test-btn');
|
||||
const sendTestBtn = document.getElementById('channel-send-test-btn');
|
||||
if (testBtn) testBtn.style.display = (ch === 'email') ? 'none' : '';
|
||||
if (sendTestBtn) sendTestBtn.style.display = (ch === 'email') ? 'none' : '';
|
||||
// Load email accounts when switching to email tab
|
||||
if (ch === 'email') loadEmailAccounts();
|
||||
}
|
||||
|
||||
async function saveSelectedChannelSettings() {
|
||||
@@ -7379,6 +7456,14 @@ async function loadChannelsStatus() {
|
||||
} catch (err) {
|
||||
console.error('[Channels] Status load failed:', err);
|
||||
}
|
||||
// Load heartbeat notify toggle
|
||||
try {
|
||||
const hbData = await api('/api/heartbeat/config');
|
||||
if (hbData?.success) {
|
||||
document.getElementById('settings-hb-notify').checked = hbData.config.notifyEnabled !== false;
|
||||
updateHbNotifyLabel();
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function saveChannelSettings(channel) {
|
||||
@@ -7450,6 +7535,173 @@ async function sendChannelTest(channel) {
|
||||
setButtonBusy(btnId, false, 'Sending...', 'Send Test');
|
||||
}
|
||||
|
||||
// ─── Email Account Management (Channel Settings) ─────────────────────────
|
||||
let emailAccountsCache = [];
|
||||
|
||||
function getCurrentUsername() {
|
||||
if (typeof currentUser === 'object' && currentUser?.username) return currentUser.username;
|
||||
try { return localStorage.getItem(LAST_USER_KEY) || ''; } catch { return ''; }
|
||||
}
|
||||
|
||||
async function loadEmailAccounts() {
|
||||
const username = getCurrentUsername();
|
||||
if (!username) {
|
||||
document.getElementById('email-accounts-list').innerHTML = '<div style="font-size:12px;color:var(--muted)">사용자 정보를 불러올 수 없습니다.</div>';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = await api(`/api/users/${encodeURIComponent(username)}/email/config`);
|
||||
const accounts = data?.accounts || [];
|
||||
emailAccountsCache = accounts;
|
||||
const count = accounts.length;
|
||||
if (count > 0) setChannelStatus('em', 'ok', `${count}개 계정 연결됨`);
|
||||
else setChannelStatus('em', 'off', '미설정');
|
||||
renderEmailAccountsList(accounts);
|
||||
} catch (err) {
|
||||
document.getElementById('email-accounts-list').innerHTML = '<div style="font-size:12px;color:var(--muted)">이메일 계정을 불러올 수 없습니다.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderEmailAccountsList(accounts) {
|
||||
const container = document.getElementById('email-accounts-list');
|
||||
if (!accounts.length) {
|
||||
container.innerHTML = '<div style="font-size:12px;color:var(--muted);margin-bottom:6px">등록된 이메일 계정이 없습니다.</div>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = accounts.map(a => {
|
||||
const esc = s => String(s||'').replace(/</g,'<');
|
||||
return `<div style="border:1px solid var(--line);border-radius:8px;padding:8px 10px;margin-bottom:6px;background:var(--panel-2)">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between">
|
||||
<div>
|
||||
<span style="font-weight:600;font-size:13px">${esc(a.id)}</span>
|
||||
<span style="font-size:11px;color:var(--muted);margin-left:6px">${esc(a.username)}</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:4px">
|
||||
<button class="btn btn-sm" onclick="testEmailAccount('${esc(a.id)}')" style="background:#fff;border:1px solid var(--line);color:var(--text);padding:3px 8px;font-size:11px">테스트</button>
|
||||
<button class="btn btn-sm" onclick="editEmailAccount('${esc(a.id)}')" style="background:#fff;border:1px solid var(--line);color:var(--text);padding:3px 8px;font-size:11px">수정</button>
|
||||
<button class="btn btn-sm" onclick="deleteEmailAccount('${esc(a.id)}')" style="background:#fff;border:1px solid #e55;color:#e55;padding:3px 8px;font-size:11px">삭제</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:2px">IMAP: ${esc(a.imap_host)}:${a.imap_port} | SMTP: ${esc(a.smtp_host)}:${a.smtp_port}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function saveEmailAccount() {
|
||||
const username = getCurrentUsername();
|
||||
if (!username) { alert('사용자 정보를 확인할 수 없습니다.'); return; }
|
||||
const id = document.getElementById('em-id').value.trim();
|
||||
if (!id) { alert('계정 ID를 입력하세요.'); return; }
|
||||
if (!/^[a-zA-Z0-9_-]{1,32}$/.test(id)) { alert('계정 ID는 1-32자의 영문, 숫자, 대시, 언더스코어만 가능합니다.'); return; }
|
||||
const username_field = document.getElementById('em-username').value.trim();
|
||||
if (!username_field) { alert('이메일 주소를 입력하세요.'); return; }
|
||||
const imap_host = document.getElementById('em-imap-host').value.trim();
|
||||
if (!imap_host) { alert('IMAP 서버를 입력하세요.'); return; }
|
||||
const smtp_host = document.getElementById('em-smtp-host').value.trim();
|
||||
if (!smtp_host) { alert('SMTP 서버를 입력하세요.'); return; }
|
||||
const payload = {
|
||||
imap_host,
|
||||
imap_port: Number(document.getElementById('em-imap-port').value) || 993,
|
||||
imap_tls: !!document.getElementById('em-imap-tls').checked,
|
||||
imap_reject_unauthorized: true,
|
||||
smtp_host,
|
||||
smtp_port: Number(document.getElementById('em-smtp-port').value) || 587,
|
||||
smtp_secure: !!document.getElementById('em-smtp-secure').checked,
|
||||
smtp_reject_unauthorized: true,
|
||||
username: username_field,
|
||||
password: document.getElementById('em-password').value || undefined,
|
||||
};
|
||||
try {
|
||||
const data = await api(`/api/users/${encodeURIComponent(username)}/email/accounts/${encodeURIComponent(id)}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (data?.success) {
|
||||
addProcessEntry('final', `Email account "${id}" saved.`);
|
||||
document.getElementById('em-password').value = '';
|
||||
await loadEmailAccounts();
|
||||
} else {
|
||||
alert('저장 실패: ' + (data?.error || '알 수 없는 오류'));
|
||||
}
|
||||
} catch (err) {
|
||||
alert('저장 실패: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function testEmailAccount(accountId) {
|
||||
const username = getCurrentUsername();
|
||||
if (!username) return;
|
||||
try {
|
||||
const data = await api(`/api/users/${encodeURIComponent(username)}/email/accounts/${encodeURIComponent(accountId)}/test`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (data?.success) {
|
||||
addProcessEntry('final', `Email account "${accountId}" IMAP connection OK.`);
|
||||
} else {
|
||||
alert(`테스트 실패: ${data?.error || '알 수 없는 오류'}`);
|
||||
}
|
||||
} catch (err) {
|
||||
alert('테스트 실패: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function editEmailAccount(accountId) {
|
||||
const acct = emailAccountsCache.find(a => a.id === accountId);
|
||||
if (!acct) return;
|
||||
document.getElementById('em-id').value = acct.id;
|
||||
document.getElementById('em-username').value = acct.username || '';
|
||||
document.getElementById('em-imap-host').value = acct.imap_host || '';
|
||||
document.getElementById('em-imap-port').value = acct.imap_port || 993;
|
||||
document.getElementById('em-smtp-host').value = acct.smtp_host || '';
|
||||
document.getElementById('em-smtp-port').value = acct.smtp_port || 587;
|
||||
document.getElementById('em-imap-tls').checked = acct.imap_tls !== false;
|
||||
document.getElementById('em-smtp-secure').checked = acct.smtp_secure === true;
|
||||
document.getElementById('em-password').value = '';
|
||||
document.getElementById('em-password').placeholder = '•••••••• (변경 시에만 입력)';
|
||||
}
|
||||
|
||||
async function deleteEmailAccount(accountId) {
|
||||
if (!confirm(`이메일 계정 "${accountId}"를 삭제하시겠습니까?`)) return;
|
||||
const username = getCurrentUsername();
|
||||
if (!username) return;
|
||||
try {
|
||||
const data = await api(`/api/users/${encodeURIComponent(username)}/email/accounts/${encodeURIComponent(accountId)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (data?.success) {
|
||||
addProcessEntry('final', `Email account "${accountId}" deleted.`);
|
||||
await loadEmailAccounts();
|
||||
} else {
|
||||
alert('삭제 실패: ' + (data?.error || '알 수 없는 오류'));
|
||||
}
|
||||
} catch (err) {
|
||||
alert('삭제 실패: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Heartbeat Notify Toggle ──────────────────────────────────────────────
|
||||
function updateHbNotifyLabel() {
|
||||
const on = document.getElementById('settings-hb-notify')?.checked;
|
||||
const label = document.getElementById('settings-hb-notify-label');
|
||||
if (label) label.textContent = on ? '알림 받기' : '알림 끄기';
|
||||
}
|
||||
|
||||
async function saveHbNotifyConfig() {
|
||||
const notifyEnabled = !!document.getElementById('settings-hb-notify')?.checked;
|
||||
try {
|
||||
const data = await api('/api/heartbeat/config', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ notifyEnabled }),
|
||||
});
|
||||
if (data?.success) {
|
||||
updateHbNotifyLabel();
|
||||
addProcessEntry('final', notifyEnabled ? '허트비트 알림 켜짐' : '허트비트 알림 꺼짐');
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
const workspace_path = document.getElementById('settings-workspace-path').value.trim();
|
||||
const allowed_paths = document.getElementById('settings-allowed-paths').value.split('\n').map(s => s.trim()).filter(Boolean);
|
||||
|
||||
Reference in New Issue
Block a user