diff --git a/backend/app.py b/backend/app.py index 8a3ffbe..85a7d1d 100644 --- a/backend/app.py +++ b/backend/app.py @@ -3953,6 +3953,198 @@ def api_conversion_factor(sung_code): }) +@app.route('/api/drug-info/drysyrup/', methods=['GET']) +def api_drysyrup_get(sung_code): + """ + 건조시럽 전체 정보 조회 API + + PostgreSQL drysyrup 테이블에서 SUNG_CODE로 전체 정보 조회 + """ + try: + session = db_manager.get_postgres_session() + if session is None: + return jsonify({ + 'success': True, + 'exists': False, + 'error': 'PostgreSQL 연결 실패' + }) + + query = text(""" + SELECT + ingredient_code, + ingredient_name, + product_name, + conversion_factor, + post_prep_amount, + main_ingredient_amt, + storage_conditions, + expiration_date + FROM drysyrup + WHERE ingredient_code = :sung_code + LIMIT 1 + """) + row = session.execute(query, {'sung_code': sung_code}).fetchone() + + if not row: + return jsonify({ + 'success': True, + 'exists': False + }) + + return jsonify({ + 'success': True, + 'exists': True, + 'sung_code': row[0], + 'ingredient_name': row[1], + 'product_name': row[2], + 'conversion_factor': float(row[3]) if row[3] is not None else None, + 'post_prep_amount': row[4], + 'main_ingredient_amt': row[5], + 'storage_conditions': row[6], + 'expiration_date': row[7] + }) + + except Exception as e: + logging.error(f"건조시럽 조회 오류 (SUNG_CODE={sung_code}): {e}") + return jsonify({ + 'success': True, + 'exists': False, + 'error': str(e) + }) + + +@app.route('/api/drug-info/drysyrup', methods=['POST']) +def api_drysyrup_create(): + """ + 건조시럽 신규 등록 API + """ + try: + data = request.get_json() + if not data or not data.get('sung_code'): + return jsonify({'success': False, 'error': '성분코드 필수'}), 400 + + session = db_manager.get_postgres_session() + if session is None: + return jsonify({'success': False, 'error': 'PostgreSQL 연결 실패'}), 500 + + # 중복 체크 + check_query = text("SELECT 1 FROM drysyrup WHERE ingredient_code = :sung_code") + existing = session.execute(check_query, {'sung_code': data['sung_code']}).fetchone() + if existing: + return jsonify({'success': False, 'error': '이미 등록된 성분코드'}), 400 + + insert_query = text(""" + INSERT INTO drysyrup ( + ingredient_code, ingredient_name, product_name, + conversion_factor, post_prep_amount, main_ingredient_amt, + storage_conditions, expiration_date + ) VALUES ( + :sung_code, :ingredient_name, :product_name, + :conversion_factor, :post_prep_amount, :main_ingredient_amt, + :storage_conditions, :expiration_date + ) + """) + + session.execute(insert_query, { + 'sung_code': data.get('sung_code'), + 'ingredient_name': data.get('ingredient_name', ''), + 'product_name': data.get('product_name', ''), + 'conversion_factor': data.get('conversion_factor'), + 'post_prep_amount': data.get('post_prep_amount', ''), + 'main_ingredient_amt': data.get('main_ingredient_amt', ''), + 'storage_conditions': data.get('storage_conditions', '실온'), + 'expiration_date': data.get('expiration_date', '') + }) + session.commit() + + return jsonify({'success': True, 'message': '등록 완료'}) + + except Exception as e: + logging.error(f"건조시럽 등록 오류: {e}") + try: + session.rollback() + except: + pass + return jsonify({'success': False, 'error': str(e)}), 500 + + +@app.route('/api/drug-info/drysyrup/', methods=['PUT']) +def api_drysyrup_update(sung_code): + """ + 건조시럽 정보 수정 API + """ + try: + data = request.get_json() + if not data: + return jsonify({'success': False, 'error': '데이터 필수'}), 400 + + session = db_manager.get_postgres_session() + if session is None: + return jsonify({'success': False, 'error': 'PostgreSQL 연결 실패'}), 500 + + # 존재 여부 확인 + check_query = text("SELECT 1 FROM drysyrup WHERE ingredient_code = :sung_code") + existing = session.execute(check_query, {'sung_code': sung_code}).fetchone() + + if not existing: + # 없으면 신규 등록으로 처리 + insert_query = text(""" + INSERT INTO drysyrup ( + ingredient_code, ingredient_name, product_name, + conversion_factor, post_prep_amount, main_ingredient_amt, + storage_conditions, expiration_date + ) VALUES ( + :sung_code, :ingredient_name, :product_name, + :conversion_factor, :post_prep_amount, :main_ingredient_amt, + :storage_conditions, :expiration_date + ) + """) + session.execute(insert_query, { + 'sung_code': sung_code, + 'ingredient_name': data.get('ingredient_name', ''), + 'product_name': data.get('product_name', ''), + 'conversion_factor': data.get('conversion_factor'), + 'post_prep_amount': data.get('post_prep_amount', ''), + 'main_ingredient_amt': data.get('main_ingredient_amt', ''), + 'storage_conditions': data.get('storage_conditions', '실온'), + 'expiration_date': data.get('expiration_date', '') + }) + else: + # 있으면 업데이트 + update_query = text(""" + UPDATE drysyrup SET + ingredient_name = :ingredient_name, + product_name = :product_name, + conversion_factor = :conversion_factor, + post_prep_amount = :post_prep_amount, + main_ingredient_amt = :main_ingredient_amt, + storage_conditions = :storage_conditions, + expiration_date = :expiration_date + WHERE ingredient_code = :sung_code + """) + session.execute(update_query, { + 'sung_code': sung_code, + 'ingredient_name': data.get('ingredient_name', ''), + 'product_name': data.get('product_name', ''), + 'conversion_factor': data.get('conversion_factor'), + 'post_prep_amount': data.get('post_prep_amount', ''), + 'main_ingredient_amt': data.get('main_ingredient_amt', ''), + 'storage_conditions': data.get('storage_conditions', '실온'), + 'expiration_date': data.get('expiration_date', '') + }) + + session.commit() + return jsonify({'success': True, 'message': '저장 완료'}) + + except Exception as e: + logging.error(f"건조시럽 수정 오류 (SUNG_CODE={sung_code}): {e}") + try: + session.rollback() + except: + pass + return jsonify({'success': False, 'error': str(e)}), 500 + + # ==================== 입고이력 API ==================== @app.route('/api/drugs//purchase-history') diff --git a/backend/templates/partials/drysyrup_modal.html b/backend/templates/partials/drysyrup_modal.html new file mode 100644 index 0000000..77966a9 --- /dev/null +++ b/backend/templates/partials/drysyrup_modal.html @@ -0,0 +1,56 @@ + +
+
+
+

🧪 건조시럽 환산계수

+ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + + ml × 환산계수 = g +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
diff --git a/backend/templates/pmr.html b/backend/templates/pmr.html index 3aa93bb..8bd5b0a 100644 --- a/backend/templates/pmr.html +++ b/backend/templates/pmr.html @@ -1226,6 +1226,133 @@ align-items: center; gap: 4px; } + + /* 건조시럽 환산계수 모달 */ + .drysyrup-modal { + display: none; + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.6); + z-index: 1100; + overflow-y: auto; + } + .drysyrup-modal.show { display: flex; justify-content: center; align-items: flex-start; padding: 40px 20px; } + .drysyrup-modal-content { + width: 100%; + max-width: 500px; + background: #fff; + border-radius: 16px; + box-shadow: 0 20px 60px rgba(0,0,0,0.3); + overflow: hidden; + } + .drysyrup-modal-header { + background: linear-gradient(135deg, #10b981, #059669); + color: #fff; + padding: 18px 25px; + display: flex; + justify-content: space-between; + align-items: center; + } + .drysyrup-modal-header h3 { margin: 0; font-size: 1.2rem; } + .drysyrup-modal-close { + background: rgba(255,255,255,0.2); + border: none; + color: #fff; + font-size: 1.5rem; + width: 36px; + height: 36px; + border-radius: 50%; + cursor: pointer; + transition: background 0.2s; + } + .drysyrup-modal-close:hover { background: rgba(255,255,255,0.3); } + .drysyrup-modal-body { padding: 25px; } + .drysyrup-form { display: flex; flex-direction: column; gap: 16px; } + .drysyrup-form-row { + display: flex; + flex-direction: column; + gap: 6px; + } + .drysyrup-form-row label { + font-size: 0.85rem; + font-weight: 600; + color: #374151; + } + .drysyrup-form-row input, + .drysyrup-form-row select { + padding: 10px 14px; + border: 1px solid #d1d5db; + border-radius: 8px; + font-size: 0.95rem; + transition: border-color 0.2s, box-shadow 0.2s; + } + .drysyrup-form-row input:focus, + .drysyrup-form-row select:focus { + outline: none; + border-color: #10b981; + box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1); + } + .drysyrup-form-row input.readonly { + background: #f3f4f6; + color: #6b7280; + cursor: not-allowed; + } + .drysyrup-form-row .hint { + font-size: 0.75rem; + color: #9ca3af; + } + .drysyrup-modal-footer { + background: #f9fafb; + padding: 16px 25px; + border-top: 1px solid #e5e7eb; + display: flex; + justify-content: space-between; + align-items: center; + } + .drysyrup-modal-footer .status-text { + font-size: 0.85rem; + color: #6b7280; + } + .drysyrup-modal-footer .button-group { + display: flex; + gap: 10px; + } + .drysyrup-modal-footer .btn-cancel { + padding: 10px 20px; + background: #e5e7eb; + color: #374151; + border: none; + border-radius: 8px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; + } + .drysyrup-modal-footer .btn-cancel:hover { background: #d1d5db; } + .drysyrup-modal-footer .btn-save { + padding: 10px 24px; + background: linear-gradient(135deg, #10b981, #059669); + color: #fff; + border: none; + border-radius: 8px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; + } + .drysyrup-modal-footer .btn-save:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3); + } + + /* 약품명 더블클릭 힌트 */ + .med-name[data-sung-code]:not([data-sung-code=""]) { + cursor: pointer; + } + .med-name[data-sung-code]:not([data-sung-code=""]):hover { + text-decoration: underline; + text-decoration-style: dotted; + } @@ -3278,5 +3405,190 @@ })(); + + +
+
+
+

🧪 건조시럽 환산계수

+ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + + ml × 환산계수 = g +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+ +