diff --git a/backend/main.py b/backend/main.py index 927cf20..a9a1525 100755 --- a/backend/main.py +++ b/backend/main.py @@ -425,59 +425,73 @@ 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) - cur.execute("SELECT key, value FROM notify_config") - rows = cur.fetchall() - db_config = {r["key"]: r["value"] for r in rows} + 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": db_config.get("discord_webhook_url", os.getenv("DISCORD_WEBHOOK_URL", "")), - "gmail_user": db_config.get("gmail_user", os.getenv("GMAIL_USER", "")), - "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", "")), + "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.get("/api/admin/notify-config") -def get_notify_config(token=Depends(require_admin), conn=Depends(get_db)): - cfg = get_notify_config_from_db(conn) - # 비밀번호는 설정 여부만 반환 (보안) - 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"], - } - -@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 모듈 환경변수 동적 업데이트 +def refresh_notifier(conn): + """활성화된 모든 채널 중 첫 번째 채널로 notifier 모듈 업데이트""" import notifier cfg = get_notify_config_from_db(conn) notifier.DISCORD_WEBHOOK_URL = cfg["discord_webhook_url"] @@ -485,8 +499,77 @@ def save_notify_config(data: NotifyConfig, token=Depends(require_admin), conn=De 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} + return { + "discord_webhook_url": db_config.get("discord_webhook_url", os.getenv("DISCORD_WEBHOOK_URL", "")), + "gmail_user": db_config.get("gmail_user", os.getenv("GMAIL_USER", "")), + "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", "")), + } + except Exception: + 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", ""), + } + @app.get("/api/admin/notify-test") async def notify_test(token=Depends(require_admin)): # Discord + Gmail (Pod 이상/복구 테스트) diff --git a/frontend/index.html b/frontend/index.html index 27def9c..449a103 100755 --- a/frontend/index.html +++ b/frontend/index.html @@ -364,50 +364,87 @@
Discord Webhook 및 이메일 알림 수신 설정을 관리합니다.
Discord Webhook 및 이메일 알림 채널을 추가·수정·삭제합니다.
채널 설정 → 연동 → 웹후크에서 URL을 복사하세요.
-myaccount.google.com/apppasswords 에서 발급하세요. 변경하지 않을 경우 비워두세요.