Skip to content

Commit f5fb0b7

Browse files
jammy0903claude
andcommitted
fix: Fastify 마이그레이션 복원 + subscription 참조 제거
- 머지 충돌 해결 과정에서 손실된 Fastify 코드 복원 - app.ts: Express → Fastify 복원 - ai/routes.ts: subscriptionService, recordAIUsage 참조 제거 - 구독 시스템 제거로 AI 사용량 제한 없음 (로그인만 필요) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e5452ba commit f5fb0b7

2 files changed

Lines changed: 779 additions & 570 deletions

File tree

packages/backend/src/app.ts

Lines changed: 114 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,31 @@
1-
import express from 'express';
2-
import cors from 'cors';
3-
import swaggerUi from 'swagger-ui-express';
1+
/**
2+
* Fastify 애플리케이션 진입점
3+
*
4+
* Express에서 Fastify로 마이그레이션됨 (2026-02)
5+
*/
6+
7+
import Fastify, { FastifyInstance } from 'fastify';
8+
import cors from '@fastify/cors';
49
import { config } from './config';
510
import { logger } from './config/logger';
611
import { initializeFirebase } from './config/firebase';
7-
import { swaggerSpec } from './config/swagger';
8-
import { rateLimit, authRateLimit, aiRateLimit, executeRateLimit, requestLogger } from './middleware';
12+
import { authPlugin, rateLimitPlugin, swaggerPlugin } from './plugins';
13+
import { lessonContentLoader } from './services/lessonContentLoader';
14+
15+
// Route imports
916
import { problemRoutes } from './modules/problems/routes';
1017
import { cSimulatorRoutes } from './modules/simulators/c/routes';
1118
import { aiRoutes } from './modules/ai/routes';
1219
import { courseRoutes } from './modules/courses/routes';
1320
import { analyticsRoutes } from './modules/analytics/routes';
1421
import { notesRoutes } from './modules/notes/routes';
1522
import { gamificationRoutes } from './modules/gamification';
16-
import adminRoutes from './modules/admin/admin.routes';
23+
import { adminRoutes } from './modules/admin/admin.routes';
1724
import { userRoutes } from './modules/users/routes';
1825
import pythonSimulatorRoutes from './modules/simulators/python/routes';
1926
import { javaSimulatorRoutes } from './modules/simulators/java/routes';
2027
import javascriptSimulatorRoutes from './modules/simulators/javascript/routes';
2128
import { standaloneQuizzesRoutes } from './modules/standalone-quizzes/routes';
22-
import { lessonContentLoader } from './services/lessonContentLoader';
2329

2430
// Firebase Admin 초기화
2531
try {
@@ -36,130 +42,140 @@ lessonContentLoader.scanFilePaths().catch((err) => {
3642
logger.warn('App will continue without pre-loaded lesson contents');
3743
});
3844

39-
const app = express();
45+
// Fastify 인스턴스 생성
46+
const app: FastifyInstance = Fastify({
47+
logger: false, // 커스텀 로거 사용
48+
bodyLimit: 10 * 1024 * 1024, // 10MB (config.server.jsonBodyLimit)
49+
trustProxy: true,
50+
});
4051

41-
// Middleware
42-
// Capacitor 앱 Origin 허용 (Android: capacitor://localhost, https://localhost)
52+
// CORS 설정
4353
const capacitorOrigins = ['capacitor://localhost', 'https://localhost', 'http://localhost'];
4454
const allowedOrigins = [...config.server.corsOrigins, ...capacitorOrigins];
4555

46-
app.use(cors({
56+
app.register(cors, {
4757
origin: config.server.isDev ? true : (origin, callback) => {
48-
// origin이 없으면 허용 (same-origin 요청)
4958
if (!origin) return callback(null, true);
50-
51-
// 허용된 Origin 체크
5259
if (allowedOrigins.some(allowed => origin.startsWith(allowed))) {
5360
return callback(null, true);
5461
}
55-
56-
callback(new Error('Not allowed by CORS'));
62+
callback(new Error('Not allowed by CORS'), false);
5763
},
58-
credentials: true
59-
}));
60-
app.use(express.json({ limit: config.server.jsonBodyLimit }));
61-
app.use(requestLogger);
62-
63-
// Routes
64-
app.get('/', (req, res) => {
65-
res.json({ status: 'ok', service: 'CodeInsight Backend API' });
64+
credentials: true,
65+
});
66+
67+
// 플러그인 등록
68+
app.register(authPlugin);
69+
app.register(rateLimitPlugin);
70+
app.register(swaggerPlugin);
71+
72+
// Request 로깅 훅
73+
app.addHook('onResponse', (request, reply, done) => {
74+
logger.info('HTTP Request', {
75+
method: request.method,
76+
url: request.url,
77+
statusCode: reply.statusCode,
78+
duration: `${Math.round(reply.elapsedTime)}ms`,
79+
ip: request.ip,
80+
});
81+
done();
82+
});
83+
84+
// =============================================
85+
// 기본 라우트
86+
// =============================================
87+
app.get('/', async () => {
88+
return { status: 'ok', service: 'CodeInsight Backend API' };
6689
});
6790

68-
app.get('/health', (req, res) => {
69-
res.json({ status: 'healthy' });
91+
app.get('/health', async () => {
92+
return { status: 'healthy' };
7093
});
7194

7295
// =============================================
73-
// API v1 Routes (현재 버전)
96+
// API v1 Routes
7497
// =============================================
75-
app.use('/api/v1/problems', rateLimit, problemRoutes);
76-
app.use('/api/v1/simulators/c', executeRateLimit, cSimulatorRoutes);
77-
app.use('/api/v1/simulators/python', executeRateLimit, pythonSimulatorRoutes);
78-
app.use('/api/v1/simulators/java', executeRateLimit, javaSimulatorRoutes);
79-
app.use('/api/v1/simulators/javascript', executeRateLimit, javascriptSimulatorRoutes);
80-
app.use('/api/v1/ai', aiRateLimit, aiRoutes);
81-
app.use('/api/v1/courses', rateLimit, courseRoutes);
82-
app.use('/api/v1/analytics', rateLimit, analyticsRoutes);
83-
app.use('/api/v1/notes', rateLimit, notesRoutes);
84-
app.use('/api/v1/gamification', rateLimit, gamificationRoutes);
85-
app.use('/api/v1/admin', rateLimit, adminRoutes);
86-
app.use('/api/v1/users', rateLimit, userRoutes);
87-
app.use('/api/v1/standalone-quizzes', rateLimit, standaloneQuizzesRoutes);
98+
app.register(problemRoutes, { prefix: '/api/v1/problems' });
99+
app.register(cSimulatorRoutes, { prefix: '/api/v1/simulators/c' });
100+
app.register(pythonSimulatorRoutes, { prefix: '/api/v1/simulators/python' });
101+
app.register(javaSimulatorRoutes, { prefix: '/api/v1/simulators/java' });
102+
app.register(javascriptSimulatorRoutes, { prefix: '/api/v1/simulators/javascript' });
103+
app.register(aiRoutes, { prefix: '/api/v1/ai' });
104+
app.register(courseRoutes, { prefix: '/api/v1/courses' });
105+
app.register(analyticsRoutes, { prefix: '/api/v1/analytics' });
106+
app.register(notesRoutes, { prefix: '/api/v1/notes' });
107+
app.register(gamificationRoutes, { prefix: '/api/v1/gamification' });
108+
app.register(adminRoutes, { prefix: '/api/v1/admin' });
109+
app.register(userRoutes, { prefix: '/api/v1/users' });
110+
app.register(standaloneQuizzesRoutes, { prefix: '/api/v1/standalone-quizzes' });
88111

89112
// =============================================
90113
// Legacy Routes (버전 없는 요청 → v1로 리다이렉트)
91114
// =============================================
92-
app.use('/api/problems', (req, res) => {
93-
res.redirect(301, `/api/v1/problems${req.path === '/' ? '' : req.path}`);
94-
});
95-
app.use('/api/memory', (req, res) => {
96-
res.redirect(301, `/api/v1/simulators/c/trace${req.path === '/' ? '' : req.path}`);
97-
});
98-
app.use('/api/submissions', (req, res) => {
99-
res.redirect(301, `/api/v1/submissions${req.path === '/' ? '' : req.path}`);
100-
});
101-
app.use('/api/users', (req, res) => {
102-
res.redirect(301, `/api/v1/users${req.path === '/' ? '' : req.path}`);
103-
});
104-
app.use('/api/c', (req, res) => {
105-
res.redirect(301, `/api/v1/c${req.path === '/' ? '' : req.path}`);
106-
});
107-
app.use('/api/ai', (req, res) => {
108-
res.redirect(301, `/api/v1/ai${req.path === '/' ? '' : req.path}`);
109-
});
110-
app.use('/api/courses', (req, res) => {
111-
res.redirect(301, `/api/v1/courses${req.path === '/' ? '' : req.path}`);
112-
});
113-
app.use('/api/analytics', (req, res) => {
114-
res.redirect(301, `/api/v1/analytics${req.path === '/' ? '' : req.path}`);
115-
});
116-
app.use('/api/notes', (req, res) => {
117-
res.redirect(301, `/api/v1/notes${req.path === '/' ? '' : req.path}`);
118-
});
119-
app.use('/api/admin', (req, res) => {
120-
res.redirect(301, `/api/v1/admin${req.path === '/' ? '' : req.path}`);
121-
});
122-
app.use('/api/gamification', (req, res) => {
123-
res.redirect(301, `/api/v1/gamification${req.path === '/' ? '' : req.path}`);
124-
});
125-
app.use('/api/standalone-quizzes', (req, res) => {
126-
res.redirect(301, `/api/v1/standalone-quizzes${req.path === '/' ? '' : req.path}`);
115+
const legacyRedirects: Record<string, string> = {
116+
'/api/problems': '/api/v1/problems',
117+
'/api/memory': '/api/v1/simulators/c/trace',
118+
'/api/submissions': '/api/v1/submissions',
119+
'/api/users': '/api/v1/users',
120+
'/api/c': '/api/v1/c',
121+
'/api/ai': '/api/v1/ai',
122+
'/api/courses': '/api/v1/courses',
123+
'/api/analytics': '/api/v1/analytics',
124+
'/api/notes': '/api/v1/notes',
125+
'/api/admin': '/api/v1/admin',
126+
'/api/gamification': '/api/v1/gamification',
127+
'/api/standalone-quizzes': '/api/v1/standalone-quizzes',
128+
};
129+
130+
Object.entries(legacyRedirects).forEach(([oldPath, newPath]) => {
131+
app.all(`${oldPath}/*`, async (request, reply) => {
132+
const subPath = request.url.replace(oldPath, '');
133+
return reply.redirect(`${newPath}${subPath}`);
134+
});
135+
app.all(oldPath, async (_request, reply) => {
136+
return reply.redirect(newPath);
137+
});
127138
});
128139

129-
// Swagger UI
130-
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, {
131-
customCss: '.swagger-ui .topbar { display: none }',
132-
customSiteTitle: 'C-OSINE API Docs'
133-
}));
134-
135-
// OpenAPI JSON endpoint
136-
app.get('/api-docs.json', (req, res) => {
137-
res.setHeader('Content-Type', 'application/json');
138-
res.send(swaggerSpec);
139-
});
140+
// =============================================
141+
// Error Handlers
142+
// =============================================
140143

141144
// 404 handler
142-
app.use((req, res) => {
143-
res.status(404).json({ error: 'Not found', path: req.path });
145+
app.setNotFoundHandler(async (request, reply) => {
146+
return reply.status(404).send({ error: 'Not found', path: request.url });
144147
});
145148

146149
// Global error handler
147-
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
150+
app.setErrorHandler(async (error, request, reply) => {
151+
const err = error as Error & { statusCode?: number };
148152
logger.error('Unhandled error', {
149153
message: err.message,
150154
stack: err.stack,
151-
url: req.url,
152-
method: req.method,
155+
url: request.url,
156+
method: request.method,
153157
});
154-
res.status(500).json({
155-
error: 'Internal server error',
156-
message: config.server.isDev ? err.message : undefined
158+
159+
const statusCode = err.statusCode || 500;
160+
return reply.status(statusCode).send({
161+
error: statusCode === 500 ? 'Internal server error' : err.message,
162+
message: config.server.isDev ? err.message : undefined,
157163
});
158164
});
159165

160-
// Start server
161-
app.listen(config.server.port, () => {
162-
logger.info(`Server running on http://localhost:${config.server.port}`);
163-
});
166+
// =============================================
167+
// Server Start
168+
// =============================================
169+
const start = async () => {
170+
try {
171+
await app.listen({ port: config.server.port, host: '0.0.0.0' });
172+
logger.info(`Server running on http://localhost:${config.server.port}`);
173+
} catch (err) {
174+
logger.error('Failed to start server:', err);
175+
process.exit(1);
176+
}
177+
};
178+
179+
start();
164180

165181
export default app;

0 commit comments

Comments
 (0)