2026-01-16 23:40:47 +09:00
|
|
|
import fs from 'fs';
|
|
|
|
|
import path from 'path';
|
|
|
|
|
import { fileURLToPath } from 'url';
|
2026-01-16 21:11:02 +09:00
|
|
|
import Fastify from 'fastify';
|
2026-01-16 23:40:47 +09:00
|
|
|
import fastifyStatic from '@fastify/static';
|
2026-01-17 13:01:35 +09:00
|
|
|
import fastifySwagger from '@fastify/swagger';
|
|
|
|
|
import scalarApiReference from '@scalar/fastify-api-reference';
|
2026-01-16 23:01:23 +09:00
|
|
|
import multipart from '@fastify/multipart';
|
2026-01-16 21:11:02 +09:00
|
|
|
import config from './config/index.js';
|
|
|
|
|
|
|
|
|
|
// 플러그인
|
|
|
|
|
import dbPlugin from './plugins/db.js';
|
|
|
|
|
import redisPlugin from './plugins/redis.js';
|
2026-01-16 21:38:54 +09:00
|
|
|
import authPlugin from './plugins/auth.js';
|
2026-01-18 18:53:57 +09:00
|
|
|
import meilisearchPlugin from './plugins/meilisearch.js';
|
2026-01-16 21:11:02 +09:00
|
|
|
import youtubeBotPlugin from './services/youtube/index.js';
|
|
|
|
|
import xBotPlugin from './services/x/index.js';
|
|
|
|
|
import schedulerPlugin from './plugins/scheduler.js';
|
|
|
|
|
|
2026-01-16 21:38:54 +09:00
|
|
|
// 라우트
|
2026-01-17 13:01:35 +09:00
|
|
|
import routes from './routes/index.js';
|
2026-01-16 21:38:54 +09:00
|
|
|
|
2026-01-16 23:40:47 +09:00
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
|
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
|
|
2026-01-16 21:11:02 +09:00
|
|
|
export async function buildApp(opts = {}) {
|
|
|
|
|
const fastify = Fastify({
|
|
|
|
|
logger: {
|
|
|
|
|
level: opts.logLevel || 'info',
|
|
|
|
|
},
|
|
|
|
|
...opts,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// config 데코레이터 등록
|
|
|
|
|
fastify.decorate('config', config);
|
|
|
|
|
|
2026-01-16 23:01:23 +09:00
|
|
|
// multipart 플러그인 등록 (파일 업로드용)
|
|
|
|
|
await fastify.register(multipart, {
|
|
|
|
|
limits: {
|
|
|
|
|
fileSize: 10 * 1024 * 1024, // 10MB
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-16 21:11:02 +09:00
|
|
|
// 플러그인 등록 (순서 중요)
|
|
|
|
|
await fastify.register(dbPlugin);
|
|
|
|
|
await fastify.register(redisPlugin);
|
2026-01-16 21:38:54 +09:00
|
|
|
await fastify.register(authPlugin);
|
2026-01-18 18:53:57 +09:00
|
|
|
await fastify.register(meilisearchPlugin);
|
2026-01-16 21:11:02 +09:00
|
|
|
await fastify.register(youtubeBotPlugin);
|
|
|
|
|
await fastify.register(xBotPlugin);
|
|
|
|
|
await fastify.register(schedulerPlugin);
|
|
|
|
|
|
2026-01-17 13:01:35 +09:00
|
|
|
// 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' },
|
2026-01-17 16:50:16 +09:00
|
|
|
{ name: 'members', description: '멤버 API' },
|
|
|
|
|
{ name: 'albums', description: '앨범 API' },
|
|
|
|
|
{ name: 'schedules', description: '일정 API' },
|
2026-01-17 13:01:35 +09:00
|
|
|
{ name: 'stats', description: '통계 API' },
|
|
|
|
|
],
|
|
|
|
|
components: {
|
|
|
|
|
securitySchemes: {
|
|
|
|
|
bearerAuth: {
|
|
|
|
|
type: 'http',
|
|
|
|
|
scheme: 'bearer',
|
|
|
|
|
bearerFormat: 'JWT',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Scalar API Reference UI
|
|
|
|
|
await fastify.register(scalarApiReference, {
|
|
|
|
|
routePrefix: '/docs',
|
|
|
|
|
configuration: {
|
|
|
|
|
theme: 'purple',
|
|
|
|
|
spec: {
|
|
|
|
|
url: '/docs/json',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// OpenAPI JSON 엔드포인트
|
|
|
|
|
fastify.get('/docs/json', { schema: { hide: true } }, async () => {
|
|
|
|
|
return fastify.swagger();
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-16 21:38:54 +09:00
|
|
|
// 라우트 등록
|
2026-01-17 13:01:35 +09:00
|
|
|
await fastify.register(routes, { prefix: '/api' });
|
2026-01-16 21:38:54 +09:00
|
|
|
|
2026-01-16 21:11:02 +09:00
|
|
|
// 헬스 체크 엔드포인트
|
|
|
|
|
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;
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-16 23:40:47 +09:00
|
|
|
// 정적 파일 서빙 (프론트엔드 빌드 결과물) - 프로덕션 모드에서만
|
|
|
|
|
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');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 21:11:02 +09:00
|
|
|
return fastify;
|
|
|
|
|
}
|