fix: 처방목록 조회 기준을 발행일에서 조제일로 변경
문제: - PMR 처방 목록이 PassDay(처방전 발행일) 기준으로 조회되어 발행일과 조제일이 다른 처방(예: 3일 전 발행, 오늘 조제)이 오늘 목록에 표시되지 않는 버그 해결: - PS_MAIN 테이블 조회 시 PassDay 대신 Indate(조제일) 기준으로 변경 - issue_date(발행일), dispense_date(조제일) 필드 추가로 구분 명확화 추가 변경: - WebSocket 연결/해제 시 토스트 알림 추가 - WebSocket 프록시 트러블슈팅 문서 추가 (NPM 설정 가이드)
This commit is contained in:
@@ -161,6 +161,51 @@
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 환자 정보 행 */
|
||||
.detail-header .patient-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* 특이사항 인라인 (환자명 옆) */
|
||||
.detail-header .cusetc-inline {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
max-width: 400px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
.detail-header .cusetc-inline.has-note {
|
||||
background: linear-gradient(135deg, #fae8ff, #f5d0fe);
|
||||
color: #86198f;
|
||||
border: 1px solid #e879f9;
|
||||
}
|
||||
.detail-header .cusetc-inline.no-note {
|
||||
background: #f9fafb;
|
||||
color: #9ca3af;
|
||||
border: 1px dashed #d1d5db;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.detail-header .cusetc-inline:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
.detail-header .cusetc-inline .cusetc-label {
|
||||
font-weight: 600;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.detail-header .cusetc-inline .cusetc-text {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* PAAI 버튼 */
|
||||
.detail-header .rx-info .paai-badge {
|
||||
background: linear-gradient(135deg, #10b981, #059669) !important;
|
||||
@@ -412,6 +457,115 @@
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* 특이사항 모달 */
|
||||
.cusetc-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.6);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.cusetc-modal-content {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
box-shadow: 0 25px 50px rgba(0,0,0,0.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
.cusetc-modal-header {
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||
color: #fff;
|
||||
padding: 18px 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.cusetc-modal-header h3 {
|
||||
font-size: 1.2rem;
|
||||
margin: 0;
|
||||
}
|
||||
.cusetc-modal-close {
|
||||
background: rgba(255,255,255,0.2);
|
||||
border: none;
|
||||
color: #fff;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
font-size: 1.3rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.cusetc-modal-close:hover { background: rgba(255,255,255,0.3); }
|
||||
.cusetc-modal-body {
|
||||
padding: 24px;
|
||||
}
|
||||
.cusetc-patient-info {
|
||||
background: #f9fafb;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 0.95rem;
|
||||
color: #374151;
|
||||
}
|
||||
.cusetc-modal-body textarea {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
padding: 14px;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
transition: border-color 0.2s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.cusetc-modal-body textarea:focus {
|
||||
outline: none;
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
.cusetc-hint {
|
||||
font-size: 0.85rem;
|
||||
color: #6b7280;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.cusetc-modal-footer {
|
||||
padding: 16px 24px;
|
||||
background: #f9fafb;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
.cusetc-btn-cancel {
|
||||
padding: 10px 20px;
|
||||
background: #fff;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.cusetc-btn-cancel:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
.cusetc-btn-save {
|
||||
padding: 10px 24px;
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.cusetc-btn-save:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.4);
|
||||
}
|
||||
|
||||
/* OTC 모달 */
|
||||
.otc-modal {
|
||||
display: none;
|
||||
@@ -820,8 +974,11 @@
|
||||
<!-- 오른쪽: 처방 상세 -->
|
||||
<div class="prescription-detail">
|
||||
<div class="detail-header" id="detailHeader" style="display:none;">
|
||||
<div class="patient-name" id="detailName">-</div>
|
||||
<div class="patient-info" id="detailInfo">-</div>
|
||||
<div class="patient-row">
|
||||
<div class="patient-name" id="detailName">-</div>
|
||||
<div class="patient-info" id="detailInfo">-</div>
|
||||
<div class="cusetc-inline" id="cusetcInline" onclick="openCusetcModal()"></div>
|
||||
</div>
|
||||
<div class="rx-info" id="rxInfo"></div>
|
||||
</div>
|
||||
<div class="medication-list" id="medicationList">
|
||||
@@ -871,6 +1028,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 특이사항 모달 -->
|
||||
<div class="cusetc-modal" id="cusetcModal">
|
||||
<div class="cusetc-modal-content">
|
||||
<div class="cusetc-modal-header">
|
||||
<h3>📝 환자 특이사항</h3>
|
||||
<button class="cusetc-modal-close" onclick="closeCusetcModal()">×</button>
|
||||
</div>
|
||||
<div class="cusetc-modal-body">
|
||||
<div class="cusetc-patient-info" id="cusetcPatientInfo"></div>
|
||||
<textarea id="cusetcTextarea" placeholder="특이사항을 입력하세요... (예: 약 삼키기 어려움, 당뇨 주의, 수유 중 등)"></textarea>
|
||||
<div class="cusetc-hint">💡 Tip: 복약지도 시 참고할 정보를 기록하세요</div>
|
||||
</div>
|
||||
<div class="cusetc-modal-footer">
|
||||
<button class="cusetc-btn-cancel" onclick="closeCusetcModal()">취소</button>
|
||||
<button class="cusetc-btn-save" onclick="saveCusetc()">💾 저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OTC 구매 이력 모달 -->
|
||||
<div class="otc-modal" id="otcModal">
|
||||
<div class="otc-modal-content">
|
||||
@@ -1001,6 +1177,44 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 환자 목록 새로고침 (현재 선택 유지)
|
||||
async function refreshPatientList() {
|
||||
const container = document.getElementById('patientItems');
|
||||
const date = document.getElementById('dateSelect').value;
|
||||
const selectedId = currentPrescriptionId; // 현재 선택된 환자 ID 저장
|
||||
|
||||
try {
|
||||
const res = await fetch(`/pmr/api/prescriptions?date=${date}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success && data.prescriptions.length > 0) {
|
||||
container.innerHTML = data.prescriptions.map(p => `
|
||||
<div class="patient-card ${p.prescription_id === selectedId ? 'active' : ''}"
|
||||
onclick="selectPatient('${p.prescription_id}', this)"
|
||||
data-id="${p.prescription_id}">
|
||||
<div class="top">
|
||||
<span class="name">${p.patient_name || '이름없음'}</span>
|
||||
<span class="order">${p.order_number || '-'}</span>
|
||||
</div>
|
||||
<div class="info">
|
||||
${p.age ? p.age + '세' : ''} ${p.gender || ''}
|
||||
${p.time ? '• ' + p.time : ''}
|
||||
</div>
|
||||
<div class="hospital">${p.hospital || ''} ${p.doctor ? '(' + p.doctor + ')' : ''}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
document.getElementById('patientCount').textContent = data.count + '명';
|
||||
|
||||
// 통계도 갱신
|
||||
loadStats(date);
|
||||
|
||||
console.log('[PMR] 환자 목록 갱신됨:', data.count + '명');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('목록 갱신 오류:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 환자 선택
|
||||
async function selectPatient(prescriptionId, element) {
|
||||
// UI 활성화
|
||||
@@ -1019,13 +1233,14 @@
|
||||
// PAAI용 처방 데이터 저장
|
||||
currentPrescriptionData = {
|
||||
pre_serial: prescriptionId,
|
||||
cus_code: data.patient.cus_code,
|
||||
cus_code: data.patient.cus_code || data.patient.code,
|
||||
name: data.patient.name,
|
||||
st1: data.disease_info?.code_1 || '',
|
||||
st1_name: data.disease_info?.name_1 || '',
|
||||
st2: data.disease_info?.code_2 || '',
|
||||
st2_name: data.disease_info?.name_2 || '',
|
||||
medications: data.medications || []
|
||||
medications: data.medications || [],
|
||||
cusetc: data.patient.cusetc || '' // 특이사항
|
||||
};
|
||||
|
||||
// 헤더 업데이트
|
||||
@@ -1046,6 +1261,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 특이사항 (환자명 옆 인라인 표시)
|
||||
const cusetc = data.patient.cusetc || '';
|
||||
const cusetcInline = document.getElementById('cusetcInline');
|
||||
if (cusetc) {
|
||||
cusetcInline.className = 'cusetc-inline has-note';
|
||||
cusetcInline.innerHTML = `<span class="cusetc-label">📝</span><span class="cusetc-text">${escapeHtml(cusetc)}</span>`;
|
||||
cusetcInline.title = cusetc;
|
||||
} else {
|
||||
cusetcInline.className = 'cusetc-inline no-note';
|
||||
cusetcInline.innerHTML = '+ 특이사항 추가';
|
||||
cusetcInline.title = '';
|
||||
}
|
||||
|
||||
document.getElementById('rxInfo').innerHTML = `
|
||||
<span>🏥 ${data.prescription.hospital || '-'}</span>
|
||||
<span>👨⚕️ ${data.prescription.doctor || '-'}</span>
|
||||
@@ -1375,6 +1603,8 @@
|
||||
|
||||
// OTC 구매 이력 체크
|
||||
async function checkOtcHistory(cusCode) {
|
||||
const preSerial = currentPrescriptionData?.pre_serial;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/pmr/api/patient/${cusCode}/otc?limit=20`);
|
||||
const data = await res.json();
|
||||
@@ -1389,12 +1619,17 @@
|
||||
otcData = null;
|
||||
}
|
||||
|
||||
// PAAI 버튼 추가 (항상 표시)
|
||||
// 서버에서 기존 PAAI 결과 확인 (트리거가 미리 생성한 결과)
|
||||
await checkPaaiStatus(preSerial);
|
||||
|
||||
// PAAI 버튼 추가 (캐시/진행중/기본 상태에 따라)
|
||||
addPaaiButton();
|
||||
} catch (err) {
|
||||
console.error('OTC check error:', err);
|
||||
otcData = null;
|
||||
// OTC 오류여도 PAAI 버튼은 추가
|
||||
|
||||
// OTC 오류여도 PAAI 상태 확인 및 버튼 추가
|
||||
await checkPaaiStatus(preSerial);
|
||||
addPaaiButton();
|
||||
}
|
||||
}
|
||||
@@ -1468,6 +1703,88 @@
|
||||
document.getElementById('otcModal').style.display = 'none';
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 특이사항 모달 함수들
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
function openCusetcModal() {
|
||||
if (!currentPrescriptionData) return;
|
||||
|
||||
const modal = document.getElementById('cusetcModal');
|
||||
const patientInfo = document.getElementById('cusetcPatientInfo');
|
||||
const textarea = document.getElementById('cusetcTextarea');
|
||||
|
||||
// 환자 정보 표시
|
||||
patientInfo.innerHTML = `
|
||||
<strong>${currentPrescriptionData.name || '환자'}</strong>
|
||||
<span style="margin-left: 10px; color: #6b7280;">고객코드: ${currentPrescriptionData.cus_code || '-'}</span>
|
||||
`;
|
||||
|
||||
// 기존 특이사항 로드
|
||||
textarea.value = currentPrescriptionData.cusetc || '';
|
||||
|
||||
// 모달 표시
|
||||
modal.style.display = 'flex';
|
||||
textarea.focus();
|
||||
}
|
||||
|
||||
function closeCusetcModal() {
|
||||
document.getElementById('cusetcModal').style.display = 'none';
|
||||
}
|
||||
|
||||
async function saveCusetc() {
|
||||
if (!currentPrescriptionData || !currentPrescriptionData.cus_code) {
|
||||
alert('❌ 환자 정보가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
const textarea = document.getElementById('cusetcTextarea');
|
||||
const newCusetc = textarea.value.trim();
|
||||
const cusCode = currentPrescriptionData.cus_code;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/members/${cusCode}/cusetc`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ cusetc: newCusetc })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
// 로컬 데이터 업데이트
|
||||
currentPrescriptionData.cusetc = newCusetc;
|
||||
|
||||
// 뱃지 업데이트
|
||||
updateCusetcBadge(newCusetc);
|
||||
|
||||
closeCusetcModal();
|
||||
|
||||
// 토스트 알림
|
||||
showPaaiToast(currentPrescriptionData.name, '특이사항이 저장되었습니다.', 'completed');
|
||||
} else {
|
||||
alert('❌ ' + (data.error || '저장 실패'));
|
||||
}
|
||||
} catch (err) {
|
||||
alert('❌ 오류: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function updateCusetcBadge(cusetc) {
|
||||
const cusetcInline = document.getElementById('cusetcInline');
|
||||
if (!cusetcInline) return;
|
||||
|
||||
if (cusetc) {
|
||||
cusetcInline.className = 'cusetc-inline has-note';
|
||||
cusetcInline.innerHTML = `<span class="cusetc-label">📝</span><span class="cusetc-text">${escapeHtml(cusetc)}</span>`;
|
||||
cusetcInline.title = cusetc;
|
||||
} else {
|
||||
cusetcInline.className = 'cusetc-inline no-note';
|
||||
cusetcInline.innerHTML = '+ 특이사항 추가';
|
||||
cusetcInline.title = '';
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// PAAI (Pharmacist Assistant AI) 함수들 - 비동기 토스트 방식
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
@@ -1476,6 +1793,43 @@
|
||||
const paaiResultCache = {}; // 환자별 분석 결과 캐시: { pre_serial: { result, patientName } }
|
||||
const paaiPendingRequests = new Set(); // 진행 중인 요청
|
||||
|
||||
// 서버에서 기존 PAAI 결과 조회 (트리거 모듈이 미리 생성한 결과)
|
||||
async function checkPaaiStatus(preSerial) {
|
||||
if (!preSerial) return;
|
||||
|
||||
// 이미 캐시에 있으면 스킵
|
||||
if (paaiResultCache[preSerial]) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/pmr/api/paai/result/${preSerial}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.exists) {
|
||||
if (data.status === 'success' && data.result) {
|
||||
// 성공한 결과 캐시에 저장
|
||||
const patientName = data.result.patient_name || '환자';
|
||||
paaiResultCache[preSerial] = {
|
||||
result: {
|
||||
success: true,
|
||||
log_id: data.log_id,
|
||||
analysis: data.result.analysis,
|
||||
kims_summary: data.result.kims_summary,
|
||||
timing: data.result.timing
|
||||
},
|
||||
patientName: patientName
|
||||
};
|
||||
console.log(`[PAAI] 기존 결과 로드: ${preSerial} (${patientName})`);
|
||||
} else if (data.status === 'generating') {
|
||||
// 생성 중인 요청으로 표시
|
||||
paaiPendingRequests.add(preSerial);
|
||||
console.log(`[PAAI] 생성 중: ${preSerial}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('PAAI status check error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function addPaaiButton() {
|
||||
const rxInfo = document.getElementById('rxInfo');
|
||||
if (!rxInfo || rxInfo.querySelector('.paai-badge')) return;
|
||||
@@ -1967,7 +2321,12 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
(function() {
|
||||
const TRIGGER_WS_URL = 'ws://localhost:8765';
|
||||
// WebSocket 연결 URL 결정
|
||||
// - HTTPS(프록시) 접속 → wss:// 프록시 경로 사용
|
||||
// - HTTP(내부) 접속 → ws:// 직접 연결
|
||||
const TRIGGER_WS_URL = location.protocol === 'https:'
|
||||
? `wss://${location.host}/ws` // NPM 프록시 경로
|
||||
: `ws://${location.hostname}:8765`; // 내부 직접 연결
|
||||
const TRIGGER_DEBUG = true;
|
||||
|
||||
let triggerWs = null;
|
||||
@@ -1991,6 +2350,7 @@
|
||||
triggerLog('✅ 연결됨');
|
||||
triggerConnected = true;
|
||||
updateTriggerIndicator(true);
|
||||
showToast('🔗 처방감지 연결됨', 'success');
|
||||
if (triggerReconnectTimer) {
|
||||
clearTimeout(triggerReconnectTimer);
|
||||
triggerReconnectTimer = null;
|
||||
@@ -2012,6 +2372,7 @@
|
||||
triggerConnected = false;
|
||||
triggerWs = null;
|
||||
updateTriggerIndicator(false);
|
||||
showToast('🔌 처방감지 연결 끊김 (재연결 중...)', 'warning');
|
||||
|
||||
// 3초 후 재연결
|
||||
triggerReconnectTimer = setTimeout(() => {
|
||||
@@ -2036,9 +2397,13 @@
|
||||
switch (event) {
|
||||
case 'prescription_detected':
|
||||
showTriggerToast(data.pre_serial, data.patient_name, 'pending', '처방 감지됨...', '📋');
|
||||
// 환자 목록 자동 갱신 (현재 선택 유지)
|
||||
refreshPatientList();
|
||||
break;
|
||||
case 'prescription_updated':
|
||||
showTriggerToast(data.pre_serial, data.patient_name, 'pending', '처방 수정됨, 재분석...', '🔄');
|
||||
// 환자 목록 갱신 (수정된 정보 반영)
|
||||
refreshPatientList();
|
||||
break;
|
||||
case 'prescription_deleted':
|
||||
removeTriggerToast(data.pre_serial);
|
||||
|
||||
Reference in New Issue
Block a user