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:
55
index.html
55
index.html
@@ -458,35 +458,26 @@ function esc(s) {
|
|||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// ─── Photo ─────────────────────────────────────────────
|
// ─── 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) {
|
async function handlePhotoUpload(flagId, file) {
|
||||||
const flag = flags.find(f => f.id === flagId);
|
const flag = flags.find(f => f.id === flagId);
|
||||||
if (!flag || !file) return;
|
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();
|
renderList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function removePhoto(flagId) {
|
async function removePhoto(flagId) {
|
||||||
const flag = flags.find(f => f.id === flagId);
|
const flag = flags.find(f => f.id === flagId);
|
||||||
if (!flag) return;
|
if (!flag) return;
|
||||||
flag.photoData = null;
|
if (flag.photoPath) await fetch(flag.photoPath, { method: 'DELETE' }).catch(() => {});
|
||||||
|
flag.photoPath = null;
|
||||||
renderList();
|
renderList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,12 +546,12 @@ function renderList() {
|
|||||||
<span>${flag.address ? esc(flag.address) : '<span class="address-loading">Loading…</span>'}</span></div>`;
|
<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>`;
|
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">
|
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" />
|
<input type="file" accept="image/*" class="photo-input" />
|
||||||
</label>`;
|
</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>`;
|
bHTML += `</div></div>`;
|
||||||
body.innerHTML = bHTML;
|
body.innerHTML = bHTML;
|
||||||
|
|
||||||
@@ -596,7 +587,7 @@ function removeFlag(id) {
|
|||||||
function addFlag(name, number) {
|
function addFlag(name, number) {
|
||||||
const flag = { id: nextId++, name, number,
|
const flag = { id: nextId++, name, number,
|
||||||
placed: false, lat: null, lng: null, address: null,
|
placed: false, lat: null, lng: null, address: null,
|
||||||
marker: null, expanded: false, photoData: null };
|
marker: null, expanded: false, photoPath: null };
|
||||||
flags.push(flag);
|
flags.push(flag);
|
||||||
return 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 flag = addFlag(String(name).trim(), String(number).trim());
|
||||||
|
|
||||||
const photo = row.Photo || row.photo || '';
|
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 lat = parseFloat(row.Latitude || row.latitude || row.위도);
|
||||||
let lng = parseFloat(row.Longitude || row.longitude || 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,
|
Number: f.number, Name: f.name,
|
||||||
Status: f.placed ? 'Placed' : 'Unplaced',
|
Status: f.placed ? 'Placed' : 'Unplaced',
|
||||||
Latitude: f.lat ?? '', Longitude: f.lng ?? '', Address: f.address ?? '',
|
Latitude: f.lat ?? '', Longitude: f.lng ?? '', Address: f.address ?? '',
|
||||||
Photo: f.photoData ?? '',
|
Photo: f.photoPath ?? '',
|
||||||
}));
|
}));
|
||||||
const wb = XLSX.utils.book_new();
|
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 }];
|
ws['!cols'] = [{ wch: 12 }, { wch: 20 }, { wch: 10 }, { wch: 12 }, { wch: 12 }, { wch: 60 }, { wch: 30 }];
|
||||||
XLSX.utils.book_append_sheet(wb, ws, 'Flags');
|
XLSX.utils.book_append_sheet(wb, ws, 'Flags');
|
||||||
try {
|
XLSX.writeFile(wb, 'flag_export.xlsx');
|
||||||
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; }
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|||||||
77
server.py
77
server.py
@@ -1,8 +1,83 @@
|
|||||||
import http.server
|
import http.server
|
||||||
import socketserver
|
import socketserver
|
||||||
|
import os
|
||||||
|
import urllib.parse
|
||||||
|
import uuid
|
||||||
|
|
||||||
PORT = 8000
|
PORT = 8000
|
||||||
Handler = http.server.SimpleHTTPRequestHandler
|
PHOTO_DIR = 'photos'
|
||||||
|
os.makedirs(PHOTO_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(http.server.SimpleHTTPRequestHandler):
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
if self.path == '/upload':
|
||||||
|
content_len = int(self.headers.get('Content-Length', 0))
|
||||||
|
body = self.rfile.read(content_len)
|
||||||
|
# parse multipart/form-data
|
||||||
|
boundary = self.headers.get('Content-Type', '').split('boundary=')[-1].encode()
|
||||||
|
if not boundary:
|
||||||
|
self.send_error(400, 'Missing boundary')
|
||||||
|
return
|
||||||
|
parts = body.split(b'--' + boundary)
|
||||||
|
filename = None
|
||||||
|
file_data = None
|
||||||
|
for part in parts:
|
||||||
|
if b'Content-Disposition' in part:
|
||||||
|
header_end = part.find(b'\r\n\r\n')
|
||||||
|
if header_end == -1:
|
||||||
|
continue
|
||||||
|
header_section = part[:header_end].decode('utf-8', errors='replace')
|
||||||
|
data = part[header_end + 4:]
|
||||||
|
data = data.rstrip(b'\r\n--')
|
||||||
|
# Check if this part has a filename
|
||||||
|
for line in header_section.split('\r\n'):
|
||||||
|
if 'filename="' in line:
|
||||||
|
orig_name = line.split('filename="')[1].split('"')[0]
|
||||||
|
ext = os.path.splitext(orig_name)[1] or '.jpg'
|
||||||
|
fname = uuid.uuid4().hex + ext
|
||||||
|
file_data = data
|
||||||
|
filename = fname
|
||||||
|
break
|
||||||
|
|
||||||
|
if filename and file_data:
|
||||||
|
path = os.path.join(PHOTO_DIR, filename)
|
||||||
|
with open(path, 'wb') as f:
|
||||||
|
f.write(file_data)
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-Type', 'application/json')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(('{"path":"photos/' + filename + '"}').encode())
|
||||||
|
else:
|
||||||
|
self.send_error(400, 'No file found')
|
||||||
|
else:
|
||||||
|
self.send_error(404)
|
||||||
|
|
||||||
|
def do_DELETE(self):
|
||||||
|
parsed = urllib.parse.urlparse(self.path)
|
||||||
|
if parsed.path.startswith('/photos/'):
|
||||||
|
rel = os.path.relpath(parsed.path, '/photos/')
|
||||||
|
# basic path safety
|
||||||
|
if '..' in rel or rel.startswith('/'):
|
||||||
|
self.send_error(400)
|
||||||
|
return
|
||||||
|
filepath = os.path.join(PHOTO_DIR, rel)
|
||||||
|
if os.path.exists(filepath):
|
||||||
|
os.remove(filepath)
|
||||||
|
self.send_response(200)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(b'{"ok":true}')
|
||||||
|
else:
|
||||||
|
self.send_error(404)
|
||||||
|
else:
|
||||||
|
self.send_error(404)
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
if '/upload' in str(args[0]) or '/photos/' in str(args[0]):
|
||||||
|
return # skip noisy upload logs
|
||||||
|
super().log_message(format, *args)
|
||||||
|
|
||||||
|
|
||||||
with socketserver.TCPServer(("", PORT), Handler) as httpd:
|
with socketserver.TCPServer(("", PORT), Handler) as httpd:
|
||||||
print(f"Serving at http://0.0.0.0:{PORT}")
|
print(f"Serving at http://0.0.0.0:{PORT}")
|
||||||
|
|||||||
Reference in New Issue
Block a user