feat: 위치 편집 기능 추가
API: - GET /api/locations - 모든 위치 목록 (461개) - PUT /api/drugs/<code>/location - 위치 업데이트/삭제 UI: - 위치 있음: 노란색 뱃지 (클릭 가능) - 위치 없음: '미지정' 회색 점선 뱃지 - 클릭 시 위치 설정 모달 열림 - 드롭다운 선택 또는 직접 입력 - person-lookup-web-local 참고하여 구현
This commit is contained in:
@@ -405,7 +405,148 @@
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.location-badge:hover {
|
||||
background: #fcd34d;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
.location-badge.unset {
|
||||
background: #f1f5f9;
|
||||
color: #94a3b8;
|
||||
border: 1px dashed #cbd5e1;
|
||||
}
|
||||
.location-badge.unset:hover {
|
||||
background: #e2e8f0;
|
||||
border-color: #8b5cf6;
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
/* 위치 모달 */
|
||||
.location-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.6);
|
||||
z-index: 2000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.location-modal.show { display: flex; }
|
||||
.location-modal-content {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
animation: modalSlideIn 0.2s ease;
|
||||
}
|
||||
@keyframes modalSlideIn {
|
||||
from { opacity: 0; transform: translateY(-20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.location-modal-content h3 {
|
||||
margin: 0 0 16px 0;
|
||||
color: #92400e;
|
||||
font-size: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.location-product-info {
|
||||
background: #fffbeb;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #fef3c7;
|
||||
}
|
||||
.location-product-info .name {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.location-product-info .code {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
.location-select-wrapper {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.location-select-wrapper label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.location-select {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.location-select:focus {
|
||||
outline: none;
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
.location-input-wrapper {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.location-input-wrapper label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.location-input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.location-input:focus {
|
||||
outline: none;
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
.location-hint {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.location-modal-btns {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.location-modal-btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.location-modal-btn.secondary { background: #f1f5f9; color: #64748b; }
|
||||
.location-modal-btn.secondary:hover { background: #e2e8f0; }
|
||||
.location-modal-btn.danger { background: #fef2f2; color: #dc2626; }
|
||||
.location-modal-btn.danger:hover { background: #fee2e2; }
|
||||
.location-modal-btn.primary { background: #f59e0b; color: #fff; }
|
||||
.location-modal-btn.primary:hover { background: #d97706; }
|
||||
|
||||
/* ── 가격 ── */
|
||||
.price {
|
||||
@@ -905,7 +1046,9 @@
|
||||
<td>${item.barcode
|
||||
? `<span class="code code-barcode">${item.barcode}</span>`
|
||||
: `<span class="code code-na">없음</span>`}</td>
|
||||
<td>${item.location ? `<span class="location-badge">${escapeHtml(item.location)}</span>` : ''}</td>
|
||||
<td>${item.location
|
||||
? `<span class="location-badge" onclick="openLocationModal('${item.drug_code}', '${escapeHtml(item.product_name)}', '${escapeHtml(item.location)}')">${escapeHtml(item.location)}</span>`
|
||||
: `<span class="location-badge unset" onclick="openLocationModal('${item.drug_code}', '${escapeHtml(item.product_name)}', '')">미지정</span>`}</td>
|
||||
<td class="stock ${(item.stock || 0) > 0 ? 'in-stock' : 'out-stock'}">${item.stock || 0}${wsStock}</td>
|
||||
<td class="price">${formatPrice(item.sale_price)}</td>
|
||||
<td>
|
||||
@@ -1402,5 +1545,124 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 위치 편집 모달 -->
|
||||
<div class="location-modal" id="locationModal">
|
||||
<div class="location-modal-content">
|
||||
<h3>📍 위치 설정</h3>
|
||||
<div class="location-product-info">
|
||||
<div class="name" id="locModalProductName">제품명</div>
|
||||
<div class="code" id="locModalDrugCode">상품코드</div>
|
||||
</div>
|
||||
|
||||
<div class="location-select-wrapper">
|
||||
<label>기존 위치에서 선택</label>
|
||||
<select class="location-select" id="locationSelect" onchange="onLocationSelectChange()">
|
||||
<option value="">-- 선택하세요 --</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="location-input-wrapper">
|
||||
<label>또는 직접 입력</label>
|
||||
<input type="text" class="location-input" id="locationInput"
|
||||
placeholder="예: 진열대1-3, 냉장고, 창고A" maxlength="20">
|
||||
<div class="location-hint">최대 20자 / 새 위치를 입력하면 목록에 추가됩니다</div>
|
||||
</div>
|
||||
|
||||
<div class="location-modal-btns">
|
||||
<button class="location-modal-btn danger" onclick="clearLocation()" id="locClearBtn" style="display:none;">삭제</button>
|
||||
<div style="flex:1;"></div>
|
||||
<button class="location-modal-btn secondary" onclick="closeLocationModal()">취소</button>
|
||||
<button class="location-modal-btn primary" onclick="saveLocation()">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ── 위치 모달 ──
|
||||
let locModalDrugCode = null;
|
||||
let locModalCurrentLocation = null;
|
||||
let allLocations = [];
|
||||
|
||||
async function openLocationModal(drugCode, productName, currentLocation) {
|
||||
locModalDrugCode = drugCode;
|
||||
locModalCurrentLocation = currentLocation;
|
||||
|
||||
document.getElementById('locModalProductName').textContent = productName;
|
||||
document.getElementById('locModalDrugCode').textContent = drugCode;
|
||||
document.getElementById('locationInput').value = currentLocation || '';
|
||||
|
||||
// 삭제 버튼 표시 (현재 위치가 있을 때만)
|
||||
document.getElementById('locClearBtn').style.display = currentLocation ? 'block' : 'none';
|
||||
|
||||
// 위치 목록 로드
|
||||
try {
|
||||
const res = await fetch('/api/locations');
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
allLocations = data.locations || [];
|
||||
const select = document.getElementById('locationSelect');
|
||||
select.innerHTML = '<option value="">-- 선택하세요 --</option>' +
|
||||
allLocations.map(loc =>
|
||||
`<option value="${escapeHtml(loc)}" ${loc === currentLocation ? 'selected' : ''}>${escapeHtml(loc)}</option>`
|
||||
).join('');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('위치 목록 로드 실패:', e);
|
||||
}
|
||||
|
||||
document.getElementById('locationModal').classList.add('show');
|
||||
document.getElementById('locationInput').focus();
|
||||
}
|
||||
|
||||
function closeLocationModal() {
|
||||
document.getElementById('locationModal').classList.remove('show');
|
||||
locModalDrugCode = null;
|
||||
locModalCurrentLocation = null;
|
||||
}
|
||||
|
||||
function onLocationSelectChange() {
|
||||
const selected = document.getElementById('locationSelect').value;
|
||||
if (selected) {
|
||||
document.getElementById('locationInput').value = selected;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveLocation() {
|
||||
const newLocation = document.getElementById('locationInput').value.trim();
|
||||
|
||||
if (!locModalDrugCode) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/drugs/${locModalDrugCode}/location`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ location_name: newLocation })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
showToast(newLocation ? `✅ 위치가 "${newLocation}"(으)로 설정되었습니다` : '✅ 위치가 삭제되었습니다', 'success');
|
||||
closeLocationModal();
|
||||
searchProducts(); // 테이블 새로고침
|
||||
} else {
|
||||
showToast(data.error || '저장 실패', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('오류: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function clearLocation() {
|
||||
if (!confirm('위치 정보를 삭제하시겠습니까?')) return;
|
||||
document.getElementById('locationInput').value = '';
|
||||
await saveLocation();
|
||||
}
|
||||
|
||||
// 모달 외부 클릭 시 닫기
|
||||
document.getElementById('locationModal')?.addEventListener('click', e => {
|
||||
if (e.target.id === 'locationModal') closeLocationModal();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user