Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 91cd2ad7b2 | |||
| cda9cd6543 | |||
| bfd7d06e5b | |||
| 1beb98ed4f |
16
.env.example
16
.env.example
@ -2,8 +2,10 @@
|
|||||||
# 필수 설정 항목 (반드시 작성해야 함)
|
# 필수 설정 항목 (반드시 작성해야 함)
|
||||||
# =====================================
|
# =====================================
|
||||||
|
|
||||||
|
# 사이트 기본 URL
|
||||||
|
URL=https://example.com
|
||||||
# 게시판 설정
|
# 게시판 설정
|
||||||
BOARD_ID=news
|
BOARD_ID=게시판이름
|
||||||
BOARD_CA_NAME=카테고리명
|
BOARD_CA_NAME=카테고리명
|
||||||
BOARD_CONTENT=글 내용
|
BOARD_CONTENT=글 내용
|
||||||
BOARD_MB_ID=user_id
|
BOARD_MB_ID=user_id
|
||||||
@ -13,7 +15,7 @@ BOARD_NICKNAME=user_nickname
|
|||||||
# Docker 환경: DB_HOST는 docker-compose의 service name 사용 (예: db, mysql)
|
# Docker 환경: DB_HOST는 docker-compose의 service name 사용 (예: db, mysql)
|
||||||
# 일반 환경: 실제 MySQL 호스트 IP/도메인 사용 (예: 192.168.1.100, db.example.com)
|
# 일반 환경: 실제 MySQL 호스트 IP/도메인 사용 (예: 192.168.1.100, db.example.com)
|
||||||
# Synology 환경: 호스트 IP 또는 도메인 명시 (localhost 사용 불가)
|
# Synology 환경: 호스트 IP 또는 도메인 명시 (localhost 사용 불가)
|
||||||
DB_HOST=db
|
DB_HOST=db.example.com
|
||||||
DB_USER=db_username
|
DB_USER=db_username
|
||||||
DB_PASSWORD=db_password
|
DB_PASSWORD=db_password
|
||||||
DB_NAME=database_name
|
DB_NAME=database_name
|
||||||
@ -38,12 +40,8 @@ MATTERMOST_URL=https://mattermost.example.com
|
|||||||
MATTERMOST_TOKEN=your-personal-access-token
|
MATTERMOST_TOKEN=your-personal-access-token
|
||||||
MATTERMOST_CHANNEL_ID=channel_id
|
MATTERMOST_CHANNEL_ID=channel_id
|
||||||
|
|
||||||
# =====================================
|
|
||||||
# 웹서버 설정
|
# 웹서버 설정
|
||||||
# =====================================
|
# DOMAIN: 카카오 챗봇에서 이미지 URL 생성 시 사용
|
||||||
|
# FLASK_DEBUG: 0(운영), 1(개발)
|
||||||
# 웹훅 도메인 (이미지 URL 생성 시 사용)
|
DOMAIN=https://webhook.example.com
|
||||||
DOMAIN=https://webhook.firstgarden.co.kr
|
|
||||||
|
|
||||||
# Flask 디버그 모드 (개발 시만 1로 설정, 운영은 0)
|
|
||||||
FLASK_DEBUG=0
|
FLASK_DEBUG=0
|
||||||
|
|||||||
211
README.md
211
README.md
@ -67,15 +67,19 @@ project-root/
|
|||||||
- 10:00 ~ 22:00 영업시간 강수 데이터 HTML 테이블 생성
|
- 10:00 ~ 22:00 영업시간 강수 데이터 HTML 테이블 생성
|
||||||
- SQLite DB에 저장
|
- SQLite DB에 저장
|
||||||
|
|
||||||
### `webhook/webhook.py` ⭐ (개선)
|
### `app/api_server.py` ⭐ (Flask 웹훅 서버)
|
||||||
- **Flask 기반 카카오 챇봇 응답 서버**
|
- **Flask 기반 카카오 챗봇 응답 서버**
|
||||||
|
- **포트**: 5000 (docker-compose에서 5151로 노출)
|
||||||
- **주요 기능**:
|
- **주요 기능**:
|
||||||
- **당일 조회**: 09:00 캡처된 **실제 강우량 데이터** 응답
|
- **당일 조회**: 09:00 캡처된 **실제 강우량 데이터** 응답
|
||||||
- **미래 날짜 조회**: 기상청 API 기반 **예보 강우량** 응답
|
- **미래 날짜 조회**: 기상청 API 기반 **예보 강우량** 응답
|
||||||
- 예보 시 "변동될 수 있음" 경고 문구 표시
|
- 예보 시 "변동될 수 있음" 경고 문구 표시
|
||||||
- 10mm 초과 시 이벤트 적용 안내
|
- 10mm 초과 시 이벤트 적용 안내
|
||||||
- 날씨 캡처 이미지 함께 전송
|
- 날씨 캡처 이미지 함께 전송
|
||||||
- **통합**: 현재 `app/api_server.py`로 통합되어 동일 컨테이너에서 실행
|
- **주요 엔드포인트**:
|
||||||
|
- `POST /webhook` - Kakaotalk 챗봇 웹훅
|
||||||
|
- `GET /health` - 헬스 체크
|
||||||
|
- `GET /data/<filename>` - 캡처 이미지 조회
|
||||||
|
|
||||||
### `app/config.py`
|
### `app/config.py`
|
||||||
- 환경 변수 로드 (`.env` 또는 컨테이너 환경 변수)
|
- 환경 변수 로드 (`.env` 또는 컨테이너 환경 변수)
|
||||||
@ -215,7 +219,206 @@ CREATE TABLE rainfall_summary (
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 💬 카카오 챗봇 응답 예시
|
## <EFBFBD> 웹훅 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>💬 카카오 챗봇 응답 예시
|
||||||
|
|
||||||
### 당일 조회 (실제 데이터)
|
### 당일 조회 (실제 데이터)
|
||||||
```
|
```
|
||||||
|
|||||||
@ -220,7 +220,7 @@ def webhook():
|
|||||||
|
|
||||||
# 이벤트 적용 여부
|
# 이벤트 적용 여부
|
||||||
if rainfall_info['total'] > 10:
|
if rainfall_info['total'] > 10:
|
||||||
lines.append("✅ 식음료 2만원 이상 결제 시 무료입장권 제공")
|
lines.append("✅ 레이니데이 이벤트 기준(10mm 초과) 충족")
|
||||||
else:
|
else:
|
||||||
lines.append("❌ 이벤트 기준(10mm 초과)을 충족하지 않음")
|
lines.append("❌ 이벤트 기준(10mm 초과)을 충족하지 않음")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -178,7 +178,7 @@ def file_upload(filename, bf_file, msg_sender=None):
|
|||||||
# ---------------------------
|
# ---------------------------
|
||||||
# 게시글 작성 함수
|
# 게시글 작성 함수
|
||||||
# ---------------------------
|
# ---------------------------
|
||||||
def write_board(board, subject, content, mb_id, nickname, ca_name=None, file_list=None, msg_sender=None):
|
def write_board(board, subject, content, mb_id, nickname, ca_name=None, file_list=None, msg_sender=None, url=None):
|
||||||
"""
|
"""
|
||||||
그누보드 게시글 및 첨부파일 등록
|
그누보드 게시글 및 첨부파일 등록
|
||||||
|
|
||||||
@ -191,9 +191,10 @@ def write_board(board, subject, content, mb_id, nickname, ca_name=None, file_lis
|
|||||||
ca_name: 카테고리 이름 (선택사항)
|
ca_name: 카테고리 이름 (선택사항)
|
||||||
file_list: 첨부파일 경로 리스트 (선택사항)
|
file_list: 첨부파일 경로 리스트 (선택사항)
|
||||||
msg_sender: MessageSender 인스턴스
|
msg_sender: MessageSender 인스턴스
|
||||||
|
url: 게시판 URL (선택사항)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple: (성공여부, 에러메시지)
|
tuple: (성공여부, 에러메시지, wr_id)
|
||||||
"""
|
"""
|
||||||
conn = None
|
conn = None
|
||||||
try:
|
try:
|
||||||
@ -289,10 +290,21 @@ def write_board(board, subject, content, mb_id, nickname, ca_name=None, file_lis
|
|||||||
f"📝 제목: {subject}\n"\
|
f"📝 제목: {subject}\n"\
|
||||||
f"📎 첨부파일: {file_count}개"
|
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:
|
if msg_sender:
|
||||||
msg_sender.send(success_msg, platforms=['mattermost'])
|
msg_sender.send(success_msg, platforms=['mattermost'])
|
||||||
|
|
||||||
return True, None
|
return True, None, wr_id
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if conn:
|
if conn:
|
||||||
@ -314,7 +326,7 @@ def write_board(board, subject, content, mb_id, nickname, ca_name=None, file_lis
|
|||||||
if msg_sender:
|
if msg_sender:
|
||||||
msg_sender.send(error_msg, platforms=['mattermost'])
|
msg_sender.send(error_msg, platforms=['mattermost'])
|
||||||
|
|
||||||
return False, error_msg
|
return False, error_msg, None
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if conn:
|
if conn:
|
||||||
@ -335,10 +347,11 @@ def main():
|
|||||||
logger.warning("Mattermost 알림이 비활성화됩니다")
|
logger.warning("Mattermost 알림이 비활성화됩니다")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 무료입장 조건에 대해서만 안내함.
|
# .env의 BOARD_CONTENT 사용 (또는 기본값 설정)
|
||||||
MAIN["content"] = """
|
if not MAIN.get("content"):
|
||||||
|
MAIN["content"] = """
|
||||||
<p>Rainy Day 이벤트 적용안내</p>
|
<p>Rainy Day 이벤트 적용안내</p>
|
||||||
<p><b>10:00 ~ 22:00까지의 예보를 합산하며, ~1mm인 경우 0.5mm로 계산합니다.</b></p>
|
<p><b>10:00 ~ 21:00까지의 예보를 합산하며, ~1mm인 경우 0.5mm로 계산합니다.</b></p>
|
||||||
<p>레이니데이 이벤트 정보 확인</p>
|
<p>레이니데이 이벤트 정보 확인</p>
|
||||||
<p><a href="https://firstgarden.co.kr/news/60">이벤트 정보 보기</a></p>
|
<p><a href="https://firstgarden.co.kr/news/60">이벤트 정보 보기</a></p>
|
||||||
"""
|
"""
|
||||||
@ -365,8 +378,7 @@ def main():
|
|||||||
|
|
||||||
file_list = [MAIN['file1'], MAIN['file2']]
|
file_list = [MAIN['file1'], MAIN['file2']]
|
||||||
|
|
||||||
# 게시글 작성
|
# 게시글 작성, wr_id = write_board(
|
||||||
success, error = write_board(
|
|
||||||
board=MAIN['board'],
|
board=MAIN['board'],
|
||||||
subject=MAIN['subject'],
|
subject=MAIN['subject'],
|
||||||
content=MAIN['content'],
|
content=MAIN['content'],
|
||||||
@ -374,6 +386,8 @@ def main():
|
|||||||
nickname=MAIN['nickname'],
|
nickname=MAIN['nickname'],
|
||||||
ca_name=MAIN['ca_name'],
|
ca_name=MAIN['ca_name'],
|
||||||
file_list=file_list,
|
file_list=file_list,
|
||||||
|
msg_sender=msg_sender,
|
||||||
|
url=MAIN.get('url')
|
||||||
msg_sender=msg_sender
|
msg_sender=msg_sender
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ logging.basicConfig(
|
|||||||
)
|
)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
WEATHER_URL = 'https://www.weather.go.kr/w/weather/forecast/short-term.do#dong/4148026200/37.73208578534846/126.79463099866948'
|
WEATHER_URL = 'https://www.weather.go.kr/w/weather/forecast/short-term.do#dong/4148026200/37.73208578534846/126.79463099866948/%EA%B2%BD%EA%B8%B0%20%ED%8C%8C%EC%A3%BC%EC%8B%9C%20%EC%83%81%EC%A7%80%EC%84%9D%EB%8F%99/SCH/%ED%8D%BC%EC%8A%A4%ED%8A%B8%EA%B0%80%EB%93%A0'
|
||||||
OUTPUT_DIR = '/data'
|
OUTPUT_DIR = '/data'
|
||||||
OUTPUT_FILENAME = f'weather_capture_{TODAY}.png'
|
OUTPUT_FILENAME = f'weather_capture_{TODAY}.png'
|
||||||
DB_PATH = '/data/weather.sqlite'
|
DB_PATH = '/data/weather.sqlite'
|
||||||
|
|||||||
Reference in New Issue
Block a user