feat: 환산계수 모달 구현 전 백업
This commit is contained in:
parent
e254c5c23d
commit
9531b74d0e
@ -3906,6 +3906,53 @@ def api_products():
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
# ==================== 건조시럽 환산계수 API ====================
|
||||
|
||||
@app.route('/api/drug-info/conversion-factor/<sung_code>')
|
||||
def api_conversion_factor(sung_code):
|
||||
"""
|
||||
건조시럽 환산계수 조회 API
|
||||
|
||||
PostgreSQL drysyrup 테이블에서 SUNG_CODE로 환산계수 조회
|
||||
mL → g 변환에 사용 (예: 120ml * 0.11 = 13.2g)
|
||||
|
||||
Args:
|
||||
sung_code: 성분코드 (예: "535000ASY")
|
||||
|
||||
Returns:
|
||||
{
|
||||
"sung_code": "535000ASY",
|
||||
"conversion_factor": 0.11,
|
||||
"ingredient_name": "아목시실린수화물·클라불란산칼륨",
|
||||
"product_name": "일성오구멘틴듀오시럽 228mg/5ml"
|
||||
}
|
||||
|
||||
연결 실패/데이터 없음 시:
|
||||
{"sung_code": "...", "conversion_factor": null, ...}
|
||||
"""
|
||||
try:
|
||||
result = db_manager.get_conversion_factor(sung_code)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'sung_code': sung_code,
|
||||
'conversion_factor': result['conversion_factor'],
|
||||
'ingredient_name': result['ingredient_name'],
|
||||
'product_name': result['product_name']
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"환산계수 조회 오류 (SUNG_CODE={sung_code}): {e}")
|
||||
# 에러 발생해도 null 반환 (서비스 중단 방지)
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'sung_code': sung_code,
|
||||
'conversion_factor': None,
|
||||
'ingredient_name': None,
|
||||
'product_name': None
|
||||
})
|
||||
|
||||
|
||||
# ==================== 입고이력 API ====================
|
||||
|
||||
@app.route('/api/drugs/<drug_code>/purchase-history')
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
PIT3000 Database Setup
|
||||
SQLAlchemy 기반 데이터베이스 연결 및 스키마 정의
|
||||
Windows/Linux 크로스 플랫폼 지원
|
||||
|
||||
PostgreSQL 지원 추가: 건조시럽 환산계수 조회 (drysyrup 테이블)
|
||||
"""
|
||||
|
||||
from sqlalchemy import create_engine, MetaData, text
|
||||
@ -87,6 +89,9 @@ class DatabaseConfig:
|
||||
|
||||
# URL 인코딩된 드라이버
|
||||
DRIVER_ENCODED = urllib.parse.quote_plus(DRIVER)
|
||||
|
||||
# PostgreSQL 연결 정보 (건조시럽 환산계수 DB)
|
||||
POSTGRES_URL = "postgresql+psycopg2://admin:trajet6640@192.168.0.39:5432/label10"
|
||||
|
||||
# 데이터베이스별 연결 문자열 (동적 드라이버 사용)
|
||||
@classmethod
|
||||
@ -135,6 +140,10 @@ class DatabaseManager:
|
||||
# SQLite 연결 추가
|
||||
self.sqlite_conn = None
|
||||
self.sqlite_db_path = Path(__file__).parent / 'mileage.db'
|
||||
|
||||
# PostgreSQL 연결 (건조시럽 환산계수)
|
||||
self.postgres_engine = None
|
||||
self.postgres_session = None
|
||||
|
||||
def get_engine(self, database='PM_BASE'):
|
||||
"""특정 데이터베이스 엔진 반환"""
|
||||
@ -220,6 +229,127 @@ class DatabaseManager:
|
||||
# 새 세션 생성
|
||||
return self.get_session(database)
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# PostgreSQL 연결 (건조시럽 환산계수)
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
def get_postgres_engine(self):
|
||||
"""
|
||||
PostgreSQL 엔진 반환 (건조시럽 환산계수 DB)
|
||||
|
||||
Returns:
|
||||
Engine 또는 None (연결 실패 시)
|
||||
"""
|
||||
if self.postgres_engine is not None:
|
||||
return self.postgres_engine
|
||||
|
||||
try:
|
||||
self.postgres_engine = create_engine(
|
||||
DatabaseConfig.POSTGRES_URL,
|
||||
pool_size=5,
|
||||
max_overflow=5,
|
||||
pool_timeout=30,
|
||||
pool_recycle=1800,
|
||||
pool_pre_ping=True,
|
||||
echo=False
|
||||
)
|
||||
# 연결 테스트
|
||||
with self.postgres_engine.connect() as conn:
|
||||
conn.execute(text("SELECT 1"))
|
||||
print("[DB Manager] PostgreSQL 연결 성공")
|
||||
return self.postgres_engine
|
||||
except Exception as e:
|
||||
print(f"[DB Manager] PostgreSQL 연결 실패 (무시됨): {e}")
|
||||
self.postgres_engine = None
|
||||
return None
|
||||
|
||||
def get_postgres_session(self):
|
||||
"""
|
||||
PostgreSQL 세션 반환 (건조시럽 환산계수 조회용)
|
||||
|
||||
Returns:
|
||||
Session 또는 None (연결 실패 시)
|
||||
"""
|
||||
engine = self.get_postgres_engine()
|
||||
if engine is None:
|
||||
return None
|
||||
|
||||
if self.postgres_session is None:
|
||||
try:
|
||||
Session = sessionmaker(bind=engine)
|
||||
self.postgres_session = Session()
|
||||
except Exception as e:
|
||||
print(f"[DB Manager] PostgreSQL 세션 생성 실패: {e}")
|
||||
return None
|
||||
else:
|
||||
# 세션 상태 체크
|
||||
try:
|
||||
self.postgres_session.execute(text("SELECT 1"))
|
||||
except Exception as e:
|
||||
print(f"[DB Manager] PostgreSQL 세션 복구 시도: {e}")
|
||||
try:
|
||||
self.postgres_session.rollback()
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
self.postgres_session.close()
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
Session = sessionmaker(bind=engine)
|
||||
self.postgres_session = Session()
|
||||
except:
|
||||
self.postgres_session = None
|
||||
return None
|
||||
|
||||
return self.postgres_session
|
||||
|
||||
def get_conversion_factor(self, sung_code):
|
||||
"""
|
||||
건조시럽 환산계수 조회
|
||||
|
||||
Args:
|
||||
sung_code: SUNG_CODE (예: "535000ASY")
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'conversion_factor': float 또는 None,
|
||||
'ingredient_name': str 또는 None,
|
||||
'product_name': str 또는 None
|
||||
}
|
||||
"""
|
||||
result = {
|
||||
'conversion_factor': None,
|
||||
'ingredient_name': None,
|
||||
'product_name': None
|
||||
}
|
||||
|
||||
session = self.get_postgres_session()
|
||||
if session is None:
|
||||
return result
|
||||
|
||||
try:
|
||||
query = text("""
|
||||
SELECT conversion_factor, ingredient_name, product_name
|
||||
FROM drysyrup
|
||||
WHERE ingredient_code = :sung_code
|
||||
LIMIT 1
|
||||
""")
|
||||
row = session.execute(query, {'sung_code': sung_code}).fetchone()
|
||||
|
||||
if row:
|
||||
result['conversion_factor'] = float(row[0]) if row[0] is not None else None
|
||||
result['ingredient_name'] = row[1]
|
||||
result['product_name'] = row[2]
|
||||
except Exception as e:
|
||||
print(f"[DB Manager] 환산계수 조회 실패 (SUNG_CODE={sung_code}): {e}")
|
||||
# 세션 롤백
|
||||
try:
|
||||
session.rollback()
|
||||
except:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
def get_sqlite_connection(self, new_connection=False):
|
||||
"""
|
||||
SQLite mileage.db 연결 반환
|
||||
@ -442,6 +572,20 @@ class DatabaseManager:
|
||||
if self.sqlite_conn:
|
||||
self.sqlite_conn.close()
|
||||
self.sqlite_conn = None
|
||||
|
||||
# PostgreSQL 연결 종료
|
||||
if self.postgres_session:
|
||||
try:
|
||||
self.postgres_session.close()
|
||||
except:
|
||||
pass
|
||||
self.postgres_session = None
|
||||
if self.postgres_engine:
|
||||
try:
|
||||
self.postgres_engine.dispose()
|
||||
except:
|
||||
pass
|
||||
self.postgres_engine = None
|
||||
|
||||
# 전역 데이터베이스 매니저 인스턴스
|
||||
db_manager = DatabaseManager()
|
||||
|
||||
@ -571,6 +571,7 @@ def preview_label():
|
||||
- frequency: 복용 횟수
|
||||
- duration: 복용 일수
|
||||
- unit: 단위 (정, 캡슐, mL 등)
|
||||
- sung_code: 성분코드 (환산계수 조회용, 선택)
|
||||
"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
@ -582,6 +583,17 @@ def preview_label():
|
||||
frequency = int(data.get('frequency', 0))
|
||||
duration = int(data.get('duration', 0))
|
||||
unit = data.get('unit', '정')
|
||||
sung_code = data.get('sung_code', '')
|
||||
|
||||
# 환산계수 조회 (sung_code가 있는 경우)
|
||||
conversion_factor = None
|
||||
if sung_code:
|
||||
try:
|
||||
from db.dbsetup import db_manager
|
||||
cf_result = db_manager.get_conversion_factor(sung_code)
|
||||
conversion_factor = cf_result.get('conversion_factor')
|
||||
except Exception as cf_err:
|
||||
logging.warning(f"환산계수 조회 실패 (무시): {cf_err}")
|
||||
|
||||
# 라벨 이미지 생성
|
||||
image = create_label_image(
|
||||
@ -591,7 +603,8 @@ def preview_label():
|
||||
dosage=dosage,
|
||||
frequency=frequency,
|
||||
duration=duration,
|
||||
unit=unit
|
||||
unit=unit,
|
||||
conversion_factor=conversion_factor
|
||||
)
|
||||
|
||||
# Base64 인코딩
|
||||
@ -602,7 +615,8 @@ def preview_label():
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'image': f'data:image/png;base64,{img_base64}'
|
||||
'image': f'data:image/png;base64,{img_base64}',
|
||||
'conversion_factor': conversion_factor
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
@ -679,9 +693,14 @@ def draw_scissor_border(draw, width, height, edge_size=5, steps=20):
|
||||
draw.line(right_points, fill="black", width=2)
|
||||
|
||||
|
||||
def create_label_image(patient_name, med_name, add_info='', dosage=0, frequency=0, duration=0, unit='정'):
|
||||
def create_label_image(patient_name, med_name, add_info='', dosage=0, frequency=0, duration=0, unit='정', conversion_factor=None):
|
||||
"""
|
||||
라벨 이미지 생성 (29mm 용지 기준) - 레거시 디자인 적용
|
||||
|
||||
Args:
|
||||
conversion_factor: 건조시럽 환산계수 (mL→g 변환용, 선택)
|
||||
- 예: 0.11이면 120ml * 0.11 = 13.2g
|
||||
- 총량 옆에 괄호로 표시: "총120mL (13.2g)/5일분"
|
||||
"""
|
||||
# 약품명 정제 (밀리그램 → mg 등)
|
||||
med_name = normalize_medication_name(med_name)
|
||||
@ -811,11 +830,21 @@ def create_label_image(patient_name, med_name, add_info='', dosage=0, frequency=
|
||||
|
||||
y += 5
|
||||
|
||||
# 총량 계산 및 표시
|
||||
# 총량 계산 및 표시 (환산계수 반영)
|
||||
if dosage > 0 and frequency > 0 and duration > 0:
|
||||
total = dosage * frequency * duration
|
||||
total_str = str(int(total)) if total == int(total) else f"{total:.2f}".rstrip('0').rstrip('.')
|
||||
total_text = f"총{total_str}{unit}/{duration}일분"
|
||||
|
||||
# 환산계수가 있으면 변환된 총량도 표시 (예: "총120mL (13.2g)/5일분")
|
||||
if conversion_factor is not None and conversion_factor > 0:
|
||||
converted_total = total * conversion_factor
|
||||
if converted_total == int(converted_total):
|
||||
converted_str = str(int(converted_total))
|
||||
else:
|
||||
converted_str = f"{converted_total:.2f}".rstrip('0').rstrip('.')
|
||||
total_text = f"총{total_str}{unit} ({converted_str}g)/{duration}일분"
|
||||
else:
|
||||
total_text = f"총{total_str}{unit}/{duration}일분"
|
||||
y = draw_centered(total_text, y, additional_font)
|
||||
|
||||
y += 5
|
||||
|
||||
@ -277,6 +277,47 @@
|
||||
background: rgba(255,255,255,0.35);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 키오스크 스타일 전화번호 입력 */
|
||||
.phone-input-kiosk {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.phone-input-kiosk .phone-prefix {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #4c1d95;
|
||||
background: #f3e8ff;
|
||||
padding: 12px 16px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.phone-input-kiosk .phone-hyphen {
|
||||
font-size: 28px;
|
||||
font-weight: 300;
|
||||
color: #9ca3af;
|
||||
}
|
||||
.phone-input-kiosk input {
|
||||
width: 80px;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
padding: 12px 8px;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.phone-input-kiosk input:focus {
|
||||
border-color: #f59e0b;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(245,158,11,0.2);
|
||||
}
|
||||
.phone-input-kiosk input::placeholder {
|
||||
color: #d1d5db;
|
||||
font-weight: 400;
|
||||
}
|
||||
.detail-header .cusetc-inline .cusetc-label {
|
||||
font-weight: 600;
|
||||
margin-right: 6px;
|
||||
@ -1336,17 +1377,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 전화번호 모달 -->
|
||||
<!-- 전화번호 모달 (키오스크 스타일) -->
|
||||
<div class="cusetc-modal" id="phoneModal">
|
||||
<div class="cusetc-modal-content">
|
||||
<div class="cusetc-modal-content" style="max-width:360px;">
|
||||
<div class="cusetc-modal-header">
|
||||
<h3>📞 환자 전화번호</h3>
|
||||
<h3>📞 전화번호 입력</h3>
|
||||
<button class="cusetc-modal-close" onclick="closePhoneModal()">×</button>
|
||||
</div>
|
||||
<div class="cusetc-modal-body">
|
||||
<div class="cusetc-patient-info" id="phonePatientInfo"></div>
|
||||
<input type="tel" id="phoneInput" placeholder="010-0000-0000" style="width:100%;padding:12px;border:2px solid #e2e8f0;border-radius:8px;font-size:16px;text-align:center;">
|
||||
<div class="cusetc-hint">💡 하이픈(-) 포함해서 입력하세요</div>
|
||||
<div class="phone-input-kiosk">
|
||||
<span class="phone-prefix">010</span>
|
||||
<span class="phone-hyphen">-</span>
|
||||
<input type="tel" id="phoneMid" maxlength="4" placeholder="0000" inputmode="numeric" pattern="[0-9]*">
|
||||
<span class="phone-hyphen">-</span>
|
||||
<input type="tel" id="phoneLast" maxlength="4" placeholder="0000" inputmode="numeric" pattern="[0-9]*">
|
||||
</div>
|
||||
</div>
|
||||
<div class="cusetc-modal-footer">
|
||||
<button class="cusetc-btn-cancel" onclick="closePhoneModal()">취소</button>
|
||||
@ -1614,7 +1660,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
${data.medications.map((m, i) => `
|
||||
<tr data-add-info="${escapeHtml(m.add_info || '')}" data-unit="${m.unit || '정'}" data-med-name="${escapeHtml(m.med_name || m.medication_code)}" ${m.is_substituted ? 'class="substituted-row"' : ''}>
|
||||
<tr data-add-info="${escapeHtml(m.add_info || '')}" data-unit="${m.unit || '정'}" data-sung-code="${m.sung_code || ''}" data-med-name="${escapeHtml(m.med_name || m.medication_code)}" ${m.is_substituted ? 'class="substituted-row"' : ''}>
|
||||
<td><input type="checkbox" class="med-check" data-code="${m.medication_code}" ${m.is_auto_print ? 'checked' : ''}></td>
|
||||
<td>
|
||||
<div class="med-name">
|
||||
@ -1871,7 +1917,7 @@
|
||||
const disabled = m.status === 'removed' ? 'disabled' : '';
|
||||
|
||||
return `
|
||||
<tr class="${rowClass}" data-add-info="${escapeHtml(m.add_info || '')}" data-unit="${m.unit || '정'}">
|
||||
<tr class="${rowClass}" data-add-info="${escapeHtml(m.add_info || '')}" data-unit="${m.unit || '정'}" data-sung-code="${m.sung_code || ''}">
|
||||
<td><input type="checkbox" class="med-check" data-code="${m.medication_code}" ${disabled}></td>
|
||||
<td>
|
||||
<div class="med-name"><span class="med-num">${i+1}</span>${m.med_name || m.medication_code}</div>
|
||||
@ -1909,7 +1955,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
${currentMedications.map((m, i) => `
|
||||
<tr data-add-info="${escapeHtml(m.add_info || '')}" data-unit="${m.unit || '정'}">
|
||||
<tr data-add-info="${escapeHtml(m.add_info || '')}" data-unit="${m.unit || '정'}" data-sung-code="${m.sung_code || ''}">
|
||||
<td><input type="checkbox" class="med-check" data-code="${m.medication_code}"></td>
|
||||
<td>
|
||||
<div class="med-name"><span class="med-num">${i+1}</span>${m.med_name || m.medication_code}</div>
|
||||
@ -2120,7 +2166,7 @@
|
||||
// 전화번호 모달 함수들
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
function openPhoneModal() {
|
||||
window.openPhoneModal = function() {
|
||||
if (!currentPrescriptionData) {
|
||||
alert('❌ 먼저 환자를 선택하세요.');
|
||||
return;
|
||||
@ -2128,30 +2174,59 @@
|
||||
|
||||
const modal = document.getElementById('phoneModal');
|
||||
const patientInfo = document.getElementById('phonePatientInfo');
|
||||
const input = document.getElementById('phoneInput');
|
||||
const phoneMid = document.getElementById('phoneMid');
|
||||
const phoneLast = document.getElementById('phoneLast');
|
||||
|
||||
patientInfo.innerHTML = `
|
||||
<strong>${currentPrescriptionData.name || '환자'}</strong>
|
||||
<span style="margin-left: 10px; color: #6b7280;">고객코드: ${currentPrescriptionData.cus_code || '-'}</span>
|
||||
`;
|
||||
|
||||
input.value = currentPrescriptionData.phone || '';
|
||||
// 기존 전화번호 파싱 (010-1234-5678 또는 01012345678)
|
||||
const existingPhone = currentPrescriptionData.phone || '';
|
||||
const digits = existingPhone.replace(/\D/g, '');
|
||||
if (digits.length >= 10) {
|
||||
phoneMid.value = digits.slice(3, 7);
|
||||
phoneLast.value = digits.slice(7, 11);
|
||||
} else {
|
||||
phoneMid.value = '';
|
||||
phoneLast.value = '';
|
||||
}
|
||||
|
||||
modal.style.display = 'flex';
|
||||
input.focus();
|
||||
}
|
||||
phoneMid.focus();
|
||||
|
||||
// 4자리 입력 시 자동 포커스 이동
|
||||
phoneMid.oninput = function() {
|
||||
this.value = this.value.replace(/\D/g, '');
|
||||
if (this.value.length >= 4) phoneLast.focus();
|
||||
};
|
||||
phoneLast.oninput = function() {
|
||||
this.value = this.value.replace(/\D/g, '');
|
||||
};
|
||||
};
|
||||
|
||||
function closePhoneModal() {
|
||||
window.closePhoneModal = function() {
|
||||
document.getElementById('phoneModal').style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
async function savePhone() {
|
||||
window.savePhone = async function() {
|
||||
if (!currentPrescriptionData || !currentPrescriptionData.cus_code) {
|
||||
alert('❌ 환자 정보가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
const input = document.getElementById('phoneInput');
|
||||
const newPhone = input.value.trim();
|
||||
const phoneMid = document.getElementById('phoneMid').value.trim();
|
||||
const phoneLast = document.getElementById('phoneLast').value.trim();
|
||||
|
||||
// 유효성 검사
|
||||
if (phoneMid.length !== 4 || phoneLast.length !== 4) {
|
||||
alert('❌ 전화번호 8자리를 모두 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 010-XXXX-XXXX 형식으로 조합
|
||||
const newPhone = `010-${phoneMid}-${phoneLast}`;
|
||||
const cusCode = currentPrescriptionData.cus_code;
|
||||
|
||||
try {
|
||||
@ -2174,7 +2249,7 @@
|
||||
} catch (err) {
|
||||
alert('❌ 오류: ' + err.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function updatePhoneBadge(phone) {
|
||||
const detailInfo = document.getElementById('detailInfo');
|
||||
@ -2738,8 +2813,10 @@
|
||||
|
||||
// 단위: data-unit 속성에서 가져오기 (SUNG_CODE 기반 자동 판별)
|
||||
const unit = tr.dataset.unit || '정';
|
||||
// 성분코드: 환산계수 조회용
|
||||
const sungCode = tr.dataset.sungCode || '';
|
||||
|
||||
console.log('Preview data:', { patientName, medName, addInfo, dosage, frequency, duration, unit });
|
||||
console.log('Preview data:', { patientName, medName, addInfo, dosage, frequency, duration, unit, sungCode });
|
||||
|
||||
try {
|
||||
const res = await fetch('/pmr/api/label/preview', {
|
||||
@ -2752,7 +2829,8 @@
|
||||
dosage: dosage,
|
||||
frequency: frequency,
|
||||
duration: duration,
|
||||
unit: unit
|
||||
unit: unit,
|
||||
sung_code: sungCode
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
193
docs/DRYSYRUP_CONVERSION.md
Normal file
193
docs/DRYSYRUP_CONVERSION.md
Normal file
@ -0,0 +1,193 @@
|
||||
# 건조시럽 환산계수 기능
|
||||
|
||||
## 개요
|
||||
|
||||
건조시럽(dry syrup)은 물로 희석하여 복용하는 시럽 형태의 의약품입니다. 복용량을 mL로 표시하지만, 실제 약 성분의 양은 g(그램)으로 환산해야 정확합니다.
|
||||
|
||||
**환산계수(conversion_factor)**를 사용하여 총 복용량(mL)을 실제 성분량(g)으로 변환합니다.
|
||||
|
||||
### 예시
|
||||
- 오구멘틴듀오시럽 228mg/5ml
|
||||
- 환산계수: 0.11
|
||||
- 총량 120mL × 0.11 = **13.2g**
|
||||
|
||||
## 아키텍처
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Flask Backend (7001) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ /api/drug-info/conversion-factor/<sung_code> │
|
||||
│ /pmr/api/label/preview (sung_code 파라미터 추가) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ DatabaseManager │
|
||||
│ ├── MSSQL (192.168.0.4) - PIT3000 │
|
||||
│ │ └── CD_GOODS.SUNG_CODE (성분코드) │
|
||||
│ ├── PostgreSQL (192.168.0.39:5432/label10) │
|
||||
│ │ └── drysyrup 테이블 (환산계수 23건) │
|
||||
│ └── SQLite - 마일리지 등 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 데이터베이스
|
||||
|
||||
### PostgreSQL 연결 정보
|
||||
```
|
||||
Host: 192.168.0.39
|
||||
Port: 5432
|
||||
Database: label10
|
||||
User: admin
|
||||
Password: trajet6640
|
||||
```
|
||||
|
||||
### drysyrup 테이블 스키마
|
||||
| 컬럼명 | 타입 | 설명 |
|
||||
|--------|------|------|
|
||||
| ingredient_code | VARCHAR | 성분코드 (SUNG_CODE와 매칭) |
|
||||
| conversion_factor | DECIMAL | 환산계수 (mL → g) |
|
||||
| ingredient_name | VARCHAR | 성분명 |
|
||||
| product_name | VARCHAR | 대표 제품명 |
|
||||
|
||||
### 매핑 관계
|
||||
- MSSQL `PM_DRUG.CD_GOODS.SUNG_CODE` = PostgreSQL `drysyrup.ingredient_code`
|
||||
|
||||
## API 명세
|
||||
|
||||
### 1. 환산계수 조회 API
|
||||
|
||||
**Endpoint:** `GET /api/drug-info/conversion-factor/<sung_code>`
|
||||
|
||||
**응답 (성공):**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"sung_code": "535000ASY",
|
||||
"conversion_factor": 0.11,
|
||||
"ingredient_name": "아목시실린수화물·클라불란산칼륨",
|
||||
"product_name": "일성오구멘틴듀오시럽 228mg/5ml"
|
||||
}
|
||||
```
|
||||
|
||||
**응답 (데이터 없음/연결 실패):**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"sung_code": "NOTEXIST",
|
||||
"conversion_factor": null,
|
||||
"ingredient_name": null,
|
||||
"product_name": null
|
||||
}
|
||||
```
|
||||
|
||||
> ⚠️ 연결 실패나 데이터 없음에도 에러 없이 null 반환 (서비스 안정성 우선)
|
||||
|
||||
### 2. 라벨 미리보기 API (확장)
|
||||
|
||||
**Endpoint:** `POST /pmr/api/label/preview`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"patient_name": "홍길동",
|
||||
"med_name": "오구멘틴듀오시럽",
|
||||
"dosage": 8,
|
||||
"frequency": 3,
|
||||
"duration": 5,
|
||||
"unit": "mL",
|
||||
"sung_code": "535000ASY"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"image": "data:image/png;base64,...",
|
||||
"conversion_factor": 0.11
|
||||
}
|
||||
```
|
||||
|
||||
### 라벨 출력 예시
|
||||
|
||||
환산계수가 있는 경우:
|
||||
```
|
||||
총120mL (13.2g)/5일분
|
||||
```
|
||||
|
||||
환산계수가 없는 경우:
|
||||
```
|
||||
총120mL/5일분
|
||||
```
|
||||
|
||||
## 코드 위치
|
||||
|
||||
### 수정된 파일
|
||||
1. **dbsetup.py** - PostgreSQL 연결 관리
|
||||
- `DatabaseConfig.POSTGRES_URL` 추가
|
||||
- `DatabaseManager.get_postgres_engine()` 추가
|
||||
- `DatabaseManager.get_postgres_session()` 추가
|
||||
- `DatabaseManager.get_conversion_factor(sung_code)` 추가
|
||||
|
||||
2. **app.py** - 환산계수 조회 API
|
||||
- `GET /api/drug-info/conversion-factor/<sung_code>`
|
||||
|
||||
3. **pmr_api.py** - 라벨 미리보기
|
||||
- `preview_label()` - sung_code 파라미터 추가
|
||||
- `create_label_image()` - conversion_factor 파라미터 추가
|
||||
|
||||
## 유료/무료 버전 구분 설계 (추후)
|
||||
|
||||
환산계수는 추후 유료 기능으로 분리 가능합니다.
|
||||
|
||||
### 설계 방안
|
||||
1. **라이선스 체크**
|
||||
- 환산계수 조회 전 라이선스 확인
|
||||
- 무료 버전: `conversion_factor: null` 반환
|
||||
- 유료 버전: 실제 값 반환
|
||||
|
||||
2. **API 분리**
|
||||
- `/api/drug-info/conversion-factor` → 유료 전용
|
||||
- 무료 버전은 API 자체를 비활성화
|
||||
|
||||
3. **현재 구현**
|
||||
- 환산계수가 없어도 라벨 출력 정상 동작
|
||||
- null 체크 후 기존 포맷 유지
|
||||
|
||||
## 예외처리
|
||||
|
||||
| 상황 | 동작 |
|
||||
|------|------|
|
||||
| PostgreSQL 연결 실패 | null 반환, 에러 로그 |
|
||||
| 데이터 없음 | null 반환 |
|
||||
| sung_code 미전달 | 환산계수 조회 skip |
|
||||
| 환산계수 0 또는 음수 | 적용 안 함 (기존 포맷) |
|
||||
|
||||
## 테스트 방법
|
||||
|
||||
```bash
|
||||
# 환산계수 조회 테스트
|
||||
curl http://localhost:7001/api/drug-info/conversion-factor/535000ASY
|
||||
|
||||
# 존재하지 않는 코드 테스트
|
||||
curl http://localhost:7001/api/drug-info/conversion-factor/NOTEXIST
|
||||
|
||||
# 라벨 미리보기 테스트 (PowerShell)
|
||||
$body = @{
|
||||
patient_name = "홍길동"
|
||||
med_name = "오구멘틴듀오시럽"
|
||||
dosage = 8
|
||||
frequency = 3
|
||||
duration = 5
|
||||
unit = "mL"
|
||||
sung_code = "535000ASY"
|
||||
} | ConvertTo-Json
|
||||
|
||||
Invoke-RestMethod -Uri "http://localhost:7001/pmr/api/label/preview" `
|
||||
-Method Post -ContentType "application/json" -Body $body
|
||||
```
|
||||
|
||||
## 변경 이력
|
||||
|
||||
| 날짜 | 내용 |
|
||||
|------|------|
|
||||
| 2026-03-12 | 최초 구현 |
|
||||
Loading…
Reference in New Issue
Block a user