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:
thug0bin
2026-03-08 13:40:12 +09:00
parent a7bcf46aaa
commit 4614fc4c0d
2 changed files with 468 additions and 2 deletions

View File

@@ -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>