feat: 장소 검색 API 추가 (카카오/구글)

- 국내: 카카오맵 API (/api/admin/kakao/places)
- 해외: 구글 Places API (/api/admin/google/places)
- YOUTUBE_API_KEY를 GOOGLE_API_KEY로 통합

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-02-03 14:05:30 +09:00
parent 65b1d931f3
commit 48f41c6db0
5 changed files with 107 additions and 5 deletions

6
.env
View file

@ -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

View file

@ -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,

View file

@ -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);
}
});
}

View file

@ -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' });
}

View file

@ -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';
/**