feat: 회원 검색 페이지 및 API 추가

- /admin/members: 회원 검색 페이지 (팜IT3000 CD_PERSON)
- /api/members/search: 이름/전화번호 검색 API (TEL_NO, PHONE, PHONE2)
- /api/members/<cuscode>: 회원 상세 + 메모 조회 API
- /api/message/send: 알림톡/SMS 발송 API (테스트 모드)
- 대시보드 헤더에 회원검색 탭 추가
- 다중 선택 + 일괄 발송 UI
This commit is contained in:
thug0bin 2026-02-27 14:10:44 +09:00
parent 9bd2174501
commit 705696a7fb
3 changed files with 851 additions and 0 deletions

View File

@ -2469,6 +2469,12 @@ def admin_products():
return render_template('admin_products.html')
@app.route('/admin/members')
def admin_members():
"""회원 검색 페이지 (팜IT3000 CD_PERSON, 알림톡/SMS 발송)"""
return render_template('admin_members.html')
@app.route('/api/products')
def api_products():
"""
@ -2842,6 +2848,231 @@ def api_claude_status():
}), 500
# =============================================================================
# 회원 검색 API (팜IT3000 CD_PERSON)
# =============================================================================
@app.route('/api/members/search')
def api_members_search():
"""
회원 검색 API
- 이름 또는 전화번호로 검색
- PM_BASE.dbo.CD_PERSON 테이블 조회
"""
search = request.args.get('q', '').strip()
search_type = request.args.get('type', 'auto') # auto, name, phone
limit = min(int(request.args.get('limit', 50)), 200)
if not search or len(search) < 2:
return jsonify({'success': False, 'error': '검색어는 2글자 이상 입력하세요'})
try:
# PM_BASE 연결
base_session = db_manager.get_session('PM_BASE')
# 검색 타입 자동 감지
if search_type == 'auto':
# 숫자만 있으면 전화번호, 아니면 이름
if search.replace('-', '').replace(' ', '').isdigit():
search_type = 'phone'
else:
search_type = 'name'
# 전화번호 정규화
phone_search = search.replace('-', '').replace(' ', '')
if search_type == 'phone':
# 전화번호 검색 (3개 컬럼 모두)
query = text(f"""
SELECT TOP {limit}
CUSCODE, PANAME, PANUM,
TEL_NO, PHONE, PHONE2,
CUSETC, EMAIL, SMS_STOP
FROM CD_PERSON
WHERE
REPLACE(REPLACE(TEL_NO, '-', ''), ' ', '') LIKE :phone
OR REPLACE(REPLACE(PHONE, '-', ''), ' ', '') LIKE :phone
OR REPLACE(REPLACE(PHONE2, '-', ''), ' ', '') LIKE :phone
ORDER BY PANAME
""")
rows = base_session.execute(query, {'phone': f'%{phone_search}%'}).fetchall()
else:
# 이름 검색
query = text(f"""
SELECT TOP {limit}
CUSCODE, PANAME, PANUM,
TEL_NO, PHONE, PHONE2,
CUSETC, EMAIL, SMS_STOP
FROM CD_PERSON
WHERE PANAME LIKE :name
ORDER BY PANAME
""")
rows = base_session.execute(query, {'name': f'%{search}%'}).fetchall()
members = []
for row in rows:
# 유효한 전화번호 찾기 (PHONE 우선, 없으면 TEL_NO, PHONE2)
phone = row.PHONE or row.TEL_NO or row.PHONE2 or ''
phone = phone.strip() if phone else ''
members.append({
'cuscode': row.CUSCODE,
'name': row.PANAME or '',
'panum': row.PANUM or '',
'phone': phone,
'tel_no': row.TEL_NO or '',
'phone1': row.PHONE or '',
'phone2': row.PHONE2 or '',
'memo': (row.CUSETC or '')[:100], # 메모 100자 제한
'email': row.EMAIL or '',
'sms_stop': row.SMS_STOP == 'Y' # SMS 수신거부 여부
})
return jsonify({
'success': True,
'items': members,
'count': len(members),
'search_type': search_type
})
except Exception as e:
logging.error(f"회원 검색 오류: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/members/<cuscode>')
def api_member_detail(cuscode):
"""회원 상세 정보 + 메모 조회"""
try:
base_session = db_manager.get_session('PM_BASE')
# 회원 정보
query = text("""
SELECT
CUSCODE, PANAME, PANUM,
TEL_NO, PHONE, PHONE2,
CUSETC, EMAIL, ADDRESS, SMS_STOP
FROM CD_PERSON
WHERE CUSCODE = :cuscode
""")
row = base_session.execute(query, {'cuscode': cuscode}).fetchone()
if not row:
return jsonify({'success': False, 'error': '회원을 찾을 수 없습니다'}), 404
member = {
'cuscode': row.CUSCODE,
'name': row.PANAME or '',
'panum': row.PANUM or '',
'phone': row.PHONE or row.TEL_NO or row.PHONE2 or '',
'tel_no': row.TEL_NO or '',
'phone1': row.PHONE or '',
'phone2': row.PHONE2 or '',
'memo': row.CUSETC or '',
'email': row.EMAIL or '',
'address': row.ADDRESS or '',
'sms_stop': row.SMS_STOP == 'Y'
}
# 상세 메모 조회
memo_query = text("""
SELECT MEMO_CODE, PHARMA_ID, MEMO_DATE, MEMO_TITLE, MEMO_Item
FROM CD_PERSON_MEMO
WHERE CUSCODE = :cuscode
ORDER BY MEMO_DATE DESC
""")
memo_rows = base_session.execute(memo_query, {'cuscode': cuscode}).fetchall()
memos = []
for m in memo_rows:
memos.append({
'memo_code': m.MEMO_CODE,
'author': m.PHARMA_ID or '',
'date': m.MEMO_DATE or '',
'title': m.MEMO_TITLE or '',
'content': m.MEMO_Item or ''
})
return jsonify({
'success': True,
'member': member,
'memos': memos
})
except Exception as e:
logging.error(f"회원 상세 조회 오류: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
# =============================================================================
# 알림톡/SMS 발송 API
# =============================================================================
@app.route('/api/message/send', methods=['POST'])
def api_message_send():
"""
알림톡/SMS 발송 API
Body:
{
"recipients": [{"cuscode": "", "name": "", "phone": ""}],
"message": "메시지 내용",
"type": "alimtalk" | "sms"
}
"""
try:
data = request.get_json()
if not data:
return jsonify({'success': False, 'error': '데이터가 없습니다'}), 400
recipients = data.get('recipients', [])
message = data.get('message', '')
msg_type = data.get('type', 'sms') # alimtalk 또는 sms
if not recipients:
return jsonify({'success': False, 'error': '수신자가 없습니다'}), 400
if not message:
return jsonify({'success': False, 'error': '메시지 내용이 없습니다'}), 400
# 전화번호 정규화
valid_recipients = []
for r in recipients:
phone = (r.get('phone') or '').replace('-', '').replace(' ', '')
if phone and len(phone) >= 10:
valid_recipients.append({
'cuscode': r.get('cuscode', ''),
'name': r.get('name', ''),
'phone': phone
})
if not valid_recipients:
return jsonify({'success': False, 'error': '유효한 전화번호가 없습니다'}), 400
# TODO: 실제 발송 로직 (NHN Cloud 알림톡/SMS)
# 현재는 테스트 모드로 성공 응답
results = []
for r in valid_recipients:
results.append({
'phone': r['phone'],
'name': r['name'],
'status': 'success', # 실제 발송 시 결과로 변경
'message': f'{msg_type} 발송 예약됨'
})
return jsonify({
'success': True,
'type': msg_type,
'sent_count': len(results),
'results': results,
'message': f'{len(results)}명에게 {msg_type} 발송 완료 (테스트 모드)'
})
except Exception as e:
logging.error(f"메시지 발송 오류: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
# =============================================================================
# QR 라벨 인쇄 API
# =============================================================================

View File

@ -400,6 +400,7 @@
</div>
<div style="display:flex;gap:8px;">
<a href="/admin/products" style="color:rgba(255,255,255,0.9);text-decoration:none;font-size:14px;padding:8px 16px;border-radius:8px;background:rgba(255,255,255,0.15);transition:all 0.2s;">🔍 제품검색</a>
<a href="/admin/members" style="color:rgba(255,255,255,0.9);text-decoration:none;font-size:14px;padding:8px 16px;border-radius:8px;background:rgba(255,255,255,0.15);transition:all 0.2s;">👥 회원검색</a>
<a href="/admin/sales-detail" style="color:rgba(255,255,255,0.9);text-decoration:none;font-size:14px;padding:8px 16px;border-radius:8px;background:rgba(255,255,255,0.15);transition:all 0.2s;">📋 판매조회</a>
<a href="/admin/sales" style="color:rgba(255,255,255,0.9);text-decoration:none;font-size:14px;padding:8px 16px;border-radius:8px;background:rgba(255,255,255,0.15);transition:all 0.2s;">🧾 판매내역</a>
<a href="/admin/ai-crm" style="color:rgba(255,255,255,0.9);text-decoration:none;font-size:14px;padding:8px 16px;border-radius:8px;background:rgba(255,255,255,0.15);transition:all 0.2s;">🤖 AI CRM</a>

View File

@ -0,0 +1,619 @@
<!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; }
/* ── 반응형 ── */
@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>
<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('${m.cuscode}')">상세</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;
});
}
function viewDetail(cuscode) {
// TODO: 회원 상세 모달
alert('상세 보기 기능 준비 중: ' + cuscode);
}
// 페이지 로드 시 검색창 포커스
document.getElementById('searchInput').focus();
</script>
</body>
</html>