initial commit

This commit is contained in:
2026-02-06 15:16:49 +09:00
commit 1c72c185ab
12 changed files with 837 additions and 0 deletions

86
services/geminiService.ts Normal file
View File

@@ -0,0 +1,86 @@
import { GoogleGenAI, Type } from "@google/genai";
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
export interface DetailedSearchResult {
title: string;
uri: string;
lat?: number;
lng?: number;
}
export const geminiService = {
async searchNearbyWiFi(query: string, location: { lat: number, lng: number }) {
try {
console.log("Stage 1: 정보 검색 시작...");
// Stage 1: Google Search/Maps Grounding을 통해 원시 데이터 확보
const searchResponse = await ai.models.generateContent({
model: "gemini-2.5-flash",
contents: `내 위치(위도: ${location.lat}, 경도: ${location.lng}) 주변에서 "${query}" 장소들을 찾아주세요.
각 장소의 이름, 주소, 그리고 가능한 경우 위도와 경도 정보를 상세히 포함해서 답변해 주세요.`,
config: {
tools: [{ googleSearch: {} }, { googleMaps: {} }],
toolConfig: {
retrievalConfig: {
latLng: {
latitude: location.lat,
longitude: location.lng
}
}
}
},
});
const rawText = searchResponse.text || "";
console.log("Stage 1 원문 결과:", rawText);
if (!rawText) {
return { text: "검색 결과를 가져오지 못했습니다.", results: [] };
}
console.log("Stage 2: 데이터 구조화 추출 시작...");
// Stage 2: 획득한 텍스트에서 JSON 형태로 좌표 및 정보 정밀 추출
const parseResponse = await ai.models.generateContent({
model: "gemini-3-flash-preview",
contents: `다음은 주변 장소 검색 결과 텍스트입니다. 이 텍스트에서 언급된 모든 장소를 추출하여 JSON 배열로 응답해 주세요.
각 객체는 title, uri(구글맵 링크), lat(숫자), lng(숫자) 속성을 가져야 합니다.
좌표가 텍스트에 직접 없더라도 문맥상 추론 가능하거나 본문에 포함되어 있다면 반드시 숫자로 변환하세요.
텍스트:
${rawText}`,
config: {
responseMimeType: "application/json",
responseSchema: {
type: Type.ARRAY,
items: {
type: Type.OBJECT,
properties: {
title: { type: Type.STRING, description: "장소 이름" },
uri: { type: Type.STRING, description: "구글 맵 링크" },
lat: { type: Type.NUMBER, description: "위도 (Latitude)" },
lng: { type: Type.NUMBER, description: "경도 (Longitude)" },
},
required: ["title", "lat", "lng"]
}
}
}
});
let results: DetailedSearchResult[] = [];
try {
results = JSON.parse(parseResponse.text || "[]");
console.log("Stage 2 추출 성공:", results);
} catch (e) {
console.error("JSON 파싱 실패:", e);
}
return { text: rawText, results };
} catch (error: any) {
console.error("Gemini 검색 에러:", error);
return { text: `에러: ${error.message}`, results: [] };
}
}
};

48
services/storage.ts Normal file
View File

@@ -0,0 +1,48 @@
import { WiFiHotspot } from '../types';
const STORAGE_KEY = 'wifi_hotspots_v2_db';
export const storageService = {
getHotspots: (): WiFiHotspot[] => {
try {
const data = localStorage.getItem(STORAGE_KEY);
if (!data) return [];
const parsed = JSON.parse(data);
return Array.isArray(parsed) ? parsed : [];
} catch (e) {
console.error("Failed to load hotspots from storage", e);
return [];
}
},
saveHotspot: (hotspot: WiFiHotspot): void => {
try {
const hotspots = storageService.getHotspots();
const index = hotspots.findIndex(h => h.id === hotspot.id);
if (index > -1) {
hotspots[index] = hotspot;
} else {
hotspots.push(hotspot);
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(hotspots));
// Trigger a storage event for cross-tab sync if needed
window.dispatchEvent(new Event('storage_updated'));
} catch (e) {
console.error("Failed to save hotspot", e);
}
},
deleteHotspot: (id: string): void => {
try {
const hotspots = storageService.getHotspots();
const filtered = hotspots.filter(h => h.id !== id);
localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered));
window.dispatchEvent(new Event('storage_updated'));
} catch (e) {
console.error("Failed to delete hotspot", e);
}
}
};