Merge PR #488: KMZ/KML place import

Resolves conflicts with Naver list import (PR #662) — kept both unified
list-import dialog and new KMZ/KML dialog. Dropped duplicate react-dom
import and unused CustomSelect import from PlacesSidebar.
This commit is contained in:
jubnl
2026-04-15 05:09:45 +02:00
25 changed files with 983 additions and 28 deletions
+29 -2
View File
@@ -14,13 +14,14 @@ import {
updatePlace,
deletePlace,
importGpx,
importMapFile,
importGoogleList,
importNaverList,
searchPlaceImage,
} from '../services/placeService';
import { onPlaceCreated, onPlaceUpdated, onPlaceDeleted } from '../services/journeyService';
const gpxUpload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 } });
const uploadMulter = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 } });
const router = express.Router({ mergeParams: true });
@@ -56,7 +57,7 @@ router.post('/', authenticate, requireTripAccess, validateStringLengths({ name:
});
// Import places from GPX file with full track geometry (must be before /:id)
router.post('/import/gpx', authenticate, requireTripAccess, gpxUpload.single('file'), (req: Request, res: Response) => {
router.post('/import/gpx', authenticate, requireTripAccess, uploadMulter.single('file'), (req: Request, res: Response) => {
const authReq = req as AuthRequest;
if (!checkPermission('place_edit', authReq.user.role, authReq.trip!.user_id, authReq.user.id, authReq.trip!.user_id !== authReq.user.id))
return res.status(403).json({ error: 'No permission' });
@@ -76,6 +77,32 @@ router.post('/import/gpx', authenticate, requireTripAccess, gpxUpload.single('fi
}
});
router.post('/import/map', authenticate, requireTripAccess, uploadMulter.single('file'), async (req: Request, res: Response) => {
const authReq = req as AuthRequest;
if (!checkPermission('place_edit', authReq.user.role, authReq.trip!.user_id, authReq.user.id, authReq.trip!.user_id !== authReq.user.id)) {
return res.status(403).json({ error: 'No permission' });
}
const { tripId } = req.params;
const file = (req as any).file;
if (!file) return res.status(400).json({ error: 'No file uploaded' });
try {
const result = await importMapFile(tripId, file.buffer, file.originalname);
if (result.count === 0) {
return res.status(400).json({ error: 'No valid Placemarks found in map file', summary: result.summary });
}
res.status(201).json(result);
for (const place of result.places) {
broadcast(tripId, 'place:created', { place }, req.headers['x-socket-id'] as string);
}
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Failed to import map file';
res.status(400).json({ error: message });
}
});
// Import places from a shared Google Maps list URL
router.post('/import/google-list', authenticate, requireTripAccess, async (req: Request, res: Response) => {
const authReq = req as AuthRequest;