feat: Flask 애플리케이션 모듈화 및 웹 대시보드 구현
- Flask Blueprint 아키텍처로 전환 (dashboard, upload, backup, status) - app.py 681줄 95줄로 축소 (86% 감소) - HTML 템플릿 모듈화 (base.html + 기능별 templates) - CSS/JS 파일 분리 (common + 기능별 파일) - 대시보드 기능 추가 (통계, 주간 예보, 방문객 추이) - 파일 업로드 웹 인터페이스 구현 - 백업/복구 관리 UI 구현 - Docker 배포 환경 개선 - .gitignore 업데이트 (uploads, backups, cache 등)
This commit is contained in:
13
app/blueprints/__init__.py
Normal file
13
app/blueprints/__init__.py
Normal file
@ -0,0 +1,13 @@
|
||||
# app/blueprints/__init__.py
|
||||
"""
|
||||
Flask Blueprints 모듈
|
||||
|
||||
각 기능별 Blueprint를 정의합니다.
|
||||
"""
|
||||
|
||||
from .dashboard import dashboard_bp
|
||||
from .upload import upload_bp
|
||||
from .backup import backup_bp
|
||||
from .status import status_bp
|
||||
|
||||
__all__ = ['dashboard_bp', 'upload_bp', 'backup_bp', 'status_bp']
|
||||
129
app/blueprints/backup.py
Normal file
129
app/blueprints/backup.py
Normal file
@ -0,0 +1,129 @@
|
||||
# app/blueprints/backup.py
|
||||
"""
|
||||
백업 관리 블루프린트
|
||||
|
||||
역할:
|
||||
- 데이터베이스 백업 생성
|
||||
- 백업 복구
|
||||
- 백업 목록 조회
|
||||
"""
|
||||
|
||||
from flask import Blueprint, render_template, request, jsonify
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
|
||||
|
||||
from app.file_processor import FileProcessor
|
||||
|
||||
backup_bp = Blueprint('backup', __name__, url_prefix='/api')
|
||||
|
||||
|
||||
def get_file_processor(upload_folder):
|
||||
"""파일 프로세서 인스턴스 반환"""
|
||||
return FileProcessor(upload_folder)
|
||||
|
||||
|
||||
@backup_bp.route('/backup-page')
|
||||
def index():
|
||||
"""백업 관리 페이지"""
|
||||
return render_template('backup.html')
|
||||
|
||||
|
||||
@backup_bp.route('/backup', methods=['POST'])
|
||||
def create_backup():
|
||||
"""
|
||||
새 백업 생성
|
||||
|
||||
응답:
|
||||
{
|
||||
'success': bool,
|
||||
'message': str,
|
||||
'filename': str (선택사항)
|
||||
}
|
||||
"""
|
||||
try:
|
||||
backup_folder = os.path.join(os.path.dirname(__file__), '..', '..', 'backups')
|
||||
os.makedirs(backup_folder, exist_ok=True)
|
||||
|
||||
file_processor = get_file_processor(backup_folder)
|
||||
backup_info = file_processor.create_database_backup()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '백업이 생성되었습니다.',
|
||||
'filename': backup_info.get('filename')
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'백업 생성 실패: {str(e)}'
|
||||
}), 500
|
||||
|
||||
|
||||
@backup_bp.route('/backups', methods=['GET'])
|
||||
def get_backups():
|
||||
"""
|
||||
백업 목록 조회
|
||||
|
||||
응답:
|
||||
{
|
||||
'backups': List[dict] - [{'filename': str, 'size': int, 'created': str}, ...]
|
||||
}
|
||||
"""
|
||||
try:
|
||||
backup_folder = os.path.join(os.path.dirname(__file__), '..', '..', 'backups')
|
||||
os.makedirs(backup_folder, exist_ok=True)
|
||||
|
||||
file_processor = get_file_processor(backup_folder)
|
||||
backups = file_processor.list_database_backups()
|
||||
|
||||
return jsonify({'backups': backups})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@backup_bp.route('/restore', methods=['POST'])
|
||||
def restore_backup():
|
||||
"""
|
||||
백업 복구
|
||||
|
||||
요청:
|
||||
{
|
||||
'filename': str - 복구할 백업 파일명
|
||||
}
|
||||
|
||||
응답:
|
||||
{
|
||||
'success': bool,
|
||||
'message': str
|
||||
}
|
||||
"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
filename = data.get('filename')
|
||||
|
||||
if not filename:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '파일명을 지정하세요.'
|
||||
}), 400
|
||||
|
||||
backup_folder = os.path.join(os.path.dirname(__file__), '..', '..', 'backups')
|
||||
file_processor = get_file_processor(backup_folder)
|
||||
file_processor.restore_database_backup(filename)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '데이터베이스가 복구되었습니다.'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'복구 실패: {str(e)}'
|
||||
}), 500
|
||||
225
app/blueprints/dashboard.py
Normal file
225
app/blueprints/dashboard.py
Normal file
@ -0,0 +1,225 @@
|
||||
# app/blueprints/dashboard.py
|
||||
"""
|
||||
대시보드 블루프린트
|
||||
|
||||
역할:
|
||||
- 데이터 통계 조회 (OKPOS, UPSolution, 날씨)
|
||||
- 주간 예보 조회
|
||||
- 방문객 추이 차트 데이터 제공
|
||||
"""
|
||||
|
||||
from flask import Blueprint, render_template, jsonify, request
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy import select, func, desc
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
|
||||
|
||||
from conf import db, db_schema
|
||||
|
||||
dashboard_bp = Blueprint('dashboard', __name__, url_prefix='/api/dashboard')
|
||||
|
||||
|
||||
@dashboard_bp.route('/')
|
||||
def index():
|
||||
"""대시보드 페이지"""
|
||||
return render_template('dashboard.html')
|
||||
|
||||
|
||||
@dashboard_bp.route('/okpos-product', methods=['GET'])
|
||||
def get_okpos_product_stats():
|
||||
"""OKPOS 상품별 통계"""
|
||||
try:
|
||||
session = db.get_session()
|
||||
|
||||
# 데이터 개수
|
||||
total_records = session.query(db_schema.OkposProduct).count()
|
||||
|
||||
# 데이터 보유 일수
|
||||
earliest = session.query(func.min(db_schema.OkposProduct.data_date)).scalar()
|
||||
latest = session.query(func.max(db_schema.OkposProduct.data_date)).scalar()
|
||||
|
||||
if earliest and latest:
|
||||
total_days = (latest - earliest).days + 1
|
||||
last_date = latest.strftime('%Y-%m-%d')
|
||||
else:
|
||||
total_days = 0
|
||||
last_date = None
|
||||
|
||||
session.close()
|
||||
|
||||
return jsonify({
|
||||
'total_records': total_records,
|
||||
'total_days': total_days,
|
||||
'last_date': last_date
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@dashboard_bp.route('/okpos-receipt', methods=['GET'])
|
||||
def get_okpos_receipt_stats():
|
||||
"""OKPOS 영수증 통계"""
|
||||
try:
|
||||
session = db.get_session()
|
||||
|
||||
total_records = session.query(db_schema.OkposReceipt).count()
|
||||
|
||||
earliest = session.query(func.min(db_schema.OkposReceipt.receipt_date)).scalar()
|
||||
latest = session.query(func.max(db_schema.OkposReceipt.receipt_date)).scalar()
|
||||
|
||||
if earliest and latest:
|
||||
total_days = (latest - earliest).days + 1
|
||||
last_date = latest.strftime('%Y-%m-%d')
|
||||
else:
|
||||
total_days = 0
|
||||
last_date = None
|
||||
|
||||
session.close()
|
||||
|
||||
return jsonify({
|
||||
'total_records': total_records,
|
||||
'total_days': total_days,
|
||||
'last_date': last_date
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@dashboard_bp.route('/upsolution', methods=['GET'])
|
||||
def get_upsolution_stats():
|
||||
"""UPSolution 통계"""
|
||||
try:
|
||||
session = db.get_session()
|
||||
|
||||
total_records = session.query(db_schema.Upsolution).count()
|
||||
|
||||
earliest = session.query(func.min(db_schema.Upsolution.sales_date)).scalar()
|
||||
latest = session.query(func.max(db_schema.Upsolution.sales_date)).scalar()
|
||||
|
||||
if earliest and latest:
|
||||
total_days = (latest - earliest).days + 1
|
||||
last_date = latest.strftime('%Y-%m-%d')
|
||||
else:
|
||||
total_days = 0
|
||||
last_date = None
|
||||
|
||||
session.close()
|
||||
|
||||
return jsonify({
|
||||
'total_records': total_records,
|
||||
'total_days': total_days,
|
||||
'last_date': last_date
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@dashboard_bp.route('/weather', methods=['GET'])
|
||||
def get_weather_stats():
|
||||
"""날씨 데이터 통계"""
|
||||
try:
|
||||
session = db.get_session()
|
||||
|
||||
total_records = session.query(db_schema.Weather).count()
|
||||
|
||||
earliest = session.query(func.min(db_schema.Weather.date)).scalar()
|
||||
latest = session.query(func.max(db_schema.Weather.date)).scalar()
|
||||
|
||||
if earliest and latest:
|
||||
total_days = (latest - earliest).days + 1
|
||||
last_date = latest.strftime('%Y-%m-%d')
|
||||
else:
|
||||
total_days = 0
|
||||
last_date = None
|
||||
|
||||
session.close()
|
||||
|
||||
return jsonify({
|
||||
'total_records': total_records,
|
||||
'total_days': total_days,
|
||||
'last_date': last_date
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@dashboard_bp.route('/weekly-forecast', methods=['GET'])
|
||||
def get_weekly_forecast():
|
||||
"""이번주 예상 날씨 & 방문객"""
|
||||
try:
|
||||
session = db.get_session()
|
||||
|
||||
# 오늘부터 7일간 데이터 조회
|
||||
today = datetime.now().date()
|
||||
end_date = today + timedelta(days=6)
|
||||
|
||||
forecast_data = []
|
||||
|
||||
for i in range(7):
|
||||
current_date = today + timedelta(days=i)
|
||||
day_name = ['월', '화', '수', '목', '금', '토', '일'][current_date.weekday()]
|
||||
|
||||
# 날씨 데이터
|
||||
weather = session.query(db_schema.Weather).filter(
|
||||
db_schema.Weather.date == current_date
|
||||
).first()
|
||||
|
||||
# 예상 방문객 (동일한 요일의 평균)
|
||||
same_day_visitors = session.query(
|
||||
func.avg(db_schema.DailyVisitor.visitors)
|
||||
).filter(
|
||||
func.dayofweek(db_schema.DailyVisitor.visit_date) == (current_date.weekday() + 2) % 7 + 1
|
||||
).scalar()
|
||||
|
||||
forecast_data.append({
|
||||
'date': current_date.strftime('%Y-%m-%d'),
|
||||
'day': day_name,
|
||||
'min_temp': weather.min_temp if weather else None,
|
||||
'max_temp': weather.max_temp if weather else None,
|
||||
'precipitation': weather.precipitation if weather else 0.0,
|
||||
'humidity': weather.humidity if weather else None,
|
||||
'expected_visitors': int(same_day_visitors) if same_day_visitors else 0
|
||||
})
|
||||
|
||||
session.close()
|
||||
|
||||
return jsonify({'forecast_data': forecast_data})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@dashboard_bp.route('/visitor-trend', methods=['GET'])
|
||||
def get_visitor_trend():
|
||||
"""방문객 추이 데이터"""
|
||||
try:
|
||||
start_date_str = request.args.get('start_date')
|
||||
end_date_str = request.args.get('end_date')
|
||||
|
||||
if not start_date_str or not end_date_str:
|
||||
return jsonify({'error': '날짜 범위를 지정하세요.'}), 400
|
||||
|
||||
start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date()
|
||||
end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date()
|
||||
|
||||
session = db.get_session()
|
||||
|
||||
visitors = session.query(
|
||||
db_schema.DailyVisitor.visit_date,
|
||||
db_schema.DailyVisitor.visitors
|
||||
).filter(
|
||||
db_schema.DailyVisitor.visit_date.between(start_date, end_date)
|
||||
).order_by(db_schema.DailyVisitor.visit_date).all()
|
||||
|
||||
session.close()
|
||||
|
||||
dates = [v[0].strftime('%Y-%m-%d') for v in visitors]
|
||||
visitor_counts = [v[1] for v in visitors]
|
||||
|
||||
return jsonify({
|
||||
'dates': dates,
|
||||
'visitors': visitor_counts
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
57
app/blueprints/status.py
Normal file
57
app/blueprints/status.py
Normal file
@ -0,0 +1,57 @@
|
||||
# app/blueprints/status.py
|
||||
"""
|
||||
시스템 상태 블루프린트
|
||||
|
||||
역할:
|
||||
- 데이터베이스 연결 상태 확인
|
||||
- 업로드 폴더 상태 확인
|
||||
"""
|
||||
|
||||
from flask import Blueprint, jsonify
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
|
||||
|
||||
from conf import db
|
||||
|
||||
status_bp = Blueprint('status', __name__, url_prefix='/api')
|
||||
|
||||
|
||||
@status_bp.route('/status', methods=['GET'])
|
||||
def get_status():
|
||||
"""
|
||||
시스템 상태 확인
|
||||
|
||||
응답:
|
||||
{
|
||||
'database': bool - 데이터베이스 연결 여부,
|
||||
'upload_folder': bool - 업로드 폴더 접근 여부
|
||||
}
|
||||
"""
|
||||
try:
|
||||
# 데이터베이스 연결 확인
|
||||
database_ok = False
|
||||
try:
|
||||
session = db.get_session()
|
||||
session.execute('SELECT 1')
|
||||
session.close()
|
||||
database_ok = True
|
||||
except Exception as e:
|
||||
print(f"Database connection error: {e}")
|
||||
|
||||
# 업로드 폴더 확인
|
||||
upload_folder = os.path.join(os.path.dirname(__file__), '..', '..', 'uploads')
|
||||
upload_folder_ok = os.path.isdir(upload_folder) and os.access(upload_folder, os.W_OK)
|
||||
|
||||
return jsonify({
|
||||
'database': database_ok,
|
||||
'upload_folder': upload_folder_ok
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'error': str(e),
|
||||
'database': False,
|
||||
'upload_folder': False
|
||||
}), 500
|
||||
83
app/blueprints/upload.py
Normal file
83
app/blueprints/upload.py
Normal file
@ -0,0 +1,83 @@
|
||||
# app/blueprints/upload.py
|
||||
"""
|
||||
파일 업로드 블루프린트
|
||||
|
||||
역할:
|
||||
- 파일 업로드 처리
|
||||
- 업로드 UI 제공
|
||||
"""
|
||||
|
||||
from flask import Blueprint, render_template, request, jsonify
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
|
||||
|
||||
from app.file_processor import FileProcessor
|
||||
|
||||
upload_bp = Blueprint('upload', __name__, url_prefix='/api/upload')
|
||||
|
||||
|
||||
def get_file_processor(upload_folder):
|
||||
"""파일 프로세서 인스턴스 반환"""
|
||||
return FileProcessor(upload_folder)
|
||||
|
||||
|
||||
@upload_bp.route('/')
|
||||
def index():
|
||||
"""파일 업로드 페이지"""
|
||||
return render_template('upload.html')
|
||||
|
||||
|
||||
@upload_bp.route('', methods=['POST'])
|
||||
def upload_files():
|
||||
"""
|
||||
파일 업로드 처리
|
||||
|
||||
요청:
|
||||
files: MultiDict[FileStorage] - 업로드된 파일들
|
||||
|
||||
응답:
|
||||
{
|
||||
'success': bool,
|
||||
'message': str,
|
||||
'files': List[dict],
|
||||
'errors': List[dict]
|
||||
}
|
||||
"""
|
||||
try:
|
||||
if 'files' not in request.files:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '업로드된 파일이 없습니다.',
|
||||
'files': [],
|
||||
'errors': []
|
||||
}), 400
|
||||
|
||||
uploaded_files = request.files.getlist('files')
|
||||
|
||||
if len(uploaded_files) == 0:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '파일을 선택하세요.',
|
||||
'files': [],
|
||||
'errors': []
|
||||
}), 400
|
||||
|
||||
# 업로드 폴더 경로
|
||||
upload_folder = os.path.join(os.path.dirname(__file__), '..', '..', 'uploads')
|
||||
os.makedirs(upload_folder, exist_ok=True)
|
||||
|
||||
# 파일 처리
|
||||
file_processor = get_file_processor(upload_folder)
|
||||
results = file_processor.process_uploads(uploaded_files)
|
||||
|
||||
return jsonify(results)
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'업로드 처리 중 오류: {str(e)}',
|
||||
'files': [],
|
||||
'errors': [str(e)]
|
||||
}), 500
|
||||
Reference in New Issue
Block a user