feat: 포인트 사용 기능 및 시간 표시 개선
- UTC to KST 시간 변환 로직 추가 (SQLite 저장 시간 표시용) - 관리자 페이지에 포인트 사용(차감) 기능 추가 - 사용자 상세 모달에 "포인트 사용" 버튼 추가 - 포인트 입력 및 차감 처리 - 마일리지 원장에 USE 타입으로 기록 - 구매 이력 시간을 MSSQL의 실제 거래 시간(InsertTime)으로 수정 - 선택적 시간 변환 적용 - 변환: users.created_at, mileage_ledger.created_at, claim_tokens.created_at - 미변환: claim_tokens.claimed_at, MSSQL 거래 시간 - 관리자 페이지에 검색 기능 추가 (사이드바) - 사용자 검색 (이름, 전화번호, 뒷자리) - 제품 검색 (약품명으로 구매자 조회) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
7627efbdfb
commit
59a33cc249
210
backend/app.py
210
backend/app.py
@ -5,9 +5,10 @@ Flask 웹 서버 - QR 마일리지 적립
|
|||||||
|
|
||||||
from flask import Flask, request, render_template, jsonify, redirect, url_for
|
from flask import Flask, request, render_template, jsonify, redirect, url_for
|
||||||
import hashlib
|
import hashlib
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone, timedelta
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
# Path setup
|
# Path setup
|
||||||
@ -20,6 +21,47 @@ app.secret_key = 'pharmacy-qr-mileage-secret-key-2026'
|
|||||||
# 데이터베이스 매니저
|
# 데이터베이스 매니저
|
||||||
db_manager = DatabaseManager()
|
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):
|
def verify_claim_token(transaction_id, nonce):
|
||||||
"""
|
"""
|
||||||
@ -355,11 +397,15 @@ def my_page():
|
|||||||
FROM users WHERE phone = ?
|
FROM users WHERE phone = ?
|
||||||
""", (phone,))
|
""", (phone,))
|
||||||
|
|
||||||
user = cursor.fetchone()
|
user_raw = cursor.fetchone()
|
||||||
|
|
||||||
if not user:
|
if not user_raw:
|
||||||
return render_template('error.html', message='등록되지 않은 전화번호입니다.')
|
return render_template('error.html', message='등록되지 않은 전화번호입니다.')
|
||||||
|
|
||||||
|
# 사용자 정보에 KST 시간 변환 적용
|
||||||
|
user = dict(user_raw)
|
||||||
|
user['created_at'] = utc_to_kst_str(user_raw['created_at'])
|
||||||
|
|
||||||
# 적립 내역 조회
|
# 적립 내역 조회
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT points, balance_after, reason, description, created_at
|
SELECT points, balance_after, reason, description, created_at
|
||||||
@ -369,7 +415,14 @@ def my_page():
|
|||||||
LIMIT 20
|
LIMIT 20
|
||||||
""", (user['id'],))
|
""", (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)
|
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:
|
for token in claimed_tokens:
|
||||||
transaction_id = token['transaction_id']
|
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_SUB + CD_GOODS JOIN
|
||||||
sale_items_query = text("""
|
sale_items_query = text("""
|
||||||
SELECT
|
SELECT
|
||||||
@ -551,7 +622,7 @@ def admin_user_detail(user_id):
|
|||||||
|
|
||||||
purchases.append({
|
purchases.append({
|
||||||
'transaction_id': transaction_id,
|
'transaction_id': transaction_id,
|
||||||
'date': str(token['claimed_at'])[:16].replace('T', ' '),
|
'date': transaction_date, # MSSQL의 실제 거래 시간 사용
|
||||||
'amount': int(token['total_amount']),
|
'amount': int(token['total_amount']),
|
||||||
'points': int(token['claimable_points']),
|
'points': int(token['claimable_points']),
|
||||||
'items_summary': items_summary,
|
'items_summary': items_summary,
|
||||||
@ -572,7 +643,7 @@ def admin_user_detail(user_id):
|
|||||||
'name': user['nickname'],
|
'name': user['nickname'],
|
||||||
'phone': user['phone'],
|
'phone': user['phone'],
|
||||||
'balance': user['mileage_balance'],
|
'balance': user['mileage_balance'],
|
||||||
'created_at': str(user['created_at'])[:16].replace('T', ' ')
|
'created_at': utc_to_kst_str(user['created_at'])
|
||||||
},
|
},
|
||||||
'mileage_history': [
|
'mileage_history': [
|
||||||
{
|
{
|
||||||
@ -580,7 +651,7 @@ def admin_user_detail(user_id):
|
|||||||
'balance_after': ml['balance_after'],
|
'balance_after': ml['balance_after'],
|
||||||
'reason': ml['reason'],
|
'reason': ml['reason'],
|
||||||
'description': ml['description'],
|
'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']
|
'transaction_id': ml['transaction_id']
|
||||||
}
|
}
|
||||||
for ml in mileage_history
|
for ml in mileage_history
|
||||||
@ -753,6 +824,93 @@ def admin_search_product():
|
|||||||
}), 500
|
}), 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')
|
@app.route('/admin')
|
||||||
def admin():
|
def admin():
|
||||||
"""관리자 페이지 - 전체 사용자 및 적립 현황"""
|
"""관리자 페이지 - 전체 사용자 및 적립 현황"""
|
||||||
@ -775,7 +933,14 @@ def admin():
|
|||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
LIMIT 20
|
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건)
|
# 최근 적립 내역 (50건)
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
@ -793,7 +958,14 @@ def admin():
|
|||||||
ORDER BY ml.created_at DESC
|
ORDER BY ml.created_at DESC
|
||||||
LIMIT 50
|
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 토큰 통계
|
# QR 토큰 통계
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
@ -820,14 +992,22 @@ def admin():
|
|||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
LIMIT 20
|
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',
|
return render_template('admin.html',
|
||||||
stats=stats,
|
stats=stats,
|
||||||
recent_users=recent_users,
|
recent_users=recent_users,
|
||||||
recent_transactions=recent_transactions,
|
recent_transactions=recent_transactions,
|
||||||
token_stats=token_stats,
|
token_stats=token_stats,
|
||||||
recent_tokens=recent_tokens)
|
recent_tokens=recent_tokens)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@ -550,6 +550,35 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 포인트 사용 모달 -->
|
||||||
|
<div id="usePointsModal" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 10000; padding: 20px; overflow-y: auto;">
|
||||||
|
<div style="max-width: 500px; margin: 100px auto; background: #fff; border-radius: 20px; padding: 32px; position: relative;">
|
||||||
|
<button onclick="closeUsePointsModal()" style="position: absolute; top: 20px; right: 20px; background: #f1f3f5; border: none; width: 36px; height: 36px; border-radius: 50%; cursor: pointer; font-size: 20px; color: #495057;">×</button>
|
||||||
|
|
||||||
|
<h2 style="font-size: 24px; font-weight: 700; color: #212529; margin-bottom: 24px; letter-spacing: -0.5px;">💳 포인트 사용</h2>
|
||||||
|
|
||||||
|
<div style="background: #f8f9fa; border-radius: 12px; padding: 16px; margin-bottom: 24px;">
|
||||||
|
<div style="color: #868e96; font-size: 13px; margin-bottom: 6px;">현재 포인트 잔액</div>
|
||||||
|
<div id="current-balance-display" style="color: #6366f1; font-size: 24px; font-weight: 700;">0P</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 24px;">
|
||||||
|
<label style="display: block; color: #495057; font-size: 14px; font-weight: 600; margin-bottom: 8px;">사용할 포인트</label>
|
||||||
|
<input type="number" id="points-to-use" placeholder="포인트 입력" min="1" style="width: 100%; padding: 14px; border: 2px solid #e9ecef; border-radius: 10px; font-size: 16px; font-family: 'Noto Sans KR', sans-serif; box-sizing: border-box;" onkeypress="if(event.key==='Enter') confirmUsePoints()">
|
||||||
|
<div id="use-points-error" style="color: #f03e3e; font-size: 13px; margin-top: 8px; display: none;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 12px;">
|
||||||
|
<button onclick="closeUsePointsModal()" style="flex: 1; padding: 14px; background: #f1f3f5; color: #495057; border: none; border-radius: 10px; font-size: 15px; font-weight: 600; cursor: pointer;">
|
||||||
|
취소
|
||||||
|
</button>
|
||||||
|
<button onclick="confirmUsePoints()" style="flex: 1; padding: 14px; background: #f03e3e; color: white; border: none; border-radius: 10px; font-size: 15px; font-weight: 600; cursor: pointer;">
|
||||||
|
사용하기
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function showTransactionDetail(transactionId) {
|
function showTransactionDetail(transactionId) {
|
||||||
document.getElementById('transactionModal').style.display = 'block';
|
document.getElementById('transactionModal').style.display = 'block';
|
||||||
@ -720,7 +749,7 @@
|
|||||||
let html = `
|
let html = `
|
||||||
<!-- 사용자 기본 정보 -->
|
<!-- 사용자 기본 정보 -->
|
||||||
<div style="background: #f8f9fa; border-radius: 12px; padding: 20px; margin-bottom: 24px;">
|
<div style="background: #f8f9fa; border-radius: 12px; padding: 20px; margin-bottom: 24px;">
|
||||||
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px;">
|
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; margin-bottom: 16px;">
|
||||||
<div>
|
<div>
|
||||||
<div style="color: #868e96; font-size: 13px; margin-bottom: 6px;">이름</div>
|
<div style="color: #868e96; font-size: 13px; margin-bottom: 6px;">이름</div>
|
||||||
<div style="color: #212529; font-size: 16px; font-weight: 600;">${user.name}</div>
|
<div style="color: #212529; font-size: 16px; font-weight: 600;">${user.name}</div>
|
||||||
@ -731,13 +760,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style="color: #868e96; font-size: 13px; margin-bottom: 6px;">포인트 잔액</div>
|
<div style="color: #868e96; font-size: 13px; margin-bottom: 6px;">포인트 잔액</div>
|
||||||
<div style="color: #6366f1; font-size: 18px; font-weight: 700;">${user.balance.toLocaleString()}P</div>
|
<div id="user-balance-display" style="color: #6366f1; font-size: 18px; font-weight: 700;">${user.balance.toLocaleString()}P</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style="color: #868e96; font-size: 13px; margin-bottom: 6px;">가입일</div>
|
<div style="color: #868e96; font-size: 13px; margin-bottom: 6px;">가입일</div>
|
||||||
<div style="color: #212529; font-size: 16px; font-weight: 600;">${user.created_at}</div>
|
<div style="color: #212529; font-size: 16px; font-weight: 600;">${user.created_at}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="text-align: right;">
|
||||||
|
<button onclick="showUsePointsModal(${user.id}, ${user.balance})" style="padding: 10px 24px; background: #f03e3e; color: white; border: none; border-radius: 10px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s;">
|
||||||
|
💳 포인트 사용
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 탭 메뉴 -->
|
<!-- 탭 메뉴 -->
|
||||||
@ -899,10 +933,97 @@
|
|||||||
document.getElementById('tab-content-' + tabName).style.display = 'block';
|
document.getElementById('tab-content-' + tabName).style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ESC 키로 사용자 모달 닫기
|
// ===== 포인트 사용 기능 =====
|
||||||
|
|
||||||
|
let currentUserId = null;
|
||||||
|
let currentUserBalance = 0;
|
||||||
|
|
||||||
|
function showUsePointsModal(userId, balance) {
|
||||||
|
currentUserId = userId;
|
||||||
|
currentUserBalance = balance;
|
||||||
|
|
||||||
|
// 현재 잔액 표시
|
||||||
|
document.getElementById('current-balance-display').innerText = balance.toLocaleString() + 'P';
|
||||||
|
|
||||||
|
// 입력 필드 초기화
|
||||||
|
document.getElementById('points-to-use').value = '';
|
||||||
|
document.getElementById('use-points-error').style.display = 'none';
|
||||||
|
|
||||||
|
// 모달 표시
|
||||||
|
document.getElementById('usePointsModal').style.display = 'block';
|
||||||
|
|
||||||
|
// 입력 필드에 포커스
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('points-to-use').focus();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeUsePointsModal() {
|
||||||
|
document.getElementById('usePointsModal').style.display = 'none';
|
||||||
|
currentUserId = null;
|
||||||
|
currentUserBalance = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmUsePoints() {
|
||||||
|
const pointsInput = document.getElementById('points-to-use');
|
||||||
|
const points = parseInt(pointsInput.value);
|
||||||
|
const errorDiv = document.getElementById('use-points-error');
|
||||||
|
|
||||||
|
// 유효성 검사
|
||||||
|
if (!points || points <= 0) {
|
||||||
|
errorDiv.innerText = '1 이상의 포인트를 입력하세요.';
|
||||||
|
errorDiv.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (points > currentUserBalance) {
|
||||||
|
errorDiv.innerText = `잔액(${currentUserBalance.toLocaleString()}P)보다 많이 사용할 수 없습니다.`;
|
||||||
|
errorDiv.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API 호출
|
||||||
|
fetch('/admin/use-points', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
user_id: currentUserId,
|
||||||
|
points: points
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
// 성공 - userId 저장 후 모달 닫고 사용자 상세 정보 새로고침
|
||||||
|
const userId = currentUserId; // closeUsePointsModal() 전에 저장
|
||||||
|
closeUsePointsModal();
|
||||||
|
showUserDetail(userId);
|
||||||
|
|
||||||
|
// 성공 메시지 표시 (선택사항)
|
||||||
|
alert(`${points.toLocaleString()}P 사용 완료!\n남은 잔액: ${data.new_balance.toLocaleString()}P`);
|
||||||
|
} else {
|
||||||
|
// 실패 - 에러 메시지 표시
|
||||||
|
errorDiv.innerText = data.message || '포인트 사용에 실패했습니다.';
|
||||||
|
errorDiv.style.display = 'block';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
errorDiv.innerText = '네트워크 오류가 발생했습니다.';
|
||||||
|
errorDiv.style.display = 'block';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ESC 키로 모달 닫기
|
||||||
document.addEventListener('keydown', function(e) {
|
document.addEventListener('keydown', function(e) {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
closeUserModal();
|
// 포인트 사용 모달이 열려있으면 먼저 닫기
|
||||||
|
if (document.getElementById('usePointsModal').style.display === 'block') {
|
||||||
|
closeUsePointsModal();
|
||||||
|
} else {
|
||||||
|
closeUserModal();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -913,6 +1034,13 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 포인트 사용 모달 배경 클릭 시 닫기
|
||||||
|
document.getElementById('usePointsModal').addEventListener('click', function(e) {
|
||||||
|
if (e.target === this) {
|
||||||
|
closeUsePointsModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ===== 검색 기능 =====
|
// ===== 검색 기능 =====
|
||||||
|
|
||||||
let currentSearchType = 'user'; // 'user' 또는 'product'
|
let currentSearchType = 'user'; // 'user' 또는 'product'
|
||||||
|
|||||||
@ -246,7 +246,7 @@
|
|||||||
{% if tx.description %}
|
{% if tx.description %}
|
||||||
<div class="transaction-desc">{{ tx.description }}</div>
|
<div class="transaction-desc">{{ tx.description }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="transaction-date">{{ tx.created_at[:16].replace('T', ' ') }}</div>
|
<div class="transaction-date">{{ tx.created_at }}</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user