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:
@@ -550,6 +550,35 @@
|
||||
</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>
|
||||
function showTransactionDetail(transactionId) {
|
||||
document.getElementById('transactionModal').style.display = 'block';
|
||||
@@ -720,7 +749,7 @@
|
||||
let html = `
|
||||
<!-- 사용자 기본 정보 -->
|
||||
<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 style="color: #868e96; font-size: 13px; margin-bottom: 6px;">이름</div>
|
||||
<div style="color: #212529; font-size: 16px; font-weight: 600;">${user.name}</div>
|
||||
@@ -731,13 +760,18 @@
|
||||
</div>
|
||||
<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 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>
|
||||
</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>
|
||||
|
||||
<!-- 탭 메뉴 -->
|
||||
@@ -899,10 +933,97 @@
|
||||
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) {
|
||||
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'
|
||||
|
||||
Reference in New Issue
Block a user