- 알림톡 발송 로그: alimtalk_logs SQLite 테이블 + DB 자동 기록 - /admin/alimtalk 페이지: 서버 로그, NHN Cloud 내역 조회, 수동 발송 테스트 - 적립일시 포맷 수정: %Y-%m-%d %H:%M (16자 초과) → %m/%d %H:%M (11자) - POS GUI 현금영수증(현영) 표시: 청록색 볼드 - 결제수납구조.md: CD_SUNAB/PS_main/SALE_MAIN 3테이블 관계 문서 - 실행구조.md: Flask 서버 + Qt GUI 실행 가이드 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
203 lines
6.5 KiB
Python
203 lines
6.5 KiB
Python
"""
|
|
NHN Cloud 알림톡 발송 서비스
|
|
마일리지 적립 완료 등 알림톡 발송 + SQLite 로깅
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
import logging
|
|
from datetime import datetime, timezone, timedelta
|
|
|
|
import requests
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# NHN Cloud 알림톡 설정
|
|
APPKEY = os.getenv('NHN_ALIMTALK_APPKEY', 'u0TLUaXXY9bfQFkY')
|
|
SECRET_KEY = os.getenv('NHN_ALIMTALK_SECRET', 'naraGEUJfpkRu1fgirKewJtwADqWQ5gY')
|
|
SENDER_KEY = os.getenv('NHN_ALIMTALK_SENDER', '341352077bce225195ccc2697fb449f723e70982')
|
|
|
|
API_BASE = f'https://api-alimtalk.cloud.toast.com/alimtalk/v2.3/appkeys/{APPKEY}'
|
|
|
|
# KST 타임존
|
|
KST = timezone(timedelta(hours=9))
|
|
|
|
|
|
def _log_to_db(template_code, recipient_no, success, result_message,
|
|
template_params=None, user_id=None, trigger_source='unknown',
|
|
transaction_id=None):
|
|
"""발송 결과를 SQLite에 저장"""
|
|
try:
|
|
from db.dbsetup import db_manager
|
|
conn = db_manager.get_sqlite_connection()
|
|
cursor = conn.cursor()
|
|
cursor.execute("""
|
|
INSERT INTO alimtalk_logs
|
|
(template_code, recipient_no, user_id, trigger_source,
|
|
template_params, success, result_message, transaction_id)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
""", (
|
|
template_code,
|
|
recipient_no,
|
|
user_id,
|
|
trigger_source,
|
|
json.dumps(template_params, ensure_ascii=False) if template_params else None,
|
|
success,
|
|
result_message,
|
|
transaction_id
|
|
))
|
|
conn.commit()
|
|
except Exception as e:
|
|
logger.warning(f"알림톡 로그 DB 저장 실패: {e}")
|
|
|
|
|
|
def _send_alimtalk(template_code, recipient_no, template_params):
|
|
"""
|
|
알림톡 발송 공통 함수
|
|
|
|
Args:
|
|
template_code: 템플릿 코드
|
|
recipient_no: 수신 번호 (01012345678)
|
|
template_params: 템플릿 변수 딕셔너리
|
|
|
|
Returns:
|
|
tuple: (성공 여부, 메시지)
|
|
"""
|
|
url = f'{API_BASE}/messages'
|
|
headers = {
|
|
'Content-Type': 'application/json;charset=UTF-8',
|
|
'X-Secret-Key': SECRET_KEY
|
|
}
|
|
data = {
|
|
'senderKey': SENDER_KEY,
|
|
'templateCode': template_code,
|
|
'recipientList': [
|
|
{
|
|
'recipientNo': recipient_no,
|
|
'templateParameter': template_params
|
|
}
|
|
]
|
|
}
|
|
|
|
try:
|
|
resp = requests.post(url, headers=headers, json=data, timeout=10)
|
|
result = resp.json()
|
|
|
|
if resp.status_code == 200 and result.get('header', {}).get('isSuccessful'):
|
|
logger.info(f"알림톡 발송 성공: {template_code} → {recipient_no}")
|
|
return (True, "발송 성공")
|
|
else:
|
|
error_msg = result.get('header', {}).get('resultMessage', str(result))
|
|
logger.warning(f"알림톡 발송 실패: {template_code} → {recipient_no}: {error_msg}")
|
|
return (False, error_msg)
|
|
|
|
except requests.exceptions.Timeout:
|
|
logger.warning(f"알림톡 발송 타임아웃: {template_code} → {recipient_no}")
|
|
return (False, "타임아웃")
|
|
except Exception as e:
|
|
logger.warning(f"알림톡 발송 오류: {template_code} → {recipient_no}: {e}")
|
|
return (False, str(e))
|
|
|
|
|
|
def build_item_summary(items):
|
|
"""구매 품목 요약 문자열 생성 (예: '타이레놀 외 3건')"""
|
|
if not items:
|
|
return "약국 구매"
|
|
first = items[0]['name']
|
|
if len(first) > 20:
|
|
first = first[:18] + '..'
|
|
if len(items) == 1:
|
|
return first
|
|
return f"{first} 외 {len(items) - 1}건"
|
|
|
|
|
|
def send_mileage_claim_alimtalk(phone, name, points, balance, items=None,
|
|
user_id=None, trigger_source='kiosk',
|
|
transaction_id=None):
|
|
"""
|
|
마일리지 적립 완료 알림톡 발송
|
|
|
|
Args:
|
|
phone: 수신 전화번호 (01012345678)
|
|
name: 고객명
|
|
points: 적립 포인트
|
|
balance: 적립 후 총 잔액
|
|
items: 구매 품목 리스트 [{'name': ..., 'qty': ..., 'total': ...}, ...]
|
|
user_id: 사용자 ID (로그용)
|
|
trigger_source: 발송 주체 ('kiosk', 'admin', 'manual')
|
|
transaction_id: 거래 ID (로그용)
|
|
|
|
Returns:
|
|
tuple: (성공 여부, 메시지)
|
|
"""
|
|
now_kst = datetime.now(KST).strftime('%m/%d %H:%M')
|
|
item_summary = build_item_summary(items)
|
|
|
|
# MILEAGE_CLAIM_V3 (발송 근거 + 구매품목 포함) 우선 시도
|
|
template_code = 'MILEAGE_CLAIM_V3'
|
|
params = {
|
|
'고객명': name,
|
|
'구매품목': item_summary,
|
|
'적립포인트': f'{points:,}',
|
|
'총잔액': f'{balance:,}',
|
|
'적립일시': now_kst,
|
|
'전화번호': phone
|
|
}
|
|
|
|
success, msg = _send_alimtalk(template_code, phone, params)
|
|
|
|
if not success:
|
|
# V3 실패 로그
|
|
_log_to_db(template_code, phone, False, msg,
|
|
template_params=params, user_id=user_id,
|
|
trigger_source=trigger_source, transaction_id=transaction_id)
|
|
|
|
# V2 폴백
|
|
template_code = 'MILEAGE_CLAIM_V2'
|
|
params = {
|
|
'고객명': name,
|
|
'적립포인트': f'{points:,}',
|
|
'총잔액': f'{balance:,}',
|
|
'적립일시': now_kst,
|
|
'전화번호': phone
|
|
}
|
|
success, msg = _send_alimtalk(template_code, phone, params)
|
|
|
|
# 최종 결과 로그
|
|
_log_to_db(template_code, phone, success, msg,
|
|
template_params=params, user_id=user_id,
|
|
trigger_source=trigger_source, transaction_id=transaction_id)
|
|
|
|
return (success, msg)
|
|
|
|
|
|
def get_nhn_send_history(start_date, end_date, page=1, page_size=15):
|
|
"""
|
|
NHN Cloud API에서 실제 발송 내역 조회
|
|
|
|
Args:
|
|
start_date: 시작일 (YYYY-MM-DD HH:mm)
|
|
end_date: 종료일 (YYYY-MM-DD HH:mm)
|
|
|
|
Returns:
|
|
list: 발송 메시지 목록
|
|
"""
|
|
url = (f'{API_BASE}/messages'
|
|
f'?startRequestDate={start_date}'
|
|
f'&endRequestDate={end_date}'
|
|
f'&pageNum={page}&pageSize={page_size}')
|
|
headers = {
|
|
'Content-Type': 'application/json;charset=UTF-8',
|
|
'X-Secret-Key': SECRET_KEY
|
|
}
|
|
|
|
try:
|
|
resp = requests.get(url, headers=headers, timeout=10)
|
|
data = resp.json()
|
|
if data.get('messageSearchResultResponse'):
|
|
return data['messageSearchResultResponse'].get('messages', [])
|
|
return []
|
|
except Exception as e:
|
|
logger.warning(f"NHN 발송내역 조회 실패: {e}")
|
|
return []
|