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 { storageService } from './services/storage'; 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: `
${iconPath}
`, className: 'custom-marker', iconSize: [44, 44], iconAnchor: [22, 22], }); }; const UserLocationIcon = L.divIcon({ html: `
`, className: 'custom-marker', iconSize: [40, 40], iconAnchor: [20, 20], }); 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(() => storageService.getHotspots()); 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 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)); } 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()); 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 = (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 }; storageService.saveHotspot(newHotspot); setHotspots(storageService.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) => ( ))}
)}
나의 WiFi: {hotspots.length} 줌 레벨: {currentZoom}
setInputLat(e.target.value)} className="w-full text-[11px] font-bold p-2 bg-gray-100 rounded-xl outline-none border border-transparent focus:border-blue-300" /> setInputLng(e.target.value)} className="w-full text-[11px] font-bold p-2 bg-gray-100 rounded-xl outline-none border border-transparent focus:border-blue-300" />
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 ? (

등록될 위치

{newHotspotPos?.lat.toFixed(6)}, {newHotspotPos?.lng.toFixed(6)}

{(['general', 'cafe', 'restaurant'] as const).map(type => ( ))}
setFormPlaceName(e.target.value)} placeholder="장소 명칭 (예: 파스쿠찌)" className="w-full p-5 rounded-2xl bg-slate-50 font-bold outline-none border-2 border-transparent focus:border-blue-500 transition-all shadow-inner" />
) : selectedHotspot && (
{selectedHotspot.iconType === 'cafe' ? : selectedHotspot.iconType === 'restaurant' ? : }

{selectedHotspot.name}

{selectedHotspot.ssid}

WiFi 비밀번호

{showPass === selectedHotspot.id ? (selectedHotspot.password || 'OPEN') : '••••••••'}

자동 연결 QR 코드

카메라로 스캔하세요
)}
)}
); }; export default App;