diff --git a/backend/app.py b/backend/app.py index 735b57e..a7ba557 100644 --- a/backend/app.py +++ b/backend/app.py @@ -5,9 +5,10 @@ Flask 웹 서버 - QR 마일리지 적립 from flask import Flask, request, render_template, jsonify, redirect, url_for import hashlib -from datetime import datetime +from datetime import datetime, timezone, timedelta import sys import os +import logging from sqlalchemy import text # Path setup @@ -20,6 +21,47 @@ app.secret_key = 'pharmacy-qr-mileage-secret-key-2026' # 데이터베이스 매니저 db_manager = DatabaseManager() +# KST 타임존 (UTC+9) +KST = timezone(timedelta(hours=9)) + + +def utc_to_kst_str(utc_time_str): + """ + UTC 시간 문자열을 KST 시간 문자열로 변환 + + Args: + utc_time_str (str): UTC 시간 문자열 (ISO 8601 형식) + + Returns: + str: KST 시간 문자열 (YYYY-MM-DD HH:MM:SS) + """ + if not utc_time_str: + return None + + try: + # ISO 8601 형식 파싱 ('2026-01-23T12:28:36' 또는 '2026-01-23 12:28:36') + utc_time_str = utc_time_str.replace(' ', 'T') # 공백을 T로 변환 + + # datetime 객체로 변환 + if 'T' in utc_time_str: + utc_time = datetime.fromisoformat(utc_time_str) + else: + utc_time = datetime.fromisoformat(utc_time_str) + + # UTC 타임존 설정 (naive datetime인 경우) + if utc_time.tzinfo is None: + utc_time = utc_time.replace(tzinfo=timezone.utc) + + # KST로 변환 + kst_time = utc_time.astimezone(KST) + + # 문자열로 반환 (초 단위까지만) + return kst_time.strftime('%Y-%m-%d %H:%M:%S') + + except Exception as e: + logging.error(f"시간 변환 실패: {utc_time_str}, 오류: {e}") + return utc_time_str # 변환 실패 시 원본 반환 + def verify_claim_token(transaction_id, nonce): """ @@ -355,11 +397,15 @@ def my_page(): FROM users WHERE phone = ? """, (phone,)) - user = cursor.fetchone() + user_raw = cursor.fetchone() - if not user: + if not user_raw: return render_template('error.html', message='등록되지 않은 전화번호입니다.') + # 사용자 정보에 KST 시간 변환 적용 + user = dict(user_raw) + user['created_at'] = utc_to_kst_str(user_raw['created_at']) + # 적립 내역 조회 cursor.execute(""" SELECT points, balance_after, reason, description, created_at @@ -369,7 +415,14 @@ def my_page(): LIMIT 20 """, (user['id'],)) - transactions = cursor.fetchall() + transactions_raw = cursor.fetchall() + + # 거래 내역에 KST 시간 변환 적용 + transactions = [] + for tx in transactions_raw: + tx_dict = dict(tx) + tx_dict['created_at'] = utc_to_kst_str(tx['created_at']) + transactions.append(tx_dict) return render_template('my_page.html', user=user, transactions=transactions) @@ -506,6 +559,24 @@ def admin_user_detail(user_id): for token in claimed_tokens: transaction_id = token['transaction_id'] + # SALE_MAIN에서 거래 시간 조회 + sale_main_query = text(""" + SELECT InsertTime + FROM SALE_MAIN + WHERE SL_NO_order = :transaction_id + """) + + sale_main = session.execute( + sale_main_query, + {'transaction_id': transaction_id} + ).fetchone() + + # 거래 시간 추출 (MSSQL의 실제 거래 시간) + if sale_main and sale_main.InsertTime: + transaction_date = str(sale_main.InsertTime)[:16].replace('T', ' ') + else: + transaction_date = '-' + # SALE_SUB + CD_GOODS JOIN sale_items_query = text(""" SELECT @@ -551,7 +622,7 @@ def admin_user_detail(user_id): purchases.append({ 'transaction_id': transaction_id, - 'date': str(token['claimed_at'])[:16].replace('T', ' '), + 'date': transaction_date, # MSSQL의 실제 거래 시간 사용 'amount': int(token['total_amount']), 'points': int(token['claimable_points']), 'items_summary': items_summary, @@ -572,7 +643,7 @@ def admin_user_detail(user_id): 'name': user['nickname'], 'phone': user['phone'], 'balance': user['mileage_balance'], - 'created_at': str(user['created_at'])[:16].replace('T', ' ') + 'created_at': utc_to_kst_str(user['created_at']) }, 'mileage_history': [ { @@ -580,7 +651,7 @@ def admin_user_detail(user_id): 'balance_after': ml['balance_after'], 'reason': ml['reason'], 'description': ml['description'], - 'created_at': str(ml['created_at'])[:16].replace('T', ' '), + 'created_at': utc_to_kst_str(ml['created_at']), 'transaction_id': ml['transaction_id'] } for ml in mileage_history @@ -753,6 +824,93 @@ def admin_search_product(): }), 500 +@app.route('/admin/use-points', methods=['POST']) +def admin_use_points(): + """관리자 페이지에서 포인트 사용 (차감)""" + try: + data = request.get_json() + user_id = data.get('user_id') + points_to_use = data.get('points') + + if not user_id or not points_to_use: + return jsonify({ + 'success': False, + 'message': '사용자 ID와 포인트를 입력하세요.' + }), 400 + + if points_to_use <= 0: + return jsonify({ + 'success': False, + 'message': '1 이상의 포인트를 입력하세요.' + }), 400 + + conn = db_manager.get_sqlite_connection() + cursor = conn.cursor() + + # 1. 현재 사용자 잔액 확인 + cursor.execute(""" + SELECT id, nickname, mileage_balance + FROM users + WHERE id = ? + """, (user_id,)) + + user = cursor.fetchone() + + if not user: + return jsonify({ + 'success': False, + 'message': '사용자를 찾을 수 없습니다.' + }), 404 + + current_balance = user['mileage_balance'] + + # 2. 잔액 확인 + if points_to_use > current_balance: + return jsonify({ + 'success': False, + 'message': f'잔액({current_balance:,}P)이 부족합니다.' + }), 400 + + # 3. 포인트 차감 + new_balance = current_balance - points_to_use + + cursor.execute(""" + UPDATE users + SET mileage_balance = ? + WHERE id = ? + """, (new_balance, user_id)) + + # 4. 마일리지 원장 기록 + cursor.execute(""" + INSERT INTO mileage_ledger + (user_id, transaction_id, points, balance_after, reason, description) + VALUES (?, NULL, ?, ?, 'USE', ?) + """, ( + user_id, + -points_to_use, # 음수로 저장 + new_balance, + f'포인트 사용 (관리자) - {points_to_use:,}P 차감' + )) + + conn.commit() + + logging.info(f"포인트 사용 완료: 사용자 ID {user_id}, 차감 {points_to_use}P, 잔액 {new_balance}P") + + return jsonify({ + 'success': True, + 'message': f'{points_to_use:,}P 사용 완료', + 'new_balance': new_balance, + 'used_points': points_to_use + }) + + except Exception as e: + logging.error(f"포인트 사용 실패: {e}") + return jsonify({ + 'success': False, + 'message': f'포인트 사용 실패: {str(e)}' + }), 500 + + @app.route('/admin') def admin(): """관리자 페이지 - 전체 사용자 및 적립 현황""" @@ -775,7 +933,14 @@ def admin(): ORDER BY created_at DESC LIMIT 20 """) - recent_users = cursor.fetchall() + recent_users_raw = cursor.fetchall() + + # 시간을 KST로 변환 + recent_users = [] + for user in recent_users_raw: + user_dict = dict(user) + user_dict['created_at'] = utc_to_kst_str(user['created_at']) + recent_users.append(user_dict) # 최근 적립 내역 (50건) cursor.execute(""" @@ -793,7 +958,14 @@ def admin(): ORDER BY ml.created_at DESC LIMIT 50 """) - recent_transactions = cursor.fetchall() + recent_transactions_raw = cursor.fetchall() + + # 시간을 KST로 변환 + recent_transactions = [] + for trans in recent_transactions_raw: + trans_dict = dict(trans) + trans_dict['created_at'] = utc_to_kst_str(trans['created_at']) + recent_transactions.append(trans_dict) # QR 토큰 통계 cursor.execute(""" @@ -820,14 +992,22 @@ def admin(): ORDER BY created_at DESC LIMIT 20 """) - recent_tokens = cursor.fetchall() + recent_tokens_raw = cursor.fetchall() + + # Convert only created_at (발행일), leave claimed_at (적립일) unconverted + recent_tokens = [] + for token in recent_tokens_raw: + token_dict = dict(token) + token_dict['created_at'] = utc_to_kst_str(token['created_at']) # Convert 발행일 + # claimed_at stays as-is (or remains None if not claimed) + recent_tokens.append(token_dict) return render_template('admin.html', - stats=stats, - recent_users=recent_users, - recent_transactions=recent_transactions, - token_stats=token_stats, - recent_tokens=recent_tokens) + stats=stats, + recent_users=recent_users, + recent_transactions=recent_transactions, + token_stats=token_stats, + recent_tokens=recent_tokens) if __name__ == '__main__': diff --git a/backend/templates/admin.html b/backend/templates/admin.html index 5d7d92f..4453979 100644 --- a/backend/templates/admin.html +++ b/backend/templates/admin.html @@ -550,6 +550,35 @@ + +
+