forked from moduwa-aac/moduwa-server
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.js
More file actions
232 lines (202 loc) · 6.33 KB
/
app.js
File metadata and controls
232 lines (202 loc) · 6.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
import 'dotenv/config';
import cors from "cors";
import express from "express";
import helmet from "helmet";
import session from "express-session";
import cookieParser from "cookie-parser";
import passport from "./auth/middlewares/passport.config.js";
import { PrismaClient } from "@prisma/client";
import { initRedis } from "./auth/services/token.service.js";
import swaggerUi from "swagger-ui-express";
import { swaggerSpec } from "./swagger/swagger.js";
import {
AiPredictionTimeoutError,
UnauthorizedError
} from './errors/app.error.js';
// 라우터
import categoryRouter from "./category/category.route.js";
import ttsRouter from "./tts/tts.route.js";
import wordsRouter from "./words/routes/words.route.js";
import aiRouter from "./ai-prediction/routes/ai.prediction.route.js";
import historyRouter from "./history/history.route.js";
import authRoutes from "./auth/routes/auth.routes.js";
import routineRouter from "./routine/routine.route.js";
import settingsRouter from "./settings/settings.route.js";
import orderRouter from "./order/order.route.js";
import ttsSettingsRouter from "./tts/settings/ttsSettings.route.js";
// 유틸리티
import responseHelper from "./utils/response.util.js";
import errorHandler from "./utils/errorHandler.js";
const app = express();
const port = process.env.PORT || 3000;
const prisma = new PrismaClient();
// 1. 공통 미들웨어
// Helmet 설정: 환경별로 다른 보안 정책 적용
if (process.env.NODE_ENV !== 'production') {
// 개발 환경: Swagger UI를 위해 보안 정책 완화
app.use(helmet({
contentSecurityPolicy: false,
crossOriginEmbedderPolicy: false,
crossOriginOpenerPolicy: false,
crossOriginResourcePolicy: false,
}));
} else {
// 프로덕션 환경: 엄격한 CSP 적용
app.use(helmet({
contentSecurityPolicy: {
directives: {
// 오디오/비디오 로딩 허용 범위에 blob 추가
...helmet.contentSecurityPolicy.getDefaultDirectives(),
"media-src": ["'self'", "blob:"],
defaultSrc: ["'self'"],
styleSrc: ["'self'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
},
},
}));
}
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(cors({
origin: process.env.CORS_ORIGIN || 'http://localhost:5173',
credentials: true // 쿠키 전송을 허용
}));
app.use(express.static("public"));
// Session 추가 (OAuth용)
app.use(
session({
secret: process.env.SESSION_SECRET || "your-session-secret",
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === "production",
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000, // 24시간
},
}),
);
// Passport 초기화
app.use(passport.initialize());
app.use(passport.session());
// 2. 응답 헬퍼 함수 등록
app.use(responseHelper);
// +) 라우터 등록
// Swagger UI: 개발/스테이징에서만 활성화 (프로덕션에서는 보안을 위해 비활성화)
if (process.env.NODE_ENV !== 'production') {
// 브라우저 캐싱 방지 — spec 변경 시 즉시 반영
app.use('/api-docs', (req, res, next) => {
res.setHeader('Cache-Control', 'no-store');
next();
});
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec, {
swaggerOptions: {
persistAuthorization: true,
displayRequestDuration: true,
}
}));
console.log('📚 Swagger UI: http://localhost:3000/api-docs');
}
// 3. 테스트용 라우트
// 루틴 문장 API
app.use("/api/routines", routineRouter);
// 그리드 커스터마이징 API
app.use("/api/settings", settingsRouter);
// AI 예측 API
app.use("/api/ai", aiRouter);
// TTS API
app.use("/api/ai/tts", ttsRouter);
app.use("/api/ai/tts-settings", ttsSettingsRouter);
// Words API
app.use("/api/words", wordsRouter);
// History API
app.use("/api/histories", historyRouter);
// Category API
app.use("/api/categories", categoryRouter);
// 테스트용 : 인증 우회
// app.use(
// "/api/categories",
// (req, res, next) => {
// req.user = { userId: "dev-test-user" };
// next();
// },
// categoryRouter,
// );
// Category - order API
app.use("/api/order", orderRouter);
// 성공 케이스 테스트
app.get("/", (req, res) => {
const sampleData = { project: "moduwa-server", status: "Running" };
res.success(sampleData, "서버가 정상 작동 중입니다.");
});
// Health Check 추가
app.get("/health/db", async (req, res, next) => {
try {
await prisma.$queryRaw`SELECT 1`;
res.success({ database: "MySQL" }, "Database connected!");
} catch (error) {
next(error);
}
});
// Auth Routes 추가
app.use("/api/auth", authRoutes);
// 에러 케이스 테스트 (직접 throw)
app.get("/error-test", (req, res, next) => {
// Service 로직에서 에러가 발생했다고 가정
try {
throw new AiPredictionTimeoutError("테스트용 AI 타임아웃 발생");
} catch (error) {
next(error); // 전역 핸들러로 전달
}
});
// 인증 에러 테스트
app.get("/auth-test", (req, res, next) => {
try {
throw new UnauthorizedError();
} catch (error) {
next(error);
}
});
// 4. 404 Not Found 핸들러
app.use((req, res, next) => {
res.status(404).error({
code: "NOT_FOUND",
message: "요청하신 API 경로를 찾을 수 없습니다.",
});
});
// 5. 전역 에러 핸들러
app.use(errorHandler);
// 서버 실행 (기존 코드 대체)
async function startServer() {
try {
await prisma.$connect();
console.log("[Database] Connected to MySQL");
await initRedis();
console.log("[Redis] Connected");
app.listen(port, () => {
console.log(`[Server] Running on port ${port}`);
console.log(`[Environment] ${process.env.NODE_ENV || "development"}`);
});
} catch (error) {
console.error("[Error] Server startup failed:", error);
await prisma.$disconnect();
process.exit(1);
}
}
// Graceful Shutdown
const gracefulShutdown = async (signal) => {
console.log(`[Shutdown] ${signal} received, closing gracefully...`);
try {
await prisma.$disconnect();
console.log("[Database] Disconnected");
process.exit(0);
} catch (error) {
console.error("[Error] Shutdown error:", error);
process.exit(1);
}
};
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
startServer();