사용자-약국 매핑 시스템 개선 및 UI 업데이트

- 자동 매핑 버그 수정: 이름만으로 자동 연결되던 문제 해결
- 매핑되지 않은 약국 목록 API 추가 (/api/pharmacies/available)
- 사용자 연결 드롭다운에서 매핑 가능한 약국만 표시하도록 개선
- 기존 잘못된 매핑 초기화하여 명시적 링크만 허용
- UI 텍스트 업데이트: "Headscale 사용자 목록" → "PQON 사용자 목록"
- UI 텍스트 업데이트: "Headscale 네트워크 사용자" → "PharmQ-ON 사용자"
- 사이드 메뉴 링크 변경: "Headplane UI" → "Medivault" (https://medivault.co.kr/)
- SQLAlchemy or_ import 추가하여 복합 조건 쿼리 지원

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
시골약사 2025-09-11 11:49:17 +09:00
parent e71cdb2cda
commit 56b72629f9
3 changed files with 77 additions and 12 deletions

View File

@ -17,6 +17,7 @@ from utils.database_new import (
sync_machines_from_headscale, sync_users_from_headscale
)
from models.farmq_models import PharmacyInfo
from sqlalchemy import or_
import subprocess
from utils.proxmox_client import ProxmoxClient
@ -349,6 +350,46 @@ def create_app(config_name=None):
'error': f'서버 오류: {str(e)}'
}), 500
@app.route('/api/pharmacies/available', methods=['GET'])
def api_get_available_pharmacies():
"""매핑되지 않은 약국 목록 가져오기"""
try:
farmq_session = get_farmq_session()
try:
# headscale_user_name이 NULL이거나 빈 문자열인 약국들만 가져오기
pharmacies = farmq_session.query(PharmacyInfo).filter(
or_(
PharmacyInfo.headscale_user_name.is_(None),
PharmacyInfo.headscale_user_name == ''
)
).all()
pharmacy_list = []
for pharmacy in pharmacies:
pharmacy_list.append({
'id': pharmacy.id,
'pharmacy_name': pharmacy.pharmacy_name,
'manager_name': pharmacy.manager_name or '미등록',
'business_number': pharmacy.business_number,
'address': pharmacy.address
})
return jsonify({
'success': True,
'pharmacies': pharmacy_list,
'count': len(pharmacy_list)
})
finally:
farmq_session.close()
except Exception as e:
print(f"❌ 매핑 가능한 약국 목록 조회 오류: {e}")
return jsonify({
'success': False,
'error': f'서버 오류: {str(e)}'
}), 500
@app.route('/api/users/<user_name>/link-pharmacy', methods=['POST'])
def api_link_user_pharmacy(user_name):
"""사용자와 약국 연결"""
@ -624,17 +665,21 @@ def create_app(config_name=None):
users_data = json.loads(result.stdout)
# FARMQ 약국 정보와 매칭
# FARMQ 약국 정보와 매칭 (명시적으로 매핑된 것만)
farmq_session = get_farmq_session()
try:
pharmacies = farmq_session.query(PharmacyInfo).all()
pharmacy_map = {p.headscale_user_name: p for p in pharmacies if p.headscale_user_name}
# 명시적으로 headscale_user_name이 설정되고 해당 사용자가 실제로 존재하는 경우만 매핑
pharmacy_map = {}
for p in pharmacies:
if p.headscale_user_name and p.headscale_user_name.strip():
pharmacy_map[p.headscale_user_name] = p
# 사용자별 노드 수 조회
for user in users_data:
user_name = user.get('name', '')
# 약국 정보 매칭
# 약국 정보 매칭 - 명시적으로 연결된 것만
pharmacy = pharmacy_map.get(user_name)
user['pharmacy'] = {
'id': pharmacy.id,

View File

@ -214,8 +214,8 @@
<!-- 빠른 링크 -->
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" href="http://localhost:3000/admin/" target="_blank">
<i class="fas fa-external-link-alt"></i> Headplane UI
<a class="nav-link" href="https://medivault.co.kr/" target="_blank">
<i class="fas fa-external-link-alt"></i> Medivault
</a>
</li>
<li class="nav-item">

View File

@ -20,7 +20,7 @@
<i class="fas fa-users text-primary"></i>
사용자 관리
</h1>
<p class="text-muted">Headscale 네트워크 사용자 및 약국 매칭 관리</p>
<p class="text-muted">PharmQ-ON 사용자 및 약국 매칭 관리</p>
</div>
<div class="d-flex gap-2">
<button class="btn btn-outline-secondary" onclick="refreshUserList()">
@ -87,7 +87,7 @@
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-list"></i> Headscale 사용자 목록
<i class="fas fa-list"></i> PQON 사용자 목록
</h5>
</div>
<div class="card-body" id="usersTableContainer">
@ -420,13 +420,33 @@ function showLinkPharmacyModal(userName) {
document.getElementById('linkUserName').textContent = userName;
selectedUserId = userName;
// 약국 목록을 셀렉트 박스에 추가
// 매핑 가능한 약국 목록을 API에서 가져오기
const select = document.getElementById('pharmacySelect');
select.innerHTML = '<option value="">약국을 선택하세요</option>';
select.innerHTML = '<option value="">로딩 중...</option>';
currentPharmacies.forEach(pharmacy => {
select.innerHTML += `<option value="${pharmacy.id}">${pharmacy.pharmacy_name} (${pharmacy.manager_name})</option>`;
});
fetch('/api/pharmacies/available')
.then(response => response.json())
.then(data => {
if (data.success) {
select.innerHTML = '<option value="">약국을 선택하세요</option>';
if (data.pharmacies.length === 0) {
select.innerHTML = '<option value="">매핑 가능한 약국이 없습니다</option>';
} else {
data.pharmacies.forEach(pharmacy => {
select.innerHTML += `<option value="${pharmacy.id}">${pharmacy.pharmacy_name} (${pharmacy.manager_name})</option>`;
});
}
} else {
select.innerHTML = '<option value="">약국 목록 로드 실패</option>';
showToast('약국 목록을 불러오는데 실패했습니다.', 'error');
}
})
.catch(error => {
console.error('약국 목록 로드 오류:', error);
select.innerHTML = '<option value="">약국 목록 로드 실패</option>';
showToast('약국 목록을 불러오는데 실패했습니다.', 'error');
});
const modal = new bootstrap.Modal(document.getElementById('linkPharmacyModal'));
modal.show();