413 lines
15 KiB
Python
413 lines
15 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:
|
|
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"] = """
|
|
<p>Rainy Day 이벤트 적용안내</p>
|
|
<p><b>10:00 ~ 21: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']]
|
|
|
|
# 게시글 작성, 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()
|