// settings-channels.js — 설정: 알림 채널(텔레그램/이메일/카카오) 연결 및 테스트.
// extracted from app.js (v4.3.19-3 split).
function toggleUserTelegram(username) {
const panel = document.getElementById(`user-telegram-panel-${username}`);
if (!panel) return;
if (panel.style.display === 'none') {
panel.style.display = 'block';
loadUserTelegramBot(username);
} else {
panel.style.display = 'none';
}
}
async function loadUserTelegramBot(username) {
const panel = document.getElementById(`user-telegram-panel-${username}`);
if (!panel) return;
panel.innerHTML = '
불러오는 중...
';
try {
const data = await api(`/api/users/${encodeURIComponent(username)}/telegram-bot`);
const statusColor = data.running ? 'var(--ok)' : 'var(--muted)';
const statusText = data.running ? `연결됨 (@${data.botUsername || '?'})` : '중지됨';
panel.innerHTML = `
텔레그램 전용 봇 — ${username}
● ${statusText}
${data.hasToken ? `` : ''}
`;
} catch (e) {
panel.innerHTML = `${e.message}
`;
}
}
async function saveUserTelegramBot(username) {
const input = document.getElementById(`tg-token-${username}`);
const msg = document.getElementById(`tg-bot-msg-${username}`);
const token = input ? input.value.trim() : '';
if (!token) { if (msg) msg.textContent = '토큰을 입력해 주세요.'; return; }
try {
const data = await api(`/api/users/${encodeURIComponent(username)}/telegram-bot`, {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ botToken: token }),
});
if (data.success) {
if (msg) { msg.style.color = 'var(--ok)'; msg.textContent = '저장됨. 봇 연결 중...'; }
setTimeout(() => loadUserTelegramBot(username), 2000);
} else {
if (msg) { msg.style.color = 'var(--err)'; msg.textContent = data.error || '저장 실패'; }
}
} catch (e) {
if (msg) { msg.style.color = 'var(--err)'; msg.textContent = e.message; }
}
}
async function deleteUserTelegramBot(username) {
if (!confirm(`${username}의 텔레그램 봇을 삭제하시겠습니까?`)) return;
const msg = document.getElementById(`tg-bot-msg-${username}`);
try {
const data = await api(`/api/users/${encodeURIComponent(username)}/telegram-bot`, {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ botToken: '' }),
});
if (data.success) loadUserTelegramBot(username);
else if (msg) { msg.style.color = 'var(--err)'; msg.textContent = data.error || '삭제 실패'; }
} catch (e) {
if (msg) { msg.style.color = 'var(--err)'; msg.textContent = e.message; }
}
}
// --- Per-User Email Accounts -------------------------------------------------
function toggleUserEmail(username) {
const panel = document.getElementById(`user-email-panel-${username}`);
if (!panel) return;
if (panel.style.display === 'none') {
panel.style.display = 'block';
loadUserEmailAccounts(username);
} else {
panel.style.display = 'none';
}
}
async function loadUserEmailAccounts(username) {
const panel = document.getElementById(`user-email-panel-${username}`);
if (!panel) return;
panel.innerHTML = '불러오는 중…
';
try {
const data = await api(`/api/users/${encodeURIComponent(username)}/email/config`);
const accounts = data.accounts || [];
const def = data.default || '';
const formId = `email-form-${username}`;
const list = accounts.length === 0
? '등록된 이메일 계정이 없습니다.
'
: accounts.map(a => `
${escapeHtml(a.id)}
${def === a.id ? '기본' : ''}
${escapeHtml(a.username)} @ ${escapeHtml(a.imap_host)}:${a.imap_port}
${a.password_set ? '' : '비번 없음'}
${def !== a.id ? `` : ''}
`).join('');
panel.innerHTML = `
이메일 계정 (${accounts.length})
${list}
+ 계정 추가/수정
`;
} catch (e) {
panel.innerHTML = `불러오기 실패: ${escapeHtml(e.message || e)}
`;
}
}
async function submitUserEmailAccount(username) {
const form = document.getElementById(`email-form-${username}`);
const msg = document.getElementById(`email-form-${username}-msg`);
if (!form) return;
const get = (k) => form.querySelector(`[data-f="${k}"]`);
const id = (get('id').value || '').trim();
if (!/^[a-zA-Z0-9_-]{1,32}$/.test(id)) { msg.textContent = '계정 ID는 1-32자 영숫자/대시/언더스코어'; return; }
const body = {
imap_host: get('imap_host').value.trim(),
imap_port: Number(get('imap_port').value) || 993,
imap_tls: get('imap_tls').checked,
smtp_host: get('smtp_host').value.trim(),
smtp_port: Number(get('smtp_port').value) || 587,
smtp_secure: get('smtp_secure').checked,
username: get('username').value.trim(),
password: get('password').value,
};
if (!body.imap_host || !body.smtp_host || !body.username) { msg.textContent = 'IMAP/SMTP 호스트와 사용자명은 필수'; return; }
msg.textContent = '저장 중…'; msg.style.color = 'var(--muted)';
try {
await api(`/api/users/${encodeURIComponent(username)}/email/accounts/${encodeURIComponent(id)}`, {
method: 'PUT', body: JSON.stringify(body),
});
loadUserEmailAccounts(username);
} catch (e) {
msg.textContent = e.message || '저장 실패'; msg.style.color = 'var(--err)';
}
}
async function deleteUserEmailAccount(username, accountId) {
if (!confirm(`계정 "${accountId}" 삭제하시겠습니까? (vault의 비밀번호도 함께 삭제됩니다)`)) return;
try {
await api(`/api/users/${encodeURIComponent(username)}/email/accounts/${encodeURIComponent(accountId)}`, { method: 'DELETE' });
loadUserEmailAccounts(username);
} catch (e) { alert(e.message || '삭제 실패'); }
}
async function setDefaultUserEmailAccount(username, accountId) {
try {
await api(`/api/users/${encodeURIComponent(username)}/email/default`, {
method: 'PUT', body: JSON.stringify({ accountId }),
});
loadUserEmailAccounts(username);
} catch (e) { alert(e.message || '기본 설정 실패'); }
}
async function testUserEmailAccount(username, accountId, btn) {
const orig = btn.textContent;
btn.disabled = true; btn.textContent = '연결 중…';
try {
const r = await api(`/api/users/${encodeURIComponent(username)}/email/accounts/${encodeURIComponent(accountId)}/test`, { method: 'POST' });
if (r.success) { btn.textContent = '✓ 성공'; setTimeout(() => { btn.textContent = orig; btn.disabled = false; }, 1500); }
else { alert(`테스트 실패: ${r.error || 'unknown'}`); btn.textContent = orig; btn.disabled = false; }
} catch (e) {
alert(e.message || '테스트 실패'); btn.textContent = orig; btn.disabled = false;
}
}
let channelsStatusLoaded = false;
function setChannelStatus(idPrefix, state, message) {
const dot = document.getElementById(`${idPrefix}-status-dot`);
const text = document.getElementById(`${idPrefix}-status-text`);
if (!dot || !text) return;
if (state === 'ok') {
dot.style.background = 'var(--ok)';
text.style.color = 'var(--text)';
} else if (state === 'warn') {
dot.style.background = '#f0ad4e';
text.style.color = '#b87b00';
} else {
dot.style.background = '#ccc';
text.style.color = 'var(--muted)';
}
text.textContent = message || '미설정';
}
function readChannelPayload(channel) {
if (channel === 'kakao') {
let userMap = {};
try { userMap = JSON.parse(document.getElementById('settings-kk-usermap')?.value || '{}'); } catch {}
return {
appKey: (document.getElementById('settings-kk-appkey')?.value || '').trim(),
channelEnabled: !!document.getElementById('settings-kk-channel-enabled')?.checked,
userMap,
};
}
if (channel === 'telegram') {
const userIdStr = (document.getElementById('settings-tg-userid')?.value || '').trim();
const allowedUserIds = userIdStr ? userIdStr.split(/[,\s]+/).map(Number).filter(n => !isNaN(n) && n > 0) : [];
return {
enabled: !!document.getElementById('settings-tg-enabled')?.checked,
botToken: (document.getElementById('settings-tg-token')?.value || '').trim(),
allowedUserIds,
streamMode: 'full',
};
}
if (channel === 'discord') {
return {
enabled: !!document.getElementById('settings-dc-enabled')?.checked,
botToken: (document.getElementById('settings-dc-token')?.value || '').trim(),
applicationId: (document.getElementById('settings-dc-appid')?.value || '').trim(),
guildId: (document.getElementById('settings-dc-guildid')?.value || '').trim(),
channelId: (document.getElementById('settings-dc-channelid')?.value || '').trim(),
webhookUrl: (document.getElementById('settings-dc-webhook')?.value || '').trim(),
};
}
return {
enabled: !!document.getElementById('settings-wa-enabled')?.checked,
accessToken: (document.getElementById('settings-wa-token')?.value || '').trim(),
phoneNumberId: (document.getElementById('settings-wa-phoneid')?.value || '').trim(),
businessAccountId: (document.getElementById('settings-wa-baid')?.value || '').trim(),
verifyToken: (document.getElementById('settings-wa-verify')?.value || '').trim(),
webhookSecret: (document.getElementById('settings-wa-secret')?.value || '').trim(),
testRecipient: (document.getElementById('settings-wa-recipient')?.value || '').trim(),
};
}
function setButtonBusy(id, busy, busyLabel, normalLabel) {
const btn = document.getElementById(id);
if (!btn) return;
btn.disabled = !!busy;
btn.textContent = busy ? busyLabel : normalLabel;
}
function getSelectedChannelType() {
return String(document.getElementById('settings-channel-select')?.value || 'telegram');
}
function onChannelTypeChange() {
const ch = getSelectedChannelType();
const formMap = {
telegram: 'channel-form-telegram',
kakao: 'channel-form-kakao',
discord: 'channel-form-discord',
whatsapp: 'channel-form-whatsapp',
email: 'channel-form-email',
};
const guideMap = {
telegram: 'channel-guide-telegram',
kakao: 'channel-guide-kakao',
discord: 'channel-guide-discord',
whatsapp: 'channel-guide-whatsapp',
email: 'channel-guide-email',
};
const statusMap = {
telegram: 'tg-status-bar',
kakao: 'kk-status-bar',
discord: 'dc-status-bar',
whatsapp: 'wa-status-bar',
email: 'em-status-bar',
};
['telegram', 'kakao', 'discord', 'whatsapp', 'email'].forEach((k) => {
const form = document.getElementById(formMap[k]);
const guide = document.getElementById(guideMap[k]);
const status = document.getElementById(statusMap[k]);
if (form) form.style.display = (k === ch) ? 'block' : 'none';
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)
// Kakao: hide "테스트" (연결 확인 없음), 테스트 전송만 노출
const testBtn = document.getElementById('channel-test-btn');
const sendTestBtn = document.getElementById('channel-send-test-btn');
if (testBtn) testBtn.style.display = (ch === 'email' || ch === 'kakao') ? 'none' : '';
if (sendTestBtn) sendTestBtn.style.display = (ch === 'email') ? 'none' : '';
if (ch === 'email') loadEmailAccounts();
if (ch === 'kakao') loadKakaoStatus();
}
async function saveSelectedChannelSettings() {
await saveChannelSettings(getSelectedChannelType());
}
async function testSelectedChannel() {
await testChannel(getSelectedChannelType());
}
async function sendSelectedChannelTest() {
await sendChannelTest(getSelectedChannelType());
}
async function loadChannelsStatus() {
try {
const data = await api('/api/channels/status');
if (!data?.success) return;
const tg = data.telegram || {};
const dc = data.discord || {};
const wa = data.whatsapp || {};
if (tg.connected && tg.polling) setChannelStatus('tg', 'ok', `Connected as @${tg.username || 'bot'}`);
else if (tg.hasToken) setChannelStatus('tg', 'warn', 'Token saved, not connected');
else setChannelStatus('tg', 'off', '미설정');
if (dc.hasToken || dc.hasWebhook) setChannelStatus('dc', 'warn', 'Credentials saved');
else setChannelStatus('dc', 'off', '미설정');
if (wa.hasAccessToken && wa.phoneNumberId) setChannelStatus('wa', 'warn', 'Credentials saved');
else setChannelStatus('wa', 'off', '미설정');
if (!channelsStatusLoaded) {
channelsStatusLoaded = true;
if (tg.hasToken) document.getElementById('settings-tg-token').placeholder = '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 (saved)';
if (dc.hasToken) document.getElementById('settings-dc-token').placeholder = '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 (saved)';
if (wa.hasAccessToken) document.getElementById('settings-wa-token').placeholder = '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022 (saved)';
document.getElementById('settings-tg-enabled').checked = !!tg.enabled;
document.getElementById('settings-tg-userid').value = (tg.allowedUserIds || []).join(', ');
document.getElementById('settings-dc-enabled').checked = !!dc.enabled;
document.getElementById('settings-dc-appid').value = dc.applicationId || '';
document.getElementById('settings-dc-guildid').value = dc.guildId || '';
document.getElementById('settings-dc-channelid').value = dc.channelId || '';
document.getElementById('settings-wa-enabled').checked = !!wa.enabled;
document.getElementById('settings-wa-phoneid').value = wa.phoneNumberId || '';
document.getElementById('settings-wa-baid').value = wa.businessAccountId || '';
document.getElementById('settings-wa-recipient').value = wa.testRecipient || '';
}
onChannelTypeChange();
} 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 loadKakaoStatus() {
try {
const data = await api('/api/kakao/status');
const n = data?.notify || {};
const c = data?.channel || {};
if (n.hasAppKey && n.hasRefreshToken) setChannelStatus('kk', 'ok', '알림 연동됨' + (c.enabled ? ' + 챗봇 활성' : ''));
else if (n.hasAppKey) setChannelStatus('kk', 'warn', 'API 키 있음 — OAuth 로그인 필요');
else setChannelStatus('kk', 'off', '미설정');
if (n.hasAppKey) document.getElementById('settings-kk-appkey').placeholder = '•••••••• (저장됨)';
document.getElementById('settings-kk-channel-enabled').checked = !!c.enabled;
const linkBtn = document.getElementById('kk-send-link-btn');
if (linkBtn) linkBtn.style.display = (n.hasRefreshToken) ? 'inline-block' : 'none';
} catch {}
}
async function kakaoOAuthStart() {
const appKey = (document.getElementById('settings-kk-appkey')?.value || '').trim();
if (appKey) {
// 먼저 appKey 저장
await saveChannelSettings('kakao');
}
const status = document.getElementById('kk-oauth-status');
if (status) status.textContent = '새 창에서 로그인 중...';
const w = window.open('/api/kakao/auth/start', '_blank', 'width=600,height=700');
if (!w) { alert('팝업이 차단됐습니다. 팝업을 허용해 주세요.'); return; }
const timer = setInterval(async () => {
if (w.closed) {
clearInterval(timer);
await loadKakaoStatus();
if (status) status.textContent = '';
}
}, 1000);
}
async function kakaoSendChannelLink() {
const btn = document.getElementById('kk-send-link-btn');
const orig = btn?.textContent;
if (btn) btn.textContent = '전송 중...';
try {
const data = await api('/api/kakao/send-channel-link', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ channelUrl: 'http://pf.kakao.com/_KxjxexfX/chat' }),
});
if (btn) btn.textContent = data.success ? '✅ 전송됨!' : '❌ 실패';
} catch {
if (btn) btn.textContent = '❌ 실패';
}
setTimeout(() => { if (btn) btn.textContent = orig; }, 3000);
}
async function saveChannelSettings(channel) {
const payload = readChannelPayload(channel);
try {
const data = await api('/api/channels/config', {
method: 'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({ channels: { [channel]: payload } }),
});
if (data?.success) {
addProcessEntry('final', `${channel} settings saved.`);
channelsStatusLoaded = false;
await loadChannelsStatus();
} else {
alert('Save failed: ' + (data?.error || 'unknown error'));
}
} catch (err) {
alert('Save failed: ' + err.message);
}
}
async function testChannel(channel) {
const btnId = document.getElementById('channel-test-btn')
? 'channel-test-btn'
: channel === 'telegram' ? 'tg-test-btn' : channel === 'discord' ? 'dc-test-btn' : 'wa-test-btn';
setButtonBusy(btnId, true, 'Testing...', 'Test');
try {
const payload = readChannelPayload(channel);
const data = await api(`/api/channels/test/${channel}`, {
method: 'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify(payload),
});
if (data?.success) {
if (channel === 'telegram') setChannelStatus('tg', 'ok', `Valid bot @${data?.bot?.username || 'telegram'}`);
if (channel === 'discord') setChannelStatus('dc', 'ok', `Valid bot @${data?.bot?.username || 'discord'}`);
if (channel === 'whatsapp') setChannelStatus('wa', 'ok', `Valid phone ${data?.account?.display_phone_number || data?.account?.id || ''}`.trim());
addProcessEntry('final', `${channel} connection test passed.`);
} else {
alert(`${channel} test failed: ${data?.error || 'unknown error'}`);
}
} catch (err) {
alert(`${channel} test failed: ${err.message}`);
}
setButtonBusy(btnId, false, 'Testing...', 'Test');
}
async function sendChannelTest(channel) {
const btnId = document.getElementById('channel-send-test-btn')
? 'channel-send-test-btn'
: channel === 'telegram' ? 'tg-send-test-btn' : channel === 'discord' ? 'dc-send-test-btn' : 'wa-send-test-btn';
setButtonBusy(btnId, true, 'Sending...', 'Send Test');
try {
const payload = readChannelPayload(channel);
const data = await api(`/api/channels/send-test/${channel}`, {
method: 'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify(payload),
});
if (data?.success) {
addProcessEntry('final', `Test message sent via ${channel}.`);
} else {
alert(`${channel} send-test failed: ${data?.error || 'unknown error'}`);
}
} catch (err) {
alert(`${channel} send-test failed: ${err.message}`);
}
setButtonBusy(btnId, false, 'Sending...', 'Send Test');
}
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 = '사용자 정보를 불러올 수 없습니다.
';
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 = '이메일 계정을 불러올 수 없습니다.
';
}
}
function renderEmailAccountsList(accounts) {
const container = document.getElementById('email-accounts-list');
if (!accounts.length) {
container.innerHTML = '등록된 이메일 계정이 없습니다.
';
return;
}
container.innerHTML = accounts.map(a => {
const esc = s => String(s||'').replace(/
${esc(a.id)}
${esc(a.username)}
IMAP: ${esc(a.imap_host)}:${a.imap_port} | SMTP: ${esc(a.smtp_host)}:${a.smtp_port}
`;
}).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);
}
}