diff --git a/backend/app.py b/backend/app.py index 183f675..1fa97c5 100644 --- a/backend/app.py +++ b/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/') +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 # ============================================================================= diff --git a/backend/templates/admin.html b/backend/templates/admin.html index 3bc63bf..abc28b6 100644 --- a/backend/templates/admin.html +++ b/backend/templates/admin.html @@ -400,6 +400,7 @@
🔍 제품검색 + 👥 회원검색 📋 판매조회 🧾 판매내역 🤖 AI CRM diff --git a/backend/templates/admin_members.html b/backend/templates/admin_members.html new file mode 100644 index 0000000..928cc2c --- /dev/null +++ b/backend/templates/admin_members.html @@ -0,0 +1,619 @@ + + + + + + 회원 검색 - 청춘약국 CRM + + + + + + +
+ +

👥 회원 검색

+

팜IT3000 회원 검색 · 알림톡/SMS 발송

+
+ +
+ +
+ +
+ 이름(예: 홍길동) 또는 전화번호(예: 01012345678) 입력 +
+
+ + + + + +
+ + + + + + + + + + + + + + + + +
이름전화번호메모상태액션
+
👥
+

이름 또는 전화번호로 회원을 검색하세요

+
+
+
+ + + + + + +