pharmacy-pos-qr-system/backend/templates/admin_members.html
thug0bin 8c3bcb525d fix: 회원 상세 - transaction_id로 POS 품목 조회 연동
- 마일리지 적립 시 저장된 transaction_id로 SALE_SUB 조회
- 적립 내역에 구매 품목 표시 (품명, 수량, 가격)
- 구매 이력 탭: QR 적립된 구매만 품목과 함께 표시
- 기존 전화번호→고객코드 매핑 로직 제거 (불필요)
2026-02-27 15:11:23 +09:00

1007 lines
33 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>회원 검색 - 청춘약국 CRM</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
background: #f8fafc;
-webkit-font-smoothing: antialiased;
color: #1e293b;
}
/* ── 헤더 ── */
.header {
background: linear-gradient(135deg, #059669 0%, #10b981 50%, #34d399 100%);
padding: 28px 32px 24px;
color: #fff;
}
.header-nav {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.header-nav a {
color: rgba(255,255,255,0.8);
text-decoration: none;
font-size: 14px;
font-weight: 500;
}
.header-nav a:hover { color: #fff; }
.header h1 {
font-size: 24px;
font-weight: 700;
letter-spacing: -0.5px;
margin-bottom: 6px;
}
.header p {
font-size: 14px;
opacity: 0.85;
}
/* ── 컨텐츠 ── */
.content {
max-width: 1100px;
margin: 0 auto;
padding: 24px 20px 60px;
}
/* ── 검색 영역 ── */
.search-section {
background: #fff;
border-radius: 14px;
padding: 24px;
margin-bottom: 20px;
border: 1px solid #e2e8f0;
}
.search-box {
display: flex;
gap: 12px;
}
.search-input {
flex: 1;
padding: 14px 18px;
border: 2px solid #e2e8f0;
border-radius: 12px;
font-size: 16px;
font-family: inherit;
transition: all 0.2s;
}
.search-input:focus {
outline: none;
border-color: #10b981;
box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.1);
}
.search-btn {
background: #10b981;
color: #fff;
border: none;
padding: 14px 32px;
border-radius: 12px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.search-btn:hover { background: #059669; }
.search-hint {
margin-top: 12px;
font-size: 13px;
color: #94a3b8;
}
/* ── 결과 카운트 ── */
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.result-count {
font-size: 14px;
color: #64748b;
}
.result-count strong {
color: #10b981;
font-weight: 700;
}
.send-selected-btn {
background: #6366f1;
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
display: none;
}
.send-selected-btn:hover { background: #4f46e5; }
.send-selected-btn.active { display: inline-flex; align-items: center; gap: 6px; }
/* ── 테이블 ── */
.table-wrap {
background: #fff;
border-radius: 14px;
border: 1px solid #e2e8f0;
overflow: hidden;
}
table {
width: 100%;
border-collapse: collapse;
}
thead th {
background: #f8fafc;
padding: 14px 16px;
font-size: 12px;
font-weight: 600;
color: #64748b;
text-align: left;
border-bottom: 1px solid #e2e8f0;
white-space: nowrap;
}
tbody td {
padding: 14px 16px;
font-size: 14px;
color: #334155;
border-bottom: 1px solid #f1f5f9;
vertical-align: middle;
}
tbody tr:hover { background: #f0fdf4; }
tbody tr.selected { background: #dcfce7; }
.member-name {
font-weight: 600;
color: #1e293b;
}
.member-phone {
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
color: #059669;
}
.member-memo {
font-size: 12px;
color: #94a3b8;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sms-stop {
background: #fef2f2;
color: #dc2626;
font-size: 11px;
padding: 2px 6px;
border-radius: 4px;
}
/* ── 버튼 ── */
.btn-send {
background: #6366f1;
color: #fff;
border: none;
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.btn-send:hover { background: #4f46e5; }
.btn-detail {
background: #f1f5f9;
color: #64748b;
border: none;
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
margin-right: 6px;
}
.btn-detail:hover { background: #e2e8f0; }
/* ── 체크박스 ── */
.checkbox {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #10b981;
}
/* ── 빈 상태 ── */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #94a3b8;
}
.empty-state .icon {
font-size: 48px;
margin-bottom: 16px;
}
/* ── 모달 ── */
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal-overlay.active { display: flex; }
.modal-box {
background: #fff;
border-radius: 16px;
padding: 24px;
max-width: 500px;
width: 90%;
}
.modal-title {
font-size: 18px;
font-weight: 700;
margin-bottom: 16px;
}
.modal-recipient {
background: #f8fafc;
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 16px;
font-size: 14px;
}
.modal-recipient strong {
color: #10b981;
}
.modal-textarea {
width: 100%;
min-height: 150px;
padding: 14px;
border: 2px solid #e2e8f0;
border-radius: 10px;
font-size: 14px;
font-family: inherit;
resize: vertical;
margin-bottom: 12px;
}
.modal-textarea:focus {
outline: none;
border-color: #6366f1;
}
.char-count {
text-align: right;
font-size: 12px;
color: #94a3b8;
margin-bottom: 16px;
}
.msg-type-toggle {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.msg-type-btn {
flex: 1;
padding: 10px;
border: 2px solid #e2e8f0;
background: #fff;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.msg-type-btn.active {
border-color: #6366f1;
background: #eef2ff;
color: #6366f1;
}
.modal-btns {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.modal-btn {
padding: 10px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
border: none;
}
.modal-btn.cancel { background: #f1f5f9; color: #64748b; }
.modal-btn.confirm { background: #6366f1; color: #fff; }
.modal-btn.confirm:hover { background: #4f46e5; }
/* ── 회원 상세 모달 ── */
.detail-modal {
max-width: 600px;
max-height: 85vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.detail-header {
background: linear-gradient(135deg, #059669, #10b981);
margin: -24px -24px 0;
padding: 24px;
border-radius: 16px 16px 0 0;
color: #fff;
}
.detail-name {
font-size: 22px;
font-weight: 700;
margin-bottom: 6px;
}
.detail-phone {
font-size: 15px;
opacity: 0.9;
}
.detail-balance {
margin-top: 12px;
padding: 12px 16px;
background: rgba(255,255,255,0.15);
border-radius: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.detail-balance-label {
font-size: 13px;
opacity: 0.9;
}
.detail-balance-value {
font-size: 24px;
font-weight: 700;
}
.detail-tabs {
display: flex;
border-bottom: 2px solid #e2e8f0;
margin: 20px -24px 0;
padding: 0 24px;
}
.detail-tab {
padding: 12px 20px;
font-size: 14px;
font-weight: 600;
color: #64748b;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
transition: all 0.2s;
}
.detail-tab:hover { color: #10b981; }
.detail-tab.active {
color: #10b981;
border-bottom-color: #10b981;
}
.detail-content {
flex: 1;
overflow-y: auto;
padding: 20px 0;
max-height: 400px;
}
.detail-empty {
text-align: center;
padding: 40px;
color: #94a3b8;
}
.detail-loading {
text-align: center;
padding: 40px;
color: #64748b;
}
/* 거래 카드 */
.tx-card {
background: #f8fafc;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
border-left: 4px solid #10b981;
}
.tx-card.negative { border-left-color: #f59e0b; }
.tx-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
}
.tx-date {
font-size: 12px;
color: #64748b;
}
.tx-points {
font-size: 18px;
font-weight: 700;
color: #10b981;
}
.tx-points.negative { color: #f59e0b; }
.tx-desc {
font-size: 13px;
color: #475569;
}
.tx-items {
margin-top: 10px;
padding-top: 10px;
border-top: 1px dashed #e2e8f0;
}
.tx-item {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #64748b;
padding: 4px 0;
}
.tx-item-name {
flex: 1;
}
.tx-item-qty {
color: #94a3b8;
margin: 0 12px;
}
.tx-item-price {
font-weight: 500;
color: #475569;
}
/* 구매 카드 */
.purchase-card {
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
}
.purchase-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.purchase-date {
font-size: 13px;
color: #64748b;
}
.purchase-total {
font-size: 16px;
font-weight: 700;
color: #1e293b;
}
.purchase-items {
border-top: 1px solid #f1f5f9;
padding-top: 12px;
}
.purchase-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
font-size: 13px;
}
.purchase-item-name {
flex: 1;
color: #334155;
}
.purchase-item-qty {
color: #94a3b8;
margin: 0 16px;
font-size: 12px;
}
.purchase-item-price {
color: #64748b;
font-weight: 500;
}
.detail-footer {
padding-top: 16px;
border-top: 1px solid #e2e8f0;
display: flex;
gap: 12px;
justify-content: flex-end;
}
/* ── 반응형 ── */
@media (max-width: 768px) {
.search-box { flex-direction: column; }
.table-wrap { overflow-x: auto; }
table { min-width: 700px; }
}
</style>
</head>
<body>
<div class="header">
<div class="header-nav">
<a href="/admin">← 관리자 홈</a>
<div>
<a href="/admin/ai-crm" style="margin-right: 16px;">AI 업셀링</a>
<a href="/admin/alimtalk">알림톡</a>
</div>
</div>
<h1>👥 회원 검색</h1>
<p>팜IT3000 회원 검색 · 알림톡/SMS 발송</p>
</div>
<div class="content">
<!-- 검색 -->
<div class="search-section">
<div class="search-box">
<input type="text" class="search-input" id="searchInput"
placeholder="이름 또는 전화번호로 검색..."
onkeypress="if(event.key==='Enter')searchMembers()">
<button class="search-btn" onclick="searchMembers()">🔍 검색</button>
</div>
<div class="search-hint">
이름(예: 홍길동) 또는 전화번호(예: 01012345678) 입력
</div>
</div>
<!-- 결과 헤더 -->
<div class="result-header" id="resultHeader" style="display:none;">
<div class="result-count">
검색 결과: <strong id="resultNum">0</strong>
</div>
<button class="send-selected-btn" id="sendSelectedBtn" onclick="openBulkSendModal()">
📨 선택 발송 (<span id="selectedCount">0</span>명)
</button>
</div>
<!-- 테이블 -->
<div class="table-wrap">
<table>
<thead>
<tr>
<th><input type="checkbox" class="checkbox" id="selectAll" onchange="toggleSelectAll()"></th>
<th>이름</th>
<th>전화번호</th>
<th>메모</th>
<th>상태</th>
<th>액션</th>
</tr>
</thead>
<tbody id="membersTableBody">
<tr>
<td colspan="6" class="empty-state">
<div class="icon">👥</div>
<p>이름 또는 전화번호로 회원을 검색하세요</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 발송 모달 -->
<div class="modal-overlay" id="sendModal" onclick="if(event.target===this)closeSendModal()">
<div class="modal-box">
<div class="modal-title">📨 메시지 발송</div>
<div class="modal-recipient" id="modalRecipient">
수신자: <strong>홍길동</strong> (010-1234-5678)
</div>
<div class="msg-type-toggle">
<button class="msg-type-btn active" data-type="sms" onclick="setMsgType('sms')">📱 SMS</button>
<button class="msg-type-btn" data-type="alimtalk" onclick="setMsgType('alimtalk')">💬 알림톡</button>
</div>
<textarea class="modal-textarea" id="messageInput" placeholder="메시지를 입력하세요..." oninput="updateCharCount()"></textarea>
<div class="char-count"><span id="charCount">0</span>/90자 (SMS 기준)</div>
<div class="modal-btns">
<button class="modal-btn cancel" onclick="closeSendModal()">취소</button>
<button class="modal-btn confirm" onclick="sendMessage()" id="sendBtn">발송</button>
</div>
</div>
</div>
<!-- 회원 상세 모달 -->
<div class="modal-overlay" id="detailModal" onclick="if(event.target===this)closeDetailModal()">
<div class="modal-box detail-modal">
<div class="detail-header">
<div class="detail-name" id="detailName">-</div>
<div class="detail-phone" id="detailPhone">-</div>
<div class="detail-balance">
<span class="detail-balance-label">💰 적립 포인트</span>
<span class="detail-balance-value" id="detailBalance">0P</span>
</div>
</div>
<div class="detail-tabs">
<div class="detail-tab active" data-tab="mileage" onclick="switchDetailTab('mileage')">📊 적립 내역</div>
<div class="detail-tab" data-tab="purchase" onclick="switchDetailTab('purchase')">🛒 구매 이력</div>
</div>
<div class="detail-content" id="detailContent">
<div class="detail-loading">데이터를 불러오는 중...</div>
</div>
<div class="detail-footer">
<button class="modal-btn cancel" onclick="closeDetailModal()">닫기</button>
<button class="modal-btn confirm" onclick="openSendFromDetail()">📨 메시지 발송</button>
</div>
</div>
</div>
<script>
let membersData = [];
let selectedMembers = new Set();
let currentMsgType = 'sms';
let sendTargets = [];
function searchMembers() {
const search = document.getElementById('searchInput').value.trim();
if (!search) {
alert('검색어를 입력하세요');
return;
}
if (search.length < 2) {
alert('2글자 이상 입력하세요');
return;
}
const tbody = document.getElementById('membersTableBody');
tbody.innerHTML = '<tr><td colspan="6" class="empty-state"><p>검색 중...</p></td></tr>';
fetch(`/api/members/search?q=${encodeURIComponent(search)}`)
.then(res => res.json())
.then(data => {
if (data.success) {
membersData = data.items;
selectedMembers.clear();
updateSelectedUI();
document.getElementById('resultHeader').style.display = 'flex';
document.getElementById('resultNum').textContent = membersData.length;
renderTable();
} else {
tbody.innerHTML = `<tr><td colspan="6" class="empty-state"><p>오류: ${data.error}</p></td></tr>`;
}
})
.catch(err => {
tbody.innerHTML = '<tr><td colspan="6" class="empty-state"><p>검색 실패</p></td></tr>';
});
}
function formatPhone(phone) {
if (!phone) return '-';
const p = phone.replace(/[^0-9]/g, '');
if (p.length === 11) {
return `${p.slice(0,3)}-${p.slice(3,7)}-${p.slice(7)}`;
} else if (p.length === 10) {
return `${p.slice(0,3)}-${p.slice(3,6)}-${p.slice(6)}`;
}
return phone;
}
function renderTable() {
const tbody = document.getElementById('membersTableBody');
if (membersData.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="empty-state"><div class="icon">📭</div><p>검색 결과가 없습니다</p></td></tr>';
return;
}
tbody.innerHTML = membersData.map((m, idx) => `
<tr class="${selectedMembers.has(idx) ? 'selected' : ''}" data-idx="${idx}">
<td><input type="checkbox" class="checkbox" ${selectedMembers.has(idx) ? 'checked' : ''} onchange="toggleSelect(${idx})"></td>
<td class="member-name">${escapeHtml(m.name)}</td>
<td class="member-phone">${formatPhone(m.phone)}</td>
<td class="member-memo" title="${escapeHtml(m.memo)}">${escapeHtml(m.memo) || '-'}</td>
<td>${m.sms_stop ? '<span class="sms-stop">수신거부</span>' : '<span style="color:#10b981;">정상</span>'}</td>
<td>
<button class="btn-detail" onclick="viewDetail(${idx})">상세</button>
<button class="btn-send" onclick="openSendModal(${idx})" ${m.sms_stop ? 'disabled style="opacity:0.5"' : ''}>발송</button>
</td>
</tr>
`).join('');
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]));
}
function toggleSelect(idx) {
if (selectedMembers.has(idx)) {
selectedMembers.delete(idx);
} else {
selectedMembers.add(idx);
}
updateSelectedUI();
renderTable();
}
function toggleSelectAll() {
const allChecked = document.getElementById('selectAll').checked;
if (allChecked) {
membersData.forEach((m, idx) => {
if (!m.sms_stop) selectedMembers.add(idx);
});
} else {
selectedMembers.clear();
}
updateSelectedUI();
renderTable();
}
function updateSelectedUI() {
const count = selectedMembers.size;
document.getElementById('selectedCount').textContent = count;
const btn = document.getElementById('sendSelectedBtn');
btn.classList.toggle('active', count > 0);
}
function openSendModal(idx) {
const m = membersData[idx];
sendTargets = [m];
document.getElementById('modalRecipient').innerHTML =
`수신자: <strong>${escapeHtml(m.name)}</strong> (${formatPhone(m.phone)})`;
document.getElementById('messageInput').value = '';
updateCharCount();
document.getElementById('sendModal').classList.add('active');
}
function openBulkSendModal() {
if (selectedMembers.size === 0) return;
sendTargets = Array.from(selectedMembers).map(idx => membersData[idx]);
const names = sendTargets.slice(0, 3).map(m => m.name).join(', ');
const more = sendTargets.length > 3 ? `${sendTargets.length - 3}` : '';
document.getElementById('modalRecipient').innerHTML =
`수신자: <strong>${escapeHtml(names)}${more}</strong> (${sendTargets.length}명)`;
document.getElementById('messageInput').value = '';
updateCharCount();
document.getElementById('sendModal').classList.add('active');
}
function closeSendModal() {
document.getElementById('sendModal').classList.remove('active');
sendTargets = [];
}
function setMsgType(type) {
currentMsgType = type;
document.querySelectorAll('.msg-type-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.type === type);
});
}
function updateCharCount() {
const len = document.getElementById('messageInput').value.length;
document.getElementById('charCount').textContent = len;
}
function sendMessage() {
const message = document.getElementById('messageInput').value.trim();
if (!message) {
alert('메시지를 입력하세요');
return;
}
if (sendTargets.length === 0) {
alert('수신자가 없습니다');
return;
}
const btn = document.getElementById('sendBtn');
btn.textContent = '발송 중...';
btn.disabled = true;
const recipients = sendTargets.map(m => ({
cuscode: m.cuscode,
name: m.name,
phone: m.phone
}));
fetch('/api/message/send', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
recipients: recipients,
message: message,
type: currentMsgType
})
})
.then(res => res.json())
.then(data => {
if (data.success) {
alert(`${data.message}`);
closeSendModal();
} else {
alert('❌ 발송 실패: ' + (data.error || '알 수 없는 오류'));
}
})
.catch(err => {
alert('❌ 오류: ' + err.message);
})
.finally(() => {
btn.textContent = '발송';
btn.disabled = false;
});
}
// ── 회원 상세 모달 ──
let detailData = null;
let currentDetailTab = 'mileage';
let currentDetailMember = null;
function viewDetail(idx) {
currentDetailMember = membersData[idx];
const phone = currentDetailMember.phone.replace(/-/g, '').replace(/ /g, '');
// 모달 열기
document.getElementById('detailModal').classList.add('active');
document.getElementById('detailName').textContent = currentDetailMember.name || '이름 없음';
document.getElementById('detailPhone').textContent = formatPhone(currentDetailMember.phone);
document.getElementById('detailBalance').textContent = '로딩...';
document.getElementById('detailContent').innerHTML = '<div class="detail-loading">데이터를 불러오는 중...</div>';
// 데이터 로드
fetch(`/api/members/history/${phone}`)
.then(res => res.json())
.then(data => {
if (data.success) {
detailData = data;
// 잔액 표시
if (data.mileage) {
document.getElementById('detailBalance').textContent =
data.mileage.balance.toLocaleString() + 'P';
} else {
document.getElementById('detailBalance').textContent = '미가입';
}
// 탭 콘텐츠 렌더링
renderDetailTab();
} else {
document.getElementById('detailContent').innerHTML =
`<div class="detail-empty">데이터 조회 실패: ${data.error}</div>`;
}
})
.catch(err => {
document.getElementById('detailContent').innerHTML =
`<div class="detail-empty">오류: ${err.message}</div>`;
});
}
function closeDetailModal() {
document.getElementById('detailModal').classList.remove('active');
detailData = null;
currentDetailMember = null;
}
function switchDetailTab(tab) {
currentDetailTab = tab;
document.querySelectorAll('.detail-tab').forEach(t => {
t.classList.toggle('active', t.dataset.tab === tab);
});
renderDetailTab();
}
function renderDetailTab() {
const content = document.getElementById('detailContent');
if (!detailData) {
content.innerHTML = '<div class="detail-empty">데이터가 없습니다</div>';
return;
}
if (currentDetailTab === 'mileage') {
renderMileageTab(content);
} else {
renderPurchaseTab(content);
}
}
function renderMileageTab(container) {
if (!detailData.mileage || !detailData.mileage.transactions || detailData.mileage.transactions.length === 0) {
container.innerHTML = '<div class="detail-empty">📭 적립 내역이 없습니다</div>';
return;
}
const txs = detailData.mileage.transactions;
container.innerHTML = txs.map(tx => {
const isPositive = tx.points > 0;
const date = tx.created_at ? new Date(tx.created_at).toLocaleString('ko-KR', {
month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'
}) : '';
// 품목 렌더링
let itemsHtml = '';
if (tx.items && tx.items.length > 0) {
itemsHtml = `
<div class="tx-items">
${tx.items.map(item => `
<div class="tx-item">
<span class="tx-item-name">${escapeHtml(item.name)}</span>
<span class="tx-item-qty">x${item.quantity}</span>
<span class="tx-item-price">${item.price.toLocaleString()}원</span>
</div>
`).join('')}
</div>
`;
}
// 금액 표시
const amountText = tx.total_amount ? ` (${tx.total_amount.toLocaleString()}원 구매)` : '';
return `
<div class="tx-card ${isPositive ? '' : 'negative'}">
<div class="tx-header">
<div class="tx-date">📅 ${date}</div>
<div class="tx-points ${isPositive ? '' : 'negative'}">
${isPositive ? '+' : ''}${tx.points.toLocaleString()}P
</div>
</div>
<div class="tx-desc">${escapeHtml(tx.description || tx.reason || '')}${amountText}</div>
${itemsHtml}
</div>
`;
}).join('');
}
function renderPurchaseTab(container) {
// 적립 내역 중 품목이 있는 것들만 추출
if (!detailData.mileage || !detailData.mileage.transactions) {
container.innerHTML = '<div class="detail-empty">📭 구매 이력이 없습니다</div>';
return;
}
const purchases = detailData.mileage.transactions.filter(tx =>
tx.items && tx.items.length > 0 && tx.points > 0
);
if (purchases.length === 0) {
container.innerHTML = '<div class="detail-empty">📭 QR 적립된 구매 이력이 없습니다</div>';
return;
}
container.innerHTML = purchases.map(p => {
// 날짜 포맷
const date = p.created_at ? new Date(p.created_at).toLocaleString('ko-KR', {
year: 'numeric', month: 'short', day: 'numeric'
}) : '';
// 품목 렌더링
const itemsHtml = (p.items || []).map(item => `
<div class="purchase-item">
<span class="purchase-item-name">${escapeHtml(item.name)}</span>
<span class="purchase-item-qty">x${item.quantity}</span>
<span class="purchase-item-price">${item.price.toLocaleString()}원</span>
</div>
`).join('');
return `
<div class="purchase-card">
<div class="purchase-header">
<span class="purchase-date">📅 ${date}</span>
<span class="purchase-total">${(p.total_amount || 0).toLocaleString()}원</span>
</div>
<div style="font-size:12px;color:#10b981;margin-bottom:8px;">+${p.points.toLocaleString()}P 적립</div>
${p.items && p.items.length > 0 ? `
<div class="purchase-items">${itemsHtml}</div>
` : ''}
</div>
`;
}).join('');
}
function openSendFromDetail() {
if (!currentDetailMember) return;
closeDetailModal();
// 발송 모달 열기
sendTargets = [currentDetailMember];
document.getElementById('modalRecipient').innerHTML =
`수신자: <strong>${escapeHtml(currentDetailMember.name)}</strong> (${formatPhone(currentDetailMember.phone)})`;
document.getElementById('messageInput').value = '';
updateCharCount();
document.getElementById('sendModal').classList.add('active');
}
// 페이지 로드 시 검색창 포커스
document.getElementById('searchInput').focus();
</script>
</body>
</html>