# -*- coding: utf-8 -*- """ gnu_autoupload.py 기능: 1. Selenium을 이용해 날씨 정보를 캡처 (weather_capture.py 호출) 2. FTP를 이용해 이미지 업로드 3. 그누보드 DB에 게시글 및 첨부파일 정보 자동 등록 4. Mattermost으로 결과 알림 발송 """ import os import sys import time import subprocess import tempfile import hashlib import logging from datetime import datetime from PIL import Image import pymysql import ftputil import traceback from config import DB_CONFIG, FTP_CONFIG, MAIN, MATTERMOST_CONFIG from weather import get_precipitation_summary from send_message import MessageSender # 로깅 설정 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # --------------------------- # MessageSender 초기화 # --------------------------- def init_message_sender(): """Mattermost 메시지 발송기 초기화""" try: mattermost_url = MATTERMOST_CONFIG.get('URL', '') mattermost_token = MATTERMOST_CONFIG.get('TOKEN', '') mattermost_channel_id = MATTERMOST_CONFIG.get('CHANNEL_ID', '') # 설정 확인 로그 logger.info(f"Mattermost 설정: URL={'설정됨' if mattermost_url else '미설정'}, " f"TOKEN={'설정됨' if mattermost_token else '미설정'}, " f"CHANNEL_ID={'설정됨' if mattermost_channel_id else '미설정'}") if not mattermost_url: logger.warning("Mattermost URL이 설정되지 않았습니다") return None msg_sender = MessageSender( mattermost_url=mattermost_url, mattermost_token=mattermost_token, mattermost_channel_id=mattermost_channel_id ) return msg_sender except Exception as e: logger.warning(f"MessageSender 초기화 실패: {e}") return None # --------------------------- # 이미지 캡처 함수 # --------------------------- def capture_image(script_path, output_path, max_attempts=5, msg_sender=None): """ 이미지 캡처 시도 Args: script_path: weather_capture.py 경로 output_path: 캡처 이미지 저장 경로 max_attempts: 최대 시도 횟수 msg_sender: MessageSender 인스턴스 Returns: tuple: (성공여부, 에러메시지) """ for attempt in range(max_attempts): logger.info(f"이미지 캡처 시도 {attempt + 1}/{max_attempts}") try: result = subprocess.run( [sys.executable, script_path], check=True, capture_output=True, text=True, timeout=60 ) if os.path.isfile(output_path): logger.info(f"이미지 캡처 성공: {output_path}") return True, None except subprocess.TimeoutExpired: logger.warning(f"캡처 타임아웃 (시도 {attempt + 1}/{max_attempts})") except subprocess.CalledProcessError as e: logger.error(f"weather_capture.py 실행 실패: {e.stderr}") except Exception as e: logger.error(f"예상치 못한 오류: {type(e).__name__}: {e}") time.sleep(2) # 모든 시도 실패 final_error = f"❌ **날씨 이미지 캡처 실패**\n\n"\ f"📅 날짜: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"\ f"🔄 시도 횟수: {max_attempts}회\n"\ f"📁 출력 경로: `{output_path}`\n"\ f"⚠️ 파일이 생성되지 않았습니다." logger.error(final_error.replace('\n', ' ')) # Mattermost 알림 전송 if msg_sender: msg_sender.send(final_error, platforms=['mattermost']) return False, final_error # --------------------------- # 파일 관련 유틸 함수 # --------------------------- def file_type(ext): """확장자에 따른 파일 타입 코드 반환""" return { 'gif': '1', 'jpeg': '2', 'jpg': '2', 'png': '3', 'swf': '4', 'psd': '5', 'bmp': '6', 'tif': '7', 'tiff': '7', 'jpc': '9', 'jp2': '10', 'jpx': '11', 'jb2': '12', 'swc': '13', 'iff': '14', 'wbmp': '15', 'xbm': '16' }.get(ext.lower(), '0') def get_filename(filename): """타임스탬프와 해시를 포함한 파일명 생성""" ms = datetime.now().microsecond encoded_name = filename.encode('utf-8') return f'{ms}_{hashlib.sha1(encoded_name).hexdigest()}' def file_upload(filename, bf_file, msg_sender=None): """ FTP 파일 업로드 Args: filename: 원본 파일 경로 bf_file: FTP에 저장될 파일명 msg_sender: MessageSender 인스턴스 Returns: bool: 성공 여부 """ try: with ftputil.FTPHost(FTP_CONFIG['HOST'], FTP_CONFIG['USER'], FTP_CONFIG['PASS']) as fh: fh.chdir(FTP_CONFIG['UPLOAD_DIR']) fh.upload(filename, bf_file) logger.info(f"FTP 업로드 완료: {filename} → {bf_file}") return True except Exception as e: error_msg = f"❌ **FTP 파일 업로드 실패**\n\n"\ f"📅 날짜: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"\ f"📁 파일: `{filename}`\n"\ f"🗂️ 대상: `{bf_file}`\n"\ f"⚠️ 오류: `{type(e).__name__}: {e}`" logger.error(error_msg.replace('\n', ' ')) if msg_sender: msg_sender.send(error_msg, platforms=['mattermost']) return False # --------------------------- # 게시글 작성 함수 # --------------------------- def write_board(board, subject, content, mb_id, nickname, ca_name=None, file_list=None, msg_sender=None, url=None): """ 그누보드 게시글 및 첨부파일 등록 Args: board: 게시판 ID subject: 제목 content: 내용 mb_id: 게시자 ID nickname: 닉네임 ca_name: 카테고리 이름 (선택사항) file_list: 첨부파일 경로 리스트 (선택사항) msg_sender: MessageSender 인스턴스 url: 게시판 URL (선택사항) Returns: tuple: (성공여부, 에러메시지, wr_id) """ conn = None try: # DB 연결 정보 디버깅 로그 logger.info(f"DB 연결 시도: HOST={DB_CONFIG['HOST']}, USER={DB_CONFIG['USER']}, DB={DB_CONFIG['DBNAME']}") conn = pymysql.connect( host=DB_CONFIG['HOST'], user=DB_CONFIG['USER'], db=DB_CONFIG['DBNAME'], password=DB_CONFIG['PASS'], charset=DB_CONFIG['CHARSET'] ) curs = conn.cursor() now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') # 게시글 번호 조회 curs.execute(f"SELECT wr_num FROM g5_write_{board}") wr_num = str(int(curs.fetchone()[0]) - 1) # 게시글 삽입 curs.execute(f"""INSERT INTO g5_write_{board} SET wr_num = {wr_num}, wr_reply = '', wr_comment = 0, ca_name = %s, wr_option = 'html1', wr_subject = %s, wr_content = %s, wr_link1 = '', wr_link2 = '', wr_link1_hit = 0, wr_link2_hit = 0, wr_hit = 1, wr_good = 0, wr_nogood = 0, mb_id = %s, wr_password = '', wr_name = %s, wr_email = '', wr_homepage = '', wr_datetime = %s, wr_last = %s, wr_ip = '111.111.111.111', wr_comment_reply = '', wr_facebook_user = '', wr_twitter_user = '', wr_1 = '', wr_2 = '', wr_3 = '', wr_4 = '', wr_5 = '', wr_6 = '', wr_7 = '', wr_8 = '', wr_9 = '', wr_10 = '' """, (ca_name, subject, content, mb_id, nickname, now, now)) # 게시글 ID 조회 curs.execute(f"SELECT wr_id FROM g5_write_{board} ORDER BY wr_id DESC LIMIT 1") wr_id = str(curs.fetchone()[0]) # 부모 글 ID 업데이트 curs.execute(f"UPDATE g5_write_{board} SET wr_parent = {wr_id} WHERE wr_id = {wr_id}") # 새 게시글 알림 추가 curs.execute(f"""INSERT INTO g5_board_new (bo_table, wr_id, wr_parent, bn_datetime, mb_id) VALUES (%s, %s, %s, %s, %s)""", (board, wr_id, wr_id, now, mb_id)) # 게시판 글 수 업데이트 curs.execute(f"SELECT bo_count_write FROM g5_board WHERE bo_table = %s", (board,)) bo_count_write = int(curs.fetchone()[0]) curs.execute(f"UPDATE g5_board SET bo_count_write = %s WHERE bo_table = %s", (bo_count_write + 1, board)) # 첨부파일 처리 file_count = 0 if file_list: for idx, file in enumerate(file_list): if not os.path.isfile(file): logger.warning(f"파일 없음: {file}") continue ext = os.path.splitext(file)[1].lstrip('.') bf_file = f"{get_filename(file)}.{ext}" if file_upload(file, bf_file, msg_sender): img_type = file_type(ext) width, height = (0, 0) if img_type != '0': try: with Image.open(file) as img: width, height = img.size except Exception as e: logger.warning(f"이미지 크기 조회 실패: {e}") size = os.path.getsize(file) curs.execute(f"""INSERT INTO g5_board_file SET bo_table = %s, wr_id = %s, bf_no = %s, bf_source = %s, bf_file = %s, bf_content = '', bf_download = 0, bf_filesize = %s, bf_width = %s, bf_height = %s, bf_type = %s, bf_datetime = %s""", (board, wr_id, idx, os.path.basename(file), bf_file, size, width, height, img_type, now)) file_count += 1 else: raise Exception(f"파일 업로드 실패: {file}") # 게시글 파일 수 업데이트 curs.execute(f"UPDATE g5_write_{board} SET wr_file = %s WHERE wr_id = %s", (file_count, wr_id)) conn.commit() logger.info("게시글 및 첨부파일 등록 완료") # 성공 메시지 전송 success_msg = f"✅ **게시글 등록 완료**\n\n"\ f"📅 날짜: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"\ f"📋 게시판: `{board}`\n"\ f"📝 제목: {subject}\n"\ f"📎 첨부파일: {file_count}개" # 캡처파일 정보 추가 (file_list에 2개 이상의 파일이 있을 경우) if file_list and len(file_list) > 1 and os.path.isfile(file_list[1]): capture_filename = os.path.basename(file_list[1]) capture_size = os.path.getsize(file_list[1]) / 1024 # KB 단위 success_msg += f"\n🖼️ 캡처파일: `{capture_filename}` ({capture_size:.1f}KB)" # 게시글 링크 추가 if url and board: post_url = f"{url.rstrip('/')}/{board}/{wr_id}" success_msg += f"\n🔗 게시글 링크: {post_url}" if msg_sender: msg_sender.send(success_msg, platforms=['mattermost']) return True, None, wr_id except Exception as e: if conn: conn.rollback() # 에러 메시지 생성 error_detail = traceback.format_exc() error_msg = f"❌ **게시글 등록 실패**\n\n"\ f"📅 날짜: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"\ f"📋 게시판: `{board}`\n"\ f"📝 제목: {subject}\n"\ f"⚠️ 오류 유형: `{type(e).__name__}`\n"\ f"💬 오류 메시지: {str(e)}\n"\ f"```\n{error_detail}\n```" logger.error(f"게시글 등록 실패: {type(e).__name__}: {e}") # Mattermost 알림 전송 if msg_sender: msg_sender.send(error_msg, platforms=['mattermost']) return False, error_msg, None finally: if conn: conn.close() # --------------------------- # 메인 실행 함수 # --------------------------- def main(): """메인 실행 함수""" logger.info("=== 날씨 정보 게시글 자동 생성 시작 ===") # MessageSender 초기화 msg_sender = init_message_sender() if not msg_sender: logger.warning("Mattermost 알림이 비활성화됩니다") try: # .env의 BOARD_CONTENT 사용 (또는 기본값 설정) if not MAIN.get("content"): MAIN["content"] = """

Rainy Day 이벤트 적용안내

10:00 ~ 21:00까지의 예보를 합산하며, ~1mm인 경우 0.5mm로 계산합니다.

레이니데이 이벤트 정보 확인

이벤트 정보 보기

""" today = datetime.today().strftime('%Y%m%d') data_dir = '/data' # 파일 저장 및 업로드 디렉토리 capture_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'weather_capture.py') weather_filename = f'weather_capture_{today}.png' weather_file = os.path.join(data_dir, weather_filename) thumb_file = os.path.join(data_dir, 'thumb.jpg') # 이미지 캡처 success, error = capture_image(capture_script, weather_file, msg_sender=msg_sender) if not success: logger.error("이미지 캡처 실패로 게시글 작성 중단") return # FTP 업로드 디렉토리 설정 FTP_CONFIG['UPLOAD_DIR'] = f"/www/data/file/{MAIN['board']}/" MAIN['subject'] = f"{datetime.now().strftime('%Y-%m-%d')} 날씨정보" MAIN['file1'] = thumb_file MAIN['file2'] = weather_file file_list = [MAIN['file1'], MAIN['file2']] # 게시글 작성, wr_id = write_board( board=MAIN['board'], subject=MAIN['subject'], content=MAIN['content'], mb_id=MAIN['mb_id'], nickname=MAIN['nickname'], ca_name=MAIN['ca_name'], file_list=file_list, msg_sender=msg_sender, url=MAIN.get('url') msg_sender=msg_sender ) if success: logger.info("=== 날씨 정보 게시글 자동 생성 완료 ===") else: logger.error("게시글 작성 실패") except Exception as e: error_msg = f"❌ **예상치 못한 오류 발생**\n\n"\ f"📅 날짜: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"\ f"⚠️ 오류: `{type(e).__name__}: {e}`\n"\ f"```\n{traceback.format_exc()}\n```" logger.error(error_msg.replace('\n', ' ')) if msg_sender: msg_sender.send(error_msg, platforms=['mattermost']) if __name__ == "__main__": main()