This commit is contained in:
kim
2026-05-09 11:35:33 +09:00
parent a77ee3bdc9
commit 82bda7409e
42 changed files with 1576 additions and 1361 deletions
+253 -1
View File
@@ -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,'&lt;');
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);