import sys import time import os import requests from PySide6.QtWidgets import ( QApplication, QDialog, QMainWindow, QVBoxLayout, QHBoxLayout, QWidget, QLabel, QLineEdit, QPushButton, QTextEdit, QTableWidget, QTableWidgetItem, QTabWidget, QMessageBox, QComboBox ) from PySide6.QtCore import QTimer, Slot, Qt from supabase import create_client, Client ############################################## # 1. Supabase 클라이언트 초기화 ############################################## # SUPABASE_URL = os.environ.get("SUPABASE_URL", "https://sp1.cckb9998.synology.me") # SUPABASE_KEY = os.environ.get("SUPABASE_KEY", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q") SUPABASE_URL = os.environ.get("SUPABASE_URL", "http://oci1ckh08045.duckdns.org:8000") SUPABASE_KEY = os.environ.get("SUPABASE_KEY", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q") supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY) ############################################## # 2. Supabase API 연동 함수들 ############################################## def admin_sign_in(email: str, password: str): """Supabase Auth를 이용한 관리자 로그인 (실패 시 error 객체 반환)""" # result = supabase.auth.sign_in_with_password(email=email, password=password) result = supabase.auth.sign_in_with_password({"email": email, "password": password}) print(f"result : {result}") user_id = result.user.id # if result.get("error"): # return None, result["error"] return user_id, None def fetch_pending_signups(): """pending_signups 테이블의 대기 중인 가입 요청을 반환""" response = supabase.table("pending_signups").select("*").execute() if response.error: print("fetch_pending_signups error:", response.error) return [] return response.data def approve_signup(user_id: str): """ 가입 승인 처리 1. pending_signups에서 해당 사용자의 레코드를 조회 2. users 테이블에 신규 사용자 추가 (멤버십은 기본 'basic') 3. pending_signups에서 해당 레코드 삭제 """ resp = supabase.table("pending_signups").select("*").eq("id", user_id).execute() if resp.error or not resp.data: return False signup = resp.data[0] # 사용자 추가 – 추가 컬럼은 실제 스키마에 맞게 수정 필요 ins = supabase.table("users").insert({ "id": signup["id"], "email": signup["email"], "nickname": signup.get("nickname", "New User"), "membership_level": "basic" }).execute() if ins.error: print("approve_signup insert error:", ins.error) return False # pending_signups에서 삭제 del_resp = supabase.table("pending_signups").delete().eq("id", user_id).execute() return del_resp.error is None def fetch_common_banned_words(): """common_banned_words 테이블의 금지어 목록을 반환""" response = supabase.table("common_banned_words").select("*").execute() if response.error: print("fetch_common_banned_words error:", response.error) return [] return response.data def add_common_banned_word(word: str, grade: str, request_reason: str): """common_banned_words 테이블에 금지어 추가""" ins = supabase.table("common_banned_words").insert({ "banned_word": word, "grade": grade, "request_reason": request_reason }).execute() if ins.error: print("add_common_banned_word error:", ins.error) return ins.error is None def update_common_banned_word(word: str, new_grade: str, request_reason: str): """common_banned_words 테이블에서 금지어 수정""" upd = supabase.table("common_banned_words").update({ "grade": new_grade, "request_reason": request_reason }).eq("banned_word", word).execute() if upd.error: print("update_common_banned_word error:", upd.error) return upd.error is None def delete_common_banned_word(word: str): """common_banned_words 테이블에서 금지어 삭제""" del_resp = supabase.table("common_banned_words").delete().eq("banned_word", word).execute() if del_resp.error: print("delete_common_banned_word error:", del_resp.error) return del_resp.error is None def fetch_users(): """users 테이블의 사용자 목록을 반환""" response = supabase.table("users").select("*").execute() if response.error: print("fetch_users error:", response.error) return [] return response.data def update_membership(user_id: str, new_level: str): """users 테이블에서 특정 사용자의 멤버십 레벨 업데이트""" upd = supabase.table("users").update({ "membership_level": new_level }).eq("id", user_id).execute() if upd.error: print("update_membership error:", upd.error) return upd.error is None ############################################## # 3. Notification Manager (텔레그램 알림) ############################################## def send_telegram_notification(bot_token: str, chat_id: str, message: str) -> dict: """ 텔레그램 봇을 통해 메시지를 전송합니다. :param bot_token: 텔레그램 봇 토큰 :param chat_id: 메시지를 받을 채팅 ID :param message: 전송할 메시지 :return: API 응답 (JSON) """ url = f"https://api.telegram.org/bot{bot_token}/sendMessage" payload = {"chat_id": chat_id, "text": message} try: response = requests.post(url, data=payload) return response.json() except Exception as e: print("Telegram notification error:", e) return {"error": str(e)} ############################################## # 4. 관리자 로그인 다이얼로그 ############################################## class AdminLoginDialog(QDialog): def __init__(self): super().__init__() self.setWindowTitle("Admin Login") self.setFixedSize(300, 150) layout = QVBoxLayout() self.label_id = QLabel("Email:") self.edit_id = QLineEdit() layout.addWidget(self.label_id) layout.addWidget(self.edit_id) self.label_pw = QLabel("Password:") self.edit_pw = QLineEdit() self.edit_pw.setEchoMode(QLineEdit.Password) layout.addWidget(self.label_pw) layout.addWidget(self.edit_pw) self.btn_login = QPushButton("Login") self.btn_login.clicked.connect(self.handle_login) layout.addWidget(self.btn_login) self.setLayout(layout) @Slot() def handle_login(self): email = self.edit_id.text().strip() password = self.edit_pw.text().strip() user, error = admin_sign_in(email, password) if error or user is None: QMessageBox.warning(self, "Login Failed", f"Invalid credentials.\nError: {error}") else: self.accept() ############################################## # 5. PendingSignupsTab (회원가입 승인 탭) ############################################## class PendingSignupsTab(QWidget): def __init__(self): super().__init__() self.init_ui() self.refresh_data() def init_ui(self): layout = QVBoxLayout() self.table = QTableWidget(0, 5) self.table.setHorizontalHeaderLabels(["ID", "Email", "Request Reason", "Submitted At", "Attachments"]) layout.addWidget(self.table) self.btn_approve = QPushButton("Approve Selected Signup") self.btn_approve.clicked.connect(self.approve_selected) layout.addWidget(self.btn_approve) self.setLayout(layout) def refresh_data(self): signups = fetch_pending_signups() self.table.setRowCount(len(signups)) for row, signup in enumerate(signups): self.table.setItem(row, 0, QTableWidgetItem(str(signup.get("id", "")))) self.table.setItem(row, 1, QTableWidgetItem(signup.get("email", ""))) self.table.setItem(row, 2, QTableWidgetItem(signup.get("request_reason", ""))) self.table.setItem(row, 3, QTableWidgetItem(signup.get("submitted_at", ""))) self.table.setItem(row, 4, QTableWidgetItem(str(signup.get("attachments", "")))) @Slot() def approve_selected(self): selected = self.table.currentRow() if selected < 0: QMessageBox.warning(self, "No selection", "Select a signup to approve.") return user_id = self.table.item(selected, 0).text() if approve_signup(user_id): QMessageBox.information(self, "Approved", f"User {user_id} approved.") self.refresh_data() else: QMessageBox.warning(self, "Error", "Failed to approve signup.") ############################################## # 6. BannedWordsTab (Common Banned Words 관리 탭) ############################################## class BannedWordsTab(QWidget): def __init__(self): super().__init__() self.init_ui() self.refresh_data() def init_ui(self): layout = QVBoxLayout() self.table = QTableWidget(0, 4) self.table.setHorizontalHeaderLabels(["Word", "Grade", "Request Reason", "Created At"]) layout.addWidget(self.table) form_layout = QHBoxLayout() # 금지어 입력 (자유 텍스트) self.edit_word = QLineEdit() self.edit_word.setPlaceholderText("Word") form_layout.addWidget(self.edit_word) # 드롭다운: 금지어 레벨 선택 ("비허용", "금지") self.combo_grade = QComboBox() self.combo_grade.addItems(["비허용", "금지"]) form_layout.addWidget(self.combo_grade) # 요청 사유 입력 (멀티라인) self.edit_request_reason = QTextEdit() self.edit_request_reason.setPlaceholderText("Request Reason (텍스트, 링크, 사진, 문서 등)") self.edit_request_reason.setFixedHeight(50) form_layout.addWidget(self.edit_request_reason) self.btn_add = QPushButton("Add Word") self.btn_add.clicked.connect(self.add_word) self.btn_update = QPushButton("Update") self.btn_update.clicked.connect(self.update_word) self.btn_delete = QPushButton("Delete") self.btn_delete.clicked.connect(self.delete_word) form_layout.addWidget(self.btn_add) form_layout.addWidget(self.btn_update) form_layout.addWidget(self.btn_delete) layout.addLayout(form_layout) self.setLayout(layout) def refresh_data(self): words = fetch_common_banned_words() self.table.setRowCount(len(words)) for row, info in enumerate(words): self.table.setItem(row, 0, QTableWidgetItem(info.get("banned_word", ""))) self.table.setItem(row, 1, QTableWidgetItem(info.get("grade", ""))) self.table.setItem(row, 2, QTableWidgetItem(info.get("request_reason", ""))) self.table.setItem(row, 3, QTableWidgetItem(info.get("created_at", ""))) @Slot() def add_word(self): word = self.edit_word.text().strip() grade = self.combo_grade.currentText() request_reason = self.edit_request_reason.toPlainText().strip() if not word or not grade: QMessageBox.warning(self, "Input Error", "Please provide word and select a grade.") return if add_common_banned_word(word, grade, request_reason): QMessageBox.information(self, "Added", f"Word '{word}' added.") self.refresh_data() else: QMessageBox.warning(self, "Error", "Failed to add word (it may already exist).") @Slot() def update_word(self): selected = self.table.currentRow() if selected < 0: QMessageBox.warning(self, "No selection", "Select a word to update.") return word = self.table.item(selected, 0).text() new_grade = self.combo_grade.currentText() request_reason = self.edit_request_reason.toPlainText().strip() if update_common_banned_word(word, new_grade, request_reason): QMessageBox.information(self, "Updated", f"Word '{word}' updated.") self.refresh_data() else: QMessageBox.warning(self, "Error", "Failed to update word.") @Slot() def delete_word(self): selected = self.table.currentRow() if selected < 0: QMessageBox.warning(self, "No selection", "Select a word to delete.") return word = self.table.item(selected, 0).text() if delete_common_banned_word(word): QMessageBox.information(self, "Deleted", f"Word '{word}' deleted.") self.refresh_data() else: QMessageBox.warning(self, "Error", "Failed to delete word.") ############################################## # 7. UserInfoTab (유저 정보 관리 탭) ############################################## class UserInfoTab(QWidget): def __init__(self): super().__init__() self.init_ui() self.refresh_data() def init_ui(self): layout = QVBoxLayout() self.table = QTableWidget(0, 4) self.table.setHorizontalHeaderLabels(["ID", "Email", "Nickname", "Membership Level"]) layout.addWidget(self.table) form_layout = QHBoxLayout() # 드롭다운: 멤버십 레벨 선택 ("basic", "premium", "vip") self.combo_membership = QComboBox() self.combo_membership.addItems(["basic", "premium", "vip"]) form_layout.addWidget(QLabel("New Membership Level:")) form_layout.addWidget(self.combo_membership) self.btn_update_membership = QPushButton("Update Membership") self.btn_update_membership.clicked.connect(self.update_membership) form_layout.addWidget(self.btn_update_membership) layout.addLayout(form_layout) self.setLayout(layout) def refresh_data(self): users = fetch_users() self.table.setRowCount(len(users)) for row, user in enumerate(users): self.table.setItem(row, 0, QTableWidgetItem(str(user.get("id", "")))) self.table.setItem(row, 1, QTableWidgetItem(user.get("email", ""))) self.table.setItem(row, 2, QTableWidgetItem(user.get("nickname", ""))) self.table.setItem(row, 3, QTableWidgetItem(user.get("membership_level", ""))) @Slot() def update_membership(self): selected = self.table.currentRow() if selected < 0: QMessageBox.warning(self, "No selection", "Select a user to update membership.") return new_level = self.combo_membership.currentText() user_id = self.table.item(selected, 0).text() if update_membership(user_id, new_level): QMessageBox.information(self, "Updated", f"User {user_id} membership updated to {new_level}.") self.refresh_data() else: QMessageBox.warning(self, "Error", "Failed to update membership.") ############################################## # 8. AdminMainWindow (관리자 메인 윈도우) ############################################## # 알림 응답 타임아웃 (예: 10초) ALERT_RESPONSE_TIMEOUT = 10000 # 텔레그램 알림용 자격증명 (실제 값 입력) TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "your_bot_token_here") TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "your_chat_id_here") class AdminMainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Admin Client") self.setGeometry(100, 100, 900, 700) central_widget = QWidget() self.setCentralWidget(central_widget) layout = QVBoxLayout(central_widget) # 탭 위젯: 회원가입 승인, 금지어 관리, 유저 정보 관리 self.tabs = QTabWidget() self.pending_signups_tab = PendingSignupsTab() self.banned_words_tab = BannedWordsTab() self.user_info_tab = UserInfoTab() self.tabs.addTab(self.pending_signups_tab, "회원가입 승인") self.tabs.addTab(self.banned_words_tab, "Common Banned Words 관리") self.tabs.addTab(self.user_info_tab, "유저 정보 관리") layout.addWidget(self.tabs) # 실시간 알림 로그 영역 self.log_area = QTextEdit() self.log_area.setReadOnly(True) layout.addWidget(self.log_area) # 알림 응답 버튼 self.btn_respond = QPushButton("Respond to Alert", self) self.btn_respond.clicked.connect(self.user_response) layout.addWidget(self.btn_respond) # 실시간 알림 타이머 (예: 5초마다 알림 시뮬레이션) self.alert_timer = QTimer(self) self.alert_timer.timeout.connect(self.receive_alert) self.alert_timer.start(5000) # 응답 타이머 (알림 후 지정 시간 내 응답 없으면 외부 알림 발송) self.response_timer = QTimer(self) self.response_timer.setSingleShot(True) self.response_timer.timeout.connect(self.handle_no_response) self.current_alert = None @Slot() def receive_alert(self): alert_message = f"Alert received at {time.strftime('%H:%M:%S')}" self.log_area.append(alert_message) self.current_alert = alert_message self.response_timer.start(ALERT_RESPONSE_TIMEOUT) @Slot() def handle_no_response(self): if self.current_alert: notification_message = f"No response to alert: {self.current_alert}" self.log_area.append("No response detected, sending push notification...") result = send_telegram_notification(TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, notification_message) self.log_area.append(f"Telegram notification sent: {result}") self.current_alert = None @Slot() def user_response(self): if self.response_timer.isActive(): self.response_timer.stop() self.log_area.append("Admin responded to the alert.") self.current_alert = None ############################################## # 9. main() 함수: 실행 진입점 ############################################## def main(): app = QApplication(sys.argv) # 관리자 로그인 다이얼로그 login_dialog = AdminLoginDialog() if login_dialog.exec() != QDialog.Accepted: sys.exit("Login canceled or failed.") window = AdminMainWindow() window.show() sys.exit(app.exec()) if __name__ == "__main__": main()