feat: 건조시럽 환산계수 모달 구현 - GET/POST/PUT API 추가 - PMR 약품명 더블클릭 → 모달 오픈 - 신규 등록/수정 기능

This commit is contained in:
thug0bin 2026-03-12 10:17:39 +09:00
parent 9531b74d0e
commit 98d370104b
3 changed files with 560 additions and 0 deletions

View File

@ -3953,6 +3953,198 @@ def api_conversion_factor(sung_code):
})
@app.route('/api/drug-info/drysyrup/<sung_code>', 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/<sung_code>', 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/<drug_code>/purchase-history')

View File

@ -0,0 +1,56 @@
<!-- 건조시럽 환산계수 모달 -->
<div id="drysyrupModal" class="drysyrup-modal">
<div class="drysyrup-modal-content">
<div class="drysyrup-modal-header">
<h3>🧪 건조시럽 환산계수</h3>
<button class="drysyrup-modal-close" onclick="closeDrysyrupModal()">&times;</button>
</div>
<div class="drysyrup-modal-body">
<div class="drysyrup-form">
<div class="drysyrup-form-row">
<label>성분코드</label>
<input type="text" id="drysyrup_sung_code" readonly class="readonly">
</div>
<div class="drysyrup-form-row">
<label>성분명</label>
<input type="text" id="drysyrup_ingredient_name" placeholder="예: 아목시실린">
</div>
<div class="drysyrup-form-row">
<label>제품명</label>
<input type="text" id="drysyrup_product_name" placeholder="예: 오구멘틴듀오시럽">
</div>
<div class="drysyrup-form-row">
<label>환산계수 (g/ml)</label>
<input type="number" id="drysyrup_conversion_factor" step="0.001" placeholder="예: 0.11">
<span class="hint">ml × 환산계수 = g</span>
</div>
<div class="drysyrup-form-row">
<label>조제 후 함량</label>
<input type="text" id="drysyrup_post_prep_amount" placeholder="예: 4.8mg/ml">
</div>
<div class="drysyrup-form-row">
<label>분말 중 주성분량</label>
<input type="text" id="drysyrup_main_ingredient_amt" placeholder="예: 0.787g/100g">
</div>
<div class="drysyrup-form-row">
<label>보관조건</label>
<select id="drysyrup_storage_conditions">
<option value="실온">실온</option>
<option value="냉장">냉장</option>
</select>
</div>
<div class="drysyrup-form-row">
<label>조제 후 유효기간</label>
<input type="text" id="drysyrup_expiration_date" placeholder="예: 15일">
</div>
</div>
</div>
<div class="drysyrup-modal-footer">
<span id="drysyrup_status" class="status-text"></span>
<div class="button-group">
<button class="btn-cancel" onclick="closeDrysyrupModal()">취소</button>
<button class="btn-save" onclick="saveDrysyrup()">💾 저장</button>
</div>
</div>
</div>
</div>

View File

@ -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;
}
</style>
</head>
<body>
@ -3278,5 +3405,190 @@
})();
</script>
<!-- 건조시럽 환산계수 모달 -->
<div id="drysyrupModal" class="drysyrup-modal">
<div class="drysyrup-modal-content">
<div class="drysyrup-modal-header">
<h3>🧪 건조시럽 환산계수</h3>
<button class="drysyrup-modal-close" onclick="closeDrysyrupModal()">&times;</button>
</div>
<div class="drysyrup-modal-body">
<div class="drysyrup-form">
<div class="drysyrup-form-row">
<label>성분코드</label>
<input type="text" id="drysyrup_sung_code" readonly class="readonly">
</div>
<div class="drysyrup-form-row">
<label>성분명</label>
<input type="text" id="drysyrup_ingredient_name" placeholder="예: 아목시실린">
</div>
<div class="drysyrup-form-row">
<label>제품명</label>
<input type="text" id="drysyrup_product_name" placeholder="예: 오구멘틴듀오시럽">
</div>
<div class="drysyrup-form-row">
<label>환산계수 (g/ml)</label>
<input type="number" id="drysyrup_conversion_factor" step="0.001" placeholder="예: 0.11">
<span class="hint">ml × 환산계수 = g</span>
</div>
<div class="drysyrup-form-row">
<label>조제 후 함량</label>
<input type="text" id="drysyrup_post_prep_amount" placeholder="예: 4.8mg/ml">
</div>
<div class="drysyrup-form-row">
<label>분말 중 주성분량</label>
<input type="text" id="drysyrup_main_ingredient_amt" placeholder="예: 0.787g/100g">
</div>
<div class="drysyrup-form-row">
<label>보관조건</label>
<select id="drysyrup_storage_conditions">
<option value="실온">실온</option>
<option value="냉장">냉장</option>
</select>
</div>
<div class="drysyrup-form-row">
<label>조제 후 유효기간</label>
<input type="text" id="drysyrup_expiration_date" placeholder="예: 15일">
</div>
</div>
</div>
<div class="drysyrup-modal-footer">
<span id="drysyrup_status" class="status-text"></span>
<div class="button-group">
<button class="btn-cancel" onclick="closeDrysyrupModal()">취소</button>
<button class="btn-save" onclick="saveDrysyrup()">💾 저장</button>
</div>
</div>
</div>
</div>
<script>
// ==================== 건조시럽 환산계수 모달 ====================
let drysyrupIsNew = false;
// 약품명 더블클릭 이벤트 등록
document.addEventListener('DOMContentLoaded', function() {
// 동적으로 생성되는 요소를 위해 이벤트 위임 사용
document.addEventListener('dblclick', function(e) {
// 약품 행(tr)에서 더블클릭 감지
const row = e.target.closest('tr[data-sung-code]');
if (row) {
const sungCode = row.dataset.sungCode;
const medName = row.dataset.medName || '';
if (sungCode) {
openDrysyrupModal(sungCode, medName);
}
}
});
});
// 모달 열기
async function openDrysyrupModal(sungCode, medName) {
const modal = document.getElementById('drysyrupModal');
const statusEl = document.getElementById('drysyrup_status');
// 폼 초기화
document.getElementById('drysyrup_sung_code').value = sungCode;
document.getElementById('drysyrup_ingredient_name').value = '';
document.getElementById('drysyrup_product_name').value = medName || '';
document.getElementById('drysyrup_conversion_factor').value = '';
document.getElementById('drysyrup_post_prep_amount').value = '';
document.getElementById('drysyrup_main_ingredient_amt').value = '';
document.getElementById('drysyrup_storage_conditions').value = '실온';
document.getElementById('drysyrup_expiration_date').value = '';
statusEl.textContent = '로딩 중...';
modal.classList.add('show');
// API 호출
try {
const resp = await fetch('/api/drug-info/drysyrup/' + encodeURIComponent(sungCode));
const data = await resp.json();
if (data.exists) {
// 기존 데이터 채우기
document.getElementById('drysyrup_ingredient_name').value = data.ingredient_name || '';
document.getElementById('drysyrup_product_name').value = data.product_name || '';
document.getElementById('drysyrup_conversion_factor').value = data.conversion_factor || '';
document.getElementById('drysyrup_post_prep_amount').value = data.post_prep_amount || '';
document.getElementById('drysyrup_main_ingredient_amt').value = data.main_ingredient_amt || '';
document.getElementById('drysyrup_storage_conditions').value = data.storage_conditions || '실온';
document.getElementById('drysyrup_expiration_date').value = data.expiration_date || '';
statusEl.textContent = '✅ 등록된 데이터';
drysyrupIsNew = false;
} else {
statusEl.textContent = '🆕 신규 등록';
drysyrupIsNew = true;
}
} catch (err) {
console.error('드라이시럽 조회 오류:', err);
statusEl.textContent = '⚠️ 조회 실패 (신규 등록 가능)';
drysyrupIsNew = true;
}
}
// 모달 닫기
function closeDrysyrupModal() {
document.getElementById('drysyrupModal').classList.remove('show');
}
// 저장
async function saveDrysyrup() {
const sungCode = document.getElementById('drysyrup_sung_code').value;
const statusEl = document.getElementById('drysyrup_status');
const data = {
sung_code: sungCode,
ingredient_name: document.getElementById('drysyrup_ingredient_name').value,
product_name: document.getElementById('drysyrup_product_name').value,
conversion_factor: parseFloat(document.getElementById('drysyrup_conversion_factor').value) || null,
post_prep_amount: document.getElementById('drysyrup_post_prep_amount').value,
main_ingredient_amt: document.getElementById('drysyrup_main_ingredient_amt').value,
storage_conditions: document.getElementById('drysyrup_storage_conditions').value,
expiration_date: document.getElementById('drysyrup_expiration_date').value
};
statusEl.textContent = '저장 중...';
try {
const url = drysyrupIsNew ? '/api/drug-info/drysyrup' : '/api/drug-info/drysyrup/' + encodeURIComponent(sungCode);
const method = drysyrupIsNew ? 'POST' : 'PUT';
const resp = await fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await resp.json();
if (result.success) {
statusEl.textContent = '✅ 저장 완료!';
window.showToast && window.showToast('환산계수 저장 완료', 'success');
setTimeout(closeDrysyrupModal, 1000);
} else {
statusEl.textContent = '❌ ' + (result.error || '저장 실패');
}
} catch (err) {
console.error('드라이시럽 저장 오류:', err);
statusEl.textContent = '❌ 저장 오류';
}
}
// ESC 키로 모달 닫기
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeDrysyrupModal();
}
});
// 모달 바깥 클릭시 닫기
document.getElementById('drysyrupModal').addEventListener('click', function(e) {
if (e.target === this) {
closeDrysyrupModal();
}
});
</script>
</body>
</html>