Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
## Overview

본 프로젝트는 지원자 정보 검토와 운영 업무 효율화를 위한 내부 관리 도구입니다.
프론트엔드는 React 기반 관리자 화면으로 구성되며, 백엔드는 Express API 서버로 동작합니다.
운영진은 목록 조회, 상세 확인, 파일 다운로드, 데이터 내보내기 기능을 통해 지원 정보를 체계적으로 관리할 수 있습니다.

## Features
Expand Down
4,886 changes: 1,188 additions & 3,698 deletions backend/package-lock.json

Large diffs are not rendered by default.

17 changes: 9 additions & 8 deletions backend/package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{
"name": "backoffice",
"version": "0.0.1",
"name": "backoffice-backend",
"version": "0.0.2",
"description": "",
"main": "src/app.js",
"main": "src/server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon src/app.js"
"start": "node src/server.js",
"dev": "nodemon src/server.js"
},
"author": "ejjem",
"license": "MIT",
Expand All @@ -17,19 +18,19 @@
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-rate-limit": "^7.5.0",
"express-session": "^1.18.0",
"fs": "^0.0.1-security",
"helmet": "^7.1.0",
"hpp": "^0.2.3",
"mongoose": "^8.12.2",
"morgan": "^1.10.0",
"multer": "^1.4.5-lts.1",
"mysql2": "^3.11.0",
"mysql2-cli": "^1.1.6",
"nodemon": "^3.1.4",
"passport": "^0.7.0",
"passport-local": "^1.0.0",
"process": "^0.11.10",
"sequelize": "^6.37.3",
"swagger-ui-express": "^5.0.1"
"swagger-ui-express": "^5.0.1",
"winston": "^3.17.0"
}
}
133 changes: 69 additions & 64 deletions backend/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ const app = express();
const morgan = require("morgan");
const winston = require("winston");

//보안
const cookieParser = require("cookie-parser");
const path = require("path");
const session = require("express-session");
Expand All @@ -20,62 +19,77 @@ const PORT = process.env.PORT;
const passportConfig = require("../src/passport");
passportConfig();

const { sequelize } = require("../src/models");
const { connectDB } = require("../src/models");
const loginRouter = require("../src/routes/login");
const memberRouter = require("../src/routes/member");
const seminaRouter = require("../src/routes/semina");
const featureRouter = require("../src/routes/feature");
const activityRouter = require("../src/routes/activity");
const { publicRouter: commentPublicRouter, adminRouter: commentAdminRouter } = require("../src/routes/comment");

const isProdOrTest = NODE_ENV === "production" || NODE_ENV === "test";
const PORT_NUMBER = Number(PORT) || 3001;

const SESSION_SECRET = process.env.COOKIE_SECRET;
if (!SESSION_SECRET && isProdOrTest) {
throw new Error("COOKIE_SECRET 환경변수가 필요합니다.");
}
const safeSessionSecret = SESSION_SECRET || "dev-only-cookie-secret";

const sessionOption = {
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
secret: safeSessionSecret,
cookie: {
maxAge: 1000 * 60 * 60 * 2, // 2시간
httpOnly: isProdOrTest, // production 또는 test이면 true
secure: isProdOrTest, // production 또는 test이면 true
...(isProdOrTest && { sameSite: "None" }), // production 또는 test이면 추가
maxAge: 1000 * 60 * 60 * 2,
httpOnly: true,
secure: isProdOrTest,
...(isProdOrTest && { sameSite: "None" }),
},
...(isProdOrTest && { proxy: true }), // production 또는 test이면 추가
...(isProdOrTest && { proxy: true }),
};

app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(cookieParser(safeSessionSecret));
app.use(session(sessionOption));
app.use(passport.initialize()); // req.user, req.login, req.isAuthenticate, req.logout
app.use(passport.session()); //connect.sid라는 이름으로 세션 쿠키가 브라우져로 전송
app.use(passport.initialize());
app.use(passport.session());
app.use(express.json());

if (process.env.NODE_ENV === "development") {
app.use(
cors({
origin: process.env.CLIENT_ORIGIN_DEV, // 클라이언트의 Origin
origin: [
process.env.CLIENT_ORIGIN_DEV,
process.env.MAIN_ORIGIN_DEV,
].filter(Boolean),
methods: ["GET", "POST", "OPTIONS", "DELETE", "PATCH"],
credentials: true, // 쿠키를 포함한 요청을 허용}));
credentials: true,
})
);
app.use(morgan("dev"));
app.use(express.urlencoded({ extended: false }));
} else {
app.use(
cors({
origin: process.env.CLIENT_ORIGIN, // 클라이언트의 Origin
methods: ["GET", "POST", "PATCH", "OPTIONS"],
credentials: true, // 쿠키를 포함한 요청을 허용}));
origin: [
process.env.CLIENT_ORIGIN,
process.env.MAIN_ORIGIN,
].filter(Boolean),
methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
credentials: true,
})
);
app.enable("trust proxy");
app.set("trust proxy", 1); // nginx 한 단계만 신뢰, XFF 첫 번째 IP 사용
app.use(morgan("combined"));
app.use(hpp());
app.use(express.urlencoded({ extended: false }));
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'none'"], // 기본적으로 모든 리소스 차단
scriptSrc: ["'none'"], // JavaScript 실행 차단 (XSS 방지)
styleSrc: ["'none'"], // 외부 스타일 차단
frameSrc: ["'none'"], // iframe 포함 금지 (Clickjacking 방어)
defaultSrc: ["'none'"],
scriptSrc: ["'none'"],
styleSrc: ["'none'"],
frameSrc: ["'none'"],
},
})
);
Expand All @@ -86,62 +100,50 @@ if (process.env.NODE_ENV === "development") {
app.use(helmet.referrerPolicy({ policy: "strict-origin-when-cross-origin" }));
}

// swagger 관련 세팅
const swaggerUi = require("swagger-ui-express");
const swaggerDocument = require("./swagger.json");

async function startServer() {
try {
// Sequelize 연결
await sequelize.authenticate();
console.log("[LOG] DB 연결 성공");

// Sequelize 테이블 동기화
await sequelize.sync();
console.log("[LOG] DB 연결 성공");

// 주기적으로 DB 연결 유지
setInterval(async () => {
try {
await sequelize.query("SELECT 1");
console.log("[LOG] DB 연결 유지 로직 작동");
} catch (err) {
console.error("[ERROR] DB 연결 체크/유지 실패: ", err);
}
}, 3600000);

// 서버 실행
app.listen(PORT, () => {
console.log(`PORT: ${PORT}`);
console.log(`swagger: http://localhost:${PORT}/api-docs`);
console.log(`server: http://localhost:${PORT}`);
});
} catch (err) {
console.error("DB 연결 실패:", err);
process.exit(1);
}
}
// 2026-05-09 startServer.js로 분리 by all4null
// async function startServer() {
// try {
// await connectDB();
// console.log("[LOG] MongoDB 연결 성공");

// app.listen(PORT_NUMBER, () => {
// console.log(`PORT: ${PORT_NUMBER}`);
// console.log(`swagger: http://localhost:${PORT_NUMBER}/api-docs`);
// console.log(`server: http://localhost:${PORT_NUMBER}`);
// });
// } catch (err) {
// console.error("DB 연결 실패:", err);
// process.exit(1);
// }
// }

// startServer();

app.get("/", (req, res) => {
res.status(200).json({ message: "backoffice backend is running" });
});

startServer();
app.get("/health", (req, res) => {
res.status(200).json({ status: "ok" });
});

//login
app.use("/bo/auth", loginRouter);

//member
app.use("/bo/member", memberRouter);

//semina
app.use("/bo/semina", seminaRouter);

//feature
app.use("/bo/feature", featureRouter);
// 하위호환: 구버전 프론트가 /feature/* 를 호출하는 경우 지원
app.use("/feature", featureRouter);
app.use("/bo", activityRouter); // activityRouter 추가
app.use("/", commentPublicRouter); // 공개 comment API (POST /comments, GET /comments)
app.use("/bo/admin", commentAdminRouter); // 관리자 comment API

//{url}/api-docs 개발시에만
if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test") {
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument));
}

//error handler
const logger = winston.createLogger({
level: "error",
format: winston.format.json(),
Expand All @@ -153,10 +155,13 @@ app.use((err, req, res, next) => {
console.log("[ERROR] error handler 동작");
console.error(err.stack || err);
} else {
logger.error(err.message || "Unexpected error"); // 운영 환경에서는 로그 파일에 저장
logger.error(err.message || "Unexpected error");
}

res.status(err.status || 500).json({
error: { message: "Internal Server Error" },
});
});

//server.js에서 app객체 사용가능하게 함
module.exports = app;
34 changes: 9 additions & 25 deletions backend/src/config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,21 @@ require("dotenv").config({
path: path.resolve(__dirname, "../../.env"),
});

const env = process.env.NODE_ENV || "development";

const config = {
development: {
username: process.env.DEV_DB_USER,
password: process.env.DEV_DB_PASSWORD,
database: process.env.DEV_DB_NAME,
host: process.env.DEV_DB_HOST,
dialect: "mysql",
port: process.env.DEV_DB_PORT || 3306,
logging: false,
mongodbUri: process.env.DEV_MONGODB_URI || process.env.MONGODB_URI,
dbName: process.env.DEV_MONGODB_DB_NAME || process.env.MONGODB_DB_NAME,
},
test: {
username: process.env.TEST_DB_USER,
password: process.env.TEST_DB_PASSWORD,
database: process.env.TEST_DB_NAME,
host: process.env.TEST_DB_HOST,
dialect: "mysql",
port: process.env.TEST_DB_PORT || 3306,
logging: false,
mongodbUri: process.env.TEST_MONGODB_URI || process.env.MONGODB_URI,
dbName: process.env.TEST_MONGODB_DB_NAME || process.env.MONGODB_DB_NAME,
},
production: {
username: process.env.PROD_DB_USER,
password: process.env.PROD_DB_PASSWORD,
database: process.env.PROD_DB_NAME,
host: process.env.PROD_DB_HOST,
dialect: "mysql",
port: process.env.PROD_DB_PORT || 3306,
logging: false,
mongodbUri: process.env.PROD_MONGODB_URI || process.env.MONGODB_URI,
dbName: process.env.PROD_MONGODB_DB_NAME || process.env.MONGODB_DB_NAME,
},
};

// ✅ 현재 NODE_ENV 값에 따라 설정 선택
const currentConfig = config[process.env.NODE_ENV];

module.exports = currentConfig;
module.exports = config[env];
Loading