# app/file_processor.py """ POS 데이터 파일 처리 및 검증 모듈 지원 형식: - UPSOLUTION: POS 데이터 (pos_update_upsolution.py에서 처리) - OKPOS: 일자별 상품별 파일, 영수증별매출상세현황 파일 """ import os import sys import logging import pandas as pd from datetime import datetime from pathlib import Path import subprocess # 프로젝트 루트 경로 추가 sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from conf import db, db_schema from lib.common import setup_logging from lib.pos_update_daily_product import process_okpos_file from lib.pos_update_upsolution import process_upsolution_file logger = setup_logging('file_processor', 'INFO') class FileProcessor: """POS 데이터 파일 처리 클래스""" # 지원하는 파일 확장자 ALLOWED_EXTENSIONS = {'.xlsx', '.xls', '.csv'} # OKPOS 파일 패턴 OKPOS_PATTERNS = { '일자별': '일자별.*상품별.*파일', '영수증별': '영수증별매출상세현황' } # UPSOLUTION 파일 패턴 UPSOLUTION_PATTERNS = {'UPSOLUTION'} def __init__(self, upload_folder): """ 초기화 Args: upload_folder (str): 파일 업로드 폴더 경로 """ self.upload_folder = upload_folder self.backup_folder = os.path.join(os.path.dirname(__file__), '..', 'dbbackup') os.makedirs(self.backup_folder, exist_ok=True) logger.info(f"파일 프로세서 초기화 - 업로드폴더: {upload_folder}") def get_file_type(self, filename): """ 파일 타입 판정 Args: filename (str): 파일명 Returns: str: 파일 타입 ('upsolution', 'okpos', 'unknown') """ filename_upper = filename.upper() # UPSOLUTION 파일 확인 if 'UPSOLUTION' in filename_upper: logger.debug(f"UPSOLUTION 파일 감지: {filename}") return 'upsolution' # OKPOS 파일 확인 if '일자별' in filename and '상품별' in filename: logger.debug(f"OKPOS 파일(일자별) 감지: {filename}") return 'okpos' if '영수증별매출상세현황' in filename: logger.debug(f"OKPOS 파일(영수증별) 감지: {filename}") return 'okpos' logger.warning(f"알 수 없는 파일 타입: {filename}") return 'unknown' def validate_file(self, filename): """ 파일 검증 Args: filename (str): 파일명 Returns: tuple[bool, str]: (성공 여부, 메시지) """ logger.info(f"파일 검증 시작: {filename}") # 파일 확장자 확인 _, ext = os.path.splitext(filename) if ext.lower() not in self.ALLOWED_EXTENSIONS: msg = f"지원하지 않는 파일 형식: {ext}" logger.warning(msg) return False, msg # 파일 타입 확인 file_type = self.get_file_type(filename) if file_type == 'unknown': msg = f"파일명을 인식할 수 없습니다. (UPSOLUTION 또는 일자별 상품별 파일이어야 함)" logger.warning(msg) return False, msg logger.info(f"파일 검증 완료: {filename} ({file_type})") return True, file_type def process_uploads(self, uploaded_files): """ 여러 파일 처리 Args: uploaded_files (list): 업로드된 파일 객체 리스트 Returns: dict: 처리 결과 { 'files': List[dict], # 처리된 파일 'errors': List[dict] # 에러 정보 } """ logger.info(f"파일 처리 시작 - {len(uploaded_files)}개 파일") files_result = [] errors_result = [] for file_obj in uploaded_files: try: filename = secure_filename(file_obj.filename) logger.info(f"파일 처리: {filename}") # 파일 검증 is_valid, file_type_or_msg = self.validate_file(filename) if not is_valid: logger.error(f"파일 검증 실패: {filename} - {file_type_or_msg}") errors_result.append({ 'filename': filename, 'message': file_type_or_msg, 'type': 'validation' }) files_result.append({ 'filename': filename, 'status': 'failed', 'message': file_type_or_msg }) continue file_type = file_type_or_msg # 파일 저장 filepath = os.path.join(self.upload_folder, filename) file_obj.save(filepath) logger.info(f"파일 저장 완료: {filepath}") # 파일 처리 result = self.process_file(filepath, file_type) if result['success']: logger.info(f"파일 처리 완료: {filename}") files_result.append({ 'filename': filename, 'status': 'success', 'message': result['message'], 'rows_inserted': result.get('rows_inserted', 0) }) # 파일 삭제 try: os.remove(filepath) logger.info(f"임시 파일 삭제: {filepath}") except Exception as e: logger.warning(f"임시 파일 삭제 실패: {filepath} - {e}") else: logger.error(f"파일 처리 실패: {filename} - {result['message']}") files_result.append({ 'filename': filename, 'status': 'failed', 'message': result['message'] }) errors_result.append({ 'filename': filename, 'message': result['message'], 'type': 'processing' }) except Exception as e: logger.error(f"파일 처리 중 예외 발생: {file_obj.filename} - {e}", exc_info=True) errors_result.append({ 'filename': file_obj.filename, 'message': f'처리 중 오류: {str(e)}', 'type': 'exception' }) files_result.append({ 'filename': file_obj.filename, 'status': 'failed', 'message': str(e) }) logger.info(f"파일 처리 완료 - 성공: {len([f for f in files_result if f['status'] == 'success'])}, 실패: {len(errors_result)}") return { 'files': files_result, 'errors': errors_result } def process_file(self, filepath, file_type): """ 개별 파일 처리 Args: filepath (str): 파일 경로 file_type (str): 파일 타입 Returns: dict: 처리 결과 { 'success': bool, 'message': str, 'rows_inserted': int } """ try: if file_type == 'okpos': logger.info(f"OKPOS 파일 처리: {filepath}") return self._process_okpos_file(filepath) elif file_type == 'upsolution': logger.info(f"UPSOLUTION 파일 처리: {filepath}") return self._process_upsolution_file(filepath) else: msg = f"지원하지 않는 파일 타입: {file_type}" logger.error(msg) return { 'success': False, 'message': msg } except Exception as e: logger.error(f"파일 처리 중 예외 발생: {filepath} - {e}", exc_info=True) return { 'success': False, 'message': f'파일 처리 중 오류: {str(e)}' } def _process_okpos_file(self, filepath): """ OKPOS 파일 처리 Args: filepath (str): 파일 경로 Returns: dict: 처리 결과 """ try: logger.info(f"OKPOS 파일 처리 시작: {filepath}") # process_okpos_file 함수 호출 result = process_okpos_file(filepath) logger.info(f"OKPOS 파일 처리 완료: {filepath} - {result.get('rows_inserted', 0)} 행") return { 'success': True, 'message': f"{result.get('rows_inserted', 0)} 행이 저장되었습니다.", 'rows_inserted': result.get('rows_inserted', 0) } except Exception as e: logger.error(f"OKPOS 파일 처리 오류: {filepath} - {e}", exc_info=True) return { 'success': False, 'message': f'OKPOS 파일 처리 오류: {str(e)}' } def _process_upsolution_file(self, filepath): """ UPSOLUTION 파일 처리 Args: filepath (str): 파일 경로 Returns: dict: 처리 결과 """ try: logger.info(f"UPSOLUTION 파일 처리 시작: {filepath}") # process_upsolution_file 함수 호출 result = process_upsolution_file(filepath) logger.info(f"UPSOLUTION 파일 처리 완료: {filepath} - {result.get('rows_inserted', 0)} 행") return { 'success': True, 'message': f"{result.get('rows_inserted', 0)} 행이 저장되었습니다.", 'rows_inserted': result.get('rows_inserted', 0) } except Exception as e: logger.error(f"UPSOLUTION 파일 처리 오류: {filepath} - {e}", exc_info=True) return { 'success': False, 'message': f'UPSOLUTION 파일 처리 오류: {str(e)}' } def create_database_backup(self): """ 데이터베이스 백업 생성 Returns: str: 백업 파일 경로 """ try: logger.info("데이터베이스 백업 시작") # 백업 파일명: backup_YYYYMMDD_HHMMSS.sql timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") backup_filename = f"backup_{timestamp}.sql" backup_path = os.path.join(self.backup_folder, backup_filename) # 데이터베이스 설정 로드 config = db.load_config() db_cfg = config['database'] # mysqldump 명령 실행 cmd = [ 'mysqldump', '-h', db_cfg['host'], '-u', db_cfg['user'], f'-p{db_cfg["password"]}', db_cfg['name'], '-v' ] logger.info(f"백업 명령 실행: mysqldump ...") with open(backup_path, 'w', encoding='utf-8') as f: process = subprocess.run( cmd, stdout=f, stderr=subprocess.PIPE, text=True ) if process.returncode != 0: error_msg = process.stderr logger.error(f"백업 실패: {error_msg}") raise Exception(f"백업 실패: {error_msg}") file_size = os.path.getsize(backup_path) logger.info(f"데이터베이스 백업 완료: {backup_path} ({file_size} bytes)") return backup_path except Exception as e: logger.error(f"데이터베이스 백업 오류: {e}", exc_info=True) raise def restore_database_backup(self, filename): """ 데이터베이스 복구 Args: filename (str): 백업 파일명 Returns: bool: 복구 성공 여부 """ try: backup_path = os.path.join(self.backup_folder, filename) if not os.path.exists(backup_path): logger.error(f"백업 파일 없음: {backup_path}") return False logger.info(f"데이터베이스 복구 시작: {backup_path}") # 데이터베이스 설정 로드 config = db.load_config() db_cfg = config['database'] # mysql 명령 실행 cmd = [ 'mysql', '-h', db_cfg['host'], '-u', db_cfg['user'], f'-p{db_cfg["password"]}', db_cfg['name'] ] logger.info(f"복구 명령 실행: mysql ...") with open(backup_path, 'r', encoding='utf-8') as f: process = subprocess.run( cmd, stdin=f, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) if process.returncode != 0: error_msg = process.stderr logger.error(f"복구 실패: {error_msg}") return False logger.info(f"데이터베이스 복구 완료: {filename}") return True except Exception as e: logger.error(f"데이터베이스 복구 오류: {e}", exc_info=True) return False def list_database_backups(self): """ 사용 가능한 백업 목록 조회 Returns: list: 백업 파일 정보 리스트 [ { 'filename': str, 'size': int, 'created': str } ] """ try: logger.debug("백업 목록 조회") backups = [] for filename in os.listdir(self.backup_folder): filepath = os.path.join(self.backup_folder, filename) if os.path.isfile(filepath) and filename.endswith('.sql'): stat = os.stat(filepath) backups.append({ 'filename': filename, 'size': stat.st_size, 'created': datetime.fromtimestamp(stat.st_mtime).isoformat() }) # 최신순 정렬 backups.sort(key=lambda x: x['created'], reverse=True) logger.debug(f"백업 목록: {len(backups)}개") return backups except Exception as e: logger.error(f"백업 목록 조회 오류: {e}", exc_info=True) return [] def secure_filename(filename): """ 파일명 보안 처리 Args: filename (str): 원본 파일명 Returns: str: 보안 처리된 파일명 """ # 경로 트래버설 방지 from werkzeug.utils import secure_filename as werkzeug_secure return werkzeug_secure(filename)