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 += `
`;
+ if (flag.photoPath) bHTML += `
`;
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}")