feat(products): 모바일 이미지 업로드 QR 시스템 추가
- API 3개 추가:
- POST /api/upload-session (세션 생성)
- GET /api/upload-session/{id} (상태 확인/폴링)
- POST /api/upload-session/{id}/image (이미지 업로드)
- 모바일 업로드 페이지 (/upload/{session_id})
- 이미지 등록 모달에 '📱 모바일' 탭 추가
- QR 스캔 → 모바일 촬영 → PC 실시간 반영
- 2초 폴링으로 업로드 완료 감지
- 세션 10분 만료, 메모리 기반 관리
- Edit 툴로 부분 수정하여 인코딩 유지
This commit is contained in:
@@ -1710,6 +1710,7 @@
|
||||
|
||||
function closeImageModal() {
|
||||
stopCamera();
|
||||
stopQrPolling();
|
||||
document.getElementById('imageModal').classList.remove('show');
|
||||
imgModalBarcode = null;
|
||||
imgModalDrugCode = null;
|
||||
@@ -1721,6 +1722,106 @@
|
||||
document.querySelectorAll('.img-tab-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.tab === tab));
|
||||
document.querySelectorAll('.img-tab-content').forEach(c => c.classList.toggle('active', c.id === 'imgTab' + tab.charAt(0).toUpperCase() + tab.slice(1)));
|
||||
if (tab === 'camera') startCamera(); else stopCamera();
|
||||
if (tab === 'qr') startQrSession(); else stopQrPolling();
|
||||
}
|
||||
|
||||
// ═══ QR 모바일 업로드 ═══
|
||||
let qrSessionId = null;
|
||||
let qrPollingInterval = null;
|
||||
|
||||
async function startQrSession() {
|
||||
const barcode = imgModalBarcode;
|
||||
if (!barcode) return;
|
||||
|
||||
document.getElementById('qrLoading').style.display = 'flex';
|
||||
document.getElementById('qrCanvas').style.display = 'none';
|
||||
document.getElementById('qrStatus').style.display = 'none';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/upload-session', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ barcode })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
qrSessionId = data.session_id;
|
||||
generateQrCode(data.qr_url);
|
||||
startQrPolling();
|
||||
} else {
|
||||
document.getElementById('qrLoading').textContent = '❌ QR 생성 실패';
|
||||
}
|
||||
} catch (err) {
|
||||
document.getElementById('qrLoading').textContent = '❌ 네트워크 오류';
|
||||
}
|
||||
}
|
||||
|
||||
function generateQrCode(url) {
|
||||
const canvas = document.getElementById('qrCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.width = 180;
|
||||
canvas.height = 180;
|
||||
|
||||
// QR 라이브러리 없이 API 사용
|
||||
const img = new Image();
|
||||
img.onload = function() {
|
||||
ctx.drawImage(img, 0, 0, 180, 180);
|
||||
document.getElementById('qrLoading').style.display = 'none';
|
||||
canvas.style.display = 'block';
|
||||
};
|
||||
img.onerror = function() {
|
||||
document.getElementById('qrLoading').textContent = '❌ QR 생성 실패';
|
||||
};
|
||||
img.src = 'https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=' + encodeURIComponent(url);
|
||||
}
|
||||
|
||||
function startQrPolling() {
|
||||
stopQrPolling();
|
||||
qrPollingInterval = setInterval(checkQrSession, 2000);
|
||||
}
|
||||
|
||||
function stopQrPolling() {
|
||||
if (qrPollingInterval) {
|
||||
clearInterval(qrPollingInterval);
|
||||
qrPollingInterval = null;
|
||||
}
|
||||
qrSessionId = null;
|
||||
}
|
||||
|
||||
async function checkQrSession() {
|
||||
if (!qrSessionId) return;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/upload-session/' + qrSessionId);
|
||||
const data = await res.json();
|
||||
|
||||
const statusEl = document.getElementById('qrStatus');
|
||||
|
||||
if (data.status === 'uploaded') {
|
||||
statusEl.style.display = 'block';
|
||||
statusEl.style.background = '#d1fae5';
|
||||
statusEl.style.color = '#065f46';
|
||||
statusEl.textContent = '✅ 이미지 업로드 완료!';
|
||||
|
||||
stopQrPolling();
|
||||
|
||||
// 1.5초 후 모달 닫고 목록 새로고침
|
||||
setTimeout(() => {
|
||||
closeImageModal();
|
||||
searchProducts();
|
||||
showToast('📱 모바일 이미지 등록 완료!', 'success');
|
||||
}, 1500);
|
||||
} else if (data.status === 'expired') {
|
||||
statusEl.style.display = 'block';
|
||||
statusEl.style.background = '#fee2e2';
|
||||
statusEl.style.color = '#991b1b';
|
||||
statusEl.textContent = '⏰ 세션 만료 - 탭을 다시 선택하세요';
|
||||
stopQrPolling();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('QR 세션 확인 오류:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function startCamera() {
|
||||
@@ -1840,8 +1941,9 @@
|
||||
</div>
|
||||
|
||||
<div class="image-modal-tabs">
|
||||
<button class="img-tab-btn active" data-tab="url" onclick="switchImageTab('url')">🔗 URL 입력</button>
|
||||
<button class="img-tab-btn" data-tab="camera" onclick="switchImageTab('camera')">📸 촬영</button>
|
||||
<button class="img-tab-btn active" data-tab="url" onclick="switchImageTab('url')">🔗 URL</button>
|
||||
<button class="img-tab-btn" data-tab="camera" onclick="switchImageTab('camera')">📸 PC촬영</button>
|
||||
<button class="img-tab-btn" data-tab="qr" onclick="switchImageTab('qr')">📱 모바일</button>
|
||||
</div>
|
||||
|
||||
<div class="img-tab-content active" id="imgTabUrl">
|
||||
@@ -1871,6 +1973,22 @@
|
||||
<button class="img-modal-btn primary" onclick="submitCapturedImage()">저장하기</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="img-tab-content" id="imgTabQr">
|
||||
<div style="text-align:center;padding:20px 0;">
|
||||
<div id="qrContainer" style="display:inline-block;background:#fff;padding:16px;border-radius:12px;box-shadow:0 2px 12px rgba(0,0,0,0.1);">
|
||||
<div id="qrLoading" style="width:180px;height:180px;display:flex;align-items:center;justify-content:center;color:#94a3b8;">
|
||||
QR 생성 중...
|
||||
</div>
|
||||
<canvas id="qrCanvas" style="display:none;"></canvas>
|
||||
</div>
|
||||
<p style="margin-top:16px;color:#64748b;font-size:14px;">📱 휴대폰으로 QR 스캔하여 촬영</p>
|
||||
<div id="qrStatus" style="margin-top:12px;padding:8px 16px;border-radius:8px;font-size:13px;display:none;"></div>
|
||||
</div>
|
||||
<div class="img-modal-btns">
|
||||
<button class="img-modal-btn secondary" onclick="closeImageModal()">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user