From d6cf4c2cc10fbe27ff417a68559351c0d61ff88e Mon Sep 17 00:00:00 2001 From: thug0bin Date: Sun, 8 Mar 2026 10:03:42 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B0=98=ED=92=88=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app.py | 152 +++ .../templates/admin_return_management.html | 1130 +++++++++++++++++ 2 files changed, 1282 insertions(+) create mode 100644 backend/templates/admin_return_management.html diff --git a/backend/app.py b/backend/app.py index 1303209..b2cf378 100644 --- a/backend/app.py +++ b/backend/app.py @@ -5234,6 +5234,158 @@ def api_kims_log_detail(log_id): return jsonify({'success': False, 'error': str(e)}) +# ───────────────────────────────────────────────────────────── +# 반품 후보 관리 +# ───────────────────────────────────────────────────────────── +@app.route('/admin/return-management') +def admin_return_management(): + """반품 후보 관리 페이지""" + return render_template('admin_return_management.html') + + +@app.route('/api/return-candidates') +def api_return_candidates(): + """반품 후보 목록 조회 API""" + status = request.args.get('status', '') + urgency = request.args.get('urgency', '') + search = request.args.get('search', '') + sort = request.args.get('sort', 'months_desc') + + try: + db_path = os.path.join(BACKEND_DIR, 'db', 'orders.db') + conn = sqlite3.connect(db_path) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + # 기본 쿼리 + query = "SELECT * FROM return_candidates WHERE 1=1" + params = [] + + # 상태 필터 + if status: + query += " AND status = ?" + params.append(status) + + # 검색어 필터 + if search: + query += " AND (drug_name LIKE ? OR drug_code LIKE ?)" + params.extend([f'%{search}%', f'%{search}%']) + + # 긴급도 필터 + if urgency == 'critical': + query += " AND (months_since_use >= 36 OR months_since_purchase >= 36)" + elif urgency == 'warning': + query += " AND ((months_since_use >= 24 OR months_since_purchase >= 24) AND (COALESCE(months_since_use, 0) < 36 AND COALESCE(months_since_purchase, 0) < 36))" + elif urgency == 'normal': + query += " AND (COALESCE(months_since_use, 0) < 24 AND COALESCE(months_since_purchase, 0) < 24)" + + # 정렬 + sort_map = { + 'months_desc': 'COALESCE(months_since_use, months_since_purchase, 0) DESC', + 'months_asc': 'COALESCE(months_since_use, months_since_purchase, 0) ASC', + 'stock_desc': 'current_stock DESC', + 'name_asc': 'drug_name ASC', + 'detected_desc': 'detected_at DESC' + } + query += f" ORDER BY {sort_map.get(sort, 'detected_at DESC')}" + + cursor.execute(query, params) + rows = cursor.fetchall() + + items = [] + for row in rows: + items.append({ + 'id': row['id'], + 'drug_code': row['drug_code'], + 'drug_name': row['drug_name'], + 'current_stock': row['current_stock'], + 'last_prescription_date': row['last_prescription_date'], + 'months_since_use': row['months_since_use'], + 'last_purchase_date': row['last_purchase_date'], + 'months_since_purchase': row['months_since_purchase'], + 'status': row['status'], + 'decision_reason': row['decision_reason'], + 'detected_at': row['detected_at'], + 'updated_at': row['updated_at'] + }) + + # 통계 계산 + cursor.execute("SELECT COUNT(*) FROM return_candidates WHERE months_since_use >= 36 OR months_since_purchase >= 36") + critical = cursor.fetchone()[0] + + cursor.execute("""SELECT COUNT(*) FROM return_candidates + WHERE (months_since_use >= 24 OR months_since_purchase >= 24) + AND (COALESCE(months_since_use, 0) < 36 AND COALESCE(months_since_purchase, 0) < 36)""") + warning = cursor.fetchone()[0] + + cursor.execute("SELECT COUNT(*) FROM return_candidates WHERE status = 'pending'") + pending = cursor.fetchone()[0] + + cursor.execute("SELECT COUNT(*) FROM return_candidates WHERE status IN ('returned', 'keep', 'disposed', 'resolved')") + processed = cursor.fetchone()[0] + + cursor.execute("SELECT COUNT(*) FROM return_candidates") + total = cursor.fetchone()[0] + + conn.close() + + return jsonify({ + 'success': True, + 'items': items, + 'stats': { + 'critical': critical, + 'warning': warning, + 'pending': pending, + 'processed': processed, + 'total': total + } + }) + + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + + +@app.route('/api/return-candidates/', methods=['PUT']) +def api_update_return_candidate(item_id): + """반품 후보 상태 변경 API""" + try: + data = request.get_json() + new_status = data.get('status') + reason = data.get('reason', '') + + if not new_status: + return jsonify({'success': False, 'error': '상태가 지정되지 않았습니다'}), 400 + + valid_statuses = ['pending', 'reviewed', 'returned', 'keep', 'disposed', 'resolved'] + if new_status not in valid_statuses: + return jsonify({'success': False, 'error': f'유효하지 않은 상태: {new_status}'}), 400 + + db_path = os.path.join(BACKEND_DIR, 'db', 'orders.db') + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + cursor.execute(""" + UPDATE return_candidates + SET status = ?, + decision_reason = ?, + reviewed_at = datetime('now', 'localtime'), + updated_at = datetime('now', 'localtime') + WHERE id = ? + """, (new_status, reason, item_id)) + + if cursor.rowcount == 0: + conn.close() + return jsonify({'success': False, 'error': '항목을 찾을 수 없습니다'}), 404 + + conn.commit() + conn.close() + + return jsonify({'success': True, 'message': '상태가 변경되었습니다'}) + + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + + @app.route('/api/kims/interaction-check', methods=['POST']) def api_kims_interaction_check(): """ diff --git a/backend/templates/admin_return_management.html b/backend/templates/admin_return_management.html new file mode 100644 index 0000000..da9e7d6 --- /dev/null +++ b/backend/templates/admin_return_management.html @@ -0,0 +1,1130 @@ + + + + + + 반품 관리 - 청춘약국 + + + + + + +
+
+
+

📦 반품 후보 관리

+

장기 미사용 의약품 반품/폐기 관리

+
+ +
+
+ +
+ +
+
+
🔴
+
-
+
3년+ 미사용
+
긴급 처리 필요
+
+
+
🟠
+
-
+
2년+ 미사용
+
검토 권장
+
+
+
📋
+
-
+
미결정
+
pending 상태
+
+
+
+
-
+
처리완료
+
반품/보류/폐기
+
+
+
📊
+
-
+
전체 후보
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+ + + + +
+ + +
+ + + + + + + + + + + + + + + + + + +
긴급약품현재고마지막 처방미사용마지막 입고상태액션
+
+
+
데이터 로딩 중...
+
+
+
+ + + +
+ + + + + +
+ + + +