Initial commit

This commit is contained in:
2025-10-05 17:37:51 +09:00
parent 5cbe9a2524
commit 3a7fabb830
219 changed files with 81295 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
from __future__ import annotations
from flask import Flask
from .home import register_home_routes
from .auth import register_auth_routes
from .admin import register_admin_routes
from .main import register_main_routes
from .xml import register_xml_routes
from .utilities import register_util_routes
from .file_view import register_file_view
def register_routes(app: Flask, socketio=None) -> None:
"""블루프린트 일괄 등록. socketio는 main 라우트에서만 사용."""
register_home_routes(app)
register_auth_routes(app)
register_admin_routes(app)
register_main_routes(app, socketio)
register_xml_routes(app)
register_util_routes(app)
register_file_view(app)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

126
backend/routes/admin.py Normal file
View File

@@ -0,0 +1,126 @@
# backend/routes/admin.py
from __future__ import annotations
import logging
from functools import wraps
from typing import Callable
from flask import (
Blueprint,
render_template,
redirect,
url_for,
flash,
abort,
request,
current_app,
)
from flask_login import login_required, current_user
from backend.models.user import User, db
admin_bp = Blueprint("admin", __name__)
# Blueprint 등록
def register_admin_routes(app):
app.register_blueprint(admin_bp)
# 관리자 권한 데코레이터
def admin_required(view_func: Callable):
@wraps(view_func)
def wrapper(*args, **kwargs):
if not current_user.is_authenticated:
return redirect(url_for("auth.login"))
if not getattr(current_user, "is_admin", False):
flash("관리자 권한이 필요합니다.", "danger")
return redirect(url_for("main.index"))
return view_func(*args, **kwargs)
return wrapper
# 관리자 대시보드
@admin_bp.route("/admin", methods=["GET"])
@login_required
@admin_required
def admin_panel():
users = db.session.query(User).order_by(User.id.asc()).all()
return render_template("admin.html", users=users)
# 사용자 승인
@admin_bp.route("/admin/approve/<int:user_id>", methods=["GET"])
@login_required
@admin_required
def approve_user(user_id: int):
user = db.session.get(User, user_id)
if not user:
abort(404)
user.is_active = True
db.session.commit()
flash("사용자가 승인되었습니다.", "success")
logging.info("✅ 승인된 사용자: %s (id=%s)", user.username, user.id)
return redirect(url_for("admin.admin_panel"))
# 사용자 삭제
@admin_bp.route("/admin/delete/<int:user_id>", methods=["GET"])
@login_required
@admin_required
def delete_user(user_id: int):
user = db.session.get(User, user_id)
if not user:
abort(404)
username = user.username
db.session.delete(user)
db.session.commit()
flash("사용자가 삭제되었습니다.", "success")
logging.info("🗑 삭제된 사용자: %s (id=%s)", username, user_id)
return redirect(url_for("admin.admin_panel"))
# ▼▼▼ 사용자 비밀번호 변경(관리자용) ▼▼▼
@admin_bp.route("/admin/users/<int:user_id>/reset_password", methods=["POST"])
@login_required
@admin_required
def reset_password(user_id: int):
"""
admin.html에서 각 사용자 행 아래 폼으로부터 POST:
- name="new_password"
- name="confirm_password"
CSRF는 템플릿에서 {{ csrf_token() }} 또는 {{ form.hidden_tag() }}로 포함되어야 합니다.
"""
new_pw = (request.form.get("new_password") or "").strip()
confirm = (request.form.get("confirm_password") or "").strip()
# 서버측 검증
if not new_pw or not confirm:
flash("비밀번호와 확인 값을 모두 입력하세요.", "warning")
return redirect(url_for("admin.admin_panel"))
if new_pw != confirm:
flash("비밀번호 확인이 일치하지 않습니다.", "warning")
return redirect(url_for("admin.admin_panel"))
if len(new_pw) < 8:
flash("비밀번호는 최소 8자 이상이어야 합니다.", "warning")
return redirect(url_for("admin.admin_panel"))
user = db.session.get(User, user_id)
if not user:
abort(404)
try:
# passlib(Argon2id) 기반 set_password 사용 (models.user에 구현됨)
user.set_password(new_pw)
db.session.commit()
flash(f"사용자(ID={user.id}) 비밀번호를 변경했습니다.", "success")
current_app.logger.info(
"ADMIN: reset password for user_id=%s by admin_id=%s",
user.id, current_user.id
)
except Exception as e:
db.session.rollback()
current_app.logger.exception("ADMIN: reset password failed: %s", e)
flash("비밀번호 변경 중 오류가 발생했습니다.", "danger")
return redirect(url_for("admin.admin_panel"))

180
backend/routes/auth.py Normal file
View File

@@ -0,0 +1,180 @@
# backend/routes/auth.py
from __future__ import annotations
import logging
import threading
from typing import Optional
from urllib.parse import urlparse, urljoin
from flask import (
Blueprint,
render_template,
redirect,
url_for,
flash,
request,
session,
current_app,
)
from flask_login import login_user, logout_user, current_user, login_required
from backend.forms.auth_forms import RegistrationForm, LoginForm
from backend.models.user import User, db
# ── (선택) Telegram: 미설정이면 조용히 패스
try:
from telegram import Bot
from telegram.constants import ParseMode
except Exception: # 라이브러리 미설치/미사용 환경
Bot = None
ParseMode = None
auth_bp = Blueprint("auth", __name__)
# ─────────────────────────────────────────────────────────────
# 유틸
# ─────────────────────────────────────────────────────────────
def _is_safe_url(target: str) -> bool:
"""로그인 후 next 파라미터의 안전성 확인(동일 호스트만 허용)."""
ref = urlparse(request.host_url)
test = urlparse(urljoin(request.host_url, target))
return (test.scheme in ("http", "https")) and (ref.netloc == test.netloc)
def _notify(text: str) -> None:
"""텔레그램 알림 (설정 없으면 바로 return)."""
token = (current_app.config.get("TELEGRAM_BOT_TOKEN") or "").strip()
chat_id = (current_app.config.get("TELEGRAM_CHAT_ID") or "").strip()
if not (token and chat_id and Bot and ParseMode):
return
def _send():
try:
bot = Bot(token=token)
bot.send_message(chat_id=chat_id, text=text, parse_mode=ParseMode.HTML)
except Exception as e:
current_app.logger.warning("Telegram send failed: %s", e)
threading.Thread(target=_send, daemon=True).start()
# ─────────────────────────────────────────────────────────────
# Blueprint 등록 훅
# ─────────────────────────────────────────────────────────────
def register_auth_routes(app):
"""app.py에서 register_routes(app, socketio) 호출 시 사용."""
app.register_blueprint(auth_bp)
@app.before_request
def _touch_session():
# 요청마다 세션 갱신(만료 슬라이딩) + 로그아웃 플래그 정리
session.modified = True
if current_user.is_authenticated and session.get("just_logged_out"):
session.pop("just_logged_out", None)
flash("세션이 만료되어 자동 로그아웃 되었습니다.", "info")
# ─────────────────────────────────────────────────────────────
# 회원가입
# ─────────────────────────────────────────────────────────────
@auth_bp.route("/register", methods=["GET", "POST"])
def register():
if current_user.is_authenticated:
current_app.logger.info("REGISTER: already auth → /index")
return redirect(url_for("main.index"))
form = RegistrationForm()
if form.validate_on_submit():
# 모델 내부에서 email/username 정규화됨(find_by_*)
if User.find_by_email(form.email.data):
flash("이미 등록된 이메일입니다.", "warning")
current_app.logger.info("REGISTER: dup email %s", form.email.data)
return render_template("register.html", form=form)
if User.find_by_username(form.username.data):
flash("이미 사용 중인 사용자명입니다.", "warning")
current_app.logger.info("REGISTER: dup username %s", form.username.data)
return render_template("register.html", form=form)
user = User(username=form.username.data, email=form.email.data, is_active=False)
user.set_password(form.password.data) # passlib: 기본 Argon2id
db.session.add(user)
db.session.commit()
_notify(
f"🆕 <b>신규 가입 요청</b>\n"
f"📛 사용자: <code>{user.username}</code>\n"
f"📧 이메일: <code>{user.email}</code>"
)
current_app.logger.info("REGISTER: created id=%s email=%s", user.id, user.email)
flash("회원가입이 완료되었습니다. 관리자의 승인을 기다려주세요.", "success")
return redirect(url_for("auth.login"))
else:
if request.method == "POST":
current_app.logger.info("REGISTER: form errors=%s", form.errors)
return render_template("register.html", form=form)
# ─────────────────────────────────────────────────────────────
# 로그인
# ─────────────────────────────────────────────────────────────
@auth_bp.route("/login", methods=["GET", "POST"])
def login():
if current_user.is_authenticated:
current_app.logger.info("LOGIN: already auth → /index")
return redirect(url_for("main.index"))
form = LoginForm()
if form.validate_on_submit():
current_app.logger.info("LOGIN: form ok email=%s", form.email.data)
user: Optional[User] = User.find_by_email(form.email.data)
if not user:
flash("이메일 또는 비밀번호가 올바르지 않습니다.", "danger")
current_app.logger.info("LOGIN: user not found")
return render_template("login.html", form=form)
pass_ok = user.check_password(form.password.data) # passlib verify(+자동 재해시)
current_app.logger.info(
"LOGIN: found id=%s active=%s pass_ok=%s",
user.id, user.is_active, pass_ok
)
if not pass_ok:
flash("이메일 또는 비밀번호가 올바르지 않습니다.", "danger")
return render_template("login.html", form=form)
if not user.is_active:
flash("계정이 아직 승인되지 않았습니다.", "warning")
return render_template("login.html", form=form)
# 성공
login_user(user, remember=form.remember.data)
session.permanent = True
_notify(f"🔐 <b>로그인 성공</b>\n👤 <code>{user.username}</code>")
current_app.logger.info("LOGIN: SUCCESS → redirect")
nxt = request.args.get("next")
if nxt and _is_safe_url(nxt):
return redirect(nxt)
return redirect(url_for("main.index"))
else:
if request.method == "POST":
current_app.logger.info("LOGIN: form errors=%s", form.errors)
return render_template("login.html", form=form)
# ─────────────────────────────────────────────────────────────
# 로그아웃
# ─────────────────────────────────────────────────────────────
@auth_bp.route("/logout", methods=["GET"])
@login_required
def logout():
if current_user.is_authenticated:
current_app.logger.info("LOGOUT: user=%s", current_user.username)
logout_user()
session["just_logged_out"] = True
return redirect(url_for("auth.login"))

View File

@@ -0,0 +1,95 @@
from __future__ import annotations
import logging
from pathlib import Path
from flask import Blueprint, request, jsonify, Response
from flask_login import login_required
from config import Config
import chardet
file_view_bp = Blueprint("file_view", __name__)
def register_file_view(app):
"""블루프린트 등록"""
app.register_blueprint(file_view_bp)
def _safe_within(base: Path, target: Path) -> bool:
"""
target 이 base 디렉터리 내부인지 검사 (경로 탈출 방지)
"""
try:
target.resolve().relative_to(base.resolve())
return True
except Exception:
return False
def _decode_bytes(raw: bytes) -> str:
"""
파일 바이트 → 문자열 디코딩 (감지 → utf-8 → cp949 순서로 시도)
"""
enc = (chardet.detect(raw).get("encoding") or "utf-8").strip().lower()
for cand in (enc, "utf-8", "cp949"):
try:
return raw.decode(cand)
except Exception:
continue
# 최후의 수단: 손실 허용 디코딩
return raw.decode("utf-8", errors="replace")
@file_view_bp.route("/view_file", methods=["GET"])
@login_required
def view_file():
"""
파일 내용을 읽어 반환.
- /view_file?folder=idrac_info&filename=abc.txt
- /view_file?folder=backup&date=<백업폴더명>&filename=abc.txt
- ?raw=1 을 붙이면 text/plain 으로 원문을 반환 (모달 표시용)
"""
folder = request.args.get("folder", "").strip()
date = request.args.get("date", "").strip()
filename = request.args.get("filename", "").strip()
want_raw = request.args.get("raw")
if not filename:
return jsonify({"error": "파일 이름이 없습니다."}), 400
# 파일명/폴더명은 유니코드 보존. 상위 경로만 제거하여 보안 유지
safe_name = Path(filename).name
if folder == "backup":
base = Path(Config.BACKUP_FOLDER)
safe_date = Path(date).name if date else ""
target = (base / safe_date / safe_name).resolve()
else:
base = Path(Config.IDRAC_INFO_FOLDER)
target = (base / safe_name).resolve()
logging.info(
"file_view: folder=%s date=%s filename=%s | base=%s | target=%s",
folder, date, filename, str(base), str(target)
)
if not _safe_within(base, target) or not target.is_file():
logging.warning("file_view: 파일 없음: %s", str(target))
return jsonify({"error": "파일을 찾을 수 없습니다."}), 404
try:
raw = target.read_bytes()
content = _decode_bytes(raw)
if want_raw:
# 텍스트 원문 반환 (모달에서 fetch().text()로 사용)
return Response(content, mimetype="text/plain; charset=utf-8")
# JSON으로 감싸서 반환 (기존 사용처 호환)
return jsonify({"content": content})
except Exception as e:
logging.error("file_view: 파일 읽기 실패: %s%s", str(target), e)
return jsonify({"error": "파일 열기 중 오류 발생"}), 500

13
backend/routes/home.py Normal file
View File

@@ -0,0 +1,13 @@
from __future__ import annotations
from flask import Blueprint, render_template
home_bp = Blueprint("home", __name__, url_prefix="/home")
def register_home_routes(app):
app.register_blueprint(home_bp)
@home_bp.route("/", methods=["GET"])
def home():
return render_template("home.html")

208
backend/routes/main.py Normal file
View File

@@ -0,0 +1,208 @@
from __future__ import annotations
import os
import time
import shutil
import zipfile
import logging
from pathlib import Path
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, session, send_from_directory, send_file
from flask_login import login_required, current_user
from concurrent.futures import ThreadPoolExecutor
from watchdog.observers import Observer
from natsort import natsorted
from backend.services.ip_processor import (
save_ip_addresses,
process_ips_concurrently,
get_progress,
on_complete,
)
from backend.services.watchdog_handler import FileCreatedHandler
from config import Config
main_bp = Blueprint("main", __name__)
executor = ThreadPoolExecutor(max_workers=Config.MAX_WORKERS)
def register_main_routes(app, socketio):
app.register_blueprint(main_bp)
@app.context_processor
def inject_user():
return dict(current_user=current_user)
@app.before_request
def make_session_permanent():
session.permanent = True
if current_user.is_authenticated:
session.modified = True
@main_bp.route("/")
@main_bp.route("/index", methods=["GET"])
@login_required
def index():
script_dir = Path(Config.SCRIPT_FOLDER)
xml_dir = Path(Config.XML_FOLDER)
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)
xml_files = [f.name for f in xml_dir.glob("*.xml")]
# 페이지네이션
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)
start = (page - 1) * Config.FILES_PER_PAGE
end = start + Config.FILES_PER_PAGE
files_to_display = [{"name": Path(f).stem, "file": f} for f in info_files[start:end]]
total_pages = (len(info_files) + Config.FILES_PER_PAGE - 1) // Config.FILES_PER_PAGE
# 백업 폴더 목록 (디렉터리만)
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)
backup_page = int(request.args.get("backup_page", 1))
start_b = (backup_page - 1) * Config.BACKUP_FILES_PER_PAGE
end_b = start_b + Config.BACKUP_FILES_PER_PAGE
backup_slice = backup_dirs[start_b:end_b]
total_backup_pages = (len(backup_dirs) + Config.BACKUP_FILES_PER_PAGE - 1) // Config.BACKUP_FILES_PER_PAGE
backup_files = {}
for d in backup_slice:
files = [f.name for f in d.iterdir() if f.is_file()]
backup_files[d.name] = {"files": files, "count": len(files)}
return render_template(
"index.html",
files_to_display=files_to_display,
page=page,
total_pages=total_pages,
backup_files=backup_files,
total_backup_pages=total_backup_pages,
backup_page=backup_page,
scripts=scripts,
xml_files=xml_files,
)
@main_bp.route("/process_ips", methods=["POST"])
@login_required
def process_ips():
ips = request.form.get("ips")
selected_script = request.form.get("script")
selected_xml_file = request.form.get("xmlFile")
if not ips or not selected_script:
return jsonify({"error": "IP 주소와 스크립트를 모두 입력하세요."}), 400
xml_file_path = None
if selected_script == "02-set_config.py" and selected_xml_file:
xml_path = Path(Config.XML_FOLDER) / selected_xml_file
if not xml_path.exists():
return jsonify({"error": "선택한 XML 파일이 존재하지 않습니다."}), 400
xml_file_path = str(xml_path)
job_id = str(time.time())
session["job_id"] = job_id
ip_files = save_ip_addresses(ips, Config.UPLOAD_FOLDER)
total_files = len(ip_files)
handler = FileCreatedHandler(job_id, total_files)
observer = Observer()
observer.schedule(handler, Config.IDRAC_INFO_FOLDER, recursive=False)
observer.start()
future = executor.submit(
process_ips_concurrently, ip_files, job_id, observer, selected_script, xml_file_path
)
future.add_done_callback(lambda x: on_complete(job_id))
logging.info(f"[AJAX] 작업 시작: {job_id}, script: {selected_script}")
return jsonify({"job_id": job_id})
@main_bp.route("/progress_status/<job_id>")
@login_required
def progress_status(job_id: str):
return jsonify({"progress": get_progress(job_id)})
@main_bp.route("/backup", methods=["POST"])
@login_required
def backup_files():
prefix = request.form.get("backup_prefix", "")
if not prefix.startswith("PO"):
flash("Backup 이름은 PO로 시작해야 합니다.")
return redirect(url_for("main.index"))
folder_name = f"{prefix}_{time.strftime('%Y%m%d')}"
backup_path = Path(Config.BACKUP_FOLDER) / folder_name
backup_path.mkdir(parents=True, exist_ok=True)
info_dir = Path(Config.IDRAC_INFO_FOLDER)
for file in info_dir.iterdir():
if file.is_file():
shutil.move(str(file), str(backup_path / file.name))
flash("백업 완료되었습니다.")
logging.info(f"백업 완료: {folder_name}")
return redirect(url_for("main.index"))
@main_bp.route("/download/<filename>")
@login_required
def download_file(filename: str):
# send_from_directory는 내부적으로 안전 검사를 수행
return send_from_directory(Config.IDRAC_INFO_FOLDER, filename, as_attachment=True)
@main_bp.route("/delete/<filename>", methods=["POST"])
@login_required
def delete_file(filename: str):
file_path = Path(Config.IDRAC_INFO_FOLDER) / filename
if file_path.exists():
try:
file_path.unlink()
flash(f"{filename} 삭제됨.")
logging.info(f"파일 삭제됨: {filename}")
except Exception as e:
logging.error(f"파일 삭제 오류: {e}")
flash("파일 삭제 중 오류가 발생했습니다.", "danger")
else:
flash("파일이 존재하지 않습니다.")
return redirect(url_for("main.index"))
@main_bp.route("/download_zip", methods=["POST"])
@login_required
def download_zip():
zip_filename = request.form.get("zip_filename", "export")
zip_path = Path(Config.TEMP_ZIP_FOLDER) / f"{zip_filename}.zip"
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zipf:
for file in Path(Config.IDRAC_INFO_FOLDER).glob("*"):
if file.is_file():
zipf.write(file, arcname=file.name)
try:
response = send_file(str(zip_path), as_attachment=True)
return response
finally:
# 응답 후 임시 ZIP 삭제
try:
if zip_path.exists():
zip_path.unlink()
except Exception as e:
logging.warning(f"임시 ZIP 삭제 실패: {e}")
@main_bp.route("/download_backup/<date>/<filename>")
@login_required
def download_backup_file(date: str, filename: str):
backup_path = Path(Config.BACKUP_FOLDER) / date
return send_from_directory(str(backup_path), filename, as_attachment=True)

195
backend/routes/utilities.py Normal file
View File

@@ -0,0 +1,195 @@
from __future__ import annotations
import os
import sys
import shutil
import subprocess
import logging
from pathlib import Path
from flask import Blueprint, request, redirect, url_for, flash, jsonify, send_file
from flask_login import login_required
from config import Config
utils_bp = Blueprint("utils", __name__)
def register_util_routes(app):
app.register_blueprint(utils_bp)
@utils_bp.route("/move_mac_files", methods=["POST"])
@login_required
def move_mac_files():
src = Path(Config.IDRAC_INFO_FOLDER)
dst = Path(Config.MAC_FOLDER)
dst.mkdir(parents=True, exist_ok=True)
moved = 0
errors = []
try:
for file in src.iterdir():
if not file.is_file():
continue
try:
# 파일이 존재하는지 확인
if not file.exists():
errors.append(f"{file.name}: 파일이 존재하지 않음")
continue
# 대상 파일이 이미 존재하는 경우 건너뛰기
target = dst / file.name
if target.exists():
logging.warning(f"⚠️ 파일이 이미 존재하여 건너뜀: {file.name}")
continue
shutil.move(str(file), str(target))
moved += 1
except Exception as e:
error_msg = f"{file.name}: {str(e)}"
errors.append(error_msg)
logging.error(f"❌ 파일 이동 실패: {error_msg}")
# 결과 로깅
if moved > 0:
logging.info(f"✅ MAC 파일 이동 완료 ({moved}개)")
if errors:
logging.warning(f"⚠️ 일부 파일 이동 실패: {errors}")
# 하나라도 성공하면 success: true 반환
return jsonify({
"success": True,
"moved": moved,
"errors": errors if errors else None
})
except Exception as e:
logging.error(f"❌ MAC 이동 중 치명적 오류: {e}")
return jsonify({"success": False, "error": str(e)})
@utils_bp.route("/move_guid_files", methods=["POST"])
@login_required
def move_guid_files():
src = Path(Config.IDRAC_INFO_FOLDER)
dst = Path(Config.GUID_FOLDER)
dst.mkdir(parents=True, exist_ok=True)
moved = 0
errors = []
try:
for file in src.iterdir():
if not file.is_file():
continue
try:
# 파일이 존재하는지 확인
if not file.exists():
errors.append(f"{file.name}: 파일이 존재하지 않음")
continue
# 대상 파일이 이미 존재하는 경우 건너뛰기
target = dst / file.name
if target.exists():
logging.warning(f"⚠️ 파일이 이미 존재하여 건너뜀: {file.name}")
continue
shutil.move(str(file), str(target))
moved += 1
except Exception as e:
error_msg = f"{file.name}: {str(e)}"
errors.append(error_msg)
logging.error(f"❌ 파일 이동 실패: {error_msg}")
# 결과 메시지
if moved > 0:
flash(f"GUID 파일이 성공적으로 이동되었습니다. ({moved}개)", "success")
logging.info(f"✅ GUID 파일 이동 완료 ({moved}개)")
if errors:
logging.warning(f"⚠️ 일부 파일 이동 실패: {errors}")
flash(f"일부 파일 이동 실패: {len(errors)}", "warning")
except Exception as e:
logging.error(f"❌ GUID 이동 오류: {e}")
flash(f"오류 발생: {e}", "danger")
return redirect(url_for("main.index"))
@utils_bp.route("/update_server_list", methods=["POST"])
@login_required
def update_server_list():
content = request.form.get("server_list_content")
if not content:
flash("내용을 입력하세요.", "warning")
return redirect(url_for("main.index"))
path = Path(Config.SERVER_LIST_FOLDER) / "server_list.txt"
try:
path.write_text(content, encoding="utf-8")
result = subprocess.run(
[sys.executable, str(Path(Config.SERVER_LIST_FOLDER) / "excel.py")],
capture_output=True,
text=True,
check=True,
cwd=str(Path(Config.SERVER_LIST_FOLDER)),
timeout=300,
)
logging.info(f"서버 리스트 스크립트 실행 결과: {result.stdout}")
flash("서버 리스트가 업데이트되었습니다.", "success")
except subprocess.CalledProcessError as e:
logging.error(f"서버 리스트 스크립트 오류: {e.stderr}")
flash(f"스크립트 실행 실패: {e.stderr}", "danger")
except Exception as e:
logging.error(f"서버 리스트 처리 오류: {e}")
flash(f"서버 리스트 처리 중 오류 발생: {e}", "danger")
return redirect(url_for("main.index"))
@utils_bp.route("/update_guid_list", methods=["POST"])
@login_required
def update_guid_list():
content = request.form.get("server_list_content")
if not content:
flash("내용을 입력하세요.", "warning")
return redirect(url_for("main.index"))
path = Path(Config.SERVER_LIST_FOLDER) / "guid_list.txt"
try:
path.write_text(content, encoding="utf-8")
result = subprocess.run(
[sys.executable, str(Path(Config.SERVER_LIST_FOLDER) / "GUIDtxtT0Execl.py")],
capture_output=True,
text=True,
check=True,
cwd=str(Path(Config.SERVER_LIST_FOLDER)),
timeout=300,
)
logging.info(f"GUID 리스트 스크립트 실행 결과: {result.stdout}")
flash("GUID 리스트가 업데이트되었습니다.", "success")
except subprocess.CalledProcessError as e:
logging.error(f"GUID 리스트 스크립트 오류: {e.stderr}")
flash(f"스크립트 실행 실패: {e.stderr}", "danger")
except Exception as e:
logging.error(f"GUID 리스트 처리 오류: {e}")
flash(f"GUID 리스트 처리 중 오류 발생: {e}", "danger")
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")

105
backend/routes/xml.py Normal file
View File

@@ -0,0 +1,105 @@
from __future__ import annotations
import logging
from pathlib import Path
from flask import Blueprint, render_template, request, redirect, url_for, flash
from flask_login import login_required
from werkzeug.utils import secure_filename
from config import Config
xml_bp = Blueprint("xml", __name__)
def register_xml_routes(app):
app.register_blueprint(xml_bp)
def allowed_file(filename: str) -> bool:
return "." in filename and filename.rsplit(".", 1)[1].lower() in Config.ALLOWED_EXTENSIONS
@xml_bp.route("/xml_management")
@login_required
def xml_management():
xml_dir = Path(Config.XML_FOLDER)
try:
files = [f.name for f in xml_dir.iterdir() if f.is_file()]
except FileNotFoundError:
files = []
flash("XML 폴더가 존재하지 않습니다.", "danger")
return render_template("manage_xml.html", xml_files=files)
@xml_bp.route("/upload_xml", methods=["POST"])
@login_required
def upload_xml():
file = request.files.get("xmlFile")
if not file or not file.filename:
flash("업로드할 파일을 선택하세요.", "warning")
return redirect(url_for("xml.xml_management"))
if allowed_file(file.filename):
filename = secure_filename(file.filename)
save_path = Path(Config.XML_FOLDER) / filename
try:
save_path.parent.mkdir(parents=True, exist_ok=True)
file.save(str(save_path))
# 텍스트 파일이므로 0644 권장
try:
save_path.chmod(0o644)
except Exception:
pass # Windows 등에서 무시
logging.info(f"XML 업로드됨: {filename}")
flash("파일이 성공적으로 업로드되었습니다.", "success")
except Exception as e:
logging.error(f"파일 업로드 오류: {e}")
flash("파일 저장 중 오류가 발생했습니다.", "danger")
else:
flash("XML 확장자만 업로드할 수 있습니다.", "warning")
return redirect(url_for("xml.xml_management"))
@xml_bp.route("/delete_xml/<filename>", methods=["POST"])
@login_required
def delete_xml(filename: str):
path = Path(Config.XML_FOLDER) / secure_filename(filename)
if path.exists():
try:
path.unlink()
flash(f"{filename} 파일이 삭제되었습니다.", "success")
logging.info(f"XML 삭제됨: {filename}")
except Exception as e:
logging.error(f"XML 삭제 오류: {e}")
flash("파일 삭제 중 오류 발생", "danger")
else:
flash("해당 파일이 존재하지 않습니다.", "warning")
return redirect(url_for("xml.xml_management"))
@xml_bp.route("/edit_xml/<filename>", methods=["GET", "POST"])
@login_required
def edit_xml(filename: str):
path = Path(Config.XML_FOLDER) / secure_filename(filename)
if not path.exists():
flash("파일을 찾을 수 없습니다.", "danger")
return redirect(url_for("xml.xml_management"))
if request.method == "POST":
new_content = request.form.get("content", "")
try:
path.write_text(new_content, encoding="utf-8")
logging.info(f"XML 수정됨: {filename}")
flash("파일이 성공적으로 수정되었습니다.", "success")
return redirect(url_for("xml.xml_management"))
except Exception as e:
logging.error(f"XML 저장 실패: {e}")
flash("파일 저장 중 오류가 발생했습니다.", "danger")
try:
content = path.read_text(encoding="utf-8")
except Exception as e:
logging.error(f"XML 열기 실패: {e}")
flash("파일 열기 중 오류가 발생했습니다.", "danger")
content = ""
return render_template("edit_xml.html", filename=filename, content=content)