diff --git a/backend/routes/apiRoutes.js b/backend/routes/apiRoutes.js index d62e9bc..51ddf1d 100644 --- a/backend/routes/apiRoutes.js +++ b/backend/routes/apiRoutes.js @@ -43,12 +43,33 @@ const upload = multer({ } }); +/** + * Wrapper for Multer middleware to catch "Request aborted" and other upload errors gracefully. + */ +const uploadSinglePanorama = (req, res, next) => { + const multerUpload = upload.single('panorama'); + multerUpload(req, res, (err) => { + if (err) { + // Bắt lỗi khi client ngắt kết nối đột ngột + if (err.message === 'Request aborted') { + console.warn(`[Multer Warning]: Upload aborted by client at ${req.method} ${req.originalUrl}`); + return res.status(499).json({ message: 'Client aborted the request' }); + } + if (err instanceof multer.MulterError) { + return res.status(400).json({ message: `Multer error: ${err.message}` }); + } + return res.status(400).json({ message: err.message }); + } + next(); + }); +}; + /** * @route POST /api/scenes * @desc Create a new 3D scene (with 360 photo, 8K resize, EXIF injection) * @access Private (Registered Users) */ -router.post('/scenes', protect, upload.single('panorama'), async (req, res) => { +router.post('/scenes', protect, uploadSinglePanorama, async (req, res) => { try { const { title, lat, lng, privacy, sharedWithUsers } = req.body; @@ -145,6 +166,7 @@ router.post('/scenes', protect, upload.single('panorama'), async (req, res) => { */ router.get('/scenes', optionalAuth, async (req, res) => { try { + console.log(`[Data Load] Bắt đầu truy vấn scenes cho: ${req.user ? req.user._id : 'Khách'}`); let query = {}; if (req.user) { @@ -172,8 +194,10 @@ router.get('/scenes', optionalAuth, async (req, res) => { .populate('owner', 'username') .populate('assetId', 'coordinates createdAt'); + console.log(`[Data Load] Đã tìm thấy ${scenes.length} scenes. Gửi phản hồi về Frontend...`); res.json(scenes); } catch (error) { + console.error(`[Data Load Error]: ${error.stack}`); res.status(500).json({ message: error.message }); } }); @@ -257,6 +281,33 @@ router.post('/scenes/:id/hotspots', protect, async (req, res) => { await scene.save(); + // LOGIC "MẸ - CON": TỰ ĐỘNG TẠO ĐIỂM QUAY LẠI + if (targetSceneId && targetSceneId !== 'null' && targetSceneId !== '' && typeof targetSceneId === 'string') { + try { + const targetScene = await Scene.findById(targetSceneId); + if (targetScene) { + const hasReverse = targetScene.hotspots.some(h => + h.targetSceneId && h.targetSceneId.toString() === scene._id.toString() + ); + + if (!hasReverse) { + const originYaw = parseFloat(yaw) || 0; + const reverseYaw = originYaw > 0 ? originYaw - 180 : originYaw + 180; + + targetScene.hotspots.push({ + pitch: 0, + yaw: reverseYaw, + text: `Quay lại: ${scene.title}`, + targetSceneId: scene._id + }); + await targetScene.save(); + } + } + } catch (err) { + console.error("Lỗi tạo hotspot ngược:", err.message); + } + } + res.status(201).json({ message: 'Hotspot added successfully', hotspots: scene.hotspots @@ -325,7 +376,7 @@ router.get('/assets/view/:assetId', verifyReferer, optionalAuth, async (req, res * @desc Update an existing scene * @access Private (Owner only) */ -router.put('/scenes/:id', protect, upload.single('panorama'), async (req, res) => { +router.put('/scenes/:id', protect, uploadSinglePanorama, async (req, res) => { try { const { title, privacy, sharedWithUsers, lat, lng } = req.body; const scene = await Scene.findById(req.params.id); @@ -396,4 +447,41 @@ router.delete('/scenes/:id', protect, async (req, res) => { } }); +/** + * @route POST /api/maintenance/reset-all + * @desc Wipe all scenes, assets, and physical files (DANGEROUS: For dev reset only) + * @access Private (Owner only) + */ +router.post('/maintenance/reset-all', protect, async (req, res) => { + try { + // 1. Xóa toàn bộ dữ liệu trong Database + await Scene.deleteMany({}); + await Asset.deleteMany({}); + // Lưu ý: Không xóa Users trừ khi bạn muốn reset cả tài khoản + + // 2. Dọn dẹp thư mục uploads (trừ các file .gitkeep hoặc thư mục temp) + const files = fs.readdirSync(uploadDir); + for (const file of files) { + const fullPath = path.join(uploadDir, file); + if (fs.lstatSync(fullPath).isFile()) { + fs.unlinkSync(fullPath); + } + } + + // 3. Dọn dẹp thư mục temp + const tempFiles = fs.readdirSync(tempDir); + for (const file of tempFiles) { + const fullPath = path.join(tempDir, file); + if (fs.lstatSync(fullPath).isFile()) { + fs.unlinkSync(fullPath); + } + } + + console.warn(`[Maintenance]: Toàn bộ dữ liệu tour đã bị xóa bởi ${req.user.username}`); + res.json({ message: 'Dữ liệu đã được xóa sạch. Hãy clear localStorage ở trình duyệt để bắt đầu lại.' }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + module.exports = router; diff --git a/backend/scripts/resetDB.js b/backend/scripts/resetDB.js new file mode 100644 index 0000000..b43c80d --- /dev/null +++ b/backend/scripts/resetDB.js @@ -0,0 +1,53 @@ +const mongoose = require('mongoose'); +const fs = require('fs'); +const path = require('path'); +const connectDB = require('../config/db'); +const Scene = require('../models/Scene'); +const Asset = require('../models/Asset'); + +const reset = async () => { + try { + console.log('--- Bắt đầu quá trình Reset Dữ liệu ---'); + + // 1. Kết nối Database + await connectDB(); + + // 2. Xóa bản ghi trong Database + console.log('1. Đang xóa dữ liệu trong MongoDB...'); + const deletedScenes = await Scene.deleteMany({}); + const deletedAssets = await Asset.deleteMany({}); + console.log(`- Đã xóa ${deletedScenes.deletedCount} scenes.`); + console.log(`- Đã xóa ${deletedAssets.deletedCount} assets.`); + + // 3. Dọn dẹp tệp tin vật lý + const uploadDir = path.join(__dirname, '../uploads'); + const tempDir = path.join(uploadDir, 'temp'); + + console.log('2. Đang dọn dẹp thư mục uploads...'); + const directories = [uploadDir, tempDir]; + + directories.forEach(dir => { + if (fs.existsSync(dir)) { + const files = fs.readdirSync(dir); + for (const file of files) { + const fullPath = path.join(dir, file); + // Chỉ xóa file, không xóa thư mục (như temp) và tránh xóa file ẩn/cấu hình + if (fs.lstatSync(fullPath).isFile() && !file.startsWith('.')) { + fs.unlinkSync(fullPath); + } + } + } + }); + console.log('--- Hoàn tất reset hệ thống! ---'); + + // Đóng kết nối + mongoose.connection.close(); + process.exit(0); + } catch (err) { + console.error('Lỗi nghiêm trọng trong quá trình reset:', err); + mongoose.connection.close(); + process.exit(1); + } +}; + +reset(); \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index ad40623..a9e84be 100644 --- a/backend/server.js +++ b/backend/server.js @@ -43,6 +43,16 @@ app.use(cors(corsOptions)); app.use(express.json()); app.use(express.urlencoded({ extended: true })); +// Request Logger Middleware +app.use((req, res, next) => { + const start = Date.now(); + res.on('finish', () => { + const duration = Date.now() - start; + console.log(`[${new Date().toISOString()}] ${req.method} ${req.originalUrl} - ${res.statusCode} (${duration}ms)`); + }); + next(); +}); + // API Routes app.use('/api/auth', authRoutes); app.use('/api', apiRoutes); diff --git a/backend/uploads/processed_1780829797452_955cbbfe.JPG.jpg b/backend/uploads/processed_1780829797452_955cbbfe.JPG.jpg deleted file mode 100644 index a6dc884..0000000 Binary files a/backend/uploads/processed_1780829797452_955cbbfe.JPG.jpg and /dev/null differ diff --git a/backend/uploads/processed_1780839280131_2afe864c.JPG.jpg b/backend/uploads/processed_1780839280131_2afe864c.JPG.jpg deleted file mode 100644 index 79b7421..0000000 Binary files a/backend/uploads/processed_1780839280131_2afe864c.JPG.jpg and /dev/null differ diff --git a/backend/uploads/processed_1780840714376_be694e76.JPG.jpg b/backend/uploads/processed_1780840714376_be694e76.JPG.jpg deleted file mode 100644 index 4e3ced3..0000000 Binary files a/backend/uploads/processed_1780840714376_be694e76.JPG.jpg and /dev/null differ diff --git a/backend/uploads/processed_1780840799223_59b417b9.JPG.jpg b/backend/uploads/processed_1780840799223_59b417b9.JPG.jpg deleted file mode 100644 index 4e3ced3..0000000 Binary files a/backend/uploads/processed_1780840799223_59b417b9.JPG.jpg and /dev/null differ diff --git a/backend/uploads/processed_1780841484128_0790991a.JPG.jpg b/backend/uploads/processed_1780841484128_0790991a.JPG.jpg deleted file mode 100644 index dfe5ad1..0000000 Binary files a/backend/uploads/processed_1780841484128_0790991a.JPG.jpg and /dev/null differ diff --git a/backend/uploads/processed_1780841540897_3ab71fcd.JPG.jpg b/backend/uploads/processed_1780841540897_3ab71fcd.JPG.jpg deleted file mode 100644 index dfe5ad1..0000000 Binary files a/backend/uploads/processed_1780841540897_3ab71fcd.JPG.jpg and /dev/null differ diff --git a/backend/uploads/processed_1780841603501_89076676.JPG.jpg b/backend/uploads/processed_1780841603501_89076676.JPG.jpg deleted file mode 100644 index dfe5ad1..0000000 Binary files a/backend/uploads/processed_1780841603501_89076676.JPG.jpg and /dev/null differ diff --git a/backend/uploads/processed_1780842150898_687ea48c.JPG.jpg b/backend/uploads/processed_1780842150898_687ea48c.JPG.jpg deleted file mode 100644 index dfe5ad1..0000000 Binary files a/backend/uploads/processed_1780842150898_687ea48c.JPG.jpg and /dev/null differ diff --git a/backend/uploads/processed_1780842325753_ca34af30.JPG.jpg b/backend/uploads/processed_1780842325753_ca34af30.JPG.jpg deleted file mode 100644 index dfe5ad1..0000000 Binary files a/backend/uploads/processed_1780842325753_ca34af30.JPG.jpg and /dev/null differ diff --git a/backend/uploads/processed_1780842476612_b84135ff.JPG.jpg b/backend/uploads/processed_1780842476612_b84135ff.JPG.jpg deleted file mode 100644 index dfe5ad1..0000000 Binary files a/backend/uploads/processed_1780842476612_b84135ff.JPG.jpg and /dev/null differ diff --git a/backend/uploads/processed_1780842543486_64531ab7.JPG.jpg b/backend/uploads/processed_1780842543486_64531ab7.JPG.jpg deleted file mode 100644 index 5f26fc4..0000000 Binary files a/backend/uploads/processed_1780842543486_64531ab7.JPG.jpg and /dev/null differ diff --git a/backend/uploads/processed_1780890373010_44576a2c.JPG.jpg b/backend/uploads/processed_1780890373010_44576a2c.JPG.jpg new file mode 100644 index 0000000..c503d59 Binary files /dev/null and b/backend/uploads/processed_1780890373010_44576a2c.JPG.jpg differ diff --git a/backend/uploads/processed_1780890413280_888fcfc5.JPG.jpg b/backend/uploads/processed_1780890413280_888fcfc5.JPG.jpg new file mode 100644 index 0000000..537a6c8 Binary files /dev/null and b/backend/uploads/processed_1780890413280_888fcfc5.JPG.jpg differ diff --git a/backend/uploads/temp/1780829804457_08557d86.JPG b/backend/uploads/temp/1780829804457_08557d86.JPG deleted file mode 100644 index 7d57932..0000000 Binary files a/backend/uploads/temp/1780829804457_08557d86.JPG and /dev/null differ diff --git a/backend/uploads/temp/1780829805324_e2790a35.JPG b/backend/uploads/temp/1780829805324_e2790a35.JPG deleted file mode 100644 index 4592ad9..0000000 Binary files a/backend/uploads/temp/1780829805324_e2790a35.JPG and /dev/null differ diff --git a/backend/uploads/temp/1780829807673_e1dc797f.JPG b/backend/uploads/temp/1780829807673_e1dc797f.JPG deleted file mode 100644 index 3f9b6b1..0000000 Binary files a/backend/uploads/temp/1780829807673_e1dc797f.JPG and /dev/null differ diff --git a/backend/utils/exifHelper.js b/backend/utils/exifHelper.js index fe08ff6..9acbabc 100644 --- a/backend/utils/exifHelper.js +++ b/backend/utils/exifHelper.js @@ -32,11 +32,11 @@ const degToDmsRational = (deg) => { const absolute = Math.abs(deg); const d = Math.floor(absolute); const m = Math.floor((absolute - d) * 60); - const s = Math.round((absolute - d - m / 60) * 3600 * 100) / 100; + const s = Math.round((absolute - d - m / 60) * 3600 * 100); return [ [d, 1], [m, 1], - [Math.round(s * 100), 100] + [s, 100] ]; }; @@ -77,7 +77,13 @@ const injectGPSCoordinates = async (filePath, lat, lng) => { exifObj["GPS"][piexif.GPSIFD.GPSLongitudeRef] = lngRef; exifObj["GPS"][piexif.GPSIFD.GPSLongitude] = degToDmsRational(lng); - const exifBytes = piexif.dump(exifObj); + // Chỉ đóng gói các IFD cần thiết để tránh lỗi 'pack' từ dữ liệu rác + const exifBytes = piexif.dump({ + "0th": exifObj["0th"] || {}, + "Exif": exifObj["Exif"] || {}, + "GPS": exifObj["GPS"] || {} + }); + const newJpegBinary = piexif.insert(exifBytes, jpegBinary); fs.writeFileSync(filePath, Buffer.from(newJpegBinary, 'binary')); diff --git a/frontend/css/resetDB.js b/frontend/css/resetDB.js new file mode 100644 index 0000000..e69de29 diff --git a/frontend/css/style.css b/frontend/css/style.css index 2031345..a9417f0 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -138,6 +138,7 @@ html, body { /* 3D Viewer Overlays (Full Screen) */ #viewer-container { + display: none; /* Khởi tạo ẩn để không chặn chuột */ position: fixed; top: 0; left: 0; @@ -169,15 +170,6 @@ html, body { #close-viewer-btn:hover { background: white; } -/* Modal Overlay */ -.modal-overlay { - position: fixed; - top: 0; left: 0; width: 100%; height: 100%; - background: rgba(0,0,0,0.7); - display: flex; align-items: center; justify-content: center; - z-index: 2000; -} - .modal-content { background: #fff; padding: 20px; diff --git a/frontend/index.html b/frontend/index.html index 9de2c7c..eabaae5 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -72,6 +72,16 @@ + +
diff --git a/frontend/js/main_map.js b/frontend/js/main_map.js index 0aa3c74..c352263 100644 --- a/frontend/js/main_map.js +++ b/frontend/js/main_map.js @@ -1,4 +1,4 @@ -const API_BASE_URL = 'http://localhost:5000/api'; +const API_BASE_URL = '/api'; // Sử dụng đường dẫn tương đối để tránh lỗi CORS/Hostname let map; let tempMarker = null; @@ -8,10 +8,27 @@ let previousSceneId = null; // Initialize when DOM is ready document.addEventListener('DOMContentLoaded', () => { - initMap(); - checkAuthStatus(); - loadScenes(); - restoreActiveScene(); + try { + console.log("--- Bắt đầu khởi tạo Frontend ---"); + if (document.getElementById('map')) { + console.log("1. Đang khởi tạo bản đồ Leaflet..."); + initMap(); + } + + // Chạy tuần tự để tránh xung đột luồng xử lý + checkAuthStatus(); // 2. Kiểm tra đăng nhập + + // Đảm bảo map đã sẵn sàng trước khi nạp data + if (map) { + loadScenes().then(() => { + console.log("4. Đang chuẩn bị khôi phục Scene cũ (nếu có)..."); + // Chỉ khôi phục khi bản đồ đã nạp xong các marker + setTimeout(restoreActiveScene, 500); + }); + } + } catch (error) { + console.error("Ứng dụng không thể khởi tạo:", error); + } }); /** @@ -23,12 +40,16 @@ function initMap() { const savedLng = localStorage.getItem('map-lng'); const savedZoom = localStorage.getItem('map-zoom'); - // Nếu có dữ liệu cũ thì dùng, không thì mặc định là Hà Nội - const startLat = savedLat ? parseFloat(savedLat) : 21.0285; - const startLng = savedLng ? parseFloat(savedLng) : 105.8542; - const startZoom = savedZoom ? parseInt(savedZoom) : 13; + // Đảm bảo tọa độ khởi tạo luôn hợp lệ + let startLat = parseFloat(savedLat); + let startLng = parseFloat(savedLng); + let startZoom = parseInt(savedZoom); - map = L.map('map').setView([startLat, startLng], startZoom); + if (isNaN(startLat)) startLat = 21.0285; + if (isNaN(startLng)) startLng = 105.8542; + if (isNaN(startZoom)) startZoom = 13; + + map = L.map('map', { zoomControl: true }).setView([startLat, startLng], startZoom); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, @@ -44,8 +65,13 @@ function initMap() { showCoverageOnHover: false, iconCreateFunction: function(cluster) { const childMarkers = cluster.getAllChildMarkers(); - // Lấy icon của Scene đầu tiên trong nhóm để làm đại diện cho cả cụm - return childMarkers[0].options.icon; + try { + // Thay vì tạo object mới phức tạp, lấy HTML của marker đầu tiên + const firstIcon = childMarkers[0].options.icon; + if (firstIcon) return firstIcon; + } catch (e) {} + // Fallback an toàn nếu có lỗi + return L.divIcon({ className: 'cluster-fallback', html: '' }); } }); @@ -174,6 +200,12 @@ function handleLogout() { localStorage.removeItem('activeScenePrivacy'); localStorage.removeItem('activeSceneToken'); localStorage.removeItem('userId'); + + // Đảm bảo đóng viewer nếu đang mở + if (typeof closeViewer === 'function') { + closeViewer(); + } + checkAuthStatus(); loadScenes(); // Reload scenes to filter out private ones alert('Logged out successfully'); @@ -193,10 +225,10 @@ function openCreateSceneModal(lat, lng) { if (tempMarker) map.removeLayer(tempMarker); tempMarker = L.marker([lat, lng]).addTo(map); + document.getElementById('create-scene-modal').style.display = 'flex'; document.getElementById('modal-scene-id').value = ''; document.getElementById('modal-lat').value = lat.toFixed(6); document.getElementById('modal-lng').value = lng.toFixed(6); - document.getElementById('create-scene-modal').style.display = 'flex'; } /** @@ -255,30 +287,34 @@ function uploadWithProgress(url, method, formData, token, prefix, callback) { const percentText = document.getElementById(`${prefix}-progress-percent`); const statusText = document.getElementById(`${prefix}-progress-status`); - container.style.display = 'block'; - statusText.innerText = "Đang tải ảnh lên..."; + if (container) container.style.display = 'block'; + if (statusText) statusText.innerText = "Đang tải ảnh lên..."; xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { const percent = Math.round((e.loaded / e.total) * 100); - bar.style.width = percent + '%'; - percentText.innerText = percent + '%'; - if (percent === 100) statusText.innerText = "Tải lên xong! Đang khởi tạo trên server..."; + if (bar) bar.style.width = percent + '%'; + if (percentText) percentText.innerText = percent + '%'; + if (percent === 100 && statusText) statusText.innerText = "Tải lên xong! Đang khởi tạo trên server..."; } }); xhr.addEventListener('load', () => { - container.style.display = 'none'; + if (container) container.style.display = 'none'; if (xhr.status >= 200 && xhr.status < 300) { callback(JSON.parse(xhr.responseText)); } else { - const err = JSON.parse(xhr.responseText); - alert('Lỗi: ' + (err.message || 'Không thể tải lên')); + let errorMsg = 'Không thể tải lên'; + try { + const err = JSON.parse(xhr.responseText); + errorMsg = err.message || errorMsg; + } catch (e) {} + alert('Lỗi: ' + errorMsg); } }); xhr.addEventListener('error', () => { - container.style.display = 'none'; + if (container) container.style.display = 'none'; alert('Lỗi kết nối mạng.'); }); @@ -298,27 +334,45 @@ async function loadScenes() { headers['Authorization'] = `Bearer ${token}`; } - const response = await fetch(`${API_BASE_URL}/scenes`, { + // Thêm timestamp để tránh lỗi 304 hang do cache trình duyệt + const timestamp = new Date().getTime(); + console.log(`3.1 Đang gửi yêu cầu lấy danh sách Scene (ts: ${timestamp})...`); + const response = await fetch(`${API_BASE_URL}/scenes?_=${timestamp}`, { method: 'GET', headers }); + console.log(`[API Response] /scenes status: ${response.status}`); if (!response.ok) throw new Error('Failed to load scenes'); const scenes = await response.json(); + + console.log(`[Data] Nhận được ${scenes.length} scenes từ server`); + if (!Array.isArray(scenes)) return; - // Xóa sạch các layers cũ và chuẩn bị lọc tọa độ + // Xóa sạch các layers cũ trước khi nạp mới markerClusterGroup.clearLayers(); const markersToAdd = []; const activeSceneId = localStorage.getItem('activeSceneId'); - const seenCoordinates = new Set(); + const seenCoordinates = new Set(); // Dùng để lọc "Ảnh mẹ" (1 marker per location) // Chỉ lặp qua danh sách Scene mẹ, lọc bỏ các hotspots trùng tọa độ scenes.forEach((scene) => { - // Tạo khóa tọa độ để đảm bảo mỗi vị trí địa lý chỉ có 1 Marker duy nhất - const coordKey = `${scene.lat.toFixed(6)},${scene.lng.toFixed(6)}`; + const latNum = parseFloat(scene.lat); + const lngNum = parseFloat(scene.lng); + + if (isNaN(latNum) || isNaN(lngNum)) return; + + // Logic lọc Ảnh mẹ: Mỗi tọa độ GPS chỉ tạo duy nhất 1 Marker đại diện + const coordKey = `${latNum.toFixed(6)},${lngNum.toFixed(6)}`; if (seenCoordinates.has(coordKey)) return; // Bỏ qua nếu tọa độ này đã có Marker seenCoordinates.add(coordKey); + // Kiểm tra an toàn dữ liệu từ MongoDB trước khi truy cập + if (!scene.assetId || !scene.assetId._id) { + console.warn(`Scene "${scene.title}" thiếu dữ liệu ảnh (AssetId), bỏ qua.`); + return; + } + let thumbUrl = `${API_BASE_URL}/assets/view/${scene.assetId._id}`; if (token) thumbUrl += `?token=${token}`; else if (scene.privacy === 'shared' && scene.shareToken) thumbUrl += `?token=${scene.shareToken}`; @@ -335,7 +389,7 @@ async function loadScenes() { iconAnchor: [32, 76] // Căn giữa ngang, đáy mũi tên tại tọa độ lat/lng }); - const marker = L.marker([scene.lat, scene.lng], { + const marker = L.marker([latNum, lngNum], { icon: calloutIcon, title: scene.title // Tooltip khi di chuột qua }); @@ -472,6 +526,7 @@ async function openScene(sceneId, privacy, shareToken, force = false) { headers['Authorization'] = `Bearer ${token}`; } + console.log(`[Viewer] Đang mở scene: ${sceneId}`); let url = `${API_BASE_URL}/scenes/${sceneId}`; if (privacy === 'shared' && shareToken) { url += `?token=${shareToken}`; @@ -703,3 +758,30 @@ async function saveHotspotToDB(pitch, yaw, text, description, targetSceneId, hot alert(error.message); } } + +/** + * Công cụ dọn dẹp toàn bộ dữ liệu (Chỉ dùng cho nhà phát triển) + * Gọi lệnh: systemReset() từ trình duyệt + */ +window.systemReset = async function() { + if (!confirm("CẢNH BÁO: Thao tác này sẽ xóa sạch TOÀN BỘ scene và ảnh trên server. Bạn có chắc chắn?")) return; + + const token = localStorage.getItem('jwt'); + try { + const response = await fetch(`${API_BASE_URL}/maintenance/reset-all`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}` } + }); + const data = await response.json(); + + if (response.ok) { + localStorage.clear(); // Xóa sạch token, vị trí map, active scene + alert(data.message); + location.reload(); + } else { + throw new Error(data.message); + } + } catch (e) { + alert("Lỗi reset: " + e.message); + } +}; diff --git a/frontend/js/viewer360.js b/frontend/js/viewer360.js index 3179d17..729e9c8 100644 --- a/frontend/js/viewer360.js +++ b/frontend/js/viewer360.js @@ -1,5 +1,6 @@ let activeViewer = null; let currentHotspots = []; +let securityApplied = false; /** * Initializes and shows the Pannellum 360° panorama viewer with security overlays. @@ -72,6 +73,9 @@ function closeViewer() { * Appends event listeners to block right-clicks and common image saving shortcuts. */ function applyViewerSecurity() { + if (securityApplied) return; // Chỉ gán sự kiện một lần duy nhất + securityApplied = true; + // Target the actual viewer element where Pannellum renders const container = document.getElementById('viewer-container'); const panoramaViewer = document.getElementById('panorama-viewer'); @@ -114,18 +118,16 @@ function applyViewerSecurity() { }); } -// Global safety shortcut listeners (F12, Ctrl+S, Ctrl+U, Ctrl+Shift+I) +// Global safety shortcut listeners (Ctrl+S, Ctrl+U) - Tạm thời cho phép F12 và Ctrl+Shift+I để debug document.addEventListener('keydown', (e) => { // Only enforce when viewer is active if (document.getElementById('viewer-container').style.display === 'block') { const isCtrlS = e.ctrlKey && (e.key === 's' || e.key === 'S'); const isCtrlU = e.ctrlKey && (e.key === 'u' || e.key === 'U'); - const isF12 = e.key === 'F12'; - const isCtrlShiftI = e.ctrlKey && e.shiftKey && (e.key === 'i' || e.key === 'I'); - if (isCtrlS || isCtrlU || isF12 || isCtrlShiftI) { + if (isCtrlS || isCtrlU) { e.preventDefault(); - alert('Security Alert: Inspection and saving functions are restricted on this viewer.'); + console.warn('Security Alert: Inspection and saving functions are restricted.'); return false; } }