diff --git a/backend/app.py b/backend/app.py index bfd28d1..f649ec9 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1885,6 +1885,97 @@ def admin(): recent_tokens=recent_tokens) +# ============================================================================ +# 알림톡 로그 +# ============================================================================ + +@app.route('/admin/alimtalk') +def admin_alimtalk(): + """알림톡 발송 로그 + NHN 발송 내역""" + conn = db_manager.get_sqlite_connection() + cursor = conn.cursor() + + # 로컬 발송 로그 (최근 50건) + cursor.execute(""" + SELECT a.*, u.nickname, u.phone as user_phone + FROM alimtalk_logs a + LEFT JOIN users u ON a.user_id = u.id + ORDER BY a.created_at DESC + LIMIT 50 + """) + local_logs = [dict(row) for row in cursor.fetchall()] + + # 통계 + cursor.execute(""" + SELECT + COUNT(*) as total, + SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as success_count, + SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) as fail_count + FROM alimtalk_logs + """) + stats = dict(cursor.fetchone()) + + # 오늘 통계 + cursor.execute(""" + SELECT + COUNT(*) as today_total, + SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as today_success + FROM alimtalk_logs + WHERE date(created_at) = date('now') + """) + today = dict(cursor.fetchone()) + stats.update(today) + + return render_template('admin_alimtalk.html', local_logs=local_logs, stats=stats) + + +@app.route('/api/admin/alimtalk/nhn-history') +def api_admin_alimtalk_nhn_history(): + """NHN Cloud 실제 발송 내역 API""" + from services.nhn_alimtalk import get_nhn_send_history + + date_str = request.args.get('date', datetime.now().strftime('%Y-%m-%d')) + start = f"{date_str} 00:00" + end = f"{date_str} 23:59" + + messages = get_nhn_send_history(start, end) + + result = [] + for m in messages: + result.append({ + 'requestDate': m.get('requestDate', ''), + 'recipientNo': m.get('recipientNo', ''), + 'templateCode': m.get('templateCode', ''), + 'messageStatus': m.get('messageStatus', ''), + 'resultCode': m.get('resultCode', ''), + 'resultMessage': m.get('resultMessage', ''), + 'content': m.get('content', ''), + }) + + return jsonify({'success': True, 'messages': result}) + + +@app.route('/api/admin/alimtalk/test-send', methods=['POST']) +def api_admin_alimtalk_test_send(): + """관리자 수동 알림톡 발송 테스트""" + from services.nhn_alimtalk import send_mileage_claim_alimtalk + + data = request.get_json() + phone = data.get('phone', '').strip().replace('-', '') + name = data.get('name', '테스트') + + if len(phone) < 10: + return jsonify({'success': False, 'message': '올바른 전화번호를 입력해주세요.'}), 400 + + success, msg = send_mileage_claim_alimtalk( + phone, name, 100, 500, + items=[{'name': '테스트 발송', 'qty': 1, 'total': 1000}], + trigger_source='admin_test' + ) + + return jsonify({'success': success, 'message': msg}) + + # ============================================================================ # 키오스크 적립 # ============================================================================ @@ -2094,9 +2185,15 @@ def api_kiosk_claim(): user_row = cursor.fetchone() user_name = user_row['nickname'] if user_row else '고객' - send_mileage_claim_alimtalk(phone, user_name, claimed_points, new_balance, items=sale_items) + logging.warning(f"[알림톡] 발송 시도: phone={phone}, name={user_name}, points={claimed_points}, balance={new_balance}, items={sale_items}") + success, msg = send_mileage_claim_alimtalk( + phone, user_name, claimed_points, new_balance, + items=sale_items, user_id=user_id, + trigger_source='kiosk', transaction_id=transaction_id + ) + logging.warning(f"[알림톡] 발송 결과: success={success}, msg={msg}") except Exception as alimtalk_err: - logging.warning(f"알림톡 발송 실패 (적립은 완료): {alimtalk_err}") + logging.warning(f"[알림톡] 발송 예외 (적립은 완료): {alimtalk_err}") return jsonify({ 'success': True, diff --git a/backend/db/dbsetup.py b/backend/db/dbsetup.py index cb89293..0168166 100644 --- a/backend/db/dbsetup.py +++ b/backend/db/dbsetup.py @@ -237,7 +237,7 @@ class DatabaseManager: print(f"[DB Manager] SQLite 스키마 초기화 완료") def _migrate_sqlite(self): - """기존 DB에 새 컬럼 추가 (마이그레이션)""" + """기존 DB에 새 컬럼/테이블 추가 (마이그레이션)""" cursor = self.sqlite_conn.cursor() cursor.execute("PRAGMA table_info(users)") columns = [row[1] for row in cursor.fetchall()] @@ -246,6 +246,29 @@ class DatabaseManager: self.sqlite_conn.commit() print("[DB Manager] SQLite 마이그레이션: users.birthday 컬럼 추가") + # alimtalk_logs 테이블 생성 + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='alimtalk_logs'") + if not cursor.fetchone(): + cursor.executescript(""" + CREATE TABLE IF NOT EXISTS alimtalk_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + template_code VARCHAR(50) NOT NULL, + recipient_no VARCHAR(20) NOT NULL, + user_id INTEGER, + trigger_source VARCHAR(20) NOT NULL, + template_params TEXT, + success BOOLEAN NOT NULL, + result_message TEXT, + transaction_id VARCHAR(20), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) + ); + CREATE INDEX IF NOT EXISTS idx_alimtalk_created ON alimtalk_logs(created_at); + CREATE INDEX IF NOT EXISTS idx_alimtalk_recipient ON alimtalk_logs(recipient_no); + """) + self.sqlite_conn.commit() + print("[DB Manager] SQLite 마이그레이션: alimtalk_logs 테이블 생성") + def test_connection(self, database='PM_BASE'): """연결 테스트""" try: diff --git a/backend/db/mileage_schema.sql b/backend/db/mileage_schema.sql index f42a36a..48415e7 100644 --- a/backend/db/mileage_schema.sql +++ b/backend/db/mileage_schema.sql @@ -80,3 +80,21 @@ CREATE TABLE IF NOT EXISTS pos_customer_links ( ); CREATE INDEX IF NOT EXISTS idx_links_cuscode ON pos_customer_links(cuscode); + +-- 6. 알림톡 발송 로그 테이블 +CREATE TABLE IF NOT EXISTS alimtalk_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + template_code VARCHAR(50) NOT NULL, + recipient_no VARCHAR(20) NOT NULL, + user_id INTEGER, + trigger_source VARCHAR(20) NOT NULL, -- 'kiosk', 'admin', 'manual' 등 + template_params TEXT, -- JSON 문자열 + success BOOLEAN NOT NULL, + result_message TEXT, + transaction_id VARCHAR(20), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) +); + +CREATE INDEX IF NOT EXISTS idx_alimtalk_created ON alimtalk_logs(created_at); +CREATE INDEX IF NOT EXISTS idx_alimtalk_recipient ON alimtalk_logs(recipient_no); diff --git a/backend/gui/pos_sales_gui.py b/backend/gui/pos_sales_gui.py index 65f262c..99786d5 100644 --- a/backend/gui/pos_sales_gui.py +++ b/backend/gui/pos_sales_gui.py @@ -78,12 +78,16 @@ class SalesQueryThread(QThread): ISNULL(S.card_total, 0) AS card_total, ISNULL(S.cash_total, 0) AS cash_total, ISNULL(M.SL_MY_total, 0) AS total_amount, - ISNULL(M.SL_MY_discount, 0) AS discount + ISNULL(M.SL_MY_discount, 0) AS discount, + S.cash_receipt_mode, + S.cash_receipt_num FROM SALE_MAIN M OUTER APPLY ( SELECT TOP 1 ISNULL(ETC_CARD, 0) + ISNULL(OTC_CARD, 0) AS card_total, - ISNULL(ETC_CASH, 0) + ISNULL(OTC_CASH, 0) AS cash_total + ISNULL(ETC_CASH, 0) + ISNULL(OTC_CASH, 0) AS cash_total, + nCASHINMODE AS cash_receipt_mode, + nAPPROVAL_NUM AS cash_receipt_num FROM CD_SUNAB WHERE PRESERIAL = M.SL_NO_order ) S @@ -96,7 +100,7 @@ class SalesQueryThread(QThread): sales_list = [] for row in rows: - order_no, insert_time, sale_amount, customer, card_total, cash_total, total_amount, discount = row + order_no, insert_time, sale_amount, customer, card_total, cash_total, total_amount, discount, cash_receipt_mode, cash_receipt_num = row # 품목 수 조회 (SALE_SUB) mssql_cursor.execute(""" @@ -136,12 +140,17 @@ class SalesQueryThread(QThread): # 결제수단 판별 card_amt = float(card_total) if card_total else 0.0 cash_amt = float(cash_total) if cash_total else 0.0 + # 현금영수증: nCASHINMODE='1' AND nAPPROVAL_NUM 존재 (mode=2는 카드거래 자동세팅) + has_cash_receipt = ( + str(cash_receipt_mode or '').strip() == '1' + and str(cash_receipt_num or '').strip() != '' + ) if card_amt > 0 and cash_amt > 0: pay_method = '카드+현금' elif card_amt > 0: pay_method = '카드' elif cash_amt > 0: - pay_method = '현금' + pay_method = '현영' if has_cash_receipt else '현금' else: pay_method = '' paid = (card_amt + cash_amt) > 0 @@ -862,6 +871,11 @@ class POSSalesGUI(QMainWindow): pay_item.setTextAlignment(Qt.AlignCenter) if sale['pay_method'] == '카드': pay_item.setForeground(QColor('#1976D2')) + elif sale['pay_method'] == '현영': + pay_item.setForeground(QColor('#00897B')) # 청록 (현금영수증) + f = QFont() + f.setBold(True) + pay_item.setFont(f) elif sale['pay_method'] == '현금': pay_item.setForeground(QColor('#E65100')) elif sale['pay_method']: diff --git a/backend/services/nhn_alimtalk.py b/backend/services/nhn_alimtalk.py index 11e1f6d..f3af174 100644 --- a/backend/services/nhn_alimtalk.py +++ b/backend/services/nhn_alimtalk.py @@ -1,9 +1,10 @@ """ NHN Cloud 알림톡 발송 서비스 -마일리지 적립 완료 등 알림톡 발송 +마일리지 적립 완료 등 알림톡 발송 + SQLite 로깅 """ import os +import json import logging from datetime import datetime, timezone, timedelta @@ -22,6 +23,34 @@ API_BASE = f'https://api-alimtalk.cloud.toast.com/alimtalk/v2.3/appkeys/{APPKEY} 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): """ 알림톡 발송 공통 함수 @@ -82,7 +111,9 @@ def build_item_summary(items): return f"{first} 외 {len(items) - 1}건" -def send_mileage_claim_alimtalk(phone, name, points, balance, items=None): +def send_mileage_claim_alimtalk(phone, name, points, balance, items=None, + user_id=None, trigger_source='kiosk', + transaction_id=None): """ 마일리지 적립 완료 알림톡 발송 @@ -92,11 +123,14 @@ def send_mileage_claim_alimtalk(phone, name, points, balance, items=None): 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('%Y-%m-%d %H:%M') + now_kst = datetime.now(KST).strftime('%m/%d %H:%M') item_summary = build_item_summary(items) # MILEAGE_CLAIM_V3 (발송 근거 + 구매품목 포함) 우선 시도 @@ -113,15 +147,56 @@ def send_mileage_claim_alimtalk(phone, name, points, balance, items=None): success, msg = _send_alimtalk(template_code, phone, params) if not success: - # V3 실패 시 V2 폴백 (구매품목 변수 없는 버전) + # 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_v2 = { + params = { '고객명': name, '적립포인트': f'{points:,}', '총잔액': f'{balance:,}', '적립일시': now_kst, '전화번호': phone } - success, msg = _send_alimtalk(template_code, phone, params_v2) + 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 [] diff --git a/backend/templates/admin.html b/backend/templates/admin.html index 95c77ea..7aae828 100644 --- a/backend/templates/admin.html +++ b/backend/templates/admin.html @@ -393,9 +393,12 @@
| 시간 | +수신번호 | +고객 | +템플릿 | +발송 주체 | +결과 | +상세 | +
|---|---|---|---|---|---|---|
| {{ log.created_at[:16] if log.created_at else '-' }} | +{{ log.recipient_no[:3] + '-' + log.recipient_no[3:7] + '-' + log.recipient_no[7:] if log.recipient_no|length >= 11 else log.recipient_no }} | +{{ log.nickname or '-' }} | +{{ log.template_code }} |
+ + {% if log.trigger_source == 'kiosk' %} + 키오스크 + {% elif log.trigger_source == 'admin_test' %} + 관리자 + {% else %} + {{ log.trigger_source }} + {% endif %} + | ++ {% if log.success %} + 성공 + {% else %} + 실패 + {% endif %} + | +
+ {% if log.template_params %}
+ 변수 보기
+ {{ log.template_params }}
+ {% endif %}
+ {% if not log.success and log.result_message %}
+ {{ log.result_message }}
+ {% endif %}
+ |
+