From a652d54ad38723702e80539322c4bbc53912b8c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=9C=EA=B3=A8=EC=95=BD=EC=82=AC?= Date: Fri, 23 Jan 2026 19:27:10 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=9D=B4=EB=A0=A5=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Flask 백엔드에 /admin/user/ API 엔드포인트 추가 - SQLite에서 사용자 정보, 마일리지 이력, 구매 이력 조회 - MSSQL에서 각 거래별 상품 상세 조회 (SALE_SUB + CD_GOODS JOIN) - "첫번째상품명 외 N개" 형식 요약 생성 - admin.html 사용자 테이블에 클릭 이벤트 추가 - 사용자 상세 모달 UI 구현 (탭 + 아코디언) - 탭: 구매 이력 / 적립 이력 분리 표시 - 아코디언: 각 구매 건 클릭 시 상품 목록 펼침/접기 - CSS 스타일 추가 (아코디언 애니메이션, 테이블 호버 효과) Co-Authored-By: Claude Sonnet 4.5 --- backend/app.py | 139 +++++++++++++++++ backend/templates/admin.html | 279 ++++++++++++++++++++++++++++++++++- 2 files changed, 416 insertions(+), 2 deletions(-) diff --git a/backend/app.py b/backend/app.py index 433e166..0cec185 100644 --- a/backend/app.py +++ b/backend/app.py @@ -456,6 +456,145 @@ def admin_transaction_detail(transaction_id): }), 500 +@app.route('/admin/user/') +def admin_user_detail(user_id): + """사용자 상세 이력 조회 - 구매 이력, 적립 이력, 구매 품목""" + try: + # 1. SQLite 연결 + conn = db_manager.get_sqlite_connection() + cursor = conn.cursor() + + # 2. 사용자 기본 정보 조회 + cursor.execute(""" + SELECT id, nickname, phone, mileage_balance, created_at + FROM users WHERE id = ? + """, (user_id,)) + user = cursor.fetchone() + + if not user: + return jsonify({ + 'success': False, + 'message': '사용자를 찾을 수 없습니다.' + }), 404 + + # 3. 마일리지 이력 조회 (최근 50건) + cursor.execute(""" + SELECT transaction_id, points, balance_after, reason, description, created_at + FROM mileage_ledger + WHERE user_id = ? + ORDER BY created_at DESC + LIMIT 50 + """, (user_id,)) + mileage_history = cursor.fetchall() + + # 4. 구매 이력 조회 (적립된 거래만, 최근 20건) + cursor.execute(""" + SELECT transaction_id, total_amount, claimable_points, claimed_at + FROM claim_tokens + WHERE claimed_by_user_id = ? + ORDER BY claimed_at DESC + LIMIT 20 + """, (user_id,)) + claimed_tokens = cursor.fetchall() + + # 5. 각 거래의 상품 상세 조회 (MSSQL) + purchases = [] + + try: + session = db_manager.get_session('PM_PRES') + + for token in claimed_tokens: + transaction_id = token['transaction_id'] + + # SALE_SUB + CD_GOODS JOIN + sale_items_query = text(""" + SELECT + S.DrugCode, + ISNULL(G.GoodsName, '(약품명 없음)') AS goods_name, + S.SL_NM_item AS quantity, + S.SL_NM_cost_a AS price, + S.SL_TOTAL_PRICE AS total + FROM SALE_SUB S + LEFT JOIN PM_DRUG.dbo.CD_GOODS G ON S.DrugCode = G.DrugCode + WHERE S.SL_NO_order = :transaction_id + ORDER BY S.DrugCode + """) + + items_raw = session.execute( + sale_items_query, + {'transaction_id': transaction_id} + ).fetchall() + + # 상품 리스트 변환 + items = [ + { + 'code': item.DrugCode, + 'name': item.goods_name, + 'qty': int(item.quantity or 0), + 'price': int(item.price or 0), + 'total': int(item.total or 0) + } + for item in items_raw + ] + + # 상품 요약 생성 ("첫번째상품명 외 N개") + if items: + first_item_name = items[0]['name'] + items_count = len(items) + if items_count == 1: + items_summary = first_item_name + else: + items_summary = f"{first_item_name} 외 {items_count - 1}개" + else: + items_summary = "상품 정보 없음" + items_count = 0 + + purchases.append({ + 'transaction_id': transaction_id, + 'date': str(token['claimed_at'])[:16].replace('T', ' '), + 'amount': int(token['total_amount']), + 'points': int(token['claimable_points']), + 'items_summary': items_summary, + 'items_count': items_count, + 'items': items + }) + + except Exception as mssql_error: + # MSSQL 연결 실패 시 빈 배열 반환 + print(f"[WARNING] MSSQL 조회 실패 (user {user_id}): {mssql_error}") + purchases = [] + + # 6. 응답 생성 + return jsonify({ + 'success': True, + 'user': { + 'id': user['id'], + 'name': user['nickname'], + 'phone': user['phone'], + 'balance': user['mileage_balance'], + 'created_at': str(user['created_at'])[:16].replace('T', ' ') + }, + 'mileage_history': [ + { + 'points': ml['points'], + 'balance_after': ml['balance_after'], + 'reason': ml['reason'], + 'description': ml['description'], + 'created_at': str(ml['created_at'])[:16].replace('T', ' '), + 'transaction_id': ml['transaction_id'] + } + for ml in mileage_history + ], + 'purchases': purchases + }) + + 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 8f6fd1a..74b26b3 100644 --- a/backend/templates/admin.html +++ b/backend/templates/admin.html @@ -176,6 +176,31 @@ padding: 10px 12px; } } + + /* 아코디언 스타일 */ + .accordion-content { + transition: max-height 0.3s ease-out; + } + + /* 탭 스타일 */ + .tab-btn { + transition: all 0.2s ease; + } + + .tab-btn:hover { + color: #6366f1 !important; + } + + /* 사용자 테이블 행 호버 */ + .section table tbody tr { + transition: all 0.2s ease; + } + + .section table tbody tr:hover { + background: #f8f9fa; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + } @@ -230,9 +255,9 @@ {% for user in recent_users %} - + {{ user.id }} - {{ user.nickname }} + {{ user.nickname }} {{ user.phone[:3] }}-{{ user.phone[3:7] }}-{{ user.phone[7:] if user.phone|length > 7 else '' }} {{ "{:,}".format(user.mileage_balance) }}P {{ user.created_at[:16].replace('T', ' ') }} @@ -342,6 +367,21 @@ + + +