feat: 관리자 페이지 사이드바 및 검색 기능 추가
- 왼쪽 사이드바 추가 (280px, 검색 UI 포함)
- 사용자 검색: 이름/전화번호/전화번호 뒷자리 검색
- 제품 검색: SQLite 적립자 기준으로 구매자 목록 표시
- 다중 매칭 시 선택 모달 표시
- 검색 결과 클릭 시 사용자 상세 모달 연동
- 모바일 반응형 (768px 이하 사이드바 숨김)
API 엔드포인트:
- GET /admin/search/user?q={검색어}&type={name|phone|phone_last}
- GET /admin/search/product?q={제품명}
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
158
backend/app.py
158
backend/app.py
@@ -595,6 +595,164 @@ def admin_user_detail(user_id):
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/admin/search/user')
|
||||
def admin_search_user():
|
||||
"""사용자 검색 (이름/전화번호/전화번호 뒷자리)"""
|
||||
query = request.args.get('q', '').strip()
|
||||
search_type = request.args.get('type', 'name') # 'name', 'phone', 'phone_last'
|
||||
|
||||
if not query:
|
||||
return jsonify({'success': False, 'message': '검색어를 입력하세요'}), 400
|
||||
|
||||
conn = db_manager.get_sqlite_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
if search_type == 'phone_last':
|
||||
# 전화번호 뒷자리 검색
|
||||
cursor.execute("""
|
||||
SELECT id, nickname, phone, mileage_balance
|
||||
FROM users
|
||||
WHERE phone LIKE ?
|
||||
ORDER BY created_at DESC
|
||||
""", (f'%{query}',))
|
||||
elif search_type == 'phone':
|
||||
# 전체 전화번호 검색
|
||||
cursor.execute("""
|
||||
SELECT id, nickname, phone, mileage_balance
|
||||
FROM users
|
||||
WHERE phone = ?
|
||||
""", (query,))
|
||||
else:
|
||||
# 이름 검색
|
||||
cursor.execute("""
|
||||
SELECT id, nickname, phone, mileage_balance
|
||||
FROM users
|
||||
WHERE nickname LIKE ?
|
||||
ORDER BY created_at DESC
|
||||
""", (f'%{query}%',))
|
||||
|
||||
results = cursor.fetchall()
|
||||
|
||||
if not results:
|
||||
return jsonify({'success': False, 'message': '검색 결과가 없습니다'}), 404
|
||||
|
||||
if len(results) == 1:
|
||||
# 단일 매칭 - user_id만 반환
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'multiple': False,
|
||||
'user_id': results[0]['id']
|
||||
})
|
||||
else:
|
||||
# 여러 명 매칭 - 선택 모달용 데이터 반환
|
||||
users = [{
|
||||
'id': row['id'],
|
||||
'name': row['nickname'],
|
||||
'phone': row['phone'],
|
||||
'balance': row['mileage_balance']
|
||||
} for row in results]
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'multiple': True,
|
||||
'users': users
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'검색 실패: {str(e)}'
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/admin/search/product')
|
||||
def admin_search_product():
|
||||
"""제품 검색 - 적립자 목록 반환 (SQLite 적립자 기준)"""
|
||||
query = request.args.get('q', '').strip()
|
||||
|
||||
if not query:
|
||||
return jsonify({'success': False, 'message': '검색어를 입력하세요'}), 400
|
||||
|
||||
conn = db_manager.get_sqlite_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# 1. MSSQL에서 제품명으로 거래번호 찾기
|
||||
session = db_manager.get_session('PM_PRES')
|
||||
|
||||
sale_items_query = text("""
|
||||
SELECT DISTINCT
|
||||
S.SL_NO_order,
|
||||
S.SL_NM_item,
|
||||
M.InsertTime
|
||||
FROM SALE_SUB S
|
||||
LEFT JOIN PM_DRUG.dbo.CD_GOODS G ON S.DrugCode = G.DrugCode
|
||||
LEFT JOIN SALE_MAIN M ON S.SL_NO_order = M.SL_NO_order
|
||||
WHERE G.GoodsName LIKE :product_name
|
||||
ORDER BY M.InsertTime DESC
|
||||
""")
|
||||
|
||||
sale_results = session.execute(sale_items_query, {
|
||||
'product_name': f'%{query}%'
|
||||
}).fetchall()
|
||||
|
||||
if not sale_results:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'results': []
|
||||
})
|
||||
|
||||
# 2. SQLite에서 적립된 거래만 필터링 (claimed_by_user_id IS NOT NULL)
|
||||
transaction_ids = [row.SL_NO_order for row in sale_results]
|
||||
placeholders = ','.join('?' * len(transaction_ids))
|
||||
|
||||
cursor.execute(f"""
|
||||
SELECT
|
||||
ct.transaction_id,
|
||||
ct.total_amount,
|
||||
ct.claimed_at,
|
||||
ct.claimed_by_user_id,
|
||||
u.nickname,
|
||||
u.phone
|
||||
FROM claim_tokens ct
|
||||
JOIN users u ON ct.claimed_by_user_id = u.id
|
||||
WHERE ct.transaction_id IN ({placeholders})
|
||||
AND ct.claimed_by_user_id IS NOT NULL
|
||||
ORDER BY ct.claimed_at DESC
|
||||
LIMIT 50
|
||||
""", transaction_ids)
|
||||
|
||||
claimed_results = cursor.fetchall()
|
||||
|
||||
# 3. 결과 조합
|
||||
results = []
|
||||
for claim_row in claimed_results:
|
||||
# 해당 거래의 MSSQL 정보 찾기
|
||||
mssql_row = next((r for r in sale_results if r.SL_NO_order == claim_row['transaction_id']), None)
|
||||
|
||||
if mssql_row:
|
||||
results.append({
|
||||
'user_id': claim_row['claimed_by_user_id'],
|
||||
'user_name': claim_row['nickname'],
|
||||
'user_phone': claim_row['phone'],
|
||||
'purchase_date': claim_row['claimed_at'][:16].replace('T', ' ') if claim_row['claimed_at'] else '-',
|
||||
'quantity': float(mssql_row.SL_NM_item or 0),
|
||||
'total_amount': int(claim_row['total_amount'])
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'results': results
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'검색 실패: {str(e)}'
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/admin')
|
||||
def admin():
|
||||
"""관리자 페이지 - 전체 사용자 및 적립 현황"""
|
||||
|
||||
Reference in New Issue
Block a user