fromis_9/backend/src/app.js

140 lines
4.5 KiB
JavaScript
Raw Normal View History

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import Fastify from 'fastify';
import fastifyStatic from '@fastify/static';
import fastifySwagger from '@fastify/swagger';
import multipart from '@fastify/multipart';
import config from './config/index.js';
import * as schemas from './schemas/index.js';
// 플러그인
import dbPlugin from './plugins/db.js';
import redisPlugin from './plugins/redis.js';
import authPlugin from './plugins/auth.js';
import meilisearchPlugin from './plugins/meilisearch.js';
import youtubeBotPlugin from './services/youtube/index.js';
import xBotPlugin from './services/x/index.js';
import schedulerPlugin from './plugins/scheduler.js';
// 라우트
import routes from './routes/index.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export async function buildApp(opts = {}) {
const fastify = Fastify({
logger: {
level: opts.logLevel || 'info',
},
...opts,
});
// config 데코레이터 등록
fastify.decorate('config', config);
// multipart 플러그인 등록 (파일 업로드용)
await fastify.register(multipart, {
limits: {
fileSize: 10 * 1024 * 1024, // 10MB
},
});
// 플러그인 등록 (순서 중요)
await fastify.register(dbPlugin);
await fastify.register(redisPlugin);
await fastify.register(authPlugin);
await fastify.register(meilisearchPlugin);
await fastify.register(youtubeBotPlugin);
await fastify.register(xBotPlugin);
await fastify.register(schedulerPlugin);
// 공유 스키마 등록 (라우트에서 $ref로 참조 가능)
fastify.addSchema({ $id: 'Album', ...schemas.albumResponse });
fastify.addSchema({ $id: 'AlbumTrack', ...schemas.albumTrack });
fastify.addSchema({ $id: 'Schedule', ...schemas.scheduleResponse });
fastify.addSchema({ $id: 'ScheduleCategory', ...schemas.scheduleCategory });
fastify.addSchema({ $id: 'Member', ...schemas.memberResponse });
fastify.addSchema({ $id: 'Photo', ...schemas.photoResponse });
// Swagger (OpenAPI) 설정
await fastify.register(fastifySwagger, {
openapi: {
info: {
title: 'fromis_9 API',
description: 'fromis_9 팬사이트 백엔드 API',
version: '2.0.0',
},
servers: [
{ url: '/', description: 'Current server' },
],
tags: [
{ name: 'auth', description: '인증 API' },
{ name: 'members', description: '멤버 API' },
{ name: 'albums', description: '앨범 API' },
{ name: 'schedules', description: '일정 API' },
{ name: 'admin/youtube', description: 'YouTube 관리 API' },
{ name: 'admin/x', description: 'X (Twitter) 관리 API' },
{ name: 'admin/bots', description: '봇 관리 API' },
{ name: 'stats', description: '통계 API' },
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
},
},
});
// OpenAPI JSON 엔드포인트 (CORS 허용 - API 문서 포털용)
fastify.get('/docs/json', { schema: { hide: true } }, async (request, reply) => {
reply.header('Access-Control-Allow-Origin', 'https://docs.caadiq.co.kr');
reply.header('Access-Control-Allow-Methods', 'GET');
return fastify.swagger();
});
// 라우트 등록
await fastify.register(routes, { prefix: '/api' });
// 헬스 체크 엔드포인트
fastify.get('/api/health', async () => {
return { status: 'ok', timestamp: new Date().toISOString() };
});
// 봇 상태 조회 엔드포인트
fastify.get('/api/bots', async () => {
const bots = fastify.scheduler.getBots();
const statuses = await Promise.all(
bots.map(async bot => {
const status = await fastify.scheduler.getStatus(bot.id);
return { ...bot, ...status };
})
);
return statuses;
});
// 정적 파일 서빙 (프론트엔드 빌드 결과물) - 프로덕션 모드에서만
const distPath = path.join(__dirname, '../dist');
if (fs.existsSync(distPath)) {
await fastify.register(fastifyStatic, {
root: distPath,
prefix: '/',
});
// SPA fallback - API 라우트가 아닌 모든 요청에 index.html 반환
fastify.setNotFoundHandler((request, reply) => {
if (request.url.startsWith('/api/')) {
return reply.code(404).send({ error: 'Not found' });
}
return reply.sendFile('index.html');
});
}
return fastify;
}