feat(반품관리): 위치 지정 기능 추가

- 위치 뱃지 클릭 시 위치 수정 모달 표시
- '미지정' 뱃지 스타일 (점선 테두리, 클릭 유도)
- 기존 위치 선택 드롭다운 + 직접 입력 가능
- 위치 삭제 기능
- products 페이지와 동일한 API 재활용 (/api/locations, /api/drugs/.../location)
- 다크 테마에 맞는 모달 스타일
- Edit 툴로 부분 수정하여 인코딩 유지
This commit is contained in:
thug0bin 2026-03-08 12:45:06 +09:00
parent e82f4be4af
commit a7bcf46aaa

View File

@ -327,8 +327,39 @@
padding: 4px 8px;
border-radius: 6px;
font-family: 'JetBrains Mono', monospace;
cursor: pointer;
transition: all 0.15s;
}
.location-empty { color: var(--text-muted); font-size: 12px; }
.location-badge:hover { background: rgba(251, 191, 36, 0.4); transform: scale(1.05); }
.location-badge.unset {
background: rgba(100, 116, 139, 0.2);
color: var(--text-muted);
border: 1px dashed var(--border);
}
.location-badge.unset:hover { background: rgba(168, 85, 247, 0.2); border-color: var(--accent-purple); color: var(--accent-purple); }
/* 위치 모달 */
.location-modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 2000; align-items: center; justify-content: center; backdrop-filter: blur(4px); }
.location-modal.show { display: flex; }
.location-modal-content { background: var(--bg-card); border-radius: 16px; padding: 24px; max-width: 400px; width: 90%; border: 1px solid var(--border); box-shadow: 0 20px 60px rgba(0,0,0,0.5); animation: locModalIn 0.2s ease; }
@keyframes locModalIn { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } }
.location-modal-content h3 { margin: 0 0 16px 0; color: var(--accent-amber); font-size: 18px; display: flex; align-items: center; gap: 8px; }
.location-product-info { background: var(--bg-secondary); border-radius: 8px; padding: 12px; margin-bottom: 16px; border: 1px solid var(--border); }
.location-product-info .name { font-weight: 600; color: var(--text-primary); margin-bottom: 4px; }
.location-product-info .code { font-size: 12px; color: var(--text-muted); font-family: 'JetBrains Mono', monospace; }
.location-select-wrapper, .location-input-wrapper { margin-bottom: 12px; }
.location-select-wrapper label, .location-input-wrapper label { display: block; font-size: 12px; font-weight: 500; color: var(--text-secondary); margin-bottom: 6px; }
.location-select, .location-input { width: 100%; padding: 12px; border: 2px solid var(--border); border-radius: 8px; font-size: 14px; font-family: inherit; background: var(--bg-primary); color: var(--text-primary); transition: border-color 0.2s; }
.location-select:focus, .location-input:focus { outline: none; border-color: var(--accent-amber); }
.location-hint { font-size: 11px; color: var(--text-muted); 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: var(--bg-secondary); color: var(--text-secondary); }
.location-modal-btn.secondary:hover { background: var(--bg-card-hover); }
.location-modal-btn.danger { background: rgba(244, 63, 94, 0.2); color: var(--accent-rose); }
.location-modal-btn.danger:hover { background: rgba(244, 63, 94, 0.3); }
.location-modal-btn.primary { background: linear-gradient(135deg, var(--accent-amber), #d97706); color: #fff; }
.location-modal-btn.primary:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(245, 158, 11, 0.4); }
/* 긴급도 배지 */
.urgency-badge {
@ -920,6 +951,34 @@
</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 allData = [];
let currentPage = 1;
@ -1059,7 +1118,11 @@
<span class="drug-code">${item.drug_code}</span>
</div>
</td>
<td class="location-cell">${item.location ? `<span class="location-badge">${escapeHtml(item.location)}</span>` : '<span class="location-empty">-</span>'}</td>
<td class="location-cell">
${item.location
? `<span class="location-badge" onclick="event.stopPropagation();openLocationModal('${item.drug_code}', '${escapeHtml(item.drug_name).replace(/'/g, "\\'")}', '${escapeHtml(item.location).replace(/'/g, "\\'")}')">${escapeHtml(item.location)}</span>`
: `<span class="location-badge unset" onclick="event.stopPropagation();openLocationModal('${item.drug_code}', '${escapeHtml(item.drug_name).replace(/'/g, "\\'")}', '')">미지정</span>`}
</td>
<td class="qty-cell">${item.current_stock || 0}</td>
<td class="amount-cell ${item.unit_price ? '' : 'zero'}">${formatPrice(item.unit_price)}</td>
<td class="amount-cell ${hasAmount ? '' : 'zero'}">${hasAmount ? '₩' + Math.round(item.recoverable_amount).toLocaleString() : '-'}</td>
@ -1284,6 +1347,91 @@
document.getElementById('purchaseModal').addEventListener('click', function(e) {
if (e.target === this) closePurchaseModal();
});
// ══════════════════ 위치 모달 ══════════════════
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();
loadData(); // 테이블 새로고침
} 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', function(e) {
if (e.target === this) closeLocationModal();
});
</script>
</body>
</html>