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
# =============================================================================