Files
fg-auto/autouploader/gnu_autoupload.py

383 lines
14 KiB
Python

# -*- 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:
msg_sender = MessageSender(
mattermost_url=MATTERMOST_CONFIG.get('URL', ''),
mattermost_token=MATTERMOST_CONFIG.get('TOKEN', ''),
mattermost_channel_id=MATTERMOST_CONFIG.get('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):
"""
그누보드 게시글 및 첨부파일 등록
Args:
board: 게시판 ID
subject: 제목
content: 내용
mb_id: 게시자 ID
nickname: 닉네임
ca_name: 카테고리 이름 (선택사항)
file_list: 첨부파일 경로 리스트 (선택사항)
msg_sender: MessageSender 인스턴스
Returns:
tuple: (성공여부, 에러메시지)
"""
conn = None
try:
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}"
if msg_sender:
msg_sender.send(success_msg, platforms=['mattermost'])
return True, None
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
finally:
if conn:
conn.close()
# ---------------------------
# 메인 실행 함수
# ---------------------------
def main():
"""메인 실행 함수"""
logger.info("=== 날씨 정보 게시글 자동 생성 시작 ===")
# MessageSender 초기화
msg_sender = init_message_sender()
if not msg_sender:
logger.warning("Mattermost 알림이 비활성화됩니다")
try:
# 무료입장 조건에 대해서만 안내함.
MAIN["content"] = """
<p>Rainy Day 이벤트 적용안내</p>
<p><b>10:00 ~ 22:00까지의 예보를 합산하며, ~1mm인 경우 0.5mm로 계산합니다.</b></p>
<p>레이니데이 이벤트 정보 확인</p>
<p><a href="https://firstgarden.co.kr/news/60">이벤트 정보 보기</a></p>
"""
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']]
# 게시글 작성
success, error = 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
)
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()