diff --git a/README.md b/README.md index d6944bd..77808cc 100644 --- a/README.md +++ b/README.md @@ -903,33 +903,4 @@ git reset --hard HEAD~1 ```python SESSION_COOKIE_SECURE = False # HTTP ν™˜κ²½ REMEMBER_COOKIE_SECURE = False - ``` - -## πŸ“„ λΌμ΄μ„ μŠ€ - -이 ν”„λ‘œμ νŠΈλŠ” MIT λΌμ΄μ„ μŠ€ ν•˜μ— λ°°ν¬λ©λ‹ˆλ‹€. - -## πŸ“§ μ—°λ½μ²˜ 및 지원 - -- **이슈 리포트**: GitHub Issuesλ₯Ό 톡해 버그 리포트 및 κΈ°λŠ₯ μš”μ²­ -- **문의**: ν”„λ‘œμ νŠΈ κ΄€λ ¨ λ¬Έμ˜μ‚¬ν•­μ€ 이슈λ₯Ό λ“±λ‘ν•΄μ£Όμ„Έμš” - -## πŸ™ κΈ°μ—¬ - -κΈ°μ—¬λ₯Ό ν™˜μ˜ν•©λ‹ˆλ‹€! Pull Requestλ₯Ό λ³΄λ‚΄μ£Όμ„Έμš”. - -1. Fork the Project -2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) -3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) -4. Push to the Branch (`git push origin feature/AmazingFeature`) -5. Open a Pull Request - -## πŸ“ λ³€κ²½ 둜그 - -### v1.0.0 (2025-11-29) -- 초기 릴리슀 -- iDRAC Redfish API 톡합 -- Telegram 봇 μ•Œλ¦Ό κΈ°λŠ₯ -- DRM μΉ΄νƒˆλ‘œκ·Έ 동기화 -- μ‚¬μš©μž 승인 μ›Œν¬ν”Œλ‘œμš° -- μ‹€μ‹œκ°„ 파일 λͺ¨λ‹ˆν„°λ§ \ No newline at end of file + ``` \ No newline at end of file diff --git a/__pycache__/telegram_bot_service.cpython-314.pyc b/__pycache__/telegram_bot_service.cpython-314.pyc index 4ffcbf9..0997c23 100644 Binary files a/__pycache__/telegram_bot_service.cpython-314.pyc and b/__pycache__/telegram_bot_service.cpython-314.pyc differ diff --git a/app.py b/app.py index 08d1917..f8c0f19 100644 --- a/app.py +++ b/app.py @@ -151,8 +151,12 @@ def start_telegram_bot_polling() -> None: # ───────────────────────────────────────────────────────────── # ν…”λ ˆκ·Έλž¨ 봇 폴링 μžλ™ μ‹œμž‘ # Flask 앱이 μ΄ˆκΈ°ν™”λ˜λ©΄ μžλ™μœΌλ‘œ 봇 폴링 μ‹œμž‘ +# 주의: Flask λ¦¬λ‘œλ”(Debug λͺ¨λ“œ) μ‚¬μš© μ‹œ 메인/μ›Œμ»€ ν”„λ‘œμ„ΈμŠ€ 쀑볡 μ‹€ν–‰ λ°©μ§€ # ───────────────────────────────────────────────────────────── -start_telegram_bot_polling() +# 1. λ¦¬λ‘œλ”μ˜ μ›Œμ»€ ν”„λ‘œμ„ΈμŠ€μΈ 경우 (WERKZEUG_RUN_MAIN = "true") +# 2. λ˜λŠ” 디버그 λͺ¨λ“œκ°€ κΊΌμ§„ 경우 (Production) +if os.environ.get("WERKZEUG_RUN_MAIN") == "true" or not app.config.get("DEBUG"): + start_telegram_bot_polling() # ───────────────────────────────────────────────────────────── @@ -162,5 +166,9 @@ if __name__ == "__main__": host = os.getenv("FLASK_HOST", "0.0.0.0") port = int(os.getenv("FLASK_PORT", 5000)) debug = os.getenv("FLASK_DEBUG", "true").lower() == "true" + + # python app.py둜 직접 μ‹€ν–‰ μ‹œ(use_reloader=False)μ—λŠ” μœ„ μ‘°κ±΄λ¬Έμ—μ„œ μ‹€ν–‰λ˜μ§€ μ•Šμ„ 수 μžˆμœΌλ―€λ‘œ + # μ—¬κΈ°μ„œ λͺ…μ‹œμ μœΌλ‘œ μ‹€ν–‰ (쀑볡 μ‹€ν–‰ λ°©μ§€ ν”Œλž˜κ·Έκ°€ μžˆμ–΄ μ•ˆμ „ν•¨) + start_telegram_bot_polling() socketio.run(app, host=host, port=port, debug=debug, allow_unsafe_werkzeug=True, use_reloader=False) \ No newline at end of file diff --git a/backend/instance/site.db b/backend/instance/site.db index ca337bf..923f48c 100644 Binary files a/backend/instance/site.db and b/backend/instance/site.db differ diff --git a/backend/routes/__pycache__/admin.cpython-314.pyc b/backend/routes/__pycache__/admin.cpython-314.pyc index aaed1bc..94b58a7 100644 Binary files a/backend/routes/__pycache__/admin.cpython-314.pyc and b/backend/routes/__pycache__/admin.cpython-314.pyc differ diff --git a/backend/routes/__pycache__/auth.cpython-314.pyc b/backend/routes/__pycache__/auth.cpython-314.pyc index 1753904..eb38eca 100644 Binary files a/backend/routes/__pycache__/auth.cpython-314.pyc and b/backend/routes/__pycache__/auth.cpython-314.pyc differ diff --git a/backend/routes/__pycache__/main.cpython-314.pyc b/backend/routes/__pycache__/main.cpython-314.pyc index 52687d9..1bab7a9 100644 Binary files a/backend/routes/__pycache__/main.cpython-314.pyc and b/backend/routes/__pycache__/main.cpython-314.pyc differ diff --git a/backend/routes/__pycache__/scp_routes.cpython-314.pyc b/backend/routes/__pycache__/scp_routes.cpython-314.pyc index 730d1b1..707851f 100644 Binary files a/backend/routes/__pycache__/scp_routes.cpython-314.pyc and b/backend/routes/__pycache__/scp_routes.cpython-314.pyc differ diff --git a/backend/routes/__pycache__/utilities.cpython-314.pyc b/backend/routes/__pycache__/utilities.cpython-314.pyc index 8e913b8..fce965f 100644 Binary files a/backend/routes/__pycache__/utilities.cpython-314.pyc and b/backend/routes/__pycache__/utilities.cpython-314.pyc differ diff --git a/backend/routes/admin.py b/backend/routes/admin.py index c9a38fb..69a4cdc 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -237,3 +237,55 @@ def test_bot(bot_id): flash(f"ν…ŒμŠ€νŠΈ μ‹€νŒ¨: {e}", "danger") return redirect(url_for("admin.settings")) + + +# β–Όβ–Όβ–Ό μ‹œμŠ€ν…œ 둜그 λ·°μ–΄ β–Όβ–Όβ–Ό +@admin_bp.route("/admin/logs", methods=["GET"]) +@login_required +@admin_required +def view_logs(): + import os + import re + from collections import deque + + log_folder = current_app.config.get('LOG_FOLDER') + log_file = os.path.join(log_folder, 'app.log') if log_folder else None + + # 1. μ‹€μ œ ANSI μ΄μŠ€μΌ€μ΄ν”„ μ½”λ“œ (\x1B둜 μ‹œμž‘) + ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]') + + # 2. ν…μŠ€νŠΈλ‘œ 찍힌 ANSI μ½”λ“œ νŒ¨ν„΄ (예: [36m, [0m λ“±) - Werkzeugκ°€ μ΄μŠ€μΌ€μ΄ν”„ 된 μƒνƒœλ‘œ λ‘œκ·Έμ— 남길 경우 λŒ€λΉ„ + literal_ansi = re.compile(r'\[[0-9;]+m') + + # 3. μ œμ–΄ 문자 제거 + control_char_re = re.compile(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]') + + logs = [] + if log_file and os.path.exists(log_file): + try: + with open(log_file, 'r', encoding='utf-8', errors='replace') as f: + raw_lines = deque(f, 1000) + + for line in raw_lines: + # A. μ‹€μ œ ANSI μ½”λ“œ 제거 + clean_line = ansi_escape.sub('', line) + + # B. λ¦¬ν„°λŸ΄ ANSI νŒ¨ν„΄ 제거 (μ‚¬μš©μžκ°€ [36m 등을 ν…μŠ€νŠΈλ‘œ 보고 μžˆλ‹€λ©΄ 이것이 원인) + clean_line = literal_ansi.sub('', clean_line) + + # C. μ œμ–΄ 문자 제거 + clean_line = control_char_re.sub('', clean_line) + + # D. μ•žλ’€ 곡백 제거 + clean_line = clean_line.strip() + + # E. 빈 쀄 μ œμ™Έ + if clean_line: + logs.append(clean_line) + + except Exception as e: + logs = [f"Error reading log file: {str(e)}"] + else: + logs = [f"Log file not found at: {log_file}"] + + return render_template("admin_logs.html", logs=logs) diff --git a/backend/routes/auth.py b/backend/routes/auth.py index 33f54d0..68a07ad 100644 --- a/backend/routes/auth.py +++ b/backend/routes/auth.py @@ -293,6 +293,13 @@ def register(): return redirect(url_for("auth.login")) else: if request.method == "POST": + # 폼 검증 μ‹€νŒ¨ μ—λŸ¬λ₯Ό Flash λ©”μ‹œμ§€λ‘œ 좜λ ₯ + for field_name, errors in form.errors.items(): + for error in errors: + # ν•„λ“œ 객체 κ°€μ Έμ˜€κΈ° (라벨 ν…μŠ€νŠΈ ν™•μΈμš©) + field = getattr(form, field_name, None) + label = field.label.text if field else field_name + flash(f"{label}: {error}", "warning") current_app.logger.info("REGISTER: form errors=%s", form.errors) return render_template("register.html", form=form) diff --git a/backend/routes/main.py b/backend/routes/main.py index 4f28d26..92cbb90 100644 --- a/backend/routes/main.py +++ b/backend/routes/main.py @@ -47,11 +47,43 @@ def index(): info_dir = Path(Config.IDRAC_INFO_FOLDER) backup_dir = Path(Config.BACKUP_FOLDER) - scripts = [f.name for f in script_dir.glob("*") if f.is_file() and f.name != ".env"] - scripts = natsorted(scripts) + # 1. 슀크립트 λͺ©λ‘ 쑰회 및 μΉ΄ν…Œκ³ λ¦¬ λΆ„λ₯˜ + all_scripts = [f.name for f in script_dir.glob("*") if f.is_file() and f.name != ".env"] + all_scripts = natsorted(all_scripts) + + grouped_scripts = {} + for script in all_scripts: + upper = script.upper() + category = "General" + if upper.startswith("GPU"): + category = "GPU" + elif upper.startswith("LOM"): + category = "LOM" + elif upper.startswith("TYPE") or upper.startswith("XE"): + category = "Server Models" + elif "MAC" in upper: + category = "MAC Info" + elif "GUID" in upper: + category = "GUID Info" + elif "SET_" in upper or "CONFIG" in upper: + category = "Configuration" + + if category not in grouped_scripts: + grouped_scripts[category] = [] + grouped_scripts[category].append(script) + + # μΉ΄ν…Œκ³ λ¦¬ μ •λ ¬ (General은 λ§ˆμ§€λ§‰μ—) + sorted_categories = sorted(grouped_scripts.keys()) + if "General" in sorted_categories: + sorted_categories.remove("General") + sorted_categories.append("General") + + grouped_scripts_sorted = {k: grouped_scripts[k] for k in sorted_categories} + + # 2. XML 파일 λͺ©λ‘ xml_files = [f.name for f in xml_dir.glob("*.xml")] - # νŽ˜μ΄μ§€λ„€μ΄μ…˜ + # 3. νŽ˜μ΄μ§€λ„€μ΄μ…˜ 및 파일 λͺ©λ‘ page = int(request.args.get("page", 1)) info_files = [f.name for f in info_dir.glob("*") if f.is_file()] info_files = natsorted(info_files) @@ -62,11 +94,10 @@ def index(): total_pages = (len(info_files) + Config.FILES_PER_PAGE - 1) // Config.FILES_PER_PAGE - # βœ… μΆ”κ°€: 10개 λ‹¨μœ„λ‘œ ν‘œμ‹œλ  νŽ˜μ΄μ§€ λ²”μœ„ 계산 start_page = ((page - 1) // 10) * 10 + 1 end_page = min(start_page + 9, total_pages) - # λ°±μ—… 폴더 λͺ©λ‘ (λ””λ ‰ν„°λ¦¬λ§Œ) + # 4. λ°±μ—… 폴더 λͺ©λ‘ backup_dirs = [d for d in backup_dir.iterdir() if d.is_dir()] backup_dirs.sort(key=lambda p: p.stat().st_mtime, reverse=True) @@ -91,7 +122,8 @@ def index(): backup_files=backup_files, total_backup_pages=total_backup_pages, backup_page=backup_page, - scripts=scripts, + scripts=all_scripts, # κΈ°μ‘΄ 리슀트 ν˜Έν™˜ + grouped_scripts=grouped_scripts_sorted, # μΉ΄ν…Œκ³ λ¦¬λ³„ λΆ„λ₯˜ xml_files=xml_files, ) diff --git a/backend/routes/scp_routes.py b/backend/routes/scp_routes.py index 7eae626..dcd10a2 100644 --- a/backend/routes/scp_routes.py +++ b/backend/routes/scp_routes.py @@ -32,26 +32,48 @@ def diff_scp(): return redirect(url_for("xml.xml_management")) # 파일 λ‚΄μš© 읽기 (LF둜 톡일) - content1 = file1_path.read_text(encoding="utf-8").replace("\r\n", "\n").splitlines() - content2 = file2_path.read_text(encoding="utf-8").replace("\r\n", "\n").splitlines() - - # Diff 생성 - diff = difflib.unified_diff( - content1, content2, - fromfile=file1_name, - tofile=file2_name, - lineterm="" - ) + # 파일 λ‚΄μš© 읽기 (LF둜 톡일) + # Monaco Editor에 원본 ν…μŠ€νŠΈλ₯Ό κ·ΈλŒ€λ‘œ μ „λ‹¬ν•˜κΈ° μœ„ν•΄ splitlines() 제거 + # 파일 λ‚΄μš© 읽기 (LF둜 톡일) + logger.info(f"Reading file1: {file1_path}") + content1 = file1_path.read_text(encoding="utf-8", errors="replace").replace("\r\n", "\n") - diff_content = "\n".join(diff) + logger.info(f"Reading file2: {file2_path}") + content2 = file2_path.read_text(encoding="utf-8", errors="replace").replace("\r\n", "\n") - return render_template("scp_diff.html", file1=file1_name, file2=file2_name, diff_content=diff_content) + logger.info(f"Content1 length: {len(content1)}, Content2 length: {len(content2)}") + + return render_template("scp_diff.html", + file1=file1_name, + file2=file2_name, + content1=content1, + content2=content2) except Exception as e: logger.error(f"Diff error: {e}") flash(f"비ꡐ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {str(e)}", "danger") return redirect(url_for("xml.xml_management")) +@scp_bp.route("/scp/content/") +@login_required +def get_scp_content(filename): + """ + XML 파일 λ‚΄μš©μ„ λ°˜ν™˜ν•˜λŠ” API (Monaco Editor용) + """ + try: + safe_name = sanitize_preserve_unicode(filename) + path = Path(Config.XML_FOLDER) / safe_name + + if not path.exists(): + return "File not found", 404 + + # ν…μŠ€νŠΈλ‘œ μ½μ–΄μ„œ λ°˜ν™˜ + content = path.read_text(encoding="utf-8", errors="replace").replace("\r\n", "\n") + return content, 200, {'Content-Type': 'text/plain; charset=utf-8'} + except Exception as e: + logger.error(f"Content read error: {e}") + return str(e), 500 + @scp_bp.route("/scp/export", methods=["POST"]) @login_required def export_scp(): diff --git a/backend/routes/utilities.py b/backend/routes/utilities.py index 34cf208..832c974 100644 --- a/backend/routes/utilities.py +++ b/backend/routes/utilities.py @@ -290,13 +290,79 @@ def update_gpu_list(): return redirect(url_for("main.index")) -@utils_bp.route("/download_excel") -@login_required -def download_excel(): - path = Path(Config.SERVER_LIST_FOLDER) / "mac_info.xlsx" - if not path.is_file(): - flash("μ—‘μ…€ νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.", "danger") - return redirect(url_for("main.index")) - logging.info(f"μ—‘μ…€ 파일 λ‹€μš΄λ‘œλ“œ: {path}") - return send_file(str(path), as_attachment=True, download_name="mac_info.xlsx") \ No newline at end of file + return send_file(str(path), as_attachment=True, download_name="mac_info.xlsx") + + +@utils_bp.route("/scan_network", methods=["POST"]) +@login_required +def scan_network(): + """ + μ§€μ •λœ IP λ²”μœ„(Start ~ End)에 λŒ€ν•΄ Ping ν…ŒμŠ€νŠΈλ₯Ό μˆ˜ν–‰ν•˜κ³  + 응닡이 μžˆλŠ” IP λͺ©λ‘μ„ λ°˜ν™˜ν•©λ‹ˆλ‹€. + """ + import ipaddress + import platform + import concurrent.futures + + data = request.get_json() + start_ip_str = data.get('start_ip') + end_ip_str = data.get('end_ip') + + if not start_ip_str or not end_ip_str: + return jsonify({"success": False, "error": "μ‹œμž‘ IP와 μ’…λ£Œ IPλ₯Ό λͺ¨λ‘ μž…λ ₯ν•΄μ£Όμ„Έμš”."}), 400 + + try: + start_ip = ipaddress.IPv4Address(start_ip_str) + end_ip = ipaddress.IPv4Address(end_ip_str) + + if start_ip > end_ip: + return jsonify({"success": False, "error": "μ‹œμž‘ IPκ°€ μ’…λ£Œ IP보닀 ν½λ‹ˆλ‹€."}), 400 + + # IP 개수 μ œν•œ (λ„ˆλ¬΄ λ§Žμ€ μŠ€μΊ” λ°©μ§€, 예: C클래슀 2개 λΆ„λŸ‰ 512개) + if int(end_ip) - int(start_ip) > 512: + return jsonify({"success": False, "error": "μŠ€μΊ” λ²”μœ„κ°€ λ„ˆλ¬΄ λ„“μŠ΅λ‹ˆλ‹€. (μ΅œλŒ€ 512개)"}), 400 + + except ValueError: + return jsonify({"success": False, "error": "μœ νš¨ν•˜μ§€ μ•Šμ€ IP μ£Όμ†Œ ν˜•μ‹μž…λ‹ˆλ‹€."}), 400 + + # Ping ν•¨μˆ˜ μ •μ˜ + def ping_ip(ip_obj): + ip = str(ip_obj) + param = '-n' if platform.system().lower() == 'windows' else '-c' + timeout_param = '-w' if platform.system().lower() == 'windows' else '-W' + # Windows: -w 200 (ms), Linux: -W 1 (s) + timeout_val = '200' if platform.system().lower() == 'windows' else '1' + + command = ['ping', param, '1', timeout_param, timeout_val, ip] + + try: + # shell=False둜 λ³΄μ•ˆ κ°•ν™”, stdout/stderr λ¬΄μ‹œ + res = subprocess.run(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + return ip if res.returncode == 0 else None + except Exception: + return None + + active_ips = [] + + # IP 리슀트 생성 + # ipaddress λͺ¨λ“ˆμ„ μ‚¬μš©ν•˜μ—¬ λ²”μœ„ λ‚΄ IP 생성 + target_ips = [] + temp_ip = start_ip + while temp_ip <= end_ip: + target_ips.append(temp_ip) + temp_ip += 1 + + # 병렬 처리 (μ΅œλŒ€ 50 μ“°λ ˆλ“œ) + with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor: + results = executor.map(ping_ip, target_ips) + + # κ²°κ³Ό μˆ˜μ§‘ (None μ œμ™Έ) + active_ips = [ip for ip in results if ip is not None] + + return jsonify({ + "success": True, + "active_ips": active_ips, + "count": len(active_ips), + "message": f"μŠ€μΊ” μ™„λ£Œ: {len(active_ips)}개의 ν™œμ„± IP 발견" + }) \ No newline at end of file diff --git a/backend/services/__pycache__/logger.cpython-314.pyc b/backend/services/__pycache__/logger.cpython-314.pyc index f111195..f1a404b 100644 Binary files a/backend/services/__pycache__/logger.cpython-314.pyc and b/backend/services/__pycache__/logger.cpython-314.pyc differ diff --git a/backend/services/logger.py b/backend/services/logger.py index 9aef772..44e1156 100644 --- a/backend/services/logger.py +++ b/backend/services/logger.py @@ -60,6 +60,19 @@ def setup_logging(app: Optional[object] = None) -> logging.Logger: # Flask μ•± λ‘œκ±°μ—λ„ 동일 ν•Έλ“€λŸ¬ 바인딩 app.logger.handlers = root.handlers app.logger.setLevel(root.level) + # 루트 둜거둜 μ „νŒŒλ˜λ©΄ λ©”μ‹œμ§€κ°€ 두 번 좜λ ₯λ˜λ―€λ‘œ λ°©μ§€ + app.logger.propagate = False + + # 제3자 라이브러리 둜그 레벨 μ‘°μ • (λ„ˆλ¬΄ μ‹œλ„λŸ¬μš΄ 경우) + # werkzeug: 기본적인 HTTP μš”μ²­ 둜그(GET/POST λ“±)λ₯Ό μˆ¨κΉ€ (WARNING μ΄μƒλ§Œ ν‘œμ‹œ) + logging.getLogger("werkzeug").setLevel(logging.WARNING) + logging.getLogger("socketio").setLevel(logging.WARNING) + logging.getLogger("engineio").setLevel(logging.WARNING) + + # httpx, telegram 라이브러리의 HTTP μš”μ²­ 둜그 숨기기 + logging.getLogger("httpx").setLevel(logging.WARNING) + logging.getLogger("httpcore").setLevel(logging.WARNING) + logging.getLogger("telegram").setLevel(logging.WARNING) root.info("Logger initialized | level=%s | file=%s", _DEF_LEVEL, log_path) return root \ No newline at end of file diff --git a/backend/static/js/index.js b/backend/static/js/index.js index c33d5f6..38b1001 100644 --- a/backend/static/js/index.js +++ b/backend/static/js/index.js @@ -1,5 +1,7 @@ document.addEventListener('DOMContentLoaded', () => { + + // ───────────────────────────────────────────────────────────── // 슀크립트 선택 μ‹œ XML λ“œλ‘­λ‹€μš΄ ν† κΈ€ // ───────────────────────────────────────────────────────────── @@ -77,6 +79,37 @@ document.addEventListener('DOMContentLoaded', () => { const csrfToken = document.querySelector('input[name="csrf_token"]')?.value || ''; + // ───────────────────────────────────────────────────────────── + // IP μž…λ ₯ 데이터 보쑴 (Local Storage) + // ───────────────────────────────────────────────────────────── + const ipTextarea = document.getElementById('ips'); + const ipForm = document.getElementById('ipForm'); + const STORAGE_KEY_IP = 'ip_input_draft'; + + if (ipTextarea) { + // 1. νŽ˜μ΄μ§€ λ‘œλ“œ μ‹œ μ €μž₯된 κ°’ 볡원 + const savedIps = localStorage.getItem(STORAGE_KEY_IP); + if (savedIps) { + ipTextarea.value = savedIps; + // 라인 수 μ—…λ°μ΄νŠΈ 트리거 + if (window.updateIpCount) window.updateIpCount(); + } + + // 2. μž…λ ₯ μ‹œλ§ˆλ‹€ μ €μž₯ + ipTextarea.addEventListener('input', () => { + localStorage.setItem(STORAGE_KEY_IP, ipTextarea.value); + // script.js에 μžˆλŠ” updateIpCount 호좜 (μžˆλ‹€λ©΄) + if (window.updateIpCount) window.updateIpCount(); + }); + + // 3. 폼 제좜 성곡 μ‹œ μ΄ˆκΈ°ν™”? + // μ‚¬μš©μžμ˜ μ˜λ„μ— 따라 닀름: "변경이 λ˜μ§€ μ•ŠλŠ” 이상 계속 κ°€μ§€κ³  있게" + // -> 제좜 후에도 μœ μ§€ν•˜λŠ” 것이 μš”μ²­ 사항에 뢀합함. + // λ§Œμ•½ 'μ„±κ³΅μ μœΌλ‘œ μž‘μ—…μ΄ λλ‚˜λ©΄ μ§€μ›Œλ‹¬λΌ'λŠ” μš”μ²­μ΄ 있으면 μ—¬κΈ°λ₯Ό μˆ˜μ •. + // ν˜„μž¬ μš”μ²­: "νŽ˜μ΄μ§€κ°€ λ¦¬μ…‹μ΄λ˜λ„ 변경이 λ˜μ§€ μ•ŠλŠ”μ΄μƒ 계속 κ°€μ§€κ³ μžˆκ²Œ" -> μœ μ§€. + } + + // ───────────────────────────────────────────────────────────── // 곡톡 POST ν•¨μˆ˜ // ───────────────────────────────────────────────────────────── @@ -160,4 +193,75 @@ document.addEventListener('DOMContentLoaded', () => { }); }, 5000); + + // ───────────────────────────────────────────────────────────── + // IP μŠ€μΊ” 둜직 (Modal) + // ───────────────────────────────────────────────────────────── + const btnScan = document.getElementById('btnStartScan'); + if (btnScan) { + btnScan.addEventListener('click', async () => { + const startIp = '10.10.0.1'; + const endIp = '10.10.0.255'; + const ipsTextarea = document.getElementById('ips'); + const progressBar = document.getElementById('progressBar'); + + // UI μƒνƒœ λ³€κ²½ (λ‘œλ”© 쀑) + const originalIcon = btnScan.innerHTML; + btnScan.disabled = true; + btnScan.innerHTML = ''; + + // 메인 μ§„ν–‰λ°” ν™œμš© + if (progressBar) { + const progressContainer = progressBar.closest('.progress'); + if (progressContainer) { + progressContainer.parentElement.classList.remove('d-none'); + } + progressBar.style.width = '100%'; + progressBar.classList.add('progress-bar-striped', 'progress-bar-animated'); + progressBar.textContent = 'λ„€νŠΈμ›Œν¬ μŠ€μΊ” 쀑... (10.10.0.1 ~ 255)'; + } + + try { + const res = await fetch('/utils/scan_network', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + body: JSON.stringify({ start_ip: startIp, end_ip: endIp }) + }); + + const data = await res.json(); + + if (data.success) { + if (data.active_ips && data.active_ips.length > 0) { + ipsTextarea.value = data.active_ips.join('\n'); + // 이벀트 트리거 + ipsTextarea.dispatchEvent(new Event('input')); + + alert(`μŠ€μΊ” μ™„λ£Œ: ${data.active_ips.length}개의 ν™œμ„± IPλ₯Ό μ°Ύμ•˜μŠ΅λ‹ˆλ‹€.`); + } else { + alert('ν™œμ„± IPλ₯Ό λ°œκ²¬ν•˜μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.'); + } + } else { + throw new Error(data.error || 'Unknown error'); + } + } catch (err) { + console.error(err); + alert('였λ₯˜ λ°œμƒ: ' + (err.message || err)); + } finally { + // μƒνƒœ 볡ꡬ + btnScan.disabled = false; + btnScan.innerHTML = originalIcon; + + if (progressBar) { + // μ§„ν–‰λ°” μ΄ˆκΈ°ν™” + progressBar.style.width = '0%'; + progressBar.textContent = '0%'; + progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated'); + } + } + }); + } + }); diff --git a/backend/templates/admin.html b/backend/templates/admin.html index bfca298..05fdda2 100644 --- a/backend/templates/admin.html +++ b/backend/templates/admin.html @@ -1,72 +1,151 @@ {# backend/templates/admin.html #} {% extends "base.html" %} +{% block title %}κ΄€λ¦¬μž νŒ¨λ„ - Dell Server Info{% endblock %} + {% block content %} -
-
-
- +
+ +
+
+

+ κ΄€λ¦¬μž νŒ¨λ„ +

+

μ‚¬μš©μž 관리 및 μ‹œμŠ€ν…œ 섀정을 μˆ˜ν–‰ν•©λ‹ˆλ‹€.

+
+ +
- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} -
- {% for cat, msg in messages %} -