diff --git a/.env b/.env index 2ef8cbb..e56c7cc 100644 --- a/.env +++ b/.env @@ -20,6 +20,8 @@ RUSTFS_BUCKET=fromis-9 # Kakao API KAKAO_REST_KEY=e7a5516bf6cb1b398857789ee2ea6eea -# YouTube API -YOUTUBE_API_KEY=AIzaSyC6l3nFlcHgLc0d1Q9WPyYQjVKTv21ZqFs +# Google API +GOOGLE_API_KEY=AIzaSyC6l3nFlcHgLc0d1Q9WPyYQjVKTv21ZqFs + +# Meilisearch MEILI_MASTER_KEY=xMLNzlGX4xYji494JOb5IMlLHULcYw91 diff --git a/backend/src/config/index.js b/backend/src/config/index.js index 92696f4..d5f811e 100644 --- a/backend/src/config/index.js +++ b/backend/src/config/index.js @@ -47,8 +47,8 @@ export default { host: process.env.REDIS_HOST || 'fromis9-redis', port: parseInt(process.env.REDIS_PORT) || 6379, }, - youtube: { - apiKey: process.env.YOUTUBE_API_KEY, + google: { + apiKey: process.env.GOOGLE_API_KEY, }, jwt: { secret: process.env.JWT_SECRET, diff --git a/backend/src/routes/admin/places.js b/backend/src/routes/admin/places.js new file mode 100644 index 0000000..ab58e4f --- /dev/null +++ b/backend/src/routes/admin/places.js @@ -0,0 +1,96 @@ +import config from '../../config/index.js'; +import { badRequest, serverError } from '../../utils/error.js'; + +const KAKAO_REST_KEY = process.env.KAKAO_REST_KEY; +const GOOGLE_API_KEY = config.google.apiKey; + +/** + * 장소 검색 관리자 라우트 + * - 국내: 카카오맵 API + * - 해외: 구글 Places API + */ +export default async function placesRoutes(fastify) { + /** + * GET /api/admin/kakao/places + * 카카오맵 장소 검색 (국내) + */ + fastify.get('/kakao/places', { + preHandler: [fastify.authenticate], + }, async (request, reply) => { + const { query } = request.query; + + if (!query || !query.trim()) { + return badRequest(reply, '검색어를 입력해주세요.'); + } + + if (!KAKAO_REST_KEY) { + return serverError(reply, '카카오 API 키가 설정되지 않았습니다.'); + } + + try { + const response = await fetch( + `https://dapi.kakao.com/v2/local/search/keyword.json?query=${encodeURIComponent(query)}&size=15`, + { + headers: { + Authorization: `KakaoAK ${KAKAO_REST_KEY}`, + }, + } + ); + + if (!response.ok) { + const errorText = await response.text(); + fastify.log.error(`카카오 API 오류: ${response.status} ${errorText}`); + return serverError(reply, '카카오 API 호출 실패'); + } + + const data = await response.json(); + return data; + } catch (err) { + fastify.log.error(`카카오 장소 검색 오류: ${err.message}`); + return serverError(reply, err.message); + } + }); + + /** + * GET /api/admin/google/places + * 구글 Places API 장소 검색 (해외) + */ + fastify.get('/google/places', { + preHandler: [fastify.authenticate], + }, async (request, reply) => { + const { query } = request.query; + + if (!query || !query.trim()) { + return badRequest(reply, '검색어를 입력해주세요.'); + } + + if (!GOOGLE_API_KEY) { + return serverError(reply, 'Google API 키가 설정되지 않았습니다.'); + } + + try { + // Places API (New) - Text Search + const response = await fetch( + `https://maps.googleapis.com/maps/api/place/textsearch/json?query=${encodeURIComponent(query)}&key=${GOOGLE_API_KEY}` + ); + + if (!response.ok) { + const errorText = await response.text(); + fastify.log.error(`Google Places API 오류: ${response.status} ${errorText}`); + return serverError(reply, 'Google API 호출 실패'); + } + + const data = await response.json(); + + if (data.status !== 'OK' && data.status !== 'ZERO_RESULTS') { + fastify.log.error(`Google Places API 상태: ${data.status} - ${data.error_message || ''}`); + return serverError(reply, `Google API 오류: ${data.status}`); + } + + return data; + } catch (err) { + fastify.log.error(`구글 장소 검색 오류: ${err.message}`); + return serverError(reply, err.message); + } + }); +} diff --git a/backend/src/routes/index.js b/backend/src/routes/index.js index 640a4ca..85dca86 100644 --- a/backend/src/routes/index.js +++ b/backend/src/routes/index.js @@ -7,6 +7,7 @@ import botsRoutes from './admin/bots.js'; import youtubeAdminRoutes from './admin/youtube.js'; import xAdminRoutes from './admin/x.js'; import concertAdminRoutes from './admin/concert.js'; +import placesAdminRoutes from './admin/places.js'; /** * 라우트 통합 @@ -39,4 +40,7 @@ export default async function routes(fastify) { // 관리자 - 콘서트 라우트 fastify.register(concertAdminRoutes, { prefix: '/admin/concert' }); + + // 관리자 - 장소 검색 라우트 + fastify.register(placesAdminRoutes, { prefix: '/admin' }); } diff --git a/backend/src/services/youtube/api.js b/backend/src/services/youtube/api.js index 168956f..5e8e277 100644 --- a/backend/src/services/youtube/api.js +++ b/backend/src/services/youtube/api.js @@ -1,7 +1,7 @@ import config from '../../config/index.js'; import { formatDate, formatTime } from '../../utils/date.js'; -const API_KEY = config.youtube.apiKey; +const API_KEY = config.google.apiKey; const API_BASE = 'https://www.googleapis.com/youtube/v3'; /**