feat: 알림 채널 다중 관리 UI 추가
Some checks failed
Build and Push Images / build-backend (push) Has been cancelled
Some checks failed
Build and Push Images / build-backend (push) Has been cancelled
This commit is contained in:
169
backend/main.py
169
backend/main.py
@@ -425,16 +425,134 @@ def update_user_pages(user_id: int, data: AccessUpdate, token=Depends(require_ad
|
||||
conn.commit()
|
||||
return {"ok": True}
|
||||
|
||||
# ─── Admin: 알림 설정 ────────────────────────────────────
|
||||
class NotifyConfig(BaseModel):
|
||||
# ─── Admin: 알림 채널 관리 (다중 채널) ────────────────────
|
||||
class NotifyChannel(BaseModel):
|
||||
id: Optional[int] = None
|
||||
name: str # 채널 이름 (식별용)
|
||||
type: str # "discord" | "email"
|
||||
discord_webhook_url: Optional[str] = ""
|
||||
gmail_user: Optional[str] = ""
|
||||
gmail_app_password: Optional[str] = ""
|
||||
alert_email_to: Optional[str] = ""
|
||||
enabled: Optional[bool] = True
|
||||
|
||||
def init_notify_channels_db():
|
||||
conn = psycopg2.connect(**DB_CONFIG)
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS notify_channels (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
type VARCHAR(20) NOT NULL,
|
||||
discord_webhook_url TEXT DEFAULT '',
|
||||
gmail_user VARCHAR(200) DEFAULT '',
|
||||
gmail_app_password TEXT DEFAULT '',
|
||||
alert_email_to VARCHAR(200) DEFAULT '',
|
||||
enabled BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
""")
|
||||
conn.commit()
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
@app.on_event("startup")
|
||||
def startup_notify_channels():
|
||||
import time
|
||||
for _ in range(10):
|
||||
try:
|
||||
init_notify_channels_db()
|
||||
print("Notify channels DB initialized")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Notify channels DB not ready... {e}")
|
||||
time.sleep(3)
|
||||
|
||||
def get_notify_config_from_db(conn) -> dict:
|
||||
"""DB에서 알림 설정 조회, 없으면 환경변수 fallback"""
|
||||
"""하위 호환: 첫 번째 활성 채널에서 설정 조회, 없으면 환경변수 fallback"""
|
||||
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||
try:
|
||||
cur.execute("SELECT * FROM notify_channels WHERE enabled=TRUE ORDER BY id LIMIT 1")
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
return {
|
||||
"discord_webhook_url": row["discord_webhook_url"] or "",
|
||||
"gmail_user": row["gmail_user"] or "",
|
||||
"gmail_app_password": row["gmail_app_password"] or "",
|
||||
"alert_email_to": row["alert_email_to"] or "",
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
"discord_webhook_url": os.getenv("DISCORD_WEBHOOK_URL", ""),
|
||||
"gmail_user": os.getenv("GMAIL_USER", ""),
|
||||
"gmail_app_password": os.getenv("GMAIL_APP_PASSWORD", ""),
|
||||
"alert_email_to": os.getenv("ALERT_EMAIL_TO", ""),
|
||||
}
|
||||
|
||||
def refresh_notifier(conn):
|
||||
"""활성화된 모든 채널 중 첫 번째 채널로 notifier 모듈 업데이트"""
|
||||
import notifier
|
||||
cfg = get_notify_config_from_db(conn)
|
||||
notifier.DISCORD_WEBHOOK_URL = cfg["discord_webhook_url"]
|
||||
notifier.GMAIL_USER = cfg["gmail_user"]
|
||||
notifier.GMAIL_APP_PASSWORD = cfg["gmail_app_password"]
|
||||
notifier.ALERT_EMAIL_TO = cfg["alert_email_to"]
|
||||
|
||||
@app.get("/api/admin/notify-channels")
|
||||
def list_notify_channels(token=Depends(require_admin), conn=Depends(get_db)):
|
||||
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||
cur.execute("SELECT id, name, type, discord_webhook_url, gmail_user, alert_email_to, enabled, created_at FROM notify_channels ORDER BY id")
|
||||
rows = cur.fetchall()
|
||||
# 비밀번호 제외하고 반환
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
@app.post("/api/admin/notify-channels")
|
||||
def create_notify_channel(data: NotifyChannel, token=Depends(require_admin), conn=Depends(get_db)):
|
||||
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||
cur.execute("""
|
||||
INSERT INTO notify_channels (name, type, discord_webhook_url, gmail_user, gmail_app_password, alert_email_to, enabled)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s) RETURNING id, name, type, discord_webhook_url, gmail_user, alert_email_to, enabled
|
||||
""", (data.name, data.type, data.discord_webhook_url or "", data.gmail_user or "",
|
||||
data.gmail_app_password or "", data.alert_email_to or "", data.enabled))
|
||||
conn.commit()
|
||||
refresh_notifier(conn)
|
||||
return cur.fetchone()
|
||||
|
||||
@app.put("/api/admin/notify-channels/{channel_id}")
|
||||
def update_notify_channel(channel_id: int, data: NotifyChannel, token=Depends(require_admin), conn=Depends(get_db)):
|
||||
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||
# 비밀번호가 비어있으면 기존값 유지
|
||||
if data.gmail_app_password and data.gmail_app_password.strip():
|
||||
cur.execute("""
|
||||
UPDATE notify_channels SET name=%s, type=%s, discord_webhook_url=%s,
|
||||
gmail_user=%s, gmail_app_password=%s, alert_email_to=%s, enabled=%s
|
||||
WHERE id=%s RETURNING id, name, type, discord_webhook_url, gmail_user, alert_email_to, enabled
|
||||
""", (data.name, data.type, data.discord_webhook_url or "", data.gmail_user or "",
|
||||
data.gmail_app_password, data.alert_email_to or "", data.enabled, channel_id))
|
||||
else:
|
||||
cur.execute("""
|
||||
UPDATE notify_channels SET name=%s, type=%s, discord_webhook_url=%s,
|
||||
gmail_user=%s, alert_email_to=%s, enabled=%s
|
||||
WHERE id=%s RETURNING id, name, type, discord_webhook_url, gmail_user, alert_email_to, enabled
|
||||
""", (data.name, data.type, data.discord_webhook_url or "", data.gmail_user or "",
|
||||
data.alert_email_to or "", data.enabled, channel_id))
|
||||
conn.commit()
|
||||
refresh_notifier(conn)
|
||||
return cur.fetchone()
|
||||
|
||||
@app.delete("/api/admin/notify-channels/{channel_id}")
|
||||
def delete_notify_channel(channel_id: int, token=Depends(require_admin), conn=Depends(get_db)):
|
||||
cur = conn.cursor()
|
||||
cur.execute("DELETE FROM notify_channels WHERE id=%s", (channel_id,))
|
||||
conn.commit()
|
||||
refresh_notifier(conn)
|
||||
return {"ok": True}
|
||||
|
||||
# ─── 하위호환: 기존 단일 설정 API ────────────────────────
|
||||
def get_notify_config_from_db_legacy(conn) -> dict:
|
||||
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||
try:
|
||||
cur.execute("SELECT key, value FROM notify_config")
|
||||
rows = cur.fetchall()
|
||||
db_config = {r["key"]: r["value"] for r in rows}
|
||||
@@ -444,49 +562,14 @@ def get_notify_config_from_db(conn) -> dict:
|
||||
"gmail_app_password": db_config.get("gmail_app_password", os.getenv("GMAIL_APP_PASSWORD", "")),
|
||||
"alert_email_to": db_config.get("alert_email_to", os.getenv("ALERT_EMAIL_TO", "")),
|
||||
}
|
||||
|
||||
@app.get("/api/admin/notify-config")
|
||||
def get_notify_config(token=Depends(require_admin), conn=Depends(get_db)):
|
||||
cfg = get_notify_config_from_db(conn)
|
||||
# 비밀번호는 설정 여부만 반환 (보안)
|
||||
except Exception:
|
||||
return {
|
||||
"discord_webhook_url": cfg["discord_webhook_url"],
|
||||
"gmail_user": cfg["gmail_user"],
|
||||
"gmail_app_password": "••••••••" if cfg["gmail_app_password"] else "",
|
||||
"alert_email_to": cfg["alert_email_to"],
|
||||
"discord_webhook_url": os.getenv("DISCORD_WEBHOOK_URL", ""),
|
||||
"gmail_user": os.getenv("GMAIL_USER", ""),
|
||||
"gmail_app_password": os.getenv("GMAIL_APP_PASSWORD", ""),
|
||||
"alert_email_to": os.getenv("ALERT_EMAIL_TO", ""),
|
||||
}
|
||||
|
||||
@app.put("/api/admin/notify-config")
|
||||
def save_notify_config(data: NotifyConfig, token=Depends(require_admin), conn=Depends(get_db)):
|
||||
cur = conn.cursor()
|
||||
fields = {
|
||||
"discord_webhook_url": data.discord_webhook_url,
|
||||
"gmail_user": data.gmail_user,
|
||||
"alert_email_to": data.alert_email_to,
|
||||
}
|
||||
# 비밀번호가 마스킹값이면 업데이트 안 함
|
||||
if data.gmail_app_password and data.gmail_app_password != "••••••••":
|
||||
fields["gmail_app_password"] = data.gmail_app_password
|
||||
|
||||
for key, value in fields.items():
|
||||
if value is not None:
|
||||
cur.execute("""
|
||||
INSERT INTO notify_config (key, value, updated_at)
|
||||
VALUES (%s, %s, NOW())
|
||||
ON CONFLICT (key) DO UPDATE SET value=EXCLUDED.value, updated_at=NOW()
|
||||
""", (key, value))
|
||||
conn.commit()
|
||||
|
||||
# notifier 모듈 환경변수 동적 업데이트
|
||||
import notifier
|
||||
cfg = get_notify_config_from_db(conn)
|
||||
notifier.DISCORD_WEBHOOK_URL = cfg["discord_webhook_url"]
|
||||
notifier.GMAIL_USER = cfg["gmail_user"]
|
||||
notifier.GMAIL_APP_PASSWORD = cfg["gmail_app_password"]
|
||||
notifier.ALERT_EMAIL_TO = cfg["alert_email_to"]
|
||||
|
||||
return {"ok": True}
|
||||
|
||||
@app.get("/api/admin/notify-test")
|
||||
async def notify_test(token=Depends(require_admin)):
|
||||
# Discord + Gmail (Pod 이상/복구 테스트)
|
||||
|
||||
@@ -364,26 +364,44 @@
|
||||
<!-- Admin: 알림 설정 -->
|
||||
<div id="page-admin-notify" class="page">
|
||||
<div class="page-header">
|
||||
<div><h2>🔔 알림 설정</h2><p>Discord Webhook 및 이메일 알림 수신 설정을 관리합니다.</p></div>
|
||||
<div><h2>🔔 알림 채널 관리</h2><p>Discord Webhook 및 이메일 알림 채널을 추가·수정·삭제합니다.</p></div>
|
||||
<button class="btn btn-sm btn-purple" onclick="testNotify()">📨 테스트 발송</button>
|
||||
</div>
|
||||
<div class="table-wrapper" style="padding:28px;max-width:640px;">
|
||||
<div style="margin-bottom:24px;">
|
||||
<h3 style="font-size:1rem;font-weight:700;margin-bottom:16px;display:flex;align-items:center;gap:8px;">
|
||||
<span style="background:#5865f2;color:#fff;border-radius:8px;padding:4px 10px;font-size:.85rem;">Discord</span>
|
||||
Webhook 설정
|
||||
</h3>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:24px;align-items:start;">
|
||||
|
||||
<!-- 좌측: 입력 폼 -->
|
||||
<div class="table-wrapper" style="padding:24px;">
|
||||
<h3 id="notify-form-title" style="font-size:1rem;font-weight:700;margin-bottom:20px;">➕ 채널 추가</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label>채널 이름 *</label>
|
||||
<input type="text" id="nc-name" placeholder="예: Discord 운영채널, Gmail 알림"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>채널 유형 *</label>
|
||||
<select id="nc-type" onchange="onNotifyTypeChange()" style="width:100%;padding:11px 14px;border:2px solid var(--border);border-radius:8px;font-size:.95rem;">
|
||||
<option value="both">Discord + Gmail 둘 다</option>
|
||||
<option value="discord">Discord 전용</option>
|
||||
<option value="email">Gmail 전용</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Discord 설정 -->
|
||||
<div id="nc-discord-section">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin:16px 0 12px;">
|
||||
<span style="background:#5865f2;color:#fff;border-radius:6px;padding:3px 10px;font-size:.8rem;font-weight:700;">Discord</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Webhook URL</label>
|
||||
<input type="text" id="nc-discord-url" placeholder="https://discord.com/api/webhooks/..."/>
|
||||
<p style="font-size:.78rem;color:var(--muted);margin-top:4px">채널 설정 → 연동 → 웹후크에서 URL을 복사하세요.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="border-top:1px solid var(--border);padding-top:24px;margin-bottom:24px;">
|
||||
<h3 style="font-size:1rem;font-weight:700;margin-bottom:16px;display:flex;align-items:center;gap:8px;">
|
||||
<span style="background:#ea4335;color:#fff;border-radius:8px;padding:4px 10px;font-size:.85rem;">Gmail</span>
|
||||
이메일 설정
|
||||
</h3>
|
||||
|
||||
<!-- Gmail 설정 -->
|
||||
<div id="nc-email-section">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin:16px 0 12px;">
|
||||
<span style="background:#ea4335;color:#fff;border-radius:6px;padding:3px 10px;font-size:.8rem;font-weight:700;">Gmail</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>발송 Gmail 계정</label>
|
||||
<input type="email" id="nc-gmail-user" placeholder="sender@gmail.com"/>
|
||||
@@ -391,23 +409,42 @@
|
||||
<div class="form-group">
|
||||
<label>Gmail 앱 비밀번호</label>
|
||||
<div class="pw-wrap">
|
||||
<input type="password" id="nc-gmail-pw" placeholder="변경 시에만 입력 (xxxx xxxx xxxx xxxx)"/>
|
||||
<input type="password" id="nc-gmail-pw" placeholder="편집 시 변경할 경우에만 입력"/>
|
||||
<button class="pw-toggle" onclick="togglePw('nc-gmail-pw',this)">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<p style="font-size:.78rem;color:var(--muted);margin-top:4px">myaccount.google.com/apppasswords 에서 발급하세요. 변경하지 않을 경우 비워두세요.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>수신 이메일 주소</label>
|
||||
<input type="email" id="nc-alert-to" placeholder="receiver@gmail.com"/>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:10px;justify-content:flex-end;">
|
||||
<button class="btn btn-sm btn-gray" onclick="loadNotifyConfig()">초기화</button>
|
||||
<button class="btn btn-sm" onclick="saveNotifyConfig()">💾 저장</button>
|
||||
|
||||
<div class="form-group" style="display:flex;align-items:center;gap:10px;">
|
||||
<input type="checkbox" id="nc-enabled" checked style="width:16px;height:16px;accent-color:var(--primary)"/>
|
||||
<label for="nc-enabled" style="cursor:pointer;margin:0;">활성화</label>
|
||||
</div>
|
||||
<div id="notify-save-msg" style="margin-top:12px;font-size:.85rem;text-align:right;"></div>
|
||||
|
||||
<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:8px;">
|
||||
<button class="btn btn-sm btn-gray" onclick="resetNotifyForm()">초기화</button>
|
||||
<button class="btn btn-sm" id="nc-save-btn" onclick="saveNotifyChannel()">💾 저장</button>
|
||||
</div>
|
||||
<div id="notify-save-msg" style="margin-top:10px;font-size:.85rem;text-align:right;"></div>
|
||||
<input type="hidden" id="nc-editing-id" value=""/>
|
||||
</div>
|
||||
|
||||
<!-- 우측: 채널 리스트 -->
|
||||
<div class="table-wrapper" style="overflow:hidden;">
|
||||
<div style="padding:16px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;">
|
||||
<h3 style="font-size:1rem;font-weight:700;">등록된 채널 목록</h3>
|
||||
<span id="nc-count" style="font-size:.8rem;color:var(--muted);"></span>
|
||||
</div>
|
||||
<div id="notify-channel-list" style="max-height:520px;overflow-y:auto;">
|
||||
<div style="padding:40px;text-align:center;color:var(--muted);">등록된 채널이 없습니다.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -960,34 +997,129 @@ async function saveAccess(){
|
||||
closeModal('access-modal');alert('권한이 저장되었습니다.');
|
||||
}
|
||||
|
||||
// ── 알림 설정 ──────────────────────────────────────────────
|
||||
async function loadNotifyConfig(){
|
||||
try{
|
||||
const data = await apiFetch('/admin/notify-config');
|
||||
document.getElementById('nc-discord-url').value = data.discord_webhook_url || '';
|
||||
document.getElementById('nc-gmail-user').value = data.gmail_user || '';
|
||||
document.getElementById('nc-gmail-pw').value = data.gmail_app_password || '';
|
||||
document.getElementById('nc-alert-to').value = data.alert_email_to || '';
|
||||
document.getElementById('notify-save-msg').textContent = '';
|
||||
}catch(e){alert('알림 설정 불러오기 실패: '+e.message);}
|
||||
// ── 알림 채널 관리 ─────────────────────────────────────────
|
||||
let notifyChannels = [];
|
||||
|
||||
function onNotifyTypeChange(){
|
||||
const type = document.getElementById('nc-type').value;
|
||||
document.getElementById('nc-discord-section').style.display = (type==='email') ? 'none' : '';
|
||||
document.getElementById('nc-email-section').style.display = (type==='discord') ? 'none' : '';
|
||||
}
|
||||
|
||||
async function saveNotifyConfig(){
|
||||
async function loadNotifyConfig(){
|
||||
try{
|
||||
notifyChannels = await apiFetch('/admin/notify-channels');
|
||||
renderNotifyChannelList();
|
||||
document.getElementById('nc-count').textContent = `총 ${notifyChannels.length}개`;
|
||||
}catch(e){ console.error('알림 채널 로드 실패:', e.message); }
|
||||
}
|
||||
|
||||
function renderNotifyChannelList(){
|
||||
const el = document.getElementById('notify-channel-list');
|
||||
if(!notifyChannels.length){
|
||||
el.innerHTML = '<div style="padding:40px;text-align:center;color:var(--muted);">등록된 채널이 없습니다.</div>';
|
||||
return;
|
||||
}
|
||||
const typeLabel = {both:'Discord+Gmail', discord:'Discord', email:'Gmail'};
|
||||
const typeColor = {both:'#7c3aed', discord:'#5865f2', email:'#ea4335'};
|
||||
el.innerHTML = notifyChannels.map(ch => `
|
||||
<div id="nc-row-${ch.id}" style="padding:14px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;cursor:pointer;transition:background .15s;"
|
||||
onmouseover="this.style.background='#f8fafc'" onmouseout="this.style.background=''"
|
||||
onclick="selectNotifyChannel(${ch.id})">
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;">
|
||||
<span style="font-weight:600;font-size:.9rem;">${escHtml(ch.name)}</span>
|
||||
<span style="background:${typeColor[ch.type]};color:#fff;border-radius:4px;padding:1px 7px;font-size:.72rem;">${typeLabel[ch.type]||ch.type}</span>
|
||||
${ch.enabled ? '' : '<span style="background:#94a3b8;color:#fff;border-radius:4px;padding:1px 7px;font-size:.72rem;">비활성</span>'}
|
||||
</div>
|
||||
<div style="font-size:.78rem;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
|
||||
${ch.discord_webhook_url ? '🔗 Webhook 설정됨' : ''}
|
||||
${ch.discord_webhook_url && ch.gmail_user ? ' · ' : ''}
|
||||
${ch.gmail_user ? `📧 ${escHtml(ch.gmail_user)} → ${escHtml(ch.alert_email_to)}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;flex-shrink:0;">
|
||||
<button class="btn btn-sm btn-gray" style="padding:5px 10px;" onclick="event.stopPropagation();selectNotifyChannel(${ch.id})">✏️</button>
|
||||
<button class="btn btn-sm btn-danger" style="padding:5px 10px;" onclick="event.stopPropagation();deleteNotifyChannel(${ch.id})">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function selectNotifyChannel(id){
|
||||
const ch = notifyChannels.find(c => c.id === id);
|
||||
if(!ch) return;
|
||||
// 기존 선택 해제
|
||||
document.querySelectorAll('[id^=nc-row-]').forEach(el => el.style.background = '');
|
||||
document.getElementById('nc-row-'+id).style.background = '#eff6ff';
|
||||
|
||||
document.getElementById('nc-editing-id').value = ch.id;
|
||||
document.getElementById('nc-name').value = ch.name;
|
||||
document.getElementById('nc-type').value = ch.type;
|
||||
document.getElementById('nc-discord-url').value = ch.discord_webhook_url || '';
|
||||
document.getElementById('nc-gmail-user').value = ch.gmail_user || '';
|
||||
document.getElementById('nc-gmail-pw').value = ''; // 보안상 비움
|
||||
document.getElementById('nc-alert-to').value = ch.alert_email_to || '';
|
||||
document.getElementById('nc-enabled').checked = ch.enabled;
|
||||
document.getElementById('notify-form-title').textContent = '✏️ 채널 편집';
|
||||
document.getElementById('nc-save-btn').textContent = '💾 수정 저장';
|
||||
document.getElementById('notify-save-msg').textContent = '';
|
||||
onNotifyTypeChange();
|
||||
}
|
||||
|
||||
function resetNotifyForm(){
|
||||
document.getElementById('nc-editing-id').value = '';
|
||||
document.getElementById('nc-name').value = '';
|
||||
document.getElementById('nc-type').value = 'both';
|
||||
document.getElementById('nc-discord-url').value = '';
|
||||
document.getElementById('nc-gmail-user').value = '';
|
||||
document.getElementById('nc-gmail-pw').value = '';
|
||||
document.getElementById('nc-alert-to').value = '';
|
||||
document.getElementById('nc-enabled').checked = true;
|
||||
document.getElementById('notify-form-title').textContent = '➕ 채널 추가';
|
||||
document.getElementById('nc-save-btn').textContent = '💾 저장';
|
||||
document.getElementById('notify-save-msg').textContent = '';
|
||||
document.querySelectorAll('[id^=nc-row-]').forEach(el => el.style.background = '');
|
||||
onNotifyTypeChange();
|
||||
}
|
||||
|
||||
async function saveNotifyChannel(){
|
||||
const msgEl = document.getElementById('notify-save-msg');
|
||||
const editingId = document.getElementById('nc-editing-id').value;
|
||||
const name = document.getElementById('nc-name').value.trim();
|
||||
if(!name){ msgEl.style.color='var(--danger)'; msgEl.textContent='❌ 채널 이름을 입력하세요.'; return; }
|
||||
|
||||
const payload = {
|
||||
name,
|
||||
type: document.getElementById('nc-type').value,
|
||||
discord_webhook_url: document.getElementById('nc-discord-url').value.trim(),
|
||||
gmail_user: document.getElementById('nc-gmail-user').value.trim(),
|
||||
gmail_app_password: document.getElementById('nc-gmail-pw').value,
|
||||
alert_email_to: document.getElementById('nc-alert-to').value.trim(),
|
||||
enabled: document.getElementById('nc-enabled').checked,
|
||||
};
|
||||
try{
|
||||
await apiFetch('/admin/notify-config',{method:'PUT',body:JSON.stringify(payload)});
|
||||
msgEl.style.color = 'var(--success)';
|
||||
msgEl.textContent = '✅ 설정이 저장되었습니다.';
|
||||
}catch(e){
|
||||
msgEl.style.color = 'var(--danger)';
|
||||
msgEl.textContent = '❌ 저장 실패: '+e.message;
|
||||
if(editingId){
|
||||
await apiFetch(`/admin/notify-channels/${editingId}`, {method:'PUT', body:JSON.stringify(payload)});
|
||||
msgEl.style.color='var(--success)'; msgEl.textContent='✅ 수정되었습니다.';
|
||||
} else {
|
||||
await apiFetch('/admin/notify-channels', {method:'POST', body:JSON.stringify(payload)});
|
||||
msgEl.style.color='var(--success)'; msgEl.textContent='✅ 채널이 추가되었습니다.';
|
||||
}
|
||||
await loadNotifyConfig();
|
||||
resetNotifyForm();
|
||||
}catch(e){
|
||||
msgEl.style.color='var(--danger)'; msgEl.textContent='❌ 저장 실패: '+e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteNotifyChannel(id){
|
||||
if(!confirm('이 채널을 삭제하시겠습니까?')) return;
|
||||
try{
|
||||
await apiFetch(`/admin/notify-channels/${id}`, {method:'DELETE'});
|
||||
await loadNotifyConfig();
|
||||
resetNotifyForm();
|
||||
}catch(e){ alert('삭제 실패: '+e.message); }
|
||||
}
|
||||
|
||||
async function testNotify(){
|
||||
|
||||
Reference in New Issue
Block a user