Store photos on server, save path in Excel instead of base64

- Add POST /upload endpoint to server.py (saves to photos/ dir)
- Add DELETE /photos/ endpoint for photo removal
- Replace base64 photoData with server-stored photoPath
- Export/import uses path string instead of large base64 data
This commit is contained in:
2026-05-15 19:08:21 +09:00
parent beee0eac64
commit 9cf163908f
2 changed files with 96 additions and 36 deletions

View File

@@ -458,35 +458,26 @@ function esc(s) {
// ═══════════════════════════════════════════════════════════
// ─── Photo ─────────────────────────────────────────────
// ═══════════════════════════════════════════════════════════
function compressImage(file, maxW = 400, quality = 0.5) {
return new Promise((resolve) => {
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = () => {
URL.revokeObjectURL(url);
let w = img.width, h = img.height;
if (w > maxW) { h = h * maxW / w; w = maxW; }
const c = document.createElement('canvas');
c.width = w; c.height = h;
const ctx = c.getContext('2d');
ctx.drawImage(img, 0, 0, w, h);
resolve(c.toDataURL('image/jpeg', quality));
};
img.src = url;
});
}
async function handlePhotoUpload(flagId, file) {
const flag = flags.find(f => f.id === flagId);
if (!flag || !file) return;
flag.photoData = await compressImage(file);
// remove old photo from server first
if (flag.photoPath) await fetch(flag.photoPath, { method: 'DELETE' }).catch(() => {});
const fd = new FormData();
fd.append('photo', file);
try {
const resp = await fetch('/upload', { method: 'POST', body: fd });
const data = await resp.json();
flag.photoPath = data.path;
} catch { flag.photoPath = null; }
renderList();
}
function removePhoto(flagId) {
async function removePhoto(flagId) {
const flag = flags.find(f => f.id === flagId);
if (!flag) return;
flag.photoData = null;
if (flag.photoPath) await fetch(flag.photoPath, { method: 'DELETE' }).catch(() => {});
flag.photoPath = null;
renderList();
}
@@ -555,12 +546,12 @@ function renderList() {
<span>${flag.address ? esc(flag.address) : '<span class="address-loading">Loading…</span>'}</span></div>`;
}
bHTML += `<div class="photo-section"><div class="photo-label">📷 Photo</div>`;
if (flag.photoData) bHTML += `<div class="photo-preview"><img src="${flag.photoData}" alt="Photo" /></div>`;
if (flag.photoPath) bHTML += `<div class="photo-preview"><img src="${flag.photoPath}" alt="Photo" /></div>`;
bHTML += `<div class="photo-actions">
<label class="photo-btn">${flag.photoData ? 'Change Photo' : 'Upload Photo'}
<label class="photo-btn">${flag.photoPath ? 'Change Photo' : 'Upload Photo'}
<input type="file" accept="image/*" class="photo-input" />
</label>`;
if (flag.photoData) bHTML += `<button class="photo-btn danger" data-action="remove-photo">Remove</button>`;
if (flag.photoPath) bHTML += `<button class="photo-btn danger" data-action="remove-photo">Remove</button>`;
bHTML += `</div></div>`;
body.innerHTML = bHTML;
@@ -596,7 +587,7 @@ function removeFlag(id) {
function addFlag(name, number) {
const flag = { id: nextId++, name, number,
placed: false, lat: null, lng: null, address: null,
marker: null, expanded: false, photoData: null };
marker: null, expanded: false, photoPath: null };
flags.push(flag);
return flag;
}
@@ -660,7 +651,7 @@ document.getElementById('file-input').addEventListener('change', async (e) => {
const flag = addFlag(String(name).trim(), String(number).trim());
const photo = row.Photo || row.photo || '';
if (String(photo).startsWith('data:')) flag.photoData = String(photo);
if (photo) flag.photoPath = String(photo);
let lat = parseFloat(row.Latitude || row.latitude || row.위도);
let lng = parseFloat(row.Longitude || row.longitude || row.경도);
@@ -706,19 +697,13 @@ document.getElementById('export-btn').addEventListener('click', () => {
Number: f.number, Name: f.name,
Status: f.placed ? 'Placed' : 'Unplaced',
Latitude: f.lat ?? '', Longitude: f.lng ?? '', Address: f.address ?? '',
Photo: f.photoData ?? '',
Photo: f.photoPath ?? '',
}));
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.json_to_sheet(rows, { cellDates: true });
const ws = XLSX.utils.json_to_sheet(rows);
ws['!cols'] = [{ wch: 12 }, { wch: 20 }, { wch: 10 }, { wch: 12 }, { wch: 12 }, { wch: 60 }, { wch: 30 }];
XLSX.utils.book_append_sheet(wb, ws, 'Flags');
try {
XLSX.writeFile(wb, 'flag_export.xlsx');
} catch (e) {
if (e.message?.includes('32767')) {
alert('Photo data is too large. Try uploading smaller photos.');
} else { throw e; }
}
XLSX.writeFile(wb, 'flag_export.xlsx');
});
// ═══════════════════════════════════════════════════════════