import React, { useState, useEffect, useCallback, useRef } from 'react';
import { MapContainer, TileLayer, Marker, Popup, useMapEvents, useMap, Circle } from 'react-leaflet';
import L from 'leaflet';
import { QRCodeCanvas } from 'qrcode.react';
import { WiFiHotspot } from './types';
import { apiService } from './services/api';
import { geminiService, DetailedSearchResult } from './services/geminiService';
import {
Wifi,
Plus,
Search,
Navigation,
MapPin,
X,
Eye,
EyeOff,
Trash2,
ZoomIn,
ZoomOut,
Target,
Coffee,
Utensils,
CheckCircle2,
Bug,
AlertCircle,
Terminal
} from 'lucide-react';
// Marker definitions
const WiFiIcon = (color: string, type: 'cafe' | 'restaurant' | 'general' = 'general') => {
let iconPath = '';
if (type === 'cafe') {
iconPath = '';
} else if (type === 'restaurant') {
iconPath = '';
} else {
iconPath = '';
}
return L.divIcon({
html: `
`,
className: 'custom-marker',
iconSize: [44, 44],
iconAnchor: [22, 22],
});
};
const UserLocationIcon = L.divIcon({
html: ``,
className: 'custom-marker',
iconSize: [40, 40],
iconAnchor: [20, 20],
});
const MapResizer = () => {
const map = useMap();
useEffect(() => {
const handleResize = () => {
setTimeout(() => {
map.invalidateSize();
}, 100);
};
// Initial resize to fix gray area on load
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [map]);
return null;
};
const AdSenseComponent = () => {
useEffect(() => {
try {
// @ts-ignore
(window.adsbygoogle = window.adsbygoogle || []).push({});
} catch (e) {
console.error('AdSense error:', e);
}
}, []);
return (
);
};
const ChangeView = ({ center, zoom }: { center: [number, number], zoom?: number }) => {
const map = useMap();
useEffect(() => {
if (center[0] !== 0 && center[1] !== 0) {
map.flyTo(center, zoom || 17, { animate: true, duration: 1.5 });
}
}, [center, map, zoom]);
return null;
};
const MapInterface = ({ onMapClick, onLocate, onAdd, onStatsUpdate, onMapMove }: any) => {
const map = useMap();
const controlsRef = useRef(null);
useEffect(() => {
if (controlsRef.current) {
L.DomEvent.disableClickPropagation(controlsRef.current);
L.DomEvent.disableScrollPropagation(controlsRef.current);
}
}, []);
useMapEvents({
click(e) {
onMapClick(e.latlng.lat, e.latlng.lng);
},
zoomend() {
onStatsUpdate(map.getZoom());
},
move() {
const center = map.getCenter();
onMapMove(center.lat, center.lng);
}
});
return (
);
};
const App: React.FC = () => {
const [hotspots, setHotspots] = useState([]);
const [searchQuery, setSearchQuery] = useState('');
const [isAdding, setIsAdding] = useState(false);
const [selectedHotspot, setSelectedHotspot] = useState(null);
const [newHotspotPos, setNewHotspotPos] = useState<{lat: number, lng: number} | null>(null);
const [userLocation, setUserLocation] = useState<[number, number]>([37.5665, 126.9780]);
const [mapCenter, setMapCenter] = useState<[number, number]>([37.5665, 126.9780]);
const [showPass, setShowPass] = useState(null);
const [isSearching, setIsSearching] = useState(false);
const [searchInfo, setSearchInfo] = useState<{text: string, results: DetailedSearchResult[]} | null>(null);
const [lastSearchRawText, setLastSearchRawText] = useState('');
const [showDebug, setShowDebug] = useState(false);
const [selectedIconType, setSelectedIconType] = useState<'general' | 'cafe' | 'restaurant'>('general');
const [formPlaceName, setFormPlaceName] = useState('');
const [currentZoom, setCurrentZoom] = useState(17);
const [inputLat, setInputLat] = useState('');
const [inputLng, setInputLng] = useState('');
const markerRef = useRef(null);
useEffect(() => {
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);
}, null, { enableHighAccuracy: true });
}
}, []);
useEffect(() => {
const handleSync = async () => setHotspots(await apiService.getHotspots());
window.addEventListener('storage_updated', handleSync);
return () => window.removeEventListener('storage_updated', handleSync);
}, []);
const handleMapClick = useCallback((lat: number, lng: number) => {
setNewHotspotPos({ lat, lng });
setIsAdding(true);
setSelectedHotspot(null);
setFormPlaceName('');
}, []);
const handleMapMove = useCallback((lat: number, lng: number) => {
setInputLat(lat.toFixed(6));
setInputLng(lng.toFixed(6));
}, []);
const handleLocate = useCallback(() => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition((pos) => {
const newPos: [number, number] = [pos.coords.latitude, pos.coords.longitude];
setUserLocation(newPos);
setMapCenter(newPos);
setInputLat(newPos[0].toFixed(6));
setInputLng(newPos[1].toFixed(6));
});
}
}, []);
const handleAddInitiate = useCallback(() => {
const lat = parseFloat(inputLat);
const lng = parseFloat(inputLng);
if (!isNaN(lat) && !isNaN(lng)) {
setNewHotspotPos({ lat, lng });
setIsAdding(true);
setSelectedHotspot(null);
setFormPlaceName('');
}
}, [inputLat, inputLng]);
// 검색 결과 클릭 시 동작 수정
const handleSearchResultClick = (r: DetailedSearchResult) => {
if (r.lat && r.lng) {
// 1. 입력창 좌표 업데이트
setInputLat(r.lat.toFixed(6));
setInputLng(r.lng.toFixed(6));
// 2. 지도 중심 이동
setMapCenter([r.lat, r.lng]);
// 3. '새 WiFi 등록' 다이얼로그 닫기 및 관련 상태 초기화
setIsAdding(false);
setSelectedHotspot(null);
setNewHotspotPos(null);
setFormPlaceName(r.title);
} else {
setShowDebug(true);
if (r.uri) window.open(r.uri, '_blank');
}
};
const saveWiFi = async (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const newHotspot: WiFiHotspot = {
id: Math.random().toString(36).substr(2, 9),
name: (formData.get('name') as string) || "WiFi 장소",
ssid: (formData.get('ssid') as string) || "알 수 없는 SSID",
password: formData.get('password') as string,
lat: newHotspotPos?.lat || parseFloat(inputLat),
lng: newHotspotPos?.lng || parseFloat(inputLng),
securityType: (formData.get('security') as any) || 'WPA2',
iconType: selectedIconType,
addedBy: 'User',
createdAt: Date.now(),
isPublic: true
};
await apiService.saveHotspot(newHotspot);
setHotspots(await apiService.getHotspots());
setIsAdding(false);
setNewHotspotPos(null);
setSelectedHotspot(newHotspot);
};
const performSearch = async () => {
if (!searchQuery.trim()) return;
setIsSearching(true);
setSearchInfo(null);
setLastSearchRawText("AI가 주변 정보를 분석하고 정밀하게 추출하는 중입니다...");
const result = await geminiService.searchNearbyWiFi(searchQuery, { lat: userLocation[0], lng: userLocation[1] });
setSearchInfo(result);
setLastSearchRawText(result.text || "결과를 찾을 수 없습니다.");
setIsSearching(false);
// 좌표가 인식된 결과들 중 내 위치와 가장 가까운 장소 찾기
const resultsWithCoords = result.results.filter(r => r.lat && r.lng);
if (resultsWithCoords.length > 0) {
let closestItem = resultsWithCoords[0];
let minDistance = Infinity;
resultsWithCoords.forEach(r => {
const distance = Math.sqrt(
Math.pow(r.lat! - userLocation[0], 2) +
Math.pow(r.lng! - userLocation[1], 2)
);
if (distance < minDistance) {
minDistance = distance;
closestItem = r;
}
});
if (closestItem.lat && closestItem.lng) {
setInputLat(closestItem.lat.toFixed(6));
setInputLng(closestItem.lng.toFixed(6));
setMapCenter([closestItem.lat, closestItem.lng]);
}
}
};
const getWifiQRValue = (hotspot: WiFiHotspot) => {
const security = hotspot.securityType === 'Open' ? 'nopass' : 'WPA';
return `WIFI:S:${hotspot.ssid};T:${security};P:${hotspot.password || ''};;`;
};
return (
setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && performSearch()}
/>
{searchInfo && (
주변 검색 결과
{showDebug && (
AI 정밀 분석 데이터
{lastSearchRawText}
)}
{searchInfo.results.map((r, i) => (
))}
)}
{/* Sponsor/AdSense Space */}
setCurrentZoom(Math.round(z))}
onMapMove={handleMapMove}
/>
{hotspots.map(spot => (
{ L.DomEvent.stopPropagation(e as any); setSelectedHotspot(spot); setIsAdding(false); setMapCenter([spot.lat, spot.lng]); } }} />
))}
{newHotspotPos && (
정확한 위치로 드래그
)}
{(selectedHotspot || isAdding) && (
{isAdding ? "새 WiFi 등록" : "장소 정보"}
{isAdding ? (
) : selectedHotspot && (
{selectedHotspot.iconType === 'cafe' ? : selectedHotspot.iconType === 'restaurant' ? : }
{selectedHotspot.name}
{selectedHotspot.ssid}
WiFi 비밀번호
{showPass === selectedHotspot.id ? (selectedHotspot.password || 'OPEN') : '••••••••'}
)}
)}
);
};
export default App;