From 9cf163908f6af166778cfc2f2a88f8c0ca195791 Mon Sep 17 00:00:00 2001 From: user01 Date: Fri, 15 May 2026 19:08:21 +0900 Subject: [PATCH] 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 --- index.html | 55 ++++++++++++++------------------------ server.py | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 96 insertions(+), 36 deletions(-) diff --git a/index.html b/index.html index e716782..008a796 100644 --- a/index.html +++ b/index.html @@ -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() { ${flag.address ? esc(flag.address) : 'Loading…'}`; } bHTML += `
📷 Photo
`; - if (flag.photoData) bHTML += `
Photo
`; + if (flag.photoPath) bHTML += `
Photo
`; bHTML += `
-
`; 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'); }); // ═══════════════════════════════════════════════════════════ diff --git a/server.py b/server.py index a6372cb..ed5fafb 100644 --- a/server.py +++ b/server.py @@ -1,8 +1,83 @@ import http.server import socketserver +import os +import urllib.parse +import uuid 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: print(f"Serving at http://0.0.0.0:{PORT}")