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' ;
49import { config } from './config' ;
510import { logger } from './config/logger' ;
611import { 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
916import { problemRoutes } from './modules/problems/routes' ;
1017import { cSimulatorRoutes } from './modules/simulators/c/routes' ;
1118import { aiRoutes } from './modules/ai/routes' ;
1219import { courseRoutes } from './modules/courses/routes' ;
1320import { analyticsRoutes } from './modules/analytics/routes' ;
1421import { notesRoutes } from './modules/notes/routes' ;
1522import { gamificationRoutes } from './modules/gamification' ;
16- import adminRoutes from './modules/admin/admin.routes' ;
23+ import { adminRoutes } from './modules/admin/admin.routes' ;
1724import { userRoutes } from './modules/users/routes' ;
1825import pythonSimulatorRoutes from './modules/simulators/python/routes' ;
1926import { javaSimulatorRoutes } from './modules/simulators/java/routes' ;
2027import javascriptSimulatorRoutes from './modules/simulators/javascript/routes' ;
2128import { standaloneQuizzesRoutes } from './modules/standalone-quizzes/routes' ;
22- import { lessonContentLoader } from './services/lessonContentLoader' ;
2329
2430// Firebase Admin 초기화
2531try {
@@ -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 설정
4353const capacitorOrigins = [ 'capacitor://localhost' , 'https://localhost' , 'http://localhost' ] ;
4454const 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
165181export default app ;
0 commit comments