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:
2026-01-23 21:19:35 +09:00
parent a652d54ad3
commit 7627efbdfb
2 changed files with 523 additions and 1 deletions

View File

@@ -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():
"""관리자 페이지 - 전체 사용자 및 적립 현황"""