diff --git a/backend/app.py b/backend/app.py index 4c73bc4..d9f1479 100644 --- a/backend/app.py +++ b/backend/app.py @@ -3542,6 +3542,79 @@ def api_products(): return jsonify({'success': False, 'error': str(e)}), 500 +# ==================== 위치 정보 API ==================== + +@app.route('/api/locations') +def api_get_all_locations(): + """모든 위치명 목록 조회""" + try: + drug_session = db_manager.get_session('PM_DRUG') + result = drug_session.execute(text(""" + SELECT DISTINCT CD_NM_sale + FROM CD_item_position + WHERE CD_NM_sale IS NOT NULL AND CD_NM_sale != '' + ORDER BY CD_NM_sale + """)) + locations = [row[0] for row in result.fetchall()] + return jsonify({ + 'success': True, + 'locations': locations, + 'total': len(locations) + }) + except Exception as e: + logging.error(f"위치 목록 조회 오류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + +@app.route('/api/drugs//location', methods=['PUT']) +def api_update_drug_location(drug_code): + """약품 위치 업데이트""" + try: + data = request.get_json() + location_name = data.get('location_name', '').strip() if data else '' + + # 위치명 길이 검증 (최대 20자) + if location_name and len(location_name) > 20: + return jsonify({'success': False, 'error': '위치명은 20자를 초과할 수 없습니다'}), 400 + + drug_session = db_manager.get_session('PM_DRUG') + + # 기존 레코드 확인 + existing = drug_session.execute(text(""" + SELECT DrugCode FROM CD_item_position WHERE DrugCode = :drug_code + """), {'drug_code': drug_code}).fetchone() + + if existing: + # UPDATE + if location_name: + drug_session.execute(text(""" + UPDATE CD_item_position SET CD_NM_sale = :location WHERE DrugCode = :drug_code + """), {'location': location_name, 'drug_code': drug_code}) + else: + # 빈 값이면 삭제 + drug_session.execute(text(""" + DELETE FROM CD_item_position WHERE DrugCode = :drug_code + """), {'drug_code': drug_code}) + else: + # INSERT (위치가 있을 때만) + if location_name: + drug_session.execute(text(""" + INSERT INTO CD_item_position (DrugCode, CD_NM_sale) VALUES (:drug_code, :location) + """), {'drug_code': drug_code, 'location': location_name}) + + drug_session.commit() + + return jsonify({ + 'success': True, + 'message': '위치 정보가 업데이트되었습니다', + 'location': location_name + }) + + except Exception as e: + logging.error(f"위치 업데이트 오류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + @app.route('/admin/sales') def admin_sales_pos(): """판매 내역 페이지 (POS 스타일, 거래별 그룹핑)""" diff --git a/backend/templates/admin_products.html b/backend/templates/admin_products.html index fd0166e..807bc32 100644 --- a/backend/templates/admin_products.html +++ b/backend/templates/admin_products.html @@ -405,7 +405,148 @@ padding: 3px 8px; border-radius: 4px; font-weight: 500; + cursor: pointer; + transition: all 0.15s; } + .location-badge:hover { + background: #fcd34d; + transform: scale(1.05); + } + .location-badge.unset { + background: #f1f5f9; + color: #94a3b8; + border: 1px dashed #cbd5e1; + } + .location-badge.unset:hover { + background: #e2e8f0; + border-color: #8b5cf6; + color: #7c3aed; + } + + /* 위치 모달 */ + .location-modal { + display: none; + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.6); + z-index: 2000; + align-items: center; + justify-content: center; + backdrop-filter: blur(4px); + } + .location-modal.show { display: flex; } + .location-modal-content { + background: #fff; + border-radius: 16px; + padding: 24px; + max-width: 400px; + width: 90%; + box-shadow: 0 20px 60px rgba(0,0,0,0.3); + animation: modalSlideIn 0.2s ease; + } + @keyframes modalSlideIn { + from { opacity: 0; transform: translateY(-20px); } + to { opacity: 1; transform: translateY(0); } + } + .location-modal-content h3 { + margin: 0 0 16px 0; + color: #92400e; + font-size: 18px; + display: flex; + align-items: center; + gap: 8px; + } + .location-product-info { + background: #fffbeb; + border-radius: 8px; + padding: 12px; + margin-bottom: 16px; + border: 1px solid #fef3c7; + } + .location-product-info .name { + font-weight: 600; + color: #1e293b; + margin-bottom: 4px; + } + .location-product-info .code { + font-size: 12px; + color: #94a3b8; + font-family: 'JetBrains Mono', monospace; + } + .location-select-wrapper { + margin-bottom: 12px; + } + .location-select-wrapper label { + display: block; + font-size: 12px; + font-weight: 500; + color: #64748b; + margin-bottom: 6px; + } + .location-select { + width: 100%; + padding: 12px; + border: 2px solid #e2e8f0; + border-radius: 8px; + font-size: 14px; + font-family: inherit; + background: #fff; + cursor: pointer; + transition: border-color 0.2s; + } + .location-select:focus { + outline: none; + border-color: #f59e0b; + } + .location-input-wrapper { + margin-bottom: 16px; + } + .location-input-wrapper label { + display: block; + font-size: 12px; + font-weight: 500; + color: #64748b; + margin-bottom: 6px; + } + .location-input { + width: 100%; + padding: 12px; + border: 2px solid #e2e8f0; + border-radius: 8px; + font-size: 14px; + font-family: inherit; + transition: border-color 0.2s; + } + .location-input:focus { + outline: none; + border-color: #f59e0b; + } + .location-hint { + font-size: 11px; + color: #94a3b8; + margin-top: 6px; + } + .location-modal-btns { + display: flex; + gap: 8px; + justify-content: flex-end; + margin-top: 16px; + } + .location-modal-btn { + padding: 10px 20px; + border: none; + border-radius: 8px; + cursor: pointer; + font-weight: 500; + font-size: 14px; + transition: all 0.15s; + } + .location-modal-btn.secondary { background: #f1f5f9; color: #64748b; } + .location-modal-btn.secondary:hover { background: #e2e8f0; } + .location-modal-btn.danger { background: #fef2f2; color: #dc2626; } + .location-modal-btn.danger:hover { background: #fee2e2; } + .location-modal-btn.primary { background: #f59e0b; color: #fff; } + .location-modal-btn.primary:hover { background: #d97706; } /* ── 가격 ── */ .price { @@ -905,7 +1046,9 @@ ${item.barcode ? `${item.barcode}` : `없음`} - ${item.location ? `${escapeHtml(item.location)}` : ''} + ${item.location + ? `${escapeHtml(item.location)}` + : `미지정`} ${item.stock || 0}${wsStock} ${formatPrice(item.sale_price)} @@ -1402,5 +1545,124 @@ + + +
+
+

📍 위치 설정

+
+
제품명
+
상품코드
+
+ +
+ + +
+ +
+ + +
최대 20자 / 새 위치를 입력하면 목록에 추가됩니다
+
+ +
+ +
+ + +
+
+
+ +