pharmacy-pos-qr-system/backend/templates/admin_members.html

1282 lines
49 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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 class="detail-tab" data-tab="prescription" onclick="switchDetailTab('prescription')">💊 조제</div>
<div class="detail-tab" data-tab="interest" onclick="switchDetailTab('interest')">💝 관심</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];
// 전화번호 우선순위: phone > phone1 > tel_no > phone2
let phone = (currentDetailMember.phone || '').replace(/-/g, '').replace(/ /g, '');
if (!phone) phone = (currentDetailMember.phone1 || '').replace(/-/g, '').replace(/ /g, '');
if (!phone) phone = (currentDetailMember.tel_no || '').replace(/-/g, '').replace(/ /g, '');
if (!phone) phone = (currentDetailMember.phone2 || '').replace(/-/g, '').replace(/ /g, '');
const displayPhone = currentDetailMember.phone || currentDetailMember.phone1 || currentDetailMember.tel_no || currentDetailMember.phone2 || '';
// 모달 열기
document.getElementById('detailModal').classList.add('active');
document.getElementById('detailName').textContent = currentDetailMember.name || '이름 없음';
document.getElementById('detailPhone').textContent = formatPhone(displayPhone) || '전화번호 없음';
document.getElementById('detailBalance').textContent = '로딩...';
document.getElementById('detailContent').innerHTML = '<div class="detail-loading">데이터를 불러오는 중...</div>';
// 전화번호 없으면 바로 안내
if (!phone) {
document.getElementById('detailBalance').textContent = '-';
document.getElementById('detailContent').innerHTML =
'<div class="detail-empty">📵 전화번호가 등록되지 않은 회원입니다<br><small style="color:#94a3b8;">POS에 전화번호를 등록하면 조회 가능합니다</small></div>';
detailData = { mileage: null, purchases: [], prescriptions: [], interests: [] };
return;
}
// 데이터 로드
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 if (currentDetailTab === 'purchase') {
renderPurchaseTab(content);
} else if (currentDetailTab === 'prescription') {
renderPrescriptionTab(content);
} else if (currentDetailTab === 'interest') {
renderInterestTab(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;
// DB는 UTC로 저장 → 'Z' 붙여서 UTC로 해석 → KST로 표시
const date = tx.created_at ? new Date(tx.created_at + 'Z').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) {
// POS 전체 구매 이력 (고객코드 기준)
if (!detailData.purchases || detailData.purchases.length === 0) {
if (!detailData.pos_customer) {
container.innerHTML = '<div class="detail-empty">📭 POS 회원으로 등록되지 않았습니다<br><small style="color:#94a3b8;">전화번호가 POS에 등록되면 구매 이력이 표시됩니다</small></div>';
} else {
container.innerHTML = '<div class="detail-empty">📭 구매 이력이 없습니다</div>';
}
return;
}
container.innerHTML = detailData.purchases.map(p => {
// 날짜 포맷
const dateStr = p.date || '';
let formattedDate = dateStr;
if (dateStr.length === 8) {
formattedDate = `${dateStr.slice(0,4)}.${dateStr.slice(4,6)}.${dateStr.slice(6,8)}`;
}
// 품목 렌더링
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">📅 ${formattedDate}</span>
<span class="purchase-total">${(p.total || 0).toLocaleString()}원</span>
</div>
${p.items && p.items.length > 0 ? `
<div class="purchase-items">${itemsHtml}</div>
` : ''}
</div>
`;
}).join('');
}
function renderPrescriptionTab(container) {
// 조제 이력 (고객코드 기준)
if (!detailData.prescriptions || detailData.prescriptions.length === 0) {
if (!detailData.pos_customer) {
container.innerHTML = '<div class="detail-empty">📭 POS 회원으로 등록되지 않았습니다<br><small style="color:#94a3b8;">전화번호가 POS에 등록되면 조제 이력이 표시됩니다</small></div>';
} else {
container.innerHTML = '<div class="detail-empty">📭 조제 이력이 없습니다</div>';
}
return;
}
container.innerHTML = detailData.prescriptions.map(rx => {
// 날짜 포맷
const dateStr = rx.date || '';
let formattedDate = dateStr;
if (dateStr.length === 8) {
formattedDate = `${dateStr.slice(0,4)}.${dateStr.slice(4,6)}.${dateStr.slice(6,8)}`;
}
// 처방 품목 렌더링 (투약량 x 횟수 x 일수)
const itemsHtml = (rx.items || []).map(item => {
const dosage = item.quantity || 1; // 1회 투약량
const freq = item.times_per_day || 1; // 1일 투약횟수
const days = item.days || 0; // 투약일수
return `
<div class="purchase-item">
<span class="purchase-item-name">${escapeHtml(item.name)}</span>
<span class="purchase-item-qty" style="min-width:100px;text-align:right;color:#6366f1;">
${dosage}× ${freq}× ${days}
</span>
</div>
`;
}).join('');
// 약품 코드 배열 (상호작용 체크용)
const drugCodes = (rx.items || []).map(item => item.drug_code).filter(c => c);
const drugCodesJson = JSON.stringify(drugCodes);
return `
<div class="purchase-card" style="border-left: 3px solid #6366f1;">
<div class="purchase-header">
<span class="purchase-date">📅 ${formattedDate}</span>
<span style="font-size:12px;color:#6366f1;">${rx.total_days || ''}일분</span>
</div>
<div style="font-size:12px;color:#64748b;margin-bottom:8px;">
🏥 ${escapeHtml(rx.hospital || '')} · ${escapeHtml(rx.doctor || '')}
</div>
${rx.items && rx.items.length > 0 ? `
<div class="purchase-items">${itemsHtml}</div>
` : ''}
${drugCodes.length >= 2 ? `
<div style="margin-top:10px;text-align:right;">
<button onclick='checkDrugInteraction(${drugCodesJson}, "${rx.pre_serial || ""}")'
style="background:linear-gradient(135deg,#8b5cf6,#6366f1);color:#fff;border:none;padding:8px 14px;border-radius:8px;font-size:12px;cursor:pointer;display:inline-flex;align-items:center;gap:6px;">
🔬 AI 상호작용 체크
</button>
</div>
` : ''}
</div>
`;
}).join('');
}
function renderInterestTab(container) {
// AI 업셀링에서 '관심있어요' 표시한 상품
if (!detailData.interests || detailData.interests.length === 0) {
container.innerHTML = '<div class="detail-empty">💝 관심 상품이 없습니다<br><small style="color:#94a3b8;">마일리지 적립 시 AI 추천에서 "관심있어요"를 누르면<br>여기에 표시됩니다</small></div>';
return;
}
container.innerHTML = detailData.interests.map(item => {
// 날짜 포맷 (DB는 UTC → KST 변환)
const date = item.created_at ? new Date(item.created_at + 'Z').toLocaleString('ko-KR', {
month: 'short', day: 'numeric'
}) : '';
// 트리거 상품 (JSON 파싱)
let triggerText = '';
try {
const triggers = JSON.parse(item.trigger_products || '[]');
if (triggers.length > 0) {
triggerText = triggers.join(', ');
}
} catch(e) {}
return `
<div class="purchase-card" style="border-left: 3px solid #ec4899;">
<div class="purchase-header">
<span style="font-size:15px;font-weight:700;color:#ec4899;">💝 ${escapeHtml(item.product)}</span>
<span class="purchase-date">${date}</span>
</div>
<div style="font-size:13px;color:#475569;margin:8px 0;line-height:1.5;">
${escapeHtml(item.message || '')}
</div>
${triggerText ? `
<div style="font-size:11px;color:#94a3b8;margin-top:8px;">
🛒 구매 상품: ${escapeHtml(triggerText)}
</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();
// ═══════════════════════════════════════════════════
// KIMS 약물 상호작용 체크
// ═══════════════════════════════════════════════════
async function checkDrugInteraction(drugCodes, preSerial) {
// 로딩 모달 표시
showInteractionModal('loading');
try {
const response = await fetch('/api/kims/interaction-check', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
drug_codes: drugCodes,
pre_serial: preSerial
})
});
const data = await response.json();
if (data.success) {
showInteractionModal('result', data);
} else {
showInteractionModal('error', data.error || '알 수 없는 오류');
}
} catch (err) {
showInteractionModal('error', '서버 연결 실패: ' + err.message);
}
}
function showInteractionModal(type, data) {
let modal = document.getElementById('interactionModal');
if (!modal) {
// 모달 생성
modal = document.createElement('div');
modal.id = 'interactionModal';
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:9999;';
modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
document.body.appendChild(modal);
}
let content = '';
if (type === 'loading') {
content = `
<div style="background:#fff;border-radius:16px;padding:40px;text-align:center;max-width:400px;">
<div style="font-size:48px;margin-bottom:16px;">🔬</div>
<div style="font-size:18px;font-weight:600;color:#334155;">상호작용 분석 중...</div>
<div style="font-size:14px;color:#64748b;margin-top:8px;">KIMS 데이터베이스 조회 중</div>
</div>
`;
} else if (type === 'error') {
content = `
<div style="background:#fff;border-radius:16px;padding:30px;max-width:400px;">
<div style="font-size:40px;text-align:center;margin-bottom:16px;">⚠️</div>
<div style="font-size:16px;font-weight:600;color:#dc2626;text-align:center;">분석 실패</div>
<div style="font-size:14px;color:#64748b;margin-top:12px;text-align:center;">${escapeHtml(data)}</div>
<div style="text-align:center;margin-top:20px;">
<button onclick="document.getElementById('interactionModal').remove()"
style="background:#6366f1;color:#fff;border:none;padding:10px 24px;border-radius:8px;cursor:pointer;">
닫기
</button>
</div>
</div>
`;
} else if (type === 'result') {
const interactions = data.interactions || [];
const drugsChecked = data.drugs_checked || [];
// 약품 목록 (상호작용 있는 약품은 빨간색/주황색 배경)
const drugsHtml = drugsChecked.map(d => {
const hasInteraction = d.has_interaction;
const bgColor = hasInteraction ? '#fef2f2' : '#f1f5f9'; // 연한 빨강 vs 회색
const borderColor = hasInteraction ? '#fca5a5' : '#e2e8f0';
const textColor = hasInteraction ? '#dc2626' : '#334155';
const icon = hasInteraction ? '⚠️ ' : '';
return `<span style="display:inline-block;background:${bgColor};border:1px solid ${borderColor};color:${textColor};padding:4px 8px;border-radius:4px;margin:2px;font-size:12px;">${icon}${escapeHtml(d.name.slice(0,20))}</span>`;
}).join('');
// 상호작용 목록
let interactionsHtml = '';
if (interactions.length === 0) {
interactionsHtml = `
<div style="text-align:center;padding:30px;">
<div style="font-size:48px;margin-bottom:12px;">✅</div>
<div style="font-size:16px;font-weight:600;color:#10b981;">상호작용 없음</div>
<div style="font-size:13px;color:#64748b;margin-top:8px;">
${data.total_pairs}개 약품 조합을 검사했습니다.<br>
주의가 필요한 상호작용이 발견되지 않았습니다.
</div>
</div>
`;
} else {
interactionsHtml = interactions.map(item => `
<div style="background:#fff;border:1px solid ${item.severity_color};border-radius:12px;padding:16px;margin-bottom:12px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<span style="font-weight:600;color:#334155;">
${escapeHtml(item.drug1_name?.slice(0,20) || '')}${escapeHtml(item.drug2_name?.slice(0,20) || '')}
</span>
<span style="background:${item.severity_color};color:#fff;padding:4px 10px;border-radius:12px;font-size:12px;font-weight:500;">
${item.severity_text}
</span>
</div>
${item.description ? `
<div style="font-size:13px;color:#475569;margin-bottom:8px;line-height:1.5;">
📋 ${escapeHtml(item.description)}
</div>
` : ''}
${item.management ? `
<div style="font-size:12px;color:#059669;background:#ecfdf5;padding:8px 12px;border-radius:6px;">
💡 ${escapeHtml(item.management)}
</div>
` : ''}
</div>
`).join('');
}
content = `
<div style="background:#f8fafc;border-radius:20px;max-width:500px;max-height:80vh;overflow:hidden;display:flex;flex-direction:column;">
<div style="background:linear-gradient(135deg,#8b5cf6,#6366f1);padding:20px 24px;color:#fff;">
<div style="font-size:18px;font-weight:700;display:flex;align-items:center;gap:10px;">
🔬 약물 상호작용 분석
</div>
<div style="font-size:13px;opacity:0.9;margin-top:6px;">
${drugsChecked.length}개 약품 · ${data.total_pairs}개 조합 검사
</div>
</div>
<div style="padding:16px 20px;border-bottom:1px solid #e2e8f0;">
<div style="font-size:12px;color:#64748b;margin-bottom:6px;">분석 약품</div>
${drugsHtml}
</div>
<div style="flex:1;overflow-y:auto;padding:16px 20px;">
${interactions.length > 0 ? `
<div style="font-size:13px;color:#dc2626;font-weight:600;margin-bottom:12px;">
⚠️ ${interactions.length}건의 상호작용 발견
</div>
` : ''}
${interactionsHtml}
</div>
<div style="padding:16px 20px;border-top:1px solid #e2e8f0;text-align:center;">
<button onclick="document.getElementById('interactionModal').remove()"
style="background:#6366f1;color:#fff;border:none;padding:12px 32px;border-radius:10px;font-size:14px;font-weight:600;cursor:pointer;">
닫기
</button>
</div>
</div>
`;
}
modal.innerHTML = content;
}
</script>
</body>
</html>