// 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); } }