feat: 환자 처방 내역 조회 기능 추가

- 환자 목록에 처방 횟수 표시 (배지)
- 환자별 처방 내역 조회 버튼 추가
- 환자 처방 내역 모달 추가 (통계 + 상세 내역)
  - 총 처방 횟수, 최근 방문일, 총 제수, 총 처방비
  - 전체 처방 내역 테이블 (조제일, 처방명, 상태 등)
  - 각 처방의 상세 보기 기능 연동
- 환자 개별 조회 API 엔드포인트 추가 (GET /api/patients/<id>)
- 환자 편집 버튼 UI 추가 (기능 준비)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
시골약사 2026-02-15 12:45:21 +00:00
parent 724af5000a
commit 45672a125f
3 changed files with 265 additions and 16 deletions

19
app.py
View File

@ -83,6 +83,25 @@ def get_patients():
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/patients/<int:patient_id>', methods=['GET'])
def get_patient(patient_id):
"""환자 개별 조회"""
try:
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT patient_id, name, phone, jumin_no, gender, birth_date, address, notes
FROM patients
WHERE patient_id = ? AND is_active = 1
""", (patient_id,))
patient_row = cursor.fetchone()
if patient_row:
return jsonify({'success': True, 'data': dict(patient_row)})
else:
return jsonify({'success': False, 'error': '환자를 찾을 수 없습니다'}), 404
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/patients', methods=['POST'])
def create_patient():
"""새 환자 등록"""

View File

@ -122,22 +122,53 @@ $(document).ready(function() {
const tbody = $('#patientsList');
tbody.empty();
// 각 환자의 처방 횟수를 가져오기 위해 처방 데이터도 로드
$.get('/api/compounds', function(compoundsResponse) {
const compounds = compoundsResponse.success ? compoundsResponse.data : [];
// 환자별 처방 횟수 계산
const compoundCounts = {};
compounds.forEach(compound => {
if (compound.patient_id) {
compoundCounts[compound.patient_id] = (compoundCounts[compound.patient_id] || 0) + 1;
}
});
response.data.forEach(patient => {
const compoundCount = compoundCounts[patient.patient_id] || 0;
tbody.append(`
<tr>
<td>${patient.name}</td>
<td><strong>${patient.name}</strong></td>
<td>${patient.phone}</td>
<td>${patient.gender === 'M' ? '남' : patient.gender === 'F' ? '여' : '-'}</td>
<td>${patient.birth_date || '-'}</td>
<td>
<span class="badge bg-primary">${compoundCount}</span>
</td>
<td>${patient.notes || '-'}</td>
<td>
<button class="btn btn-sm btn-outline-primary">
<button class="btn btn-sm btn-outline-info view-patient-compounds"
data-id="${patient.patient_id}"
data-name="${patient.name}">
<i class="bi bi-file-medical"></i>
</button>
<button class="btn btn-sm btn-outline-primary edit-patient"
data-id="${patient.patient_id}">
<i class="bi bi-pencil"></i>
</button>
</td>
</tr>
`);
});
// 처방내역 버튼 이벤트
$('.view-patient-compounds').on('click', function() {
const patientId = $(this).data('id');
const patientName = $(this).data('name');
viewPatientCompounds(patientId, patientName);
});
});
}
});
}
@ -173,6 +204,113 @@ $(document).ready(function() {
});
});
// 환자 처방 내역 조회
function viewPatientCompounds(patientId, patientName) {
// 환자 정보 가져오기
$.get(`/api/patients/${patientId}`, function(patientResponse) {
if (patientResponse.success) {
const patient = patientResponse.data;
// 환자 기본 정보 표시
$('#patientCompoundsName').text(patient.name);
$('#patientInfoName').text(patient.name);
$('#patientInfoPhone').text(patient.phone || '-');
$('#patientInfoGender').text(patient.gender === 'M' ? '남성' : patient.gender === 'F' ? '여성' : '-');
$('#patientInfoBirth').text(patient.birth_date || '-');
// 환자의 처방 내역 가져오기
$.get(`/api/patients/${patientId}/compounds`, function(compoundsResponse) {
if (compoundsResponse.success) {
const compounds = compoundsResponse.data;
// 통계 계산
const totalCompounds = compounds.length;
let totalJe = 0;
let totalAmount = 0;
let lastVisit = '-';
if (compounds.length > 0) {
compounds.forEach(c => {
totalJe += c.je_count || 0;
totalAmount += c.sell_price_total || 0;
});
lastVisit = compounds[0].compound_date || '-';
}
// 통계 표시
$('#patientTotalCompounds').text(totalCompounds + '회');
$('#patientLastVisit').text(lastVisit);
$('#patientTotalJe').text(totalJe + '제');
$('#patientTotalAmount').text(formatCurrency(totalAmount));
// 처방 내역 테이블 표시
const tbody = $('#patientCompoundsList');
tbody.empty();
if (compounds.length === 0) {
tbody.append(`
<tr>
<td colspan="10" class="text-center text-muted">처방 내역이 없습니다.</td>
</tr>
`);
} else {
compounds.forEach(compound => {
// 상태 뱃지
let statusBadge = '';
switch(compound.status) {
case 'PREPARED':
statusBadge = '<span class="badge bg-success">조제완료</span>';
break;
case 'DISPENSED':
statusBadge = '<span class="badge bg-primary">출고완료</span>';
break;
case 'CANCELLED':
statusBadge = '<span class="badge bg-danger">취소</span>';
break;
default:
statusBadge = '<span class="badge bg-secondary">대기</span>';
}
tbody.append(`
<tr>
<td>${compound.compound_date || '-'}</td>
<td>${compound.formula_name || '직접조제'}</td>
<td>${compound.je_count || 0}</td>
<td>${compound.cheop_total || 0}</td>
<td>${compound.pouch_total || 0}</td>
<td>${formatCurrency(compound.cost_total || 0)}</td>
<td>${formatCurrency(compound.sell_price_total || 0)}</td>
<td>${statusBadge}</td>
<td>${compound.prescription_no || '-'}</td>
<td>
<button class="btn btn-sm btn-outline-info view-compound-detail"
data-id="${compound.compound_id}">
<i class="bi bi-eye"></i>
</button>
</td>
</tr>
`);
});
// 상세보기 버튼 이벤트
$('.view-compound-detail').on('click', function() {
const compoundId = $(this).data('id');
viewCompoundDetail(compoundId);
});
}
// 모달 표시
$('#patientCompoundsModal').modal('show');
}
}).fail(function() {
alert('처방 내역을 불러오는데 실패했습니다.');
});
}
}).fail(function() {
alert('환자 정보를 불러오는데 실패했습니다.');
});
}
// 처방 목록 로드
function loadFormulas() {
$.get('/api/formulas', function(response) {

View File

@ -205,8 +205,9 @@
<th>전화번호</th>
<th>성별</th>
<th>생년월일</th>
<th width="80">처방 횟수</th>
<th>메모</th>
<th>작업</th>
<th width="180">작업</th>
</tr>
</thead>
<tbody id="patientsList">
@ -1024,6 +1025,97 @@
</div>
</div>
<!-- 환자 처방 내역 모달 -->
<div class="modal fade" id="patientCompoundsModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title">
<i class="bi bi-person-circle"></i>
<span id="patientCompoundsName">환자명</span>의 처방 내역
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<!-- 환자 기본 정보 -->
<div class="card mb-3">
<div class="card-body">
<div class="row">
<div class="col-md-3">
<strong>환자명:</strong> <span id="patientInfoName"></span>
</div>
<div class="col-md-3">
<strong>전화번호:</strong> <span id="patientInfoPhone"></span>
</div>
<div class="col-md-3">
<strong>성별:</strong> <span id="patientInfoGender"></span>
</div>
<div class="col-md-3">
<strong>생년월일:</strong> <span id="patientInfoBirth"></span>
</div>
</div>
</div>
</div>
<!-- 처방 통계 -->
<div class="row mb-3">
<div class="col-md-3">
<div class="stat-card bg-primary text-white">
<h6>총 처방 횟수</h6>
<h4 id="patientTotalCompounds">0회</h4>
</div>
</div>
<div class="col-md-3">
<div class="stat-card bg-success text-white">
<h6>최근 방문</h6>
<h4 id="patientLastVisit">-</h4>
</div>
</div>
<div class="col-md-3">
<div class="stat-card bg-info text-white">
<h6>총 제수</h6>
<h4 id="patientTotalJe">0제</h4>
</div>
</div>
<div class="col-md-3">
<div class="stat-card bg-warning text-white">
<h6>총 처방비</h6>
<h4 id="patientTotalAmount">₩0</h4>
</div>
</div>
</div>
<!-- 처방 내역 테이블 -->
<h6><i class="bi bi-list-ul"></i> 처방 내역</h6>
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead class="table-dark">
<tr>
<th width="100">조제일</th>
<th>처방명</th>
<th width="60">제수</th>
<th width="60">첩수</th>
<th width="80">파우치</th>
<th width="100">원가</th>
<th width="100">판매가</th>
<th width="80">상태</th>
<th width="100">처방전번호</th>
<th width="100">작업</th>
</tr>
</thead>
<tbody id="patientCompoundsList">
<!-- Dynamic content -->
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">닫기</button>
</div>
</div>
</div>
</div>
<!-- Formula Modal -->
<div class="modal fade" id="formulaModal" tabindex="-1">
<div class="modal-dialog modal-lg">