- 마일리지 적립 시 저장된 transaction_id로 SALE_SUB 조회 - 적립 내역에 구매 품목 표시 (품명, 수량, 가격) - 구매 이력 탭: QR 적립된 구매만 품목과 함께 표시 - 기존 전화번호→고객코드 매핑 로직 제거 (불필요)
1007 lines
33 KiB
HTML
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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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>
|