## Features - 한국어 Flask 관리 인터페이스 with Bootstrap 5 - Headscale과 분리된 독립 데이터베이스 구조 - 약국 관리 시스템 (pharmacy management) - 머신 모니터링 및 상태 관리 - 실시간 대시보드 with 통계 및 알림 - Headscale 사용자명과 약국명 분리 관리 ## Database Architecture - 별도 FARMQ SQLite DB (farmq.sqlite) - Headscale DB와 외래키 충돌 방지 - 느슨한 결합 설계 (ID 참조만 사용) ## UI Components - 반응형 대시보드 with 실시간 통계 - 약국별 머신 상태 모니터링 - 한국어 지역화 및 사용자 친화적 인터페이스 - 머신 온라인/오프라인 상태 표시 (24시간 타임아웃) ## API Endpoints - `/api/sync/machines` - Headscale 머신 동기화 - `/api/sync/users` - Headscale 사용자 동기화 - `/api/pharmacy/<id>/update` - 약국 정보 업데이트 - 대시보드 통계 및 알림 API ## Problem Resolution - Fixed foreign key conflicts preventing Windows client connections - Resolved machine online status detection with proper timeout handling - Separated technical Headscale usernames from business pharmacy names 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
239 lines
8.9 KiB
Python
239 lines
8.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
팜큐 약국 관리 시스템 - Flask 애플리케이션
|
|
Headscale + Headplane 고도화 관리자 페이지
|
|
"""
|
|
|
|
from flask import Flask, render_template, jsonify, request, redirect, url_for
|
|
import os
|
|
from datetime import datetime
|
|
from config import config
|
|
from utils.database_new import (
|
|
init_database, get_session, close_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
|
|
)
|
|
|
|
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_database(app.config['SQLALCHEMY_DATABASE_URI'])
|
|
|
|
# 요청 종료 후 세션 정리
|
|
@app.teardown_appcontext
|
|
def cleanup(error):
|
|
close_session()
|
|
|
|
# 메인 대시보드
|
|
@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}")
|
|
|
|
return render_template('machines/detail.html', machine=details)
|
|
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>/update', methods=['PUT'])
|
|
def api_update_pharmacy(pharmacy_id):
|
|
"""약국 정보 업데이트 API"""
|
|
try:
|
|
from utils.database_new import get_farmq_session
|
|
from models.farmq_models import PharmacyInfo
|
|
|
|
data = request.get_json()
|
|
session = get_farmq_session()
|
|
|
|
try:
|
|
pharmacy = session.query(PharmacyInfo).filter(
|
|
PharmacyInfo.id == pharmacy_id
|
|
).first()
|
|
|
|
if not pharmacy:
|
|
return jsonify({'error': '약국을 찾을 수 없습니다.'}), 404
|
|
|
|
# 업데이트 가능한 필드들
|
|
if 'pharmacy_name' in data:
|
|
pharmacy.pharmacy_name = data['pharmacy_name']
|
|
if 'business_number' in data:
|
|
pharmacy.business_number = data['business_number']
|
|
if 'manager_name' in data:
|
|
pharmacy.manager_name = data['manager_name']
|
|
if 'phone' in data:
|
|
pharmacy.phone = data['phone']
|
|
if 'address' in data:
|
|
pharmacy.address = data['address']
|
|
|
|
pharmacy.updated_at = datetime.now()
|
|
session.commit()
|
|
|
|
return jsonify({
|
|
'message': '약국 정보가 업데이트되었습니다.',
|
|
'pharmacy': pharmacy.to_dict()
|
|
})
|
|
|
|
finally:
|
|
session.close()
|
|
|
|
except Exception as e:
|
|
return jsonify({'error': 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("─" * 60)
|
|
|
|
app.run(
|
|
host='0.0.0.0',
|
|
port=5001,
|
|
debug=True,
|
|
use_reloader=True
|
|
) |