546 lines
14 KiB
Markdown
546 lines
14 KiB
Markdown
## 퍼스트가든용 기상청 API를 활용해 공지사항 자동 등록 시스템
|
||
|
||
기상정보에 따른 이벤트 진행에 대한 정확한 기준 부여를 위해 기상청 API를 사용하여, 영업시간 내 강수정보를 파악하고 자동으로 공지를 올리기 위한 프로젝트.
|
||
|
||
---
|
||
|
||
## 📁 폴더 구조
|
||
|
||
```
|
||
project-root/
|
||
├── logs/ # 크론 실행 로그 저장 경로
|
||
│ └── cron.log # Crontab 실행 로그
|
||
│
|
||
├── data/ # SQLite DB, 캡처 이미지 저장 경로 (공용 볼륨)
|
||
│ ├── weather.sqlite # 날씨 DB (precipitation, summary 테이블)
|
||
│ └── weather_capture_YYYYMMDD.png # 일자별 날씨 캡처 이미지
|
||
│
|
||
├── app/ # gnu-autouploader + API 서버 (통합)
|
||
│ ├── gnu_autoupload.py # 메인 실행 스크립트 (Selenium → FTP → DB)
|
||
│ ├── weather_capture.py # Selenium 기반 날씨 이미지 캡처 + 강우량 추출
|
||
│ ├── weather.py # 기상청 API 데이터 처리 및 sqlite 저장
|
||
│ ├── send_message.py # Mattermost 알림 발송
|
||
│ ├── selenium_manager.py # Selenium 브라우저 관리
|
||
│ ├── api_server.py # Flask 기반 카카오 챗봇 웹훅 서버 ⭐
|
||
│ ├── config.py # 설정값 로드 (DB, FTP, API KEY 등)
|
||
│ ├── requirements.txt # Python 의존성
|
||
│ └── run.sh # 수동 실행용 셸 스크립트 (개발 시 사용)
|
||
│
|
||
├── webhook/ # (더 이상 사용 안 함 - app/api_server.py로 통합)
|
||
│ └── webhook.py # (참고용 아카이브)
|
||
│
|
||
├── build/
|
||
│ ├── app/
|
||
│ │ ├── Dockerfile # gnu-autouploader + Flask 통합 이미지
|
||
│ │ ├── entrypoint.sh # Flask + Cron 동시 실행 스크립트 ⭐
|
||
│ │ └── run.sh # (위의 app/run.sh와 동일)
|
||
│ └── webhook/
|
||
│ └── Dockerfile # (더 이상 사용 안 함)
|
||
│
|
||
├── .env.example # 환경 변수 템플릿 (.env로 복사하여 수정)
|
||
├── docker-compose.yml # Docker Compose 서비스 정의 (gnu-autouploader만)
|
||
└── README.md # 프로젝트 문서
|
||
```
|
||
|
||
---
|
||
|
||
## 📋 주요 스크립트 설명
|
||
|
||
### `app/gnu_autoupload.py` (메인)
|
||
- **역할**: 날씨 캡처 → FTP 업로드 → 그누보드 DB에 게시글 등록
|
||
- **실행 방식**:
|
||
- 매일 09:00 Crontab 자동 실행
|
||
- `docker exec` 또는 `run.sh`로 수동 실행 가능
|
||
- **오류 발생 시**: Mattermost으로 알림 발송
|
||
|
||
### `app/weather_capture.py` ⭐ (개선)
|
||
- **이전**: Selenium으로 웹페이지 캡처만 수행
|
||
- **현재**:
|
||
- Selenium으로 기상청 웹페이지 접근
|
||
- **페이지에서 강우량 데이터 자동 추출** (10시~21시)
|
||
- '-'는 0mm, '~1'은 0.5mm로 자동 계산
|
||
- **추출된 강우량을 SQLite에 저장** (`rainfall_capture`, `rainfall_summary` 테이블)
|
||
- 웹페이지 이미지 캡처 저장
|
||
|
||
### `app/weather.py`
|
||
- 기상청 API에서 시간별 강수량 데이터 수집
|
||
- 10:00 ~ 22:00 영업시간 강수 데이터 HTML 테이블 생성
|
||
- SQLite DB에 저장
|
||
|
||
### `app/api_server.py` ⭐ (Flask 웹훅 서버)
|
||
- **Flask 기반 카카오 챗봇 응답 서버**
|
||
- **포트**: 5000 (docker-compose에서 5151로 노출)
|
||
- **주요 기능**:
|
||
- **당일 조회**: 09:00 캡처된 **실제 강우량 데이터** 응답
|
||
- **미래 날짜 조회**: 기상청 API 기반 **예보 강우량** 응답
|
||
- 예보 시 "변동될 수 있음" 경고 문구 표시
|
||
- 10mm 초과 시 이벤트 적용 안내
|
||
- 날씨 캡처 이미지 함께 전송
|
||
- **주요 엔드포인트**:
|
||
- `POST /webhook` - Kakaotalk 챗봇 웹훅
|
||
- `GET /health` - 헬스 체크
|
||
- `GET /data/<filename>` - 캡처 이미지 조회
|
||
|
||
### `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
|
||
```
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## 🎯 시스템 동작 플로우
|
||
|
||
### 아키텍처 (통합 구조)
|
||
```
|
||
[gnu-autouploader 컨테이너] (포트 5151:5000 노출)
|
||
├─ Crontab 데몬 (포그라운드)
|
||
│ └─ 매일 09:00 크론 작업 실행
|
||
│
|
||
└─ Flask 웹서버 (백그라운드)
|
||
└─ 포트 5000에서 지속 실행
|
||
```
|
||
|
||
### 일일 작업 플로우
|
||
```
|
||
매일 09:00
|
||
↓
|
||
[Crontab 작업 시작]
|
||
├─ 1. weather_capture.py 실행
|
||
│ ├─ Selenium으로 기상청 웹페이지 접근
|
||
│ ├─ 강우량 데이터 추출 (10시~21시) ⭐
|
||
│ ├─ SQLite 저장
|
||
│ └─ 웹페이지 이미지 캡처 저장
|
||
├─ 2. weather.py 실행 (선택적)
|
||
│ └─ API 기반 시간별 강수 데이터 저장
|
||
└─ 3. gnu_autoupload.py 실행
|
||
├─ 캡처된 이미지 FTP 업로드
|
||
└─ 그누보드 게시글 자동 등록
|
||
|
||
동시에 실행 중: Flask 웹서버
|
||
↓ (사용자 요청 시)
|
||
[Flask 웹훅 엔드포인트]
|
||
├─ "오늘의 강우량은?" → SQLite에서 실제 데이터 응답 ✓
|
||
├─ "내일 강우량은?" → API 예보 데이터 + 경고 문구 응답 ⚠️
|
||
└─ "강우량 10mm 초과?" → 이벤트 적용 여부 자동 판단
|
||
```
|
||
|
||
---
|
||
|
||
## 🗄️ SQLite 데이터베이스 스키마
|
||
|
||
### `rainfall_capture` (웹페이지 캡처 데이터)
|
||
```sql
|
||
CREATE TABLE rainfall_capture (
|
||
id INTEGER PRIMARY KEY,
|
||
date TEXT, -- 'YYYYMMDD'
|
||
hour INTEGER, -- 10~21 (10시~21시)
|
||
rainfall REAL -- mm 단위
|
||
);
|
||
```
|
||
|
||
### `rainfall_summary` (일일 합계)
|
||
```sql
|
||
CREATE TABLE rainfall_summary (
|
||
id INTEGER PRIMARY KEY,
|
||
date TEXT UNIQUE, -- 'YYYYMMDD'
|
||
total_rainfall REAL, -- mm 단위
|
||
capture_time TEXT -- '2025-12-19 09:00:00'
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
## <20> 웹훅 API 사용법
|
||
|
||
### 웹훅 엔드포인트
|
||
|
||
**URL**: `https://webhook.firstgarden.co.kr/webhook` (또는 `DOMAIN/webhook`)
|
||
|
||
**요청 방식**: `POST`
|
||
|
||
### 1. 카카오 챗봇 연동 (자동)
|
||
|
||
#### 요청 형식 (카카오로부터)
|
||
```json
|
||
{
|
||
"userRequest": {
|
||
"text": "12월 19일 강우량은?",
|
||
"user": {
|
||
"id": "user_123",
|
||
"properties": {}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 응답 형식 (카카오에게)
|
||
```json
|
||
{
|
||
"version": "2.0",
|
||
"template": {
|
||
"outputs": [
|
||
{
|
||
"simpleText": {
|
||
"text": "📅 12월 19일(금)...(응답 내용)..."
|
||
}
|
||
},
|
||
{
|
||
"simpleImage": {
|
||
"imageUrl": "https://webhook.firstgarden.co.kr/data/weather_capture_20251219.png",
|
||
"altText": "날씨 이미지"
|
||
}
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. 직접 API 호출 (수동 테스트)
|
||
|
||
#### cURL로 테스트
|
||
```bash
|
||
# 당일 조회
|
||
curl -X POST https://webhook.firstgarden.co.kr/webhook \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"userRequest": {"text": "오늘 레이니데이 적용?"}}'
|
||
|
||
# 특정 날짜 조회
|
||
curl -X POST https://webhook.firstgarden.co.kr/webhook \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"userRequest": {"text": "12월 20일 레이니데이"}}'
|
||
|
||
# 헬스 체크
|
||
curl https://webhook.firstgarden.co.kr/health
|
||
```
|
||
|
||
#### Python으로 테스트
|
||
```python
|
||
import requests
|
||
|
||
webhook_url = "https://webhook.firstgarden.co.kr/webhook"
|
||
|
||
payload = {
|
||
"userRequest": {
|
||
"text": "내일 레이니데이?"
|
||
}
|
||
}
|
||
|
||
response = requests.post(webhook_url, json=payload)
|
||
print(response.json())
|
||
```
|
||
|
||
### 3. 응답 분석 규칙
|
||
|
||
챗봇은 사용자 입력에서 **날짜 패턴**을 자동으로 감지합니다:
|
||
|
||
| 입력 예시 | 인식 날짜 | 데이터 출처 |
|
||
|---------|---------|----------|
|
||
| "오늘 강우량" | 당일 | SQLite (09:00 캡처) ✅ 실제값 |
|
||
| "12월 19일" | 2025-12-19 | 당일이면 SQLite, 미래면 API ⚠️ |
|
||
| "내일" | 내일 | API 예보 |
|
||
| "모레" | 모레 | API 예보 |
|
||
| "12월 25일" | 2025-12-25 | API 예보 |
|
||
| "날짜 지정 없음" | 당일 | SQLite (09:00 캡처) |
|
||
|
||
### 4. 웹훅 응답 예시
|
||
|
||
#### 당일 실제 데이터 (SQLite)
|
||
```
|
||
📅 12월 19일(금)
|
||
📊 실제 강수량 (09:00 캡처 기준)
|
||
|
||
10:00 → ☀️ 강수 없음
|
||
11:00 → ☀️ 강수 없음
|
||
12:00 → 0.5mm
|
||
13:00 → 0.5mm
|
||
14:00 → 1.2mm
|
||
...
|
||
21:00 → 2.3mm
|
||
|
||
💧 총 강수량: 5.2mm
|
||
❌ 이벤트 기준(10mm 초과)을 충족하지 않음
|
||
|
||
🔗 게시글 링크: https://firstgarden.co.kr/news/123
|
||
```
|
||
|
||
#### 미래 날짜 예보 (API)
|
||
```
|
||
📅 12월 20일(토)
|
||
📊 예보 강수량 (08:00 발표 기준)
|
||
|
||
10:00 → 1.2mm
|
||
11:00 → 2.1mm
|
||
12:00 → 3.5mm
|
||
...
|
||
21:00 → 0.8mm
|
||
|
||
💧 총 강수량: 12.5mm
|
||
✅ 레이니데이 적용 가능
|
||
|
||
⚠️ 이는 기상청 08:00 발표 예보입니다. 실제 이벤트 적용 기준은 당일 09:00 캡처 데이터입니다.
|
||
```
|
||
|
||
#### 이미지 첨부 (자동)
|
||
```
|
||
응답에 자동으로 캡처된 날씨 이미지가 함께 전송됩니다.
|
||
```
|
||
|
||
### 5. 헬스 체크
|
||
|
||
```bash
|
||
# 엔드포인트 상태 확인
|
||
curl https://webhook.firstgarden.co.kr/health
|
||
|
||
# 응답
|
||
{
|
||
"status": "healthy",
|
||
"timestamp": "2025-12-19 10:30:45"
|
||
}
|
||
```
|
||
|
||
### 6. 이미지 직접 조회
|
||
|
||
```bash
|
||
# 캡처 이미지 조회 (브라우저에서도 열기 가능)
|
||
https://webhook.firstgarden.co.kr/data/weather_capture_20251219.png
|
||
```
|
||
|
||
---
|
||
## 🎯 카카오 챗봇 설정
|
||
|
||
### 1. 카카오 디벨로퍼 콘솔에서
|
||
|
||
1. [카카오 디벨로퍼 콘솔](https://developers.kakao.com/) 접속
|
||
2. 앱 생성 → "채팅" 선택
|
||
3. **구성 > 채팅 설정** 이동
|
||
4. **스킬 추가** 클릭
|
||
- 스킬명: `레이니데이 이벤트`
|
||
- URL: `https://webhook.firstgarden.co.kr/webhook`
|
||
- HTTP 메서드: `POST`
|
||
5. **인텐트** 설정 (예시)
|
||
- "강우량은?", "날씨는?", "이벤트 조건?", "무료 입장?", "강수량", "강우" 등
|
||
|
||
### 2. 테스트 채널에서 확인
|
||
- 카카오톡 채팅 → 챗봇에 메시지 전송 → 웹훅이 자동으로 응답
|
||
|
||
---
|
||
|
||
## 📊 게시글 등록 완료 알림
|
||
|
||
`gnu_autoupload.py`가 성공적으로 게시글을 등록할 때, Mattermost 알림이 발송됩니다:
|
||
|
||
```
|
||
✅ **게시글 등록 완료**
|
||
|
||
📅 날짜: 2025-12-19 09:05:30
|
||
📋 게시판: `news`
|
||
📝 제목: 2025-12-19 날씨정보
|
||
📎 첨부파일: 2개
|
||
🖼️ 캡처파일: `weather_capture_20251219.png` (1024.5KB)
|
||
🔗 게시글 링크: https://firstgarden.co.kr/news/123
|
||
```
|
||
|
||
**포함 정보**:
|
||
- 등록 일시
|
||
- 게시판 ID
|
||
- 게시글 제목
|
||
- 첨부파일 개수
|
||
- 캡처 이미지 정보
|
||
- **게시글 직접 링크** (`URL/BOARD_ID/wr_id` 형식)
|
||
|
||
---
|
||
## <20>💬 카카오 챗봇 응답 예시
|
||
|
||
### 당일 조회 (실제 데이터)
|
||
```
|
||
📅 12월 19일(금)
|
||
📊 실제 강수량 (09:00 캡처 기준)
|
||
|
||
10:00 → ☀️ 강수 없음
|
||
11:00 → ☀️ 강수 없음
|
||
12:00 → 0.5mm
|
||
...
|
||
21:00 → 2.3mm
|
||
|
||
💧 총 강수량: 5.2mm
|
||
❌ 이벤트 기준(10mm 초과)을 충족하지 않음
|
||
|
||
[날씨 캡처 이미지]
|
||
```
|
||
|
||
### 미래 날짜 조회 (API 예보)
|
||
```
|
||
📅 12월 20일(토)
|
||
📊 예보 강수량 (08:00 발표 기준)
|
||
|
||
10:00 → 1.2mm
|
||
11:00 → 2.1mm
|
||
...
|
||
21:00 → 0.8mm
|
||
|
||
💧 총 강수량: 12.5mm
|
||
✅ 식음료 2만원 이상 결제 시 무료입장권 제공
|
||
|
||
⚠️ 이는 기상청 08:00 발표 예보입니다. 실제 이벤트 적용 기준은 당일 09:00 캡처 데이터입니다.
|
||
```
|
||
|
||
---
|
||
|
||
## ⚙️ 크론탭 설정
|
||
|
||
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 gnu-autouploader /usr/bin/python /app/weather_capture.py
|
||
|
||
# 메인 작업 (게시글 등록)
|
||
docker exec gnu-autouploader /usr/bin/python /app/gnu_autoupload.py
|
||
|
||
# 기상청 API 데이터 (선택사항)
|
||
docker exec gnu-autouploader /usr/bin/python /app/weather.py
|
||
```
|
||
|
||
### 로그 확인
|
||
|
||
```bash
|
||
# Crontab + Flask 통합 로그
|
||
docker-compose logs -f gnu-autouploader
|
||
|
||
# Crontab 실행 로그만
|
||
docker exec gnu-autouploader tail -f /logs/cron.log
|
||
|
||
# Flask 웹서버 로그
|
||
docker exec gnu-autouploader tail -f /logs/flask.log
|
||
```
|