add sqllite

This commit is contained in:
2026-02-06 15:35:54 +09:00
parent e7b7414d73
commit 921455749e
9 changed files with 2745 additions and 26 deletions

45
App.tsx
View File

@@ -4,7 +4,7 @@ import { MapContainer, TileLayer, Marker, Popup, useMapEvents, useMap, Circle }
import L from 'leaflet';
import { QRCodeCanvas } from 'qrcode.react';
import { WiFiHotspot } from './types';
import { storageService } from './services/storage';
import { apiService } from './services/api';
import { geminiService, DetailedSearchResult } from './services/geminiService';
import {
Wifi,
@@ -105,7 +105,7 @@ const MapInterface = ({ onMapClick, onLocate, onAdd, onStatsUpdate, onMapMove }:
};
const App: React.FC = () => {
const [hotspots, setHotspots] = useState<WiFiHotspot[]>(() => storageService.getHotspots());
const [hotspots, setHotspots] = useState<WiFiHotspot[]>([]);
const [searchQuery, setSearchQuery] = useState('');
const [isAdding, setIsAdding] = useState(false);
const [selectedHotspot, setSelectedHotspot] = useState<WiFiHotspot | null>(null);
@@ -127,30 +127,35 @@ const App: React.FC = () => {
const markerRef = useRef<any>(null);
useEffect(() => {
const saved = storageService.getHotspots();
if (saved.length > 0) {
setHotspots(saved);
const lastSpot = saved[saved.length-1];
setMapCenter([lastSpot.lat, lastSpot.lng]);
setInputLat(lastSpot.lat.toFixed(6));
setInputLng(lastSpot.lng.toFixed(6));
}
const loadHotspots = async () => {
const saved = await apiService.getHotspots();
if (saved.length > 0) {
setHotspots(saved);
const lastSpot = saved[saved.length-1];
setMapCenter([lastSpot.lat, lastSpot.lng]);
setInputLat(lastSpot.lat.toFixed(6));
setInputLng(lastSpot.lng.toFixed(6));
} else if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition((pos) => {
const newPos: [number, number] = [pos.coords.latitude, pos.coords.longitude];
setMapCenter(newPos);
setInputLat(newPos[0].toFixed(6));
setInputLng(newPos[1].toFixed(6));
});
}
};
loadHotspots();
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition((pos) => {
const newPos: [number, number] = [pos.coords.latitude, pos.coords.longitude];
setUserLocation(newPos);
if (saved.length === 0) {
setMapCenter(newPos);
setInputLat(newPos[0].toFixed(6));
setInputLng(newPos[1].toFixed(6));
}
}, null, { enableHighAccuracy: true });
}
}, []);
useEffect(() => {
const handleSync = () => setHotspots(storageService.getHotspots());
const handleSync = async () => setHotspots(await apiService.getHotspots());
window.addEventListener('storage_updated', handleSync);
return () => window.removeEventListener('storage_updated', handleSync);
}, []);
@@ -211,7 +216,7 @@ const App: React.FC = () => {
}
};
const saveWiFi = (e: React.FormEvent<HTMLFormElement>) => {
const saveWiFi = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const newHotspot: WiFiHotspot = {
@@ -228,8 +233,8 @@ const App: React.FC = () => {
isPublic: true
};
storageService.saveHotspot(newHotspot);
setHotspots(storageService.getHotspots());
await apiService.saveHotspot(newHotspot);
setHotspots(await apiService.getHotspots());
setIsAdding(false);
setNewHotspotPos(null);
setSelectedHotspot(newHotspot);
@@ -457,7 +462,7 @@ const App: React.FC = () => {
<div className="flex flex-col gap-4 pt-6">
<button onClick={() => { if(selectedHotspot.password) { navigator.clipboard.writeText(selectedHotspot.password); alert("비밀번호가 복사되었습니다!"); } }} className="w-full py-6 bg-slate-900 hover:bg-black text-white font-black rounded-3xl shadow-xl transition-all"> </button>
<button onClick={() => { if(confirm("삭제하시겠습니까?")) { storageService.deleteHotspot(selectedHotspot.id); setHotspots(storageService.getHotspots()); setSelectedHotspot(null); } }} className="w-full py-5 text-red-500 hover:bg-red-50 font-black rounded-3xl transition-colors flex items-center justify-center gap-2">
<button onClick={async () => { if(confirm("삭제하시겠습니까?")) { await apiService.deleteHotspot(selectedHotspot.id); setHotspots(await apiService.getHotspots()); setSelectedHotspot(null); } }} className="w-full py-5 text-red-500 hover:bg-red-50 font-black rounded-3xl transition-colors flex items-center justify-center gap-2">
<Trash2 size={20} />
</button>
</div>

2552
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,13 +9,17 @@
"preview": "vite preview"
},
"dependencies": {
"@google/genai": "1.40.0",
"cors": "^2.8.6",
"express": "^5.2.1",
"leaflet": "1.9.4",
"lucide-react": "0.460.0",
"qrcode.react": "^4.2.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"lucide-react": "0.460.0",
"leaflet": "1.9.4",
"react-leaflet": "5.0.0",
"qrcode.react": "^4.2.0",
"@google/genai": "1.40.0"
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7"
},
"devDependencies": {
"@types/node": "^22.14.0",

107
server.js Normal file
View File

@@ -0,0 +1,107 @@
import express from 'express';
import sqlite3 from 'sqlite3';
import cors from 'cors';
import { open } from 'sqlite';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(cors());
app.use(express.json());
// Database setup
let db;
async function initializeDB() {
db = await open({
filename: path.join(__dirname, 'wifi_markers.db'),
driver: sqlite3.Database
});
// Enable WAL mode for concurrency
await db.exec('PRAGMA journal_mode = WAL;');
// Create table
await db.exec(`
CREATE TABLE IF NOT EXISTS markers (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
ssid TEXT NOT NULL,
password TEXT,
lat REAL NOT NULL,
lng REAL NOT NULL,
securityType TEXT NOT NULL,
iconType TEXT NOT NULL,
description TEXT,
addedBy TEXT,
createdAt INTEGER,
isPublic INTEGER DEFAULT 1
)
`);
console.log('Database initialized with WAL mode enabled.');
}
// Routes
app.get('/api/markers', async (req, res) => {
try {
const markers = await db.all('SELECT * FROM markers');
// Convert isPublic from 0/1 to boolean
const formattedMarkers = markers.map(m => ({
...m,
isPublic: !!m.isPublic
}));
res.json(formattedMarkers);
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Failed to fetch markers' });
}
});
app.post('/api/markers', async (req, res) => {
const { id, name, ssid, password, lat, lng, securityType, iconType, description, addedBy, createdAt, isPublic } = req.body;
try {
await db.run(`
INSERT OR REPLACE INTO markers (id, name, ssid, password, lat, lng, securityType, iconType, description, addedBy, createdAt, isPublic)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [id, name, ssid, password, lat, lng, securityType, iconType, description || '', addedBy, createdAt, isPublic ? 1 : 0]);
res.json({ success: true });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Failed to save marker' });
}
});
app.delete('/api/markers/:id', async (req, res) => {
const { id } = req.params;
try {
await db.run('DELETE FROM markers WHERE id = ?', [id]);
res.json({ success: true });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Failed to delete marker' });
}
});
// Serve static files from dist
app.use(express.static(path.join(__dirname, 'dist')));
// Fallback for SPA
app.get(/.*/, (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});
initializeDB().then(() => {
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
});

47
services/api.ts Normal file
View File

@@ -0,0 +1,47 @@
import { WiFiHotspot } from '../types';
const API_Base_URL = 'http://localhost:3000/api/markers';
export const apiService = {
getHotspots: async (): Promise<WiFiHotspot[]> => {
try {
const response = await fetch(API_Base_URL);
if (!response.ok) throw new Error('Failed to fetch markers');
const data = await response.json();
return data;
} catch (e) {
console.error("Failed to load hotspots from API", e);
return [];
}
},
saveHotspot: async (hotspot: WiFiHotspot): Promise<void> => {
try {
const response = await fetch(API_Base_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(hotspot),
});
if (!response.ok) throw new Error('Failed to save marker');
// Trigger a storage event to refresh UI if needed (keeping compatibility)
window.dispatchEvent(new Event('storage_updated'));
} catch (e) {
console.error("Failed to save hotspot", e);
}
},
deleteHotspot: async (id: string): Promise<void> => {
try {
const response = await fetch(`${API_Base_URL}/${id}`, {
method: 'DELETE',
});
if (!response.ok) throw new Error('Failed to delete marker');
window.dispatchEvent(new Event('storage_updated'));
} catch (e) {
console.error("Failed to delete hotspot", e);
}
}
};

View File

@@ -6,8 +6,14 @@ export default defineConfig(({ mode }) => {
const env = loadEnv(mode, '.', '');
return {
server: {
port: 3000,
port: 5173,
host: '0.0.0.0',
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true
}
}
},
plugins: [react()],
define: {

BIN
wifi_markers.db Normal file

Binary file not shown.

BIN
wifi_markers.db-shm Normal file

Binary file not shown.

BIN
wifi_markers.db-wal Normal file

Binary file not shown.