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:
231
backend/app.py
231
backend/app.py
@@ -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
|
||||
# =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user