@@ -1031,6 +1074,53 @@ $('vcSel').onchange = async ()=>{
toast(channelId?'음성채널 참여 요청을 보냈습니다':'나가기 요청을 보냈습니다');
}catch(e){ toast('요청 실패: '+e); }
};
+// --- 화이트/블랙리스트 팝업 (유저·역할 검색 → 화이트/블랙 추가·제거) ------ #
+const LKEY = {wu:'whitelistUsers', bu:'blacklistUsers', wr:'whitelistRoles', br:'blacklistRoles'};
+async function openLists(){
+ const guildId = $('guildSel').value || (botState&&botState.current&&botState.current.guildId) || '';
+ if(!guildId){ toast('먼저 서버를 선택하세요'); return; }
+ const g = ((botState&&botState.guilds)||[]).find(x=>x.id===guildId) || {members:[],roles:[]};
+ let lists;
+ try{ lists = (await (await fetch('/api/bot/lists?guildId='+encodeURIComponent(guildId))).json()).lists; }
+ catch(e){ lists = {whitelistUsers:[],blacklistUsers:[],whitelistRoles:[],blacklistRoles:[]}; }
+ openModal('청취 화이트/블랙리스트', '');
+ $('modalBody').innerHTML =
+ '화이트리스트에 넣으면 그 대상만 청취(비어있으면 전체 청취), 블랙리스트는 제외됩니다. 유저/역할별로 추가할 수 있어요.
'
+ +''
+ +'
'
+ +''
+ +'화이트리스트 (그 대상만 청취)
'
+ +'블랙리스트 (제외)
';
+ const has=(arr,id)=>(arr||[]).some(x=>x.id===id);
+ function add(kind,item){ const k=LKEY[kind]; if(!has(lists[k],item.id)) lists[k].push(item); renderChips(); }
+ function rm(k,id){ lists[k]=(lists[k]||[]).filter(x=>x.id!==id); renderChips(); }
+ function renderResults(){
+ const type=$('lstType').value, q=$('lstSearch').value.trim().toLowerCase();
+ const src = type==='user' ? (g.members||[]) : (g.roles||[]);
+ const rows = src.filter(x=>!q || (x.name||'').toLowerCase().includes(q)).slice(0,100);
+ $('lstResults').innerHTML = rows.length ? rows.map(x=>
+ ''+esc(x.name)+(x.bot?' (봇)':'')+''
+ +''
+ +'
'
+ ).join('') : '결과 없음 · 봇이 아는 멤버/역할만 검색됩니다
';
+ }
+ const chip=(k,cls,x)=>''+esc(x.name)+' ';
+ const empty='비어있음';
+ function renderChips(){
+ $('chipsW').innerHTML = [...lists.whitelistUsers.map(x=>chip('whitelistUsers','w',x)),...lists.whitelistRoles.map(x=>chip('whitelistRoles','w',x))].join('') || (empty+' (전체 청취)');
+ $('chipsB').innerHTML = [...lists.blacklistUsers.map(x=>chip('blacklistUsers','b',x)),...lists.blacklistRoles.map(x=>chip('blacklistRoles','b',x))].join('') || empty;
+ }
+ $('lstType').onchange=renderResults; $('lstSearch').oninput=renderResults;
+ $('lstResults').onclick=(e)=>{ const b=e.target.closest('.mini'); if(!b)return; add(b.dataset.k,{id:b.dataset.id,name:b.dataset.nm}); };
+ $('chipsW').onclick=$('chipsB').onclick=(e)=>{ const b=e.target.closest('button'); if(!b)return; rm(b.dataset.k,b.dataset.id); };
+ $('lstSave').onclick=async()=>{
+ try{ await fetch('/api/bot/lists',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({guildId,lists})}); toast('청취 필터 저장됨 · 봇에 곧 반영'); closeModal(); }
+ catch(e){ toast('저장 실패: '+e); }
+ };
+ renderResults(); renderChips();
+}
+$('wlBtn').onclick = openLists; $('blBtn').onclick = openLists;
+
async function pollBot(){
try{ renderBot(await (await fetch('/api/bot/state')).json()); }catch(e){}
}