diff --git a/backend/app.py b/backend/app.py index 0cec185..735b57e 100644 --- a/backend/app.py +++ b/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(): """관리자 페이지 - 전체 사용자 및 적립 현황""" diff --git a/backend/templates/admin.html b/backend/templates/admin.html index 74b26b3..5d7d92f 100644 --- a/backend/templates/admin.html +++ b/backend/templates/admin.html @@ -201,6 +201,135 @@ transform: translateY(-1px); box-shadow: 0 2px 4px rgba(0,0,0,0.05); } + + /* 사이드바 레이아웃 */ + .layout-wrapper { + display: flex; + max-width: 1600px; + margin: 0 auto; + } + + .sidebar { + width: 280px; + background: #ffffff; + min-height: calc(100vh - 112px); + padding: 24px 16px; + box-shadow: 2px 0 8px rgba(0,0,0,0.04); + } + + .sidebar-title { + font-size: 18px; + font-weight: 700; + color: #212529; + margin-bottom: 20px; + padding: 0 8px; + } + + .search-container { + margin-bottom: 24px; + } + + .search-type-toggle { + display: flex; + gap: 8px; + margin-bottom: 12px; + } + + .search-type-btn { + flex: 1; + padding: 10px; + border: 2px solid #e9ecef; + background: #fff; + border-radius: 10px; + cursor: pointer; + font-size: 13px; + font-weight: 600; + color: #868e96; + transition: all 0.2s; + } + + .search-type-btn.active { + border-color: #6366f1; + background: #f8f9ff; + color: #6366f1; + } + + .search-input-wrapper { + position: relative; + } + + .search-input { + width: 100%; + padding: 12px 16px; + border: 2px solid #e9ecef; + border-radius: 12px; + font-size: 14px; + font-family: 'Noto Sans KR', sans-serif; + transition: all 0.2s; + } + + .search-input:focus { + outline: none; + border-color: #6366f1; + box-shadow: 0 0 0 4px rgba(99,102,241,0.08); + } + + .search-btn { + width: 100%; + padding: 12px; + margin-top: 8px; + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + color: #fff; + border: none; + border-radius: 12px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + } + + .search-btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(99,102,241,0.3); + } + + .search-btn:active { + transform: translateY(0); + } + + /* 검색 결과 모달 */ + .search-results-modal { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0,0,0,0.5); + z-index: 10000; + padding: 20px; + overflow-y: auto; + } + + .search-results-content { + max-width: 900px; + margin: 40px auto; + background: #fff; + border-radius: 20px; + padding: 32px; + position: relative; + } + + .container { + flex: 1; + padding: 24px; + } + + @media (max-width: 768px) { + .sidebar { + display: none; + } + } @@ -211,7 +340,30 @@ -
+
+ + + +
@@ -350,6 +502,22 @@ {% endif %}
+
+
+ + +
+
+ + +

검색 결과

+ +
+
+
검색 결과가 여기에 표시됩니다.
+
+
+
@@ -744,6 +912,202 @@ closeUserModal(); } }); + + // ===== 검색 기능 ===== + + let currentSearchType = 'user'; // 'user' 또는 'product' + + function switchSearchType(type) { + currentSearchType = type; + + // 버튼 스타일 변경 + document.getElementById('btn-search-user').classList.toggle('active', type === 'user'); + document.getElementById('btn-search-product').classList.toggle('active', type === 'product'); + + // placeholder 변경 + const searchInput = document.getElementById('searchInput'); + if (type === 'user') { + searchInput.placeholder = '이름 또는 전화번호 입력'; + } else { + searchInput.placeholder = '제품명 입력'; + } + + searchInput.value = ''; + } + + function performSearch() { + const query = document.getElementById('searchInput').value.trim(); + + if (!query) { + alert('검색어를 입력하세요.'); + return; + } + + if (currentSearchType === 'user') { + searchUsers(query); + } else { + searchProducts(query); + } + } + + function searchUsers(query) { + // 전화번호 뒷자리인지 확인 (숫자만 있고 4-7자리) + const isPhoneLast = /^\d{4,7}$/.test(query); + const type = isPhoneLast ? 'phone_last' : (query.match(/^\d+$/) ? 'phone' : 'name'); + + fetch(`/admin/search/user?q=${encodeURIComponent(query)}&type=${type}`) + .then(response => response.json()) + .then(data => { + if (data.success) { + if (data.multiple) { + // 여러 명 매칭 → 선택 모달 + showUserSelectionModal(data.users, query); + } else if (data.user_id) { + // 단일 매칭 → 바로 사용자 상세 모달 + showUserDetail(data.user_id); + } else { + alert('검색 결과가 없습니다.'); + } + } else { + alert(data.message || '검색 실패'); + } + }) + .catch(error => { + alert('검색 중 오류가 발생했습니다.'); + console.error(error); + }); + } + + function searchProducts(query) { + fetch(`/admin/search/product?q=${encodeURIComponent(query)}`) + .then(response => response.json()) + .then(data => { + if (data.success) { + showProductSearchResults(data.results, query); + } else { + alert(data.message || '검색 실패'); + } + }) + .catch(error => { + alert('검색 중 오류가 발생했습니다.'); + console.error(error); + }); + } + + function showUserSelectionModal(users, query) { + let html = ` +
+ "${query}" 검색 결과: ${users.length}명 +
+ + + + + + + + + + + `; + + users.forEach(user => { + html += ` + + + + + + + `; + }); + + html += ` + +
이름전화번호포인트선택
${user.name}${user.phone}${user.balance.toLocaleString()}P + +
+ `; + + document.getElementById('searchResultTitle').textContent = '사용자 선택'; + document.getElementById('searchResultContent').innerHTML = html; + document.getElementById('searchResultsModal').style.display = 'block'; + } + + function selectUser(userId) { + closeSearchResults(); + showUserDetail(userId); + } + + function showProductSearchResults(results, query) { + if (results.length === 0) { + document.getElementById('searchResultTitle').textContent = '검색 결과'; + document.getElementById('searchResultContent').innerHTML = ` +
+
검색 결과가 없습니다.
+
제품명 "${query}"를 구매하고 적립한 사용자가 없습니다.
+
+ `; + document.getElementById('searchResultsModal').style.display = 'block'; + return; + } + + let html = ` +
+ "${query}" 구매 적립자: ${results.length}명 +
+ + + + + + + + + + + + `; + + results.forEach(result => { + html += ` + + + + + + + + `; + }); + + html += ` + +
이름전화번호구매일시수량구매금액
${result.user_name}${result.user_phone}${result.purchase_date}${result.quantity}${result.total_amount.toLocaleString()}원
+ `; + + document.getElementById('searchResultTitle').textContent = '제품 구매 적립자'; + document.getElementById('searchResultContent').innerHTML = html; + document.getElementById('searchResultsModal').style.display = 'block'; + } + + function closeSearchResults() { + document.getElementById('searchResultsModal').style.display = 'none'; + } + + // 검색 결과 모달 배경 클릭 시 닫기 + document.getElementById('searchResultsModal').addEventListener('click', function(e) { + if (e.target === this) { + closeSearchResults(); + } + }); + + // ESC 키로 검색 결과 모달도 닫기 + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + closeSearchResults(); + } + });