diff --git a/.env.example b/.env.example index 402aeb7..64fdd0e 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,19 @@ +# ===================================== +# 필수 설정 항목 (반드시 작성해야 함) +# ===================================== + # 게시판 설정 BOARD_ID=news -BOARD_CA_NAME=레이니데이 -BOARD_CONTENT=08:00 기상청 단기예보 +BOARD_CA_NAME=카테고리명 +BOARD_CONTENT=글 내용 BOARD_MB_ID=user_id BOARD_NICKNAME=user_nickname # 데이터베이스 설정 -DB_HOST=db.example.com +# Docker 환경: DB_HOST는 docker-compose의 service name 사용 (예: db, mysql) +# 일반 환경: 실제 MySQL 호스트 IP/도메인 사용 (예: 192.168.1.100, db.example.com) +# Synology 환경: 호스트 IP 또는 도메인 명시 (localhost 사용 불가) +DB_HOST=db DB_USER=db_username DB_PASSWORD=db_password DB_NAME=database_name @@ -18,10 +25,15 @@ FTP_USER=ftp_username FTP_PASSWORD=ftp_password FTP_UPLOAD_DIR=/data/file/news/ -# 날씨 API 서비스 키 +# 날씨 API 서비스 키 (기상청 API) SERVICE_KEY=your_weather_api_key_here -# Mattermost 알림 설정 +# ===================================== +# 선택적 설정 항목 (없어도 실행 가능) +# ===================================== + +# Mattermost 알림 설정 (오류 발생 시 알림 받기 원할 때 설정) +# URL 반드시 http:// 또는 https://로 시작해야 함 MATTERMOST_URL=https://mattermost.example.com MATTERMOST_TOKEN=your-personal-access-token MATTERMOST_CHANNEL_ID=channel_id diff --git a/README.md b/README.md index 758180c..afb11e7 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,206 @@ -## 퍼스트가든용 기상청 API를 활용해 공지사항 자동 등록하는 이미지 -기상정보에 따른 이벤트 진행에 대한 정확한 기준 부여를 위해 기상청 API를 사용, 영업시간 내 강수정보를 파악하고 해당 공지를 올리기 위한 프로젝트. +## 퍼스트가든용 기상청 API를 활용해 공지사항 자동 등록 시스템 -### weather.py -- 기상청 API를 활용해 데이터 출력 +기상정보에 따른 이벤트 진행에 대한 정확한 기준 부여를 위해 기상청 API를 사용하여, 영업시간 내 강수정보를 파악하고 자동으로 공지를 올리기 위한 프로젝트. -### weather_capture.py -- 기상청 날씨누리 단기예보 페이지를 캡처하여 이미지로 저장 -- 클레임 방지를 위해 '최근발표시각'을 표시하여 캡처함 +--- -### gnu_autoupload.py -- 위 파일들의 데이터를 그누보드 게시판에 등록하는 일을 수행 +## 📁 폴더 구조 -### config.sample.py -- 환경정보 저장(DB정보 등), config.py 로 파일명 변경하여 사용 - -### 폴더 구조 +``` project-root/ -├── data/ # sqlite DB, 캡처 이미지 저장 경로 (공용 볼륨) +├── logs/ # 크론 실행 로그 저장 경로 +│ └── cron.log # Crontab 실행 로그 +│ +├── data/ # SQLite DB, 캡처 이미지 저장 경로 (공용 볼륨) │ ├── weather.sqlite # 날씨 DB (precipitation, summary 테이블) │ └── weather_capture_YYYYMMDD.png # 일자별 날씨 캡처 이미지 │ -├── autouploader/ # gnu-autouploader 앱 소스 +├── app/ # gnu-autouploader 앱 소스 (Dockerfile에서 복사) │ ├── gnu_autoupload.py # 메인 실행 스크립트 (Selenium → FTP → DB) │ ├── weather_capture.py # Selenium 기반 날씨 이미지 캡처 │ ├── weather.py # 기상청 API 데이터 처리 및 sqlite 저장 -│ ├── config.py # 설정값 (DB, FTP, API KEY 등) -│ └── run.sh # 자동 실행용 셸 스크립트 (cron에서 호출) +│ ├── send_message.py # Mattermost 알림 발송 +│ ├── selenium_manager.py # Selenium 브라우저 관리 +│ ├── config.py # 설정값 로드 (DB, FTP, API KEY 등) +│ ├── requirements.txt # Python 의존성 +│ └── run.sh # 수동 실행용 셸 스크립트 (개발 시 사용) │ -├── webhook/ # 카카오 챗봇 응답 서버 -│ ├── webhook.py # Flask 기반 응답 서버 -│ └── config.py # 환경 설정 (예: IMAGE_SERVER_URL) +├── webhook/ # Synology Chat 웹훅 응답 서버 +│ └── webhook.py # Flask 기반 응답 서버 │ ├── build/ -│ ├── autouploader/ -│ │ └── Dockerfile # gnu-autouploader용 Dockerfile +│ ├── app/ +│ │ ├── Dockerfile # gnu-autouploader 컨테이너 이미지 +│ │ └── run.sh # (위의 app/run.sh와 동일) │ └── webhook/ │ └── Dockerfile # webhook 서버용 Dockerfile │ -├── docker-compose.yml # 전체 서비스 구성 정의 +├── .env.example # 환경 변수 템플릿 (.env로 복사하여 수정) +├── docker-compose.yml # Docker Compose 서비스 정의 └── README.md # 프로젝트 문서 +``` + +--- + +## 📋 주요 스크립트 설명 + +### `app/gnu_autoupload.py` (메인) +- **역할**: 날씨 캡처 → FTP 업로드 → 그누보드 DB에 게시글 등록 +- **실행 방식**: + - 매일 09:00 Crontab 자동 실행 + - `docker exec` 또는 `run.sh`로 수동 실행 가능 +- **오류 발생 시**: Mattermost으로 알림 발송 + +### `app/weather_capture.py` +- Selenium을 사용해 기상청 날씨누리 웹 페이지 캡처 +- '최근발표시각' 표시 (출처 명시) + +### `app/weather.py` +- 기상청 API에서 시간별 강수량 데이터 수집 +- 10:00 ~ 22:00 영업시간 강수 데이터 HTML 테이블 생성 +- SQLite DB에 저장 + +### `app/config.py` +- 환경 변수 로드 (`.env` 또는 컨테이너 환경 변수) +- 필수 변수 부재 시 즉시 오류 출력 후 종료 + +--- + +## 🚀 설치 & 실행 + +### 1. 환경 설정 + +```bash +# .env 파일 생성 +cp .env.example .env + +# 필수 정보 입력 +vim .env +``` + +### 필수 환경 변수 + +```env +# 게시판 정보 +BOARD_ID=news +BOARD_CA_NAME=카테고리명 +BOARD_CONTENT=글 내용 +BOARD_MB_ID=user_id +BOARD_NICKNAME=닉네임 + +# MySQL 연결 (로컬: localhost, Docker: db 서비스명, Synology: 실제 호스트 IP/도메인) +DB_HOST=localhost +DB_USER=db_user +DB_PASSWORD=db_password +DB_NAME=database_name + +# FTP 업로드 +FTP_HOST=ftp.example.com +FTP_USER=ftp_user +FTP_PASSWORD=ftp_password +FTP_UPLOAD_DIR=/data/file/news/ + +# 기상청 API +SERVICE_KEY=your_api_key_here + +# Mattermost 알림 (선택사항) +MATTERMOST_URL=https://mattermost.example.com +MATTERMOST_TOKEN=token +MATTERMOST_CHANNEL_ID=channel_id +``` + +### 2. Docker Compose 실행 + +```bash +# 빌드 및 실행 +docker-compose up -d + +# 로그 확인 +docker-compose logs -f gnu-autouploader + +# 크론 실행 로그 확인 +docker exec gnu-autouploader tail -f /logs/cron.log +``` + +### 3. 수동 실행 + +```bash +# 컨테이너에서 직접 실행 +docker exec -it gnu-autouploader /usr/bin/python /app/gnu_autoupload.py + +# 또는 run.sh 사용 +docker exec -it gnu-autouploader /app/run.sh +``` + +--- + +## ⚙️ 크론탭 설정 + +Docker 컨테이너 내부에서 매일 **09:00**에 자동 실행됩니다. + +``` +0 9 * * * /usr/bin/python /app/gnu_autoupload.py >> /logs/cron.log 2>&1 +``` + +**로그 확인:** +```bash +docker exec gnu-autouploader tail -f /logs/cron.log +``` + +--- + +## 🔍 문제 해결 + +### MySQL 연결 오류 +- **오류**: `Can't connect to MySQL server on 'localhost'` +- **원인**: Docker 컨테이너에서 localhost는 컨테이너 자신을 가리킴 +- **해결**: + - Docker 환경: `DB_HOST=db` (docker-compose 서비스명) + - Synology: `DB_HOST=192.168.x.x` (호스트 IP) + +### Mattermost 알림 실패 +- **오류**: `Invalid URL '/api/v4/posts': No scheme supplied` +- **원인**: URL에 `http://` 또는 `https://`가 없음 +- **해결**: `MATTERMOST_URL=https://mattermost.example.com` 명시 + +### 크론탭에서 환경 변수 미로드 +- **원인**: 기존 버전에서 crontab이 `.env` 파일 접근 불가 +- **해결**: 현재는 `docker-compose.yml`에서 `.env`를 volume mount + env_file로 처리 + +--- + +## 📝 설정 변경 후 재배포 + +```bash +# 컨테이너 재시작 +docker-compose restart gnu-autouploader + +# 또는 재빌드 +docker-compose up -d --build +``` + +--- + +## 🛠️ 개발 & 디버깅 + +### 로컬 테스트 (가상환경) + +```bash +# 가상환경 생성 +python -m venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\activate + +# 의존성 설치 +pip install -r app/requirements.txt + +# .env 파일 생성 후 직접 실행 +python app/gnu_autoupload.py +``` + +### Docker 내 직접 실행 + +```bash +docker exec -it gnu-autouploader bash +cd /app +python gnu_autoupload.py +``` diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..26fc667 --- /dev/null +++ b/app/config.py @@ -0,0 +1,70 @@ +import os +import sys +from datetime import datetime + +# .env 파일 로드 (python-dotenv 사용) +# Docker 환경: docker-compose.yml에서 env_file로 환경 변수 주입 + volume mount로 .env 파일 접근 +# 로컬 개발 환경: python-dotenv로 .env 파일 로드 +try: + from dotenv import load_dotenv + load_dotenv() +except ImportError: + pass + +TODAY = datetime.now().strftime('%Y%m%d') + + +def _get_required_env(key: str, description: str = "") -> str: + """필수 환경 변수 조회. 없으면 에러 출력 후 종료""" + value = os.getenv(key) + if not value: + desc = f" ({description})" if description else "" + error_msg = f"[ERROR] 필수 환경 변수가 설정되지 않았습니다: {key}{desc}" + print(error_msg) + sys.exit(1) + return value + + +def _get_optional_env(key: str) -> str: + """선택적 환경 변수 조회. 없으면 빈 문자열 반환""" + return os.getenv(key, '') + + +# 게시판 설정 (필수) +MAIN = { + 'board': _get_required_env('BOARD_ID', '게시판 ID'), + 'ca_name': _get_required_env('BOARD_CA_NAME', '게시판 카테고리'), + 'subject': '', + 'content': _get_required_env('BOARD_CONTENT', '게시판 기본 내용'), + 'mb_id': _get_required_env('BOARD_MB_ID', '게시자 ID'), + 'nickname': _get_required_env('BOARD_NICKNAME', '게시자 닉네임'), + 'file1': '', + 'file2': '', +} + +# 데이터베이스 설정 (필수) +DB_CONFIG = { + 'HOST': _get_required_env('DB_HOST', 'MySQL 호스트'), + 'USER': _get_required_env('DB_USER', 'MySQL 사용자명'), + 'DBNAME': _get_required_env('DB_NAME', 'MySQL 데이터베이스명'), + 'PASS': _get_required_env('DB_PASSWORD', 'MySQL 비밀번호'), + 'CHARSET': _get_optional_env('DB_CHARSET') or 'utf8mb4', +} + +# FTP 설정 (필수) +FTP_CONFIG = { + 'HOST': _get_required_env('FTP_HOST', 'FTP 호스트'), + 'USER': _get_required_env('FTP_USER', 'FTP 사용자명'), + 'PASS': _get_required_env('FTP_PASSWORD', 'FTP 비밀번호'), + 'UPLOAD_DIR': _get_required_env('FTP_UPLOAD_DIR', 'FTP 업로드 디렉토리'), +} + +# 날씨 API 서비스 키 (필수) +serviceKey = _get_required_env('SERVICE_KEY', '기상청 API 서비스 키') + +# Mattermost 설정 (선택적 - 없어도 실행 가능하지만 알림은 미발송) +MATTERMOST_CONFIG = { + 'URL': _get_optional_env('MATTERMOST_URL'), + 'TOKEN': _get_optional_env('MATTERMOST_TOKEN'), + 'CHANNEL_ID': _get_optional_env('MATTERMOST_CHANNEL_ID'), +} diff --git a/autouploader/gnu_autoupload.py b/app/gnu_autoupload.py similarity index 93% rename from autouploader/gnu_autoupload.py rename to app/gnu_autoupload.py index dab3fc8..f49fe8e 100644 --- a/autouploader/gnu_autoupload.py +++ b/app/gnu_autoupload.py @@ -40,10 +40,23 @@ logger = logging.getLogger(__name__) 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_CONFIG.get('URL', ''), - mattermost_token=MATTERMOST_CONFIG.get('TOKEN', ''), - mattermost_channel_id=MATTERMOST_CONFIG.get('CHANNEL_ID', '') + mattermost_url=mattermost_url, + mattermost_token=mattermost_token, + mattermost_channel_id=mattermost_channel_id ) return msg_sender except Exception as e: @@ -184,6 +197,9 @@ def write_board(board, subject, content, mb_id, nickname, ca_name=None, file_lis """ 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'], diff --git a/autouploader/requirements.txt b/app/requirements.txt similarity index 100% rename from autouploader/requirements.txt rename to app/requirements.txt diff --git a/autouploader/selenium_manager.py b/app/selenium_manager.py similarity index 100% rename from autouploader/selenium_manager.py rename to app/selenium_manager.py diff --git a/autouploader/send_message.py b/app/send_message.py similarity index 94% rename from autouploader/send_message.py rename to app/send_message.py index 41bc50f..abafd40 100644 --- a/autouploader/send_message.py +++ b/app/send_message.py @@ -46,6 +46,11 @@ class MessageSender: def _send_to_mattermost(self, message: str, use_webhook: bool): try: + # Mattermost URL 검증 + if not self.mattermost_url or not self.mattermost_url.startswith(('http://', 'https://')): + print(f"[ERROR] Mattermost URL이 유효하지 않습니다: {self.mattermost_url}") + return False + if use_webhook: response = requests.post( self.mattermost_url, diff --git a/autouploader/weather.py b/app/weather.py similarity index 100% rename from autouploader/weather.py rename to app/weather.py diff --git a/autouploader/weather_capture.py b/app/weather_capture.py similarity index 100% rename from autouploader/weather_capture.py rename to app/weather_capture.py diff --git a/autouploader/config.py b/autouploader/config.py deleted file mode 100644 index fb22b86..0000000 --- a/autouploader/config.py +++ /dev/null @@ -1,62 +0,0 @@ -import os -from datetime import datetime - -# .env 파일 로드 (python-dotenv 사용) -try: - from dotenv import load_dotenv - load_dotenv() -except ImportError: - pass - -TODAY = datetime.now().strftime('%Y%m%d') - -# 게시판 설정 -MAIN = { - 'board': os.getenv('BOARD_ID', 'news'), - 'ca_name': os.getenv('BOARD_CA_NAME', '레이니데이'), - 'subject': '', - 'content': os.getenv('BOARD_CONTENT', '08:00 기상청 단기예보'), - 'mb_id': os.getenv('BOARD_MB_ID', 'user_id'), - 'nickname': os.getenv('BOARD_NICKNAME', 'user_nickname'), - 'file1': '', - 'file2': '', -} - -# 데이터베이스 설정 -DB_CONFIG = { - 'HOST': os.getenv('DB_HOST', 'localhost'), - 'USER': os.getenv('DB_USER', 'db_user'), - 'DBNAME': os.getenv('DB_NAME', 'database'), - 'PASS': os.getenv('DB_PASSWORD', 'password'), - 'CHARSET': os.getenv('DB_CHARSET', 'utf8mb4'), -} - -# FTP 설정 -FTP_CONFIG = { - 'HOST': os.getenv('FTP_HOST', 'ftp.example.com'), - 'USER': os.getenv('FTP_USER', 'ftp_user'), - 'PASS': os.getenv('FTP_PASSWORD', 'ftp_password'), - 'UPLOAD_DIR': os.getenv('FTP_UPLOAD_DIR', '/data/file/news/'), -} - -# 날씨 API 서비스 키 -serviceKey = os.getenv('SERVICE_KEY', '') - -# Mattermost 설정 -MATTERMOST_CONFIG = { - 'URL': os.getenv('MATTERMOST_URL', ''), - 'TOKEN': os.getenv('MATTERMOST_TOKEN', ''), - 'CHANNEL_ID': os.getenv('MATTERMOST_CHANNEL_ID', ''), -} - - -def write_board(): - # ... existing code ... - conn.close() - return True, None # 명시적 반환 추가 - -def some_function(): - # ... existing code ... - success, error = capture_image(capture_script, weather_file) # msg_sender 추가 - if not success: - return diff --git a/autouploader/config.sample.py b/autouploader/config.sample.py deleted file mode 100644 index 714077c..0000000 --- a/autouploader/config.sample.py +++ /dev/null @@ -1,42 +0,0 @@ -from datetime import datetime - -TODAY = datetime.now().strftime('%Y%m%d') - -# FTP 서버 설정 -FTP_CONFIG = { - 'HOST' : 'ftp.example.com', - 'USER' : 'ftp_username', - 'PASS' : 'ftp_password', - 'UPLOAD_DIR' : '/data/file/news/' -} - -# 데이터베이스 설정 (MySQL) -DB_CONFIG = { - 'HOST' : 'db.example.com', - 'USER' : 'db_username', - 'DBNAME' : 'database_name', - 'PASS' : 'db_password', - 'CHARSET': 'utf8mb4', -} - -# 게시판 설정 (subject, content, file1, file2는 스크립트 진행중 설정됨) -MAIN = { - 'board' : 'news', - 'ca_name' : 'category_name', - 'subject' : '', - 'content' : '', - 'mb_id' : 'user_id', - 'nickname' : 'user_nickname', - 'file1' : '', - 'file2' : '', -} - -# 날씨 API 서비스 키 -serviceKey = 'your_weather_api_key_here' - -# Mattermost 알림 설정 -MATTERMOST_CONFIG = { - 'URL': 'https://mattermost.example.com', - 'TOKEN': 'your-personal-access-token', - 'CHANNEL_ID': 'channel_id', -} diff --git a/autouploader/run.sh b/autouploader/run.sh deleted file mode 100644 index 594027c..0000000 --- a/autouploader/run.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -echo "run.sh 실행됨: $(date '+%Y-%m-%d %H:%M:%S')" -python3 /app/gnu_autoupload.py >> /proc/1/fd/1 2>&1 diff --git a/build/autouploader/Dockerfile b/build/app/Dockerfile similarity index 69% rename from build/autouploader/Dockerfile rename to build/app/Dockerfile index 128fc91..34c8c11 100644 --- a/build/autouploader/Dockerfile +++ b/build/app/Dockerfile @@ -27,20 +27,22 @@ RUN pip install --no-cache-dir --upgrade pip && \ ftputil \ pillow \ pyvirtualdisplay \ - requests + requests \ + python-dotenv WORKDIR /app RUN fc-cache -f -v -# autouploader 디렉토리의 모든 Python 스크립트 및 설정 파일 복사 -# context: . (프로젝트 루트) 이므로 autouploader/ 에서 복사 -COPY autouploader/ /app/ +COPY app/ /app/ -RUN chmod +x /app/run.sh +# 로그 디렉토리 생성 +RUN mkdir -p /logs && chmod 777 /logs -# crontab 등록 (docker logs에 출력되도록 설정) -RUN echo "0 9 * * * /app/run.sh 2>&1" | crontab - +# Crontab 설정: 매일 09:00에 절대 경로로 Python 실행 +# cron은 컨테이너의 환경 변수를 상속받으므로 env_file로 주입된 변수들을 사용 가능 +RUN echo "0 9 * * * /usr/bin/python /app/gnu_autoupload.py >> /logs/cron.log 2>&1" | crontab - && \ + chmod 666 /logs -# Cron 로그를 docker logs로 보내기 위해 포그라운드에서 실행 -CMD ["/bin/bash", "-c", "cron -f"] +# Cron을 포그라운드에서 실행 (docker logs에 출력되도록) +CMD ["/usr/sbin/cron", "-f"] diff --git a/build/autouploader/run.sh b/build/app/run.sh similarity index 75% rename from build/autouploader/run.sh rename to build/app/run.sh index 36cded5..6c0cfe8 100644 --- a/build/autouploader/run.sh +++ b/build/app/run.sh @@ -1,6 +1,8 @@ #!/bin/bash -# 로그 출력 함수 +# 이 스크립트는 수동 실행 시 사용됩니다. +# Crontab은 python을 직접 실행하므로 이 스크립트를 거치지 않습니다. + log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" } @@ -9,10 +11,9 @@ log "========================================" log "날씨 정보 자동 게시글 생성 시작" log "========================================" -# Python 스크립트 실행 cd /app if [ -f "gnu_autoupload.py" ]; then - python3 /app/gnu_autoupload.py 2>&1 + /usr/bin/python gnu_autoupload.py 2>&1 EXIT_CODE=$? if [ $EXIT_CODE -eq 0 ]; then log "✅ 실행 완료 (종료 코드: $EXIT_CODE)" diff --git a/docker-compose.yml b/docker-compose.yml index c255817..148fa0d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,12 +2,14 @@ services: gnu-autouploader: build: context: . - dockerfile: ./build/autouploader/Dockerfile + dockerfile: ./build/app/Dockerfile image: reg.firstgarden.co.kr/gnu-autouploader:latest container_name: gnu-autouploader volumes: - ./data:/data -# - ./autouploader:/app + - ./logs:/logs + - ./.env:/app/.env:ro +# - ./app:/app env_file: - .env restart: unless-stopped