headscale-tailscale-replace.../farmq-admin/app.py
시골약사 f3965a67fd 머신 상세 페이지 Headscale CLI 기반 완전 재구현
- get_machine_detail 함수를 Headscale CLI 기반으로 완전 교체
- 기존 FARMQ DB 의존성에서 실시간 Headscale 데이터로 전환
- strftime 템플릿 오류 완전 해결 (datetime 객체 타입 체크 추가)
- 실제 머신 정보 표시: 호스트명, IP 주소, 온라인 상태, 사용자 정보
- 약국 정보 매핑: Headscale 사용자명을 통한 약국 연동
- 시간 정보 인간화: "N시간 전", "N분 전" 형식으로 표시
- 네트워크 정보: IPv4/IPv6 주소, 엔드포인트, 키 정보 표시
- 조건부 모니터링 데이터 표시 (향후 확장 대비)
- 전체 머신 상세 페이지 기능 정상화

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 12:07:53 +09:00

861 lines
33 KiB
Python

#!/usr/bin/env python3
"""
팜큐 약국 관리 시스템 - Flask 애플리케이션
Headscale + Headplane 고도화 관리자 페이지
"""
from flask import Flask, render_template, jsonify, request, redirect, url_for
import os
import json
from datetime import datetime
import uuid
from config import config
from utils.database_new import (
init_databases, get_farmq_session,
get_dashboard_stats, get_all_pharmacies_with_stats, get_all_machines_with_details,
get_machine_detail, get_pharmacy_detail, get_active_alerts,
sync_machines_from_headscale, sync_users_from_headscale
)
from models.farmq_models import PharmacyInfo
from sqlalchemy import or_
import subprocess
from utils.proxmox_client import ProxmoxClient
def create_app(config_name=None):
"""Flask 애플리케이션 팩토리"""
app = Flask(__name__)
# 설정 로드
config_name = config_name or os.environ.get('FLASK_ENV', 'default')
app.config.from_object(config[config_name])
# 데이터베이스 초기화
init_databases(
headscale_db_uri='sqlite:////srv/headscale-setup/data/db.sqlite',
farmq_db_uri='sqlite:///farmq.db'
)
# 데이터 동기화 실행
sync_users_from_headscale()
sync_machines_from_headscale()
# VNC 세션 관리 (메모리 기반)
vnc_sessions = {}
# Proxmox 서버 설정
PROXMOX_HOST = "pve7.0bin.in"
PROXMOX_USERNAME = "root@pam"
PROXMOX_PASSWORD = "trajet6640"
# 메인 대시보드
@app.route('/')
def dashboard():
"""메인 대시보드"""
try:
# 새로운 통합 통계 함수 사용
stats = get_dashboard_stats()
stats['alerts'] = get_active_alerts()[:5] # 최신 5개만
stats['performance'] = {'status': 'good', 'summary': '모든 시스템이 정상 작동 중입니다.'}
# 약국별 상태 (상위 10개)
pharmacies = get_all_pharmacies_with_stats()[:10]
return render_template('dashboard/index.html',
stats=stats,
pharmacies=pharmacies)
except Exception as e:
print(f"❌ Dashboard error: {e}")
return render_template('error.html', error=str(e)), 500
# 약국 관리
@app.route('/pharmacy')
def pharmacy_list():
"""약국 목록"""
try:
pharmacies = get_all_pharmacies_with_stats()
return render_template('pharmacy/list.html', pharmacies=pharmacies)
except Exception as e:
return render_template('error.html', error=str(e)), 500
@app.route('/pharmacy/<int:pharmacy_id>')
def pharmacy_detail(pharmacy_id):
"""약국 상세 정보"""
try:
detail_data = get_pharmacy_detail(pharmacy_id)
if not detail_data:
return render_template('error.html', error='약국을 찾을 수 없습니다.'), 404
return render_template('pharmacy/detail.html',
pharmacy=detail_data['pharmacy'],
machines=detail_data['machines'])
except Exception as e:
print(f"❌ Pharmacy detail error: {e}")
return render_template('error.html', error=str(e)), 500
# 머신 관리
@app.route('/machines')
def machine_list():
"""머신 목록"""
try:
machines = get_all_machines_with_details()
return render_template('machines/list.html', machines=machines)
except Exception as e:
print(f"❌ Machine list error: {e}")
return render_template('error.html', error=str(e)), 500
@app.route('/machines/<int:machine_id>')
def machine_detail(machine_id):
"""머신 상세 정보"""
try:
print(f"🔍 Getting details for machine ID: {machine_id}")
details = get_machine_detail(machine_id)
if not details:
print(f"❌ No details found for machine ID: {machine_id}")
return render_template('error.html', error='머신을 찾을 수 없습니다.'), 404
hostname = details.get('hostname', 'Unknown')
print(f"✅ Rendering detail page for machine: {hostname}")
# 템플릿에 필요한 추가 변수들
is_online = details.get('online', False)
last_seen = details.get('last_seen')
# 시간 형식화
if last_seen:
from datetime import datetime, timezone
if isinstance(last_seen, datetime):
now = datetime.now(timezone.utc) if last_seen.tzinfo else datetime.now()
delta = now - last_seen
if delta.days > 0:
last_seen_humanized = f"{delta.days}일 전"
elif delta.seconds > 3600:
last_seen_humanized = f"{delta.seconds // 3600}시간 전"
elif delta.seconds > 60:
last_seen_humanized = f"{delta.seconds // 60}분 전"
else:
last_seen_humanized = "방금 전"
else:
last_seen_humanized = "알 수 없음"
else:
last_seen_humanized = "알 수 없음"
return render_template('machines/detail.html',
machine=details,
is_online=is_online,
last_seen_humanized=last_seen_humanized)
except Exception as e:
print(f"❌ Error in machine_detail route: {e}")
import traceback
traceback.print_exc()
return render_template('error.html', error=f'머신 상세 정보 로드 중 오류: {str(e)}'), 500
# API 엔드포인트
@app.route('/api/dashboard/stats')
def api_dashboard_stats():
"""대시보드 통계 API"""
try:
stats = get_dashboard_stats()
stats['performance'] = {'status': 'good', 'summary': '모든 시스템이 정상 작동 중입니다.'}
return jsonify(stats)
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/alerts')
def api_alerts():
"""실시간 알림 API"""
try:
alerts = get_active_alerts()
return jsonify(alerts)
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/machines/<int:machine_id>/monitoring')
def api_machine_monitoring(machine_id):
"""머신 모니터링 데이터 API"""
try:
details = get_machine_detail(machine_id)
if not details:
return jsonify({'error': '머신을 찾을 수 없습니다.'}), 404
# 최근 모니터링 데이터 반환
metrics_history = details.get('metrics_history', [])
return jsonify(metrics_history[:20]) # 최근 20개 데이터
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/sync/machines')
def api_sync_machines():
"""Headscale에서 머신 정보 동기화 API"""
try:
result = sync_machines_from_headscale()
return jsonify(result)
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/sync/users')
def api_sync_users():
"""Headscale에서 사용자 정보 동기화 API"""
try:
result = sync_users_from_headscale()
return jsonify(result)
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/pharmacy/<int:pharmacy_id>', methods=['GET'])
def api_get_pharmacy(pharmacy_id):
"""개별 약국 정보 조회 API"""
try:
from utils.database_new import get_farmq_session
from models.farmq_models import PharmacyInfo
session = get_farmq_session()
try:
pharmacy = session.query(PharmacyInfo).filter(
PharmacyInfo.id == pharmacy_id
).first()
if not pharmacy:
return jsonify({'error': '약국을 찾을 수 없습니다.'}), 404
return jsonify({
'pharmacy': {
'id': pharmacy.id,
'pharmacy_name': pharmacy.pharmacy_name or '',
'business_number': pharmacy.business_number or '',
'manager_name': pharmacy.manager_name or '',
'phone': pharmacy.phone or '',
'address': pharmacy.address or '',
'proxmox_host': pharmacy.proxmox_host or '',
'user_id': pharmacy.headscale_user_name or '', # user_id 대신 headscale_user_name 사용
'headscale_user_name': pharmacy.headscale_user_name or ''
}
})
finally:
session.close()
except Exception as e:
return jsonify({'error': str(e)}), 500
# 약국 관리 API
@app.route('/api/pharmacy', methods=['POST'])
def api_create_pharmacy():
"""새 약국 생성"""
try:
data = request.get_json()
# 필수 필드 확인
pharmacy_name = data.get('pharmacy_name', '').strip()
if not pharmacy_name:
return jsonify({
'success': False,
'error': '약국명은 필수입니다.'
}), 400
# FARMQ 데이터베이스에 약국 생성
farmq_session = get_farmq_session()
try:
new_pharmacy = PharmacyInfo(
pharmacy_name=pharmacy_name,
business_number=data.get('business_number', '').strip(),
manager_name=data.get('manager_name', '').strip(),
phone=data.get('phone', '').strip(),
address=data.get('address', '').strip(),
proxmox_host=data.get('proxmox_host', '').strip(),
headscale_user_name=data.get('headscale_user_name', '').strip(),
status='active'
)
farmq_session.add(new_pharmacy)
farmq_session.commit()
return jsonify({
'success': True,
'message': f'약국 "{pharmacy_name}"가 성공적으로 생성되었습니다.',
'pharmacy': new_pharmacy.to_dict()
})
finally:
farmq_session.close()
except Exception as e:
print(f"❌ 약국 생성 오류: {e}")
return jsonify({
'success': False,
'error': f'서버 오류: {str(e)}'
}), 500
@app.route('/api/pharmacy/<int:pharmacy_id>', methods=['PUT'])
def api_update_pharmacy(pharmacy_id):
"""약국 정보 수정"""
try:
data = request.get_json()
farmq_session = get_farmq_session()
try:
pharmacy = farmq_session.query(PharmacyInfo).filter(
PharmacyInfo.id == pharmacy_id
).first()
if not pharmacy:
return jsonify({
'success': False,
'error': '약국을 찾을 수 없습니다.'
}), 404
# 필드 업데이트
if 'pharmacy_name' in data:
pharmacy.pharmacy_name = data['pharmacy_name'].strip()
if 'business_number' in data:
pharmacy.business_number = data['business_number'].strip()
if 'manager_name' in data:
pharmacy.manager_name = data['manager_name'].strip()
if 'phone' in data:
pharmacy.phone = data['phone'].strip()
if 'address' in data:
pharmacy.address = data['address'].strip()
if 'proxmox_host' in data:
pharmacy.proxmox_host = data['proxmox_host'].strip()
if 'headscale_user_name' in data:
pharmacy.headscale_user_name = data['headscale_user_name'].strip()
pharmacy.updated_at = datetime.now()
farmq_session.commit()
return jsonify({
'success': True,
'message': f'약국 "{pharmacy.pharmacy_name}" 정보가 수정되었습니다.',
'pharmacy': pharmacy.to_dict()
})
finally:
farmq_session.close()
except Exception as e:
print(f"❌ 약국 수정 오류: {e}")
return jsonify({
'success': False,
'error': f'서버 오류: {str(e)}'
}), 500
@app.route('/api/pharmacy/<int:pharmacy_id>/delete', methods=['DELETE'])
def api_delete_pharmacy(pharmacy_id):
"""약국 삭제"""
try:
farmq_session = get_farmq_session()
try:
pharmacy = farmq_session.query(PharmacyInfo).filter(
PharmacyInfo.id == pharmacy_id
).first()
if not pharmacy:
return jsonify({
'success': False,
'error': '약국을 찾을 수 없습니다.'
}), 404
pharmacy_name = pharmacy.pharmacy_name
farmq_session.delete(pharmacy)
farmq_session.commit()
return jsonify({
'success': True,
'message': f'약국 "{pharmacy_name}"가 삭제되었습니다.'
})
finally:
farmq_session.close()
except Exception as e:
print(f"❌ 약국 삭제 오류: {e}")
return jsonify({
'success': False,
'error': f'서버 오류: {str(e)}'
}), 500
@app.route('/api/pharmacies/available', methods=['GET'])
def api_get_available_pharmacies():
"""매핑되지 않은 약국 목록 가져오기"""
try:
farmq_session = get_farmq_session()
try:
# headscale_user_name이 NULL이거나 빈 문자열인 약국들만 가져오기
pharmacies = farmq_session.query(PharmacyInfo).filter(
or_(
PharmacyInfo.headscale_user_name.is_(None),
PharmacyInfo.headscale_user_name == ''
)
).all()
pharmacy_list = []
for pharmacy in pharmacies:
pharmacy_list.append({
'id': pharmacy.id,
'pharmacy_name': pharmacy.pharmacy_name,
'manager_name': pharmacy.manager_name or '미등록',
'business_number': pharmacy.business_number,
'address': pharmacy.address
})
return jsonify({
'success': True,
'pharmacies': pharmacy_list,
'count': len(pharmacy_list)
})
finally:
farmq_session.close()
except Exception as e:
print(f"❌ 매핑 가능한 약국 목록 조회 오류: {e}")
return jsonify({
'success': False,
'error': f'서버 오류: {str(e)}'
}), 500
@app.route('/api/users/<user_name>/link-pharmacy', methods=['POST'])
def api_link_user_pharmacy(user_name):
"""사용자와 약국 연결"""
try:
data = request.get_json()
pharmacy_id = data.get('pharmacy_id')
if not pharmacy_id:
return jsonify({
'success': False,
'error': '약국 ID가 필요합니다.'
}), 400
farmq_session = get_farmq_session()
try:
# 약국 존재 확인
pharmacy = farmq_session.query(PharmacyInfo).filter(
PharmacyInfo.id == pharmacy_id
).first()
if not pharmacy:
return jsonify({
'success': False,
'error': '약국을 찾을 수 없습니다.'
}), 404
# 사용자 존재 확인 (Headscale CLI 사용)
result = subprocess.run(
['docker', 'exec', 'headscale', 'headscale', 'users', 'list', '-o', 'json'],
capture_output=True,
text=True,
check=True
)
users_data = json.loads(result.stdout)
user = next((u for u in users_data if u['name'] == user_name), None)
if not user:
return jsonify({
'success': False,
'error': f'사용자 "{user_name}"를 찾을 수 없습니다.'
}), 404
# 약국에 사용자 연결
pharmacy.headscale_user_name = user_name
pharmacy.headscale_user_id = user['id']
pharmacy.updated_at = datetime.now()
farmq_session.commit()
return jsonify({
'success': True,
'message': f'사용자 "{user_name}"가 약국 "{pharmacy.pharmacy_name}"에 연결되었습니다.'
})
finally:
farmq_session.close()
except subprocess.CalledProcessError as e:
error_msg = e.stderr if e.stderr else e.stdout
return jsonify({
'success': False,
'error': f'사용자 확인 실패: {error_msg}'
}), 400
except Exception as e:
print(f"❌ 사용자-약국 연결 오류: {e}")
return jsonify({
'success': False,
'error': f'서버 오류: {str(e)}'
}), 500
# VNC 관리 라우트들
@app.route('/vms')
def vm_list():
"""VM 목록 페이지"""
try:
# Proxmox 클라이언트 생성 및 로그인
client = ProxmoxClient(PROXMOX_HOST, PROXMOX_USERNAME, PROXMOX_PASSWORD)
if not client.login():
return render_template('error.html',
error='Proxmox 서버에 연결할 수 없습니다.'), 500
# VM 목록 가져오기
vms = client.get_vm_list()
# 통계 계산
total_vms = len(vms)
running_vms = len([vm for vm in vms if vm.get('status') == 'running'])
stopped_vms = total_vms - running_vms
vnc_ready_vms = running_vms # 실행 중인 VM은 모두 VNC 가능
return render_template('vm_list.html',
vms=vms,
host=PROXMOX_HOST,
total_vms=total_vms,
running_vms=running_vms,
stopped_vms=stopped_vms,
vnc_ready_vms=vnc_ready_vms)
except Exception as e:
print(f"❌ VM 목록 오류: {e}")
return render_template('error.html', error=str(e)), 500
@app.route('/api/vm/vnc', methods=['POST'])
def api_vm_vnc():
"""VNC 연결 세션 생성 API"""
try:
data = request.get_json()
node = data.get('node')
vmid = int(data.get('vmid'))
vm_name = data.get('vm_name', f'VM-{vmid}')
# Proxmox 클라이언트 생성 및 로그인
client = ProxmoxClient(PROXMOX_HOST, PROXMOX_USERNAME, PROXMOX_PASSWORD)
if not client.login():
return jsonify({'error': 'Proxmox 서버 로그인 실패'}), 500
# VNC 티켓 생성
vnc_data = client.get_vnc_ticket(node, vmid)
if not vnc_data:
return jsonify({'error': 'VNC 티켓 생성 실패'}), 500
# 세션 ID 생성
session_id = str(uuid.uuid4())
# VNC 세션 저장
vnc_sessions[session_id] = {
'node': node,
'vmid': vmid,
'vm_name': vm_name,
'websocket_url': vnc_data['websocket_url'],
'created_at': datetime.now()
}
return jsonify({
'session_id': session_id,
'vm_name': vm_name,
'success': True
})
except Exception as e:
print(f"❌ VNC API 오류: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/vnc/<session_id>')
def vnc_console(session_id):
"""VNC 콘솔 페이지"""
try:
# 세션 확인
if session_id not in vnc_sessions:
return render_template('error.html',
error='유효하지 않은 VNC 세션입니다.'), 404
session_data = vnc_sessions[session_id]
# Proxmox 기본 noVNC URL로 리다이렉트
proxmox_vnc_url = f"https://{PROXMOX_HOST}:443/?console=kvm&vmid={session_data['vmid']}&node={session_data['node']}"
# 리다이렉트 페이지 표시
return render_template('vnc_redirect.html',
vm_name=session_data['vm_name'],
vmid=session_data['vmid'],
node=session_data['node'],
proxmox_url=proxmox_vnc_url,
host=PROXMOX_HOST)
except Exception as e:
print(f"❌ VNC 콘솔 오류: {e}")
return render_template('error.html', error=str(e)), 500
@app.route('/api/vm/start', methods=['POST'])
def api_vm_start():
"""VM 시작 API"""
try:
data = request.get_json()
node = data.get('node')
vmid = int(data.get('vmid'))
# Proxmox 클라이언트 생성 및 로그인
client = ProxmoxClient(PROXMOX_HOST, PROXMOX_USERNAME, PROXMOX_PASSWORD)
if not client.login():
return jsonify({'error': 'Proxmox 서버 로그인 실패'}), 500
# VM 시작
success = client.start_vm(node, vmid)
if success:
return jsonify({'message': f'VM {vmid} 시작 명령을 전송했습니다.', 'success': True})
else:
return jsonify({'error': 'VM 시작 실패'}), 500
except Exception as e:
print(f"❌ VM 시작 오류: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/api/vm/stop', methods=['POST'])
def api_vm_stop():
"""VM 정지 API"""
try:
data = request.get_json()
node = data.get('node')
vmid = int(data.get('vmid'))
# Proxmox 클라이언트 생성 및 로그인
client = ProxmoxClient(PROXMOX_HOST, PROXMOX_USERNAME, PROXMOX_PASSWORD)
if not client.login():
return jsonify({'error': 'Proxmox 서버 로그인 실패'}), 500
# VM 정지
success = client.stop_vm(node, vmid)
if success:
return jsonify({'message': f'VM {vmid} 정지 명령을 전송했습니다.', 'success': True})
else:
return jsonify({'error': 'VM 정지 실패'}), 500
except Exception as e:
print(f"❌ VM 정지 오류: {e}")
return jsonify({'error': str(e)}), 500
# 노드 삭제 API
@app.route('/api/nodes/<int:node_id>/delete', methods=['DELETE'])
def api_delete_node(node_id):
"""노드 삭제 API"""
try:
# Headscale CLI를 통해 노드 삭제
result = subprocess.run(
['docker', 'exec', 'headscale', 'headscale', 'nodes', 'delete', '-i', str(node_id), '--force'],
capture_output=True,
text=True,
check=True
)
# 삭제 성공
return jsonify({
'success': True,
'message': f'노드 {node_id}가 성공적으로 삭제되었습니다.',
'output': result.stdout
})
except subprocess.CalledProcessError as e:
# Headscale CLI 오류
error_msg = e.stderr if e.stderr else e.stdout
return jsonify({
'success': False,
'error': f'노드 삭제 실패: {error_msg}'
}), 400
except Exception as e:
# 기타 오류
print(f"❌ 노드 삭제 오류: {e}")
return jsonify({
'success': False,
'error': f'서버 오류: {str(e)}'
}), 500
# 사용자 관리 라우트
@app.route('/users')
def users_list():
"""사용자 관리 페이지"""
return render_template('users/list.html')
# 사용자 관리 API
@app.route('/api/users', methods=['GET'])
def api_get_users():
"""Headscale 사용자 목록 조회"""
try:
# Headscale CLI를 통해 사용자 목록 조회
result = subprocess.run(
['docker', 'exec', 'headscale', 'headscale', 'users', 'list', '-o', 'json'],
capture_output=True,
text=True,
check=True
)
users_data = json.loads(result.stdout)
# FARMQ 약국 정보와 매칭 (명시적으로 매핑된 것만)
farmq_session = get_farmq_session()
try:
pharmacies = farmq_session.query(PharmacyInfo).all()
# 명시적으로 headscale_user_name이 설정되고 해당 사용자가 실제로 존재하는 경우만 매핑
pharmacy_map = {}
for p in pharmacies:
if p.headscale_user_name and p.headscale_user_name.strip():
pharmacy_map[p.headscale_user_name] = p
# 사용자별 노드 수 조회
for user in users_data:
user_name = user.get('name', '')
# 약국 정보 매칭 - 명시적으로 연결된 것만
pharmacy = pharmacy_map.get(user_name)
user['pharmacy'] = {
'id': pharmacy.id,
'name': pharmacy.pharmacy_name,
'manager': pharmacy.manager_name
} if pharmacy else None
# 해당 사용자의 노드 수 조회
node_result = subprocess.run(
['docker', 'exec', 'headscale', 'headscale', 'nodes', 'list', '-o', 'json'],
capture_output=True,
text=True,
check=True
)
nodes_data = json.loads(node_result.stdout)
user['node_count'] = len([n for n in nodes_data if n.get('user', {}).get('name') == user_name])
return jsonify({'success': True, 'users': users_data})
finally:
farmq_session.close()
except subprocess.CalledProcessError as e:
error_msg = e.stderr if e.stderr else e.stdout
return jsonify({
'success': False,
'error': f'사용자 목록 조회 실패: {error_msg}'
}), 400
except Exception as e:
print(f"❌ 사용자 목록 조회 오류: {e}")
return jsonify({
'success': False,
'error': f'서버 오류: {str(e)}'
}), 500
@app.route('/api/users', methods=['POST'])
def api_create_user():
"""새 Headscale 사용자 생성"""
try:
data = request.get_json()
user_name = data.get('name', '').strip()
display_name = data.get('display_name', '').strip()
email = data.get('email', '').strip()
if not user_name:
return jsonify({
'success': False,
'error': '사용자 이름은 필수입니다.'
}), 400
# Headscale CLI를 통해 사용자 생성
cmd = ['docker', 'exec', 'headscale', 'headscale', 'users', 'create', user_name]
if display_name:
cmd.extend(['-d', display_name])
if email:
cmd.extend(['-e', email])
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=True
)
return jsonify({
'success': True,
'message': f'사용자 "{user_name}"가 성공적으로 생성되었습니다.',
'output': result.stdout
})
except subprocess.CalledProcessError as e:
error_msg = e.stderr if e.stderr else e.stdout
return jsonify({
'success': False,
'error': f'사용자 생성 실패: {error_msg}'
}), 400
except Exception as e:
print(f"❌ 사용자 생성 오류: {e}")
return jsonify({
'success': False,
'error': f'서버 오류: {str(e)}'
}), 500
@app.route('/api/users/<user_name>/delete', methods=['DELETE'])
def api_delete_user(user_name):
"""Headscale 사용자 삭제"""
try:
# Headscale CLI를 통해 사용자 삭제
result = subprocess.run(
['docker', 'exec', 'headscale', 'headscale', 'users', 'destroy',
'--name', user_name, '--force'],
capture_output=True,
text=True,
check=True
)
return jsonify({
'success': True,
'message': f'사용자 "{user_name}"가 성공적으로 삭제되었습니다.',
'output': result.stdout
})
except subprocess.CalledProcessError as e:
error_msg = e.stderr if e.stderr else e.stdout
return jsonify({
'success': False,
'error': f'사용자 삭제 실패: {error_msg}'
}), 400
except Exception as e:
print(f"❌ 사용자 삭제 오류: {e}")
return jsonify({
'success': False,
'error': f'서버 오류: {str(e)}'
}), 500
# 에러 핸들러
@app.errorhandler(404)
def not_found_error(error):
return render_template('error.html',
error='페이지를 찾을 수 없습니다.',
error_code=404), 404
@app.errorhandler(500)
def internal_error(error):
return render_template('error.html',
error='내부 서버 오류가 발생했습니다.',
error_code=500), 500
return app
# 개발 서버 실행
if __name__ == '__main__':
app = create_app()
# 개발 환경에서만 실행
if app.config.get('DEBUG'):
print("🚀 Starting FARMQ Admin System...")
print(f"📊 Dashboard: http://localhost:5001")
print(f"🏥 Pharmacy Management: http://localhost:5001/pharmacy")
print(f"💻 Machine Management: http://localhost:5001/machines")
print(f"🖥️ VM Management (VNC): http://localhost:5001/vms")
print("" * 60)
app.run(
host='0.0.0.0',
port=5001,
debug=True,
use_reloader=True
)