Ver3.0 ๐ Node.js์ Express๋ก ๋ง๋๋ ์ผํ๋ชฐ API ์๋ฒ ์๋ฒฝ ๊ฐ์ด๋

๐ Node.js์ Express๋ก ๋ง๋๋ ์ผํ๋ชฐ API ์๋ฒ ์๋ฒฝ ๊ฐ์ด๋
์ค์ ํ๋ก์ ํธ๋ก ๋ฐฐ์ฐ๋ ๋ฐฑ์๋ ๊ฐ๋ฐ์ ๋ชจ๋ ๊ฒ
๐ฏ ์ Node.js์ Express๋ก ์ผํ๋ชฐ API๋ฅผ ๋ง๋ค๊น?
์์ฆ ์ผํ๋ชฐ ํ๋ ๋ง๋ค๋ ค๊ณ ํ๋ฉด ์ ๋ง ๊ณ ๋ฏผ์ด ๋ง์์ง์ง? ๐ค ํ๋ก ํธ์๋๋ React๋ Vue๋ก ํ๊ณ , ๋ชจ๋ฐ์ผ ์ฑ๋ ๋ฐ๋ก ๋ง๋ค์ด์ผ ํ๊ณ ... ๊ทธ๋ด ๋ ํ์ํ ๊ฒ ๋ฐ๋ก API ์๋ฒ์ผ!
Node.js์ Express๋ฅผ ์ ํํ๋ ์ด์ ๋ ๋ช ํํด. ์ฒซ์งธ, **JavaScript ํ๋๋ก ํ๋ก ํธ์ ๋ฐฑ์๋๋ฅผ ๋ชจ๋ ๋ค๋ฃฐ ์ ์๋ค**๋ ์ ์ด ์์ฒญ๋ ์ฅ์ ์ด์ผ. ๋์งธ, **๋น๋๊ธฐ ์ฒ๋ฆฌ์ ์ต์ ํ**๋์ด ์์ด์ ๋์์ ๋ง์ ์ฌ์ฉ์ ์์ฒญ์ ์ฒ๋ฆฌํ๊ธฐ ์ข์. ์ ์งธ, **npm ์ํ๊ณ**๊ฐ ์ด๋ง์ด๋งํ๊ฒ ๋ฐฉ๋ํด์ ํ์ํ ๊ธฐ๋ฅ์ ๋๋ถ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก ํด๊ฒฐํ ์ ์์ง.
์ฌ๋ฅ๋ท(https://www.jaenung.net) ๊ฐ์ ํ๋ซํผ์์๋ ๋ฐฑ์๋ ๊ฐ๋ฐ ์ฌ๋ฅ์ ๊ฑฐ๋ํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์๋ฐ, Node.js ๊ธฐ๋ฐ API ๊ฐ๋ฐ์ ์ ๋ง ์์๊ฐ ๋์ ๋ถ์ผ์ผ. ๐
๐ ๏ธ ๊ฐ๋ฐ ํ๊ฒฝ ์ธํ ํ๊ธฐ
ํ์ ๋๊ตฌ ์ค์น
๋ณธ๊ฒฉ์ ์ผ๋ก ์์ํ๊ธฐ ์ ์ ๊ฐ๋ฐ ํ๊ฒฝ๋ถํฐ ์ ๋๋ก ๊ฐ์ถฐ๋ณด์! โ๏ธ
1. Node.js ์ค์น
๊ณต์ ์ฌ์ดํธ(nodejs.org)์์ LTS ๋ฒ์ ์ ๋ค์ด๋ก๋ํด. ํ์ฌ ๊ธฐ์ค์ผ๋ก 18.x ์ด์ ๋ฒ์ ์ ์ถ์ฒํด. ์ค์น ํ ํฐ๋ฏธ๋์์ ํ์ธํด๋ณด์:
node --version
npm --version
2. ํ๋ก์ ํธ ์ด๊ธฐํ
์๋ก์ด ํด๋๋ฅผ ๋ง๋ค๊ณ npm ํ๋ก์ ํธ๋ฅผ ์ด๊ธฐํํด:
mkdir shopping-mall-api
cd shopping-mall-api
npm init -y
3. Express ๋ฐ ํ์ ํจํค์ง ์ค์น
์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ํ์ํ ํจํค์ง๋ค์ ์ค์นํ ์ฐจ๋ก์ผ:
npm install express
npm install mongoose dotenv cors
npm install jsonwebtoken bcrypt
npm install express-validator
npm install --save-dev nodemon
โข mongoose: MongoDB ODM
โข dotenv: ํ๊ฒฝ๋ณ์ ๊ด๋ฆฌ
โข cors: ํฌ๋ก์ค ์ค๋ฆฌ์ง ์์ฒญ ์ฒ๋ฆฌ
โข jsonwebtoken: JWT ์ธ์ฆ
โข bcrypt: ๋น๋ฐ๋ฒํธ ์ํธํ
โข express-validator: ์ ๋ ฅ๊ฐ ๊ฒ์ฆ
โข nodemon: ์๋ ์ฌ์์ (๊ฐ๋ฐ์ฉ)
ํ๋ก์ ํธ ๊ตฌ์กฐ ์ค๊ณ
๊น๋ํ ํ๋ก์ ํธ ๊ตฌ์กฐ๋ ์ ์ง๋ณด์์ ํต์ฌ์ด์ผ! ๐ ๋ค์๊ณผ ๊ฐ์ ๊ตฌ์กฐ๋ฅผ ์ถ์ฒํด:
shopping-mall-api/
โโโ src/
โ โโโ config/
โ โ โโโ database.js
โ โโโ models/
โ โ โโโ User.js
โ โ โโโ Product.js
โ โ โโโ Order.js
โ โ โโโ Cart.js
โ โโโ routes/
โ โ โโโ auth.js
โ โ โโโ products.js
โ โ โโโ orders.js
โ โ โโโ cart.js
โ โโโ controllers/
โ โ โโโ authController.js
โ โ โโโ productController.js
โ โ โโโ orderController.js
โ โ โโโ cartController.js
โ โโโ middlewares/
โ โ โโโ auth.js
โ โ โโโ errorHandler.js
โ โโโ utils/
โ โโโ validators.js
โโโ .env
โโโ .gitignore
โโโ package.json
โโโ server.js
์ด๋ฐ ๊ตฌ์กฐ๋ฅผ ์ฌ์ฉํ๋ฉด **๊ด์ฌ์ฌ์ ๋ถ๋ฆฌ(Separation of Concerns)**๊ฐ ๋ช ํํด์ ธ์ ๋์ค์ ์ฝ๋๋ฅผ ์ฐพ๊ฑฐ๋ ์์ ํ๊ธฐ๊ฐ ํจ์ฌ ์ฌ์์ ธ. โจ
๐ ๊ธฐ๋ณธ ์๋ฒ ๊ตฌ์ถํ๊ธฐ
Express ์๋ฒ ์ด๊ธฐ ์ค์
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์๋ฒ๋ฅผ ๋ง๋ค์ด๋ณด์! ๐ป server.js ํ์ผ์ ์์ฑํ๊ณ ๋ค์ ์ฝ๋๋ฅผ ์์ฑํด:
const express = require('express');
const cors = require('cors');
const dotenv = require('dotenv');
// ํ๊ฒฝ๋ณ์ ๋ก๋
dotenv.config();
// Express ์ฑ ์์ฑ
const app = express();
// ๋ฏธ๋ค์จ์ด ์ค์
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// ๊ธฐ๋ณธ ๋ผ์ฐํธ
app.get('/', (req, res) => {
res.json({
message: '์ผํ๋ชฐ API ์๋ฒ์ ์ค์ ๊ฒ์ ํ์ํฉ๋๋ค!',
version: '1.0.0'
});
});
// ์๋ฒ ์์
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`์๋ฒ๊ฐ ํฌํธ ${PORT}์์ ์คํ ์ค์
๋๋ค.`);
});
์ด ์ฝ๋๋ ์ ๋ง ๊ธฐ๋ณธ์ ์ธ Express ์๋ฒ์ผ. cors()๋ ํ๋ก ํธ์๋์์ API๋ฅผ ํธ์ถํ ์ ์๊ฒ ํด์ฃผ๊ณ , express.json()์ JSON ํํ์ ์์ฒญ ๋ณธ๋ฌธ์ ํ์ฑํด์ฃผ์ง.
ํ๊ฒฝ๋ณ์ ์ค์
.env ํ์ผ์ ๋ง๋ค์ด์ ์ค์ํ ์ค์ ๊ฐ๋ค์ ๊ด๋ฆฌํ์:
PORT=5000
MONGODB_URI=mongodb://localhost:27017/shopping-mall
JWT_SECRET=your-super-secret-key-change-this-in-production
JWT_EXPIRE=7d
NODE_ENV=development
.env ํ์ผ์ ์ ๋๋ก Git์ ์ปค๋ฐํ๋ฉด ์ ๋ผ! .gitignore์ ๋ฐ๋์ ์ถ๊ฐํด์ผ ํด. JWT_SECRET์ ์ค์ ์ด์ ํ๊ฒฝ์์๋ ๋ณต์กํ๊ณ ๊ธด ๋๋ค ๋ฌธ์์ด์ ์ฌ์ฉํด์ผ ํด.
MongoDB ์ฐ๊ฒฐ ์ค์
src/config/database.js ํ์ผ์ ๋ง๋ค์ด์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ์ ์ค์ ํ์:
const mongoose = require('mongoose');
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log('MongoDB ์ฐ๊ฒฐ ์ฑ๊ณต!');
} catch (error) {
console.error('MongoDB ์ฐ๊ฒฐ ์คํจ:', error.message);
process.exit(1);
}
};
module.exports = connectDB;
๊ทธ๋ฆฌ๊ณ server.js์์ ์ด ํจ์๋ฅผ ํธ์ถํด:
const connectDB = require('./src/config/database');
// MongoDB ์ฐ๊ฒฐ
connectDB();
๐ฆ ๋ฐ์ดํฐ ๋ชจ๋ธ ์ค๊ณํ๊ธฐ
์ผํ๋ชฐ API์ ํต์ฌ์ ๋ฐ์ดํฐ ๋ชจ๋ธ์ด์ผ! ๐๏ธ ์ ๋๋ก ์ค๊ณํ๋ฉด ๋์ค์ ๊ธฐ๋ฅ ์ถ๊ฐ๊ฐ ํจ์ฌ ์ฌ์์ ธ.
User ๋ชจ๋ธ (ํ์ ์ ๋ณด)
src/models/User.js ํ์ผ์ ๋ง๋ค์ด๋ณด์:
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const userSchema = new mongoose.Schema({
email: {
type: String,
required: [true, '์ด๋ฉ์ผ์ ํ์์
๋๋ค'],
unique: true,
lowercase: true,
trim: true,
match: [/^\S+@\S+\.\S+$/, '์ ํจํ ์ด๋ฉ์ผ์ ์
๋ ฅํด์ฃผ์ธ์']
},
password: {
type: String,
required: [true, '๋น๋ฐ๋ฒํธ๋ ํ์์
๋๋ค'],
minlength: [6, '๋น๋ฐ๋ฒํธ๋ ์ต์ 6์ ์ด์์ด์ด์ผ ํฉ๋๋ค'],
select: false
},
name: {
type: String,
required: [true, '์ด๋ฆ์ ํ์์
๋๋ค'],
trim: true
},
phone: {
type: String,
required: [true, '์ ํ๋ฒํธ๋ ํ์์
๋๋ค']
},
address: {
zipCode: String,
street: String,
detail: String
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
createdAt: {
type: Date,
default: Date.now
}
});
// ๋น๋ฐ๋ฒํธ ์ํธํ ๋ฏธ๋ค์จ์ด
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
// ๋น๋ฐ๋ฒํธ ํ์ธ ๋ฉ์๋
userSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
module.exports = mongoose.model('User', userSchema);
โข select: false๋ก ๋น๋ฐ๋ฒํธ๋ ๊ธฐ๋ณธ ์กฐํ์์ ์ ์ธ๋ผ
โข comparePassword ๋ฉ์๋๋ก ๋ก๊ทธ์ธ ์ ๋น๋ฐ๋ฒํธ ๊ฒ์ฆ์ ์ฝ๊ฒ ํ ์ ์์ด
Product ๋ชจ๋ธ (์ํ ์ ๋ณด)
src/models/Product.js๋ฅผ ๋ง๋ค์ด๋ณด์:
const mongoose = require('mongoose');
const productSchema = new mongoose.Schema({
name: {
type: String,
required: [true, '์ํ๋ช
์ ํ์์
๋๋ค'],
trim: true,
maxlength: [100, '์ํ๋ช
์ 100์๋ฅผ ์ด๊ณผํ ์ ์์ต๋๋ค']
},
description: {
type: String,
required: [true, '์ํ ์ค๋ช
์ ํ์์
๋๋ค'],
maxlength: [2000, '์ค๋ช
์ 2000์๋ฅผ ์ด๊ณผํ ์ ์์ต๋๋ค']
},
price: {
type: Number,
required: [true, '๊ฐ๊ฒฉ์ ํ์์
๋๋ค'],
min: [0, '๊ฐ๊ฒฉ์ 0 ์ด์์ด์ด์ผ ํฉ๋๋ค']
},
discountPrice: {
type: Number,
min: [0, 'ํ ์ธ๊ฐ๋ 0 ์ด์์ด์ด์ผ ํฉ๋๋ค']
},
category: {
type: String,
required: [true, '์นดํ
๊ณ ๋ฆฌ๋ ํ์์
๋๋ค'],
enum: ['์๋ฅ', '์ ์์ ํ', '์ํ', '๋์', '์ํ์ฉํ', '๊ธฐํ']
},
stock: {
type: Number,
required: [true, '์ฌ๊ณ ๋ ํ์์
๋๋ค'],
min: [0, '์ฌ๊ณ ๋ 0 ์ด์์ด์ด์ผ ํฉ๋๋ค'],
default: 0
},
images: [{
url: String,
alt: String
}],
rating: {
average: {
type: Number,
default: 0,
min: 0,
max: 5
},
count: {
type: Number,
default: 0
}
},
isActive: {
type: Boolean,
default: true
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
});
// ์
๋ฐ์ดํธ ์ updatedAt ์๋ ๊ฐฑ์
productSchema.pre('save', function(next) {
this.updatedAt = Date.now();
next();
});
// ๊ฐ์ ํ๋: ํ ์ธ์จ ๊ณ์ฐ
productSchema.virtual('discountRate').get(function() {
if (this.discountPrice && this.price > this.discountPrice) {
return Math.round(((this.price - this.discountPrice) / this.price) * 100);
}
return 0;
});
module.exports = mongoose.model('Product', productSchema);
์ฌ๊ธฐ์ ์ฃผ๋ชฉํ ์ ์ virtual ํ๋์ผ! โจ ์ค์ ๋ก DB์ ์ ์ฅ๋์ง ์์ง๋ง, ์กฐํํ ๋ ์๋์ผ๋ก ๊ณ์ฐ๋๋ ํ๋๋ฅผ ๋ง๋ค ์ ์์ด. ํ ์ธ์จ ๊ฐ์ ๊ณ์ฐ๊ฐ์ ์ ์ฉํ์ง.
Order ๋ชจ๋ธ (์ฃผ๋ฌธ ์ ๋ณด)
์ฃผ๋ฌธ ๋ชจ๋ธ์ ์ข ๋ ๋ณต์กํด. src/models/Order.js:
const mongoose = require('mongoose');
const orderSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
orderNumber: {
type: String,
unique: true,
required: true
},
items: [{
product: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Product',
required: true
},
name: String,
price: Number,
quantity: {
type: Number,
required: true,
min: 1
},
subtotal: Number
}],
totalAmount: {
type: Number,
required: true
},
shippingAddress: {
name: String,
phone: String,
zipCode: String,
street: String,
detail: String
},
paymentMethod: {
type: String,
enum: ['card', 'bank', 'virtual', 'mobile'],
required: true
},
paymentStatus: {
type: String,
enum: ['pending', 'paid', 'failed', 'refunded'],
default: 'pending'
},
orderStatus: {
type: String,
enum: ['pending', 'confirmed', 'preparing', 'shipped', 'delivered', 'cancelled'],
default: 'pending'
},
createdAt: {
type: Date,
default: Date.now
},
paidAt: Date,
deliveredAt: Date
});
// ์ฃผ๋ฌธ๋ฒํธ ์๋ ์์ฑ
orderSchema.pre('save', async function(next) {
if (!this.orderNumber) {
const date = new Date();
const dateStr = date.toISOString().slice(0, 10).replace(/-/g, '');
const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0');
this.orderNumber = `ORD${dateStr}${random}`;
}
next();
});
module.exports = mongoose.model('Order', orderSchema);
Cart ๋ชจ๋ธ (์ฅ๋ฐ๊ตฌ๋)
๋ง์ง๋ง์ผ๋ก ์ฅ๋ฐ๊ตฌ๋ ๋ชจ๋ธ์ด์ผ. src/models/Cart.js:
const mongoose = require('mongoose');
const cartSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
unique: true
},
items: [{
product: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Product',
required: true
},
quantity: {
type: Number,
required: true,
min: 1,
default: 1
},
addedAt: {
type: Date,
default: Date.now
}
}],
updatedAt: {
type: Date,
default: Date.now
}
});
// ์
๋ฐ์ดํธ ์ updatedAt ์๋ ๊ฐฑ์
cartSchema.pre('save', function(next) {
this.updatedAt = Date.now();
next();
});
module.exports = mongoose.model('Cart', cartSchema);
๐ ์ธ์ฆ ์์คํ ๊ตฌ์ถํ๊ธฐ
์ผํ๋ชฐ์์ ์ธ์ฆ์ ์ ๋ง ์ค์ํด! ๐ JWT(JSON Web Token)๋ฅผ ์ฌ์ฉํด์ ์์ ํ๊ณ ํ์ฅ ๊ฐ๋ฅํ ์ธ์ฆ ์์คํ ์ ๋ง๋ค์ด๋ณด์.
JWT ๋ฏธ๋ค์จ์ด ์์ฑ
src/middlewares/auth.js ํ์ผ์ ๋ง๋ค์ด๋ณด์:
const jwt = require('jsonwebtoken');
const User = require('../models/User');
exports.protect = async (req, res, next) => {
try {
let token;
// ํค๋์์ ํ ํฐ ์ถ์ถ
if (req.headers.authorization &&
req.headers.authorization.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
}
if (!token) {
return res.status(401).json({
success: false,
message: '์ธ์ฆ ํ ํฐ์ด ์์ต๋๋ค'
});
}
// ํ ํฐ ๊ฒ์ฆ
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// ์ฌ์ฉ์ ์ ๋ณด ์กฐํ
req.user = await User.findById(decoded.id);
if (!req.user) {
return res.status(401).json({
success: false,
message: '์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค'
});
}
next();
} catch (error) {
return res.status(401).json({
success: false,
message: '์ ํจํ์ง ์์ ํ ํฐ์
๋๋ค'
});
}
};
// ๊ด๋ฆฌ์ ๊ถํ ํ์ธ
exports.authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
message: '์ ๊ทผ ๊ถํ์ด ์์ต๋๋ค'
});
}
next();
};
};
์ธ์ฆ ์ปจํธ๋กค๋ฌ ์์ฑ
src/controllers/authController.js๋ฅผ ๋ง๋ค์ด์ ํ์๊ฐ์
, ๋ก๊ทธ์ธ ๋ก์ง์ ๊ตฌํํ์:
const jwt = require('jsonwebtoken');
const User = require('../models/User');
// JWT ํ ํฐ ์์ฑ ํจ์
const generateToken = (id) => {
return jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRE
});
};
// ํ์๊ฐ์
exports.register = async (req, res) => {
try {
const { email, password, name, phone } = req.body;
// ์ด๋ฉ์ผ ์ค๋ณต ํ์ธ
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({
success: false,
message: '์ด๋ฏธ ๊ฐ์
๋ ์ด๋ฉ์ผ์
๋๋ค'
});
}
// ์ฌ์ฉ์ ์์ฑ
const user = await User.create({
email,
password,
name,
phone
});
// ํ ํฐ ์์ฑ
const token = generateToken(user._id);
res.status(201).json({
success: true,
message: 'ํ์๊ฐ์
์ด ์๋ฃ๋์์ต๋๋ค',
token,
user: {
id: user._id,
email: user.email,
name: user.name,
role: user.role
}
});
} catch (error) {
res.status(500).json({
success: false,
message: 'ํ์๊ฐ์
์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค',
error: error.message
});
}
};
// ๋ก๊ทธ์ธ
exports.login = async (req, res) => {
try {
const { email, password } = req.body;
// ์
๋ ฅ๊ฐ ํ์ธ
if (!email || !password) {
return res.status(400).json({
success: false,
message: '์ด๋ฉ์ผ๊ณผ ๋น๋ฐ๋ฒํธ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์'
});
}
// ์ฌ์ฉ์ ์กฐํ (๋น๋ฐ๋ฒํธ ํฌํจ)
const user = await User.findOne({ email }).select('+password');
if (!user) {
return res.status(401).json({
success: false,
message: '์ด๋ฉ์ผ ๋๋ ๋น๋ฐ๋ฒํธ๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค'
});
}
// ๋น๋ฐ๋ฒํธ ํ์ธ
const isPasswordCorrect = await user.comparePassword(password);
if (!isPasswordCorrect) {
return res.status(401).json({
success: false,
message: '์ด๋ฉ์ผ ๋๋ ๋น๋ฐ๋ฒํธ๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค'
});
}
// ํ ํฐ ์์ฑ
const token = generateToken(user._id);
res.status(200).json({
success: true,
message: '๋ก๊ทธ์ธ ์ฑ๊ณต',
token,
user: {
id: user._id,
email: user.email,
name: user.name,
role: user.role
}
});
} catch (error) {
res.status(500).json({
success: false,
message: '๋ก๊ทธ์ธ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค',
error: error.message
});
}
};
// ๋ด ์ ๋ณด ์กฐํ
exports.getMe = async (req, res) => {
try {
const user = await User.findById(req.user.id);
res.status(200).json({
success: true,
user
});
} catch (error) {
res.status(500).json({
success: false,
message: '์ฌ์ฉ์ ์ ๋ณด ์กฐํ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค',
error: error.message
});
}
};
์ธ์ฆ ๋ผ์ฐํธ ์ค์
src/routes/auth.js๋ฅผ ๋ง๋ค์ด์ ๋ผ์ฐํธ๋ฅผ ์ฐ๊ฒฐํ์:
const express = require('express');
const router = express.Router();
const { register, login, getMe } = require('../controllers/authController');
const { protect } = require('../middlewares/auth');
router.post('/register', register);
router.post('/login', login);
router.get('/me', protect, getMe);
module.exports = router;
๊ทธ๋ฆฌ๊ณ server.js์์ ์ด ๋ผ์ฐํธ๋ฅผ ๋ฑ๋กํด:
const authRoutes = require('./src/routes/auth');
app.use('/api/auth', authRoutes);
2. ํด๋ผ์ด์ธํธ๋ ์ด ํ ํฐ์ ์ ์ฅํ๊ณ , ์ดํ ์์ฒญ๋ง๋ค ํค๋์ ํฌํจ์์ผ
3. ์๋ฒ๋ ํ ํฐ์ ๊ฒ์ฆํด์ ์ฌ์ฉ์๋ฅผ ์๋ณํ๊ณ ๊ถํ์ ํ์ธํด
4. ํ ํฐ์๋ ๋ง๋ฃ ์๊ฐ์ด ์์ด์ ๋ณด์์ด ๊ฐํ๋ผ ๐ก๏ธ
๐๏ธ ์ํ ๊ด๋ฆฌ API ๊ตฌ์ถํ๊ธฐ
์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์ผํ๋ชฐ์ ํต์ฌ ๊ธฐ๋ฅ์ธ ์ํ ๊ด๋ฆฌ API๋ฅผ ๋ง๋ค์ด๋ณด์! ๐ฆ
์ํ ์ปจํธ๋กค๋ฌ ์์ฑ
src/controllers/productController.js๋ฅผ ๋ง๋ค์ด๋ณด์:
const Product = require('../models/Product');
// ์ํ ๋ชฉ๋ก ์กฐํ (ํํฐ๋ง, ์ ๋ ฌ, ํ์ด์ง๋ค์ด์
)
exports.getProducts = async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 12;
const skip = (page - 1) * limit;
// ํํฐ ์กฐ๊ฑด ๊ตฌ์ฑ
const filter = { isActive: true };
if (req.query.category) {
filter.category = req.query.category;
}
if (req.query.minPrice || req.query.maxPrice) {
filter.price = {};
if (req.query.minPrice) filter.price.$gte = parseInt(req.query.minPrice);
if (req.query.maxPrice) filter.price.$lte = parseInt(req.query.maxPrice);
}
if (req.query.search) {
filter.$or = [
{ name: { $regex: req.query.search, $options: 'i' } },
{ description: { $regex: req.query.search, $options: 'i' } }
];
}
// ์ ๋ ฌ ์ต์
let sort = {};
if (req.query.sort === 'price-asc') sort.price = 1;
else if (req.query.sort === 'price-desc') sort.price = -1;
else if (req.query.sort === 'rating') sort['rating.average'] = -1;
else sort.createdAt = -1;
// ์ํ ์กฐํ
const products = await Product.find(filter)
.sort(sort)
.skip(skip)
.limit(limit);
const total = await Product.countDocuments(filter);
res.status(200).json({
success: true,
count: products.length,
total,
page,
totalPages: Math.ceil(total / limit),
products
});
} catch (error) {
res.status(500).json({
success: false,
message: '์ํ ๋ชฉ๋ก ์กฐํ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค',
error: error.message
});
}
};
// ์ํ ์์ธ ์กฐํ
exports.getProduct = async (req, res) => {
try {
const product = await Product.findById(req.params.id);
if (!product) {
return res.status(404).json({
success: false,
message: '์ํ์ ์ฐพ์ ์ ์์ต๋๋ค'
});
}
res.status(200).json({
success: true,
product
});
} catch (error) {
res.status(500).json({
success: false,
message: '์ํ ์กฐํ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค',
error: error.message
});
}
};
// ์ํ ๋ฑ๋ก (๊ด๋ฆฌ์ ์ ์ฉ)
exports.createProduct = async (req, res) => {
try {
const product = await Product.create(req.body);
res.status(201).json({
success: true,
message: '์ํ์ด ๋ฑ๋ก๋์์ต๋๋ค',
product
});
} catch (error) {
res.status(500).json({
success: false,
message: '์ํ ๋ฑ๋ก ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค',
error: error.message
});
}
};
// ์ํ ์์ (๊ด๋ฆฌ์ ์ ์ฉ)
exports.updateProduct = async (req, res) => {
try {
const product = await Product.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!product) {
return res.status(404).json({
success: false,
message: '์ํ์ ์ฐพ์ ์ ์์ต๋๋ค'
});
}
res.status(200).json({
success: true,
message: '์ํ์ด ์์ ๋์์ต๋๋ค',
product
});
} catch (error) {
res.status(500).json({
success: false,
message: '์ํ ์์ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค',
error: error.message
});
}
};
// ์ํ ์ญ์ (๊ด๋ฆฌ์ ์ ์ฉ)
exports.deleteProduct = async (req, res) => {
try {
const product = await Product.findByIdAndUpdate(
req.params.id,
{ isActive: false },
{ new: true }
);
if (!product) {
return res.status(404).json({
success: false,
message: '์ํ์ ์ฐพ์ ์ ์์ต๋๋ค'
});
}
res.status(200).json({
success: true,
message: '์ํ์ด ์ญ์ ๋์์ต๋๋ค'
});
} catch (error) {
res.status(500).json({
success: false,
message: '์ํ ์ญ์ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค',
error: error.message
});
}
};
์ํ ๋ผ์ฐํธ ์ค์
src/routes/products.js๋ฅผ ๋ง๋ค์:
const express = require('express');
const router = express.Router();
const {
getProducts,
getProduct,
createProduct,
updateProduct,
deleteProduct
} = require('../controllers/productController');
const { protect, authorize } = require('../middlewares/auth');
router.get('/', getProducts);
router.get('/:id', getProduct);
router.post('/', protect, authorize('admin'), createProduct);
router.put('/:id', protect, authorize('admin'), updateProduct);
router.delete('/:id', protect, authorize('admin'), deleteProduct);
module.exports = router;
server.js์ ๋ผ์ฐํธ ๋ฑ๋ก:
const productRoutes = require('./src/routes/products');
app.use('/api/products', productRoutes);
๐ ์ฅ๋ฐ๊ตฌ๋ API ๊ตฌ์ถํ๊ธฐ
์ฅ๋ฐ๊ตฌ๋๋ ์ผํ๋ชฐ์์ ์ ๋ง ์ค์ํ ๊ธฐ๋ฅ์ด์ผ! ๐ ์ฌ์ฉ์๊ฐ ์ฌ๋ฌ ์ํ์ ๋ด์๋๊ณ ๋์ค์ ํ ๋ฒ์ ์ฃผ๋ฌธํ ์ ์๊ฒ ํด์ฃผ์ง.
์ฅ๋ฐ๊ตฌ๋ ์ปจํธ๋กค๋ฌ ์์ฑ
src/controllers/cartController.js๋ฅผ ๋ง๋ค์ด๋ณด์:
const Cart = require('../models/Cart');
const Product = require('../models/Product');
// ์ฅ๋ฐ๊ตฌ๋ ์กฐํ
exports.getCart = async (req, res) => {
try {
let cart = await Cart.findOne({ user: req.user.id })
.populate('items.product', 'name price discountPrice images stock');
if (!cart) {
cart = await Cart.create({ user: req.user.id, items: [] });
}
// ์ด ๊ธ์ก ๊ณ์ฐ
let totalAmount = 0;
cart.items.forEach(item => {
if (item.product) {
const price = item.product.discountPrice || item.product.price;
totalAmount += price * item.quantity;
}
});
res.status(200).json({
success: true,
cart,
totalAmount
});
} catch (error) {
res.status(500).json({
success: false,
message: '์ฅ๋ฐ๊ตฌ๋ ์กฐํ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค',
error: error.message
});
}
};
// ์ฅ๋ฐ๊ตฌ๋์ ์ํ ์ถ๊ฐ
exports.addToCart = async (req, res) => {
try {
const { productId, quantity } = req.body;
// ์ํ ์กด์ฌ ํ์ธ
const product = await Product.findById(productId);
if (!product) {
return res.status(404).json({
success: false,
message: '์ํ์ ์ฐพ์ ์ ์์ต๋๋ค'
});
}
// ์ฌ๊ณ ํ์ธ
if (product.stock < quantity) {
return res.status(400).json({
success: false,
message: '์ฌ๊ณ ๊ฐ ๋ถ์กฑํฉ๋๋ค'
});
}
// ์ฅ๋ฐ๊ตฌ๋ ์กฐํ ๋๋ ์์ฑ
let cart = await Cart.findOne({ user: req.user.id });
if (!cart) {
cart = await Cart.create({ user: req.user.id, items: [] });
}
// ์ด๋ฏธ ์ฅ๋ฐ๊ตฌ๋์ ์๋ ์ํ์ธ์ง ํ์ธ
const existingItemIndex = cart.items.findIndex(
item => item.product.toString() === productId
);
if (existingItemIndex > -1) {
// ๊ธฐ์กด ์ํ ์๋ ์ฆ๊ฐ
cart.items[existingItemIndex].quantity += quantity;
} else {
// ์ ์ํ ์ถ๊ฐ
cart.items.push({ product: productId, quantity });
}
await cart.save();
// populateํด์ ๋ฐํ
cart = await cart.populate('items.product', 'name price discountPrice images');
res.status(200).json({
success: true,
message: '์ฅ๋ฐ๊ตฌ๋์ ์ถ๊ฐ๋์์ต๋๋ค',
cart
});
} catch (error) {
res.status(500).json({
success: false,
message: '์ฅ๋ฐ๊ตฌ๋ ์ถ๊ฐ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค',
error: error.message
});
}
};
// ์ฅ๋ฐ๊ตฌ๋ ์ํ ์๋ ๋ณ๊ฒฝ
exports.updateCartItem = async (req, res) => {
try {
const { productId, quantity } = req.body;
if (quantity < 1) {
return res.status(400).json({
success: false,
message: '์๋์ 1 ์ด์์ด์ด์ผ ํฉ๋๋ค'
});
}
const cart = await Cart.findOne({ user: req.user.id });
if (!cart) {
return res.status(404).json({
success: false,
message: '์ฅ๋ฐ๊ตฌ๋๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค'
});
}
const itemIndex = cart.items.findIndex(
item => item.product.toString() === productId
);
if (itemIndex === -1) {
return res.status(404).json({
success: false,
message: '์ฅ๋ฐ๊ตฌ๋์ ํด๋น ์ํ์ด ์์ต๋๋ค'
});
}
cart.items[itemIndex].quantity = quantity;
await cart.save();
await cart.populate('items.product', 'name price discountPrice images');
res.status(200).json({
success: true,
message: '์๋์ด ๋ณ๊ฒฝ๋์์ต๋๋ค',
cart
});
} catch (error) {
res.status(500).json({
success: false,
message: '์๋ ๋ณ๊ฒฝ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค',
error: error.message
});
}
};
// ์ฅ๋ฐ๊ตฌ๋์์ ์ํ ์ ๊ฑฐ
exports.removeFromCart = async (req, res) => {
try {
const { productId } = req.params;
const cart = await Cart.findOne({ user: req.user.id });
if (!cart) {
return res.status(404).json({
success: false,
message: '์ฅ๋ฐ๊ตฌ๋๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค'
});
}
cart.items = cart.items.filter(
item => item.product.toString() !== productId
);
await cart.save();
await cart.populate('items.product', 'name price discountPrice images');
res.status(200).json({
success: true,
message: '์ํ์ด ์ ๊ฑฐ๋์์ต๋๋ค',
cart
});
} catch (error) {
res.status(500).json({
success: false,
message: '์ํ ์ ๊ฑฐ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค',
error: error.message
});
}
};
// ์ฅ๋ฐ๊ตฌ๋ ๋น์ฐ๊ธฐ
exports.clearCart = async (req, res) => {
try {
const cart = await Cart.findOne({ user: req.user.id });
if (!cart) {
return res.status(404).json({
success: false,
message: '์ฅ๋ฐ๊ตฌ๋๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค'
});
}
cart.items = [];
await cart.save();
res.status(200).json({
success: true,
message: '์ฅ๋ฐ๊ตฌ๋๊ฐ ๋น์์ก์ต๋๋ค',
cart
});
} catch (error) {
res.status(500).json({
success: false,
message: '์ฅ๋ฐ๊ตฌ๋ ๋น์ฐ๊ธฐ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค',
error: error.message
});
}
};
์ฅ๋ฐ๊ตฌ๋ ๋ผ์ฐํธ ์ค์
src/routes/cart.js:
const express = require('express');
const router = express.Router();
const {
getCart,
addToCart,
updateCartItem,
removeFromCart,
clearCart
} = require('../controllers/cartController');
const { protect } = require('../middlewares/auth');
router.use(protect); // ๋ชจ๋ ์ฅ๋ฐ๊ตฌ๋ ๋ผ์ฐํธ๋ ์ธ์ฆ ํ์
router.get('/', getCart);
router.post('/add', addToCart);
router.put('/update', updateCartItem);
router.delete('/remove/:productId', removeFromCart);
router.delete('/clear', clearCart);
module.exports = router;
server.js์ ๋ฑ๋ก:
const cartRoutes = require('./src/routes/cart');
app.use('/api/cart', cartRoutes);
๐ฆ ์ฃผ๋ฌธ ์ฒ๋ฆฌ API ๊ตฌ์ถํ๊ธฐ
๋๋์ด ์ฃผ๋ฌธ ์ฒ๋ฆฌ API๋ฅผ ๋ง๋ค ์ฐจ๋ก์ผ! ๐ณ ์ด๊ฑด ์ผํ๋ชฐ์ ๊ฐ์ฅ ํต์ฌ์ ์ธ ๊ธฐ๋ฅ์ด์ง.
์ฃผ๋ฌธ ์ปจํธ๋กค๋ฌ ์์ฑ
src/controllers/orderController.js:
const Order = require('../models/Order');
const Cart = require('../models/Cart');
const Product = require('../models/Product');
// ์ฃผ๋ฌธ ์์ฑ
exports.createOrder = async (req, res) => {
try {
const { shippingAddress, paymentMethod } = req.body;
// ์ฅ๋ฐ๊ตฌ๋ ์กฐํ
const cart = await Cart.findOne({ user: req.user.id })
.populate('items.product');
if (!cart || cart.items.length === 0) {
return res.status(400).json({
success: false,
message: '์ฅ๋ฐ๊ตฌ๋๊ฐ ๋น์ด์์ต๋๋ค'
});
}
// ์ฃผ๋ฌธ ์์ดํ
๊ตฌ์ฑ ๋ฐ ์ฌ๊ณ ํ์ธ
const orderItems = [];
let totalAmount = 0;
for (const item of cart.items) {
const product = item.product;
// ์ฌ๊ณ ํ์ธ
if (product.stock < item.quantity) {
return res.status(400).json({
success: false,
message: `${product.name}์ ์ฌ๊ณ ๊ฐ ๋ถ์กฑํฉ๋๋ค`
});
}
const price = product.discountPrice || product.price;
const subtotal = price * item.quantity;
orderItems.push({
product: product._id,
name: product.name,
price: price,
quantity: item.quantity,
subtotal: subtotal
});
totalAmount += subtotal;
// ์ฌ๊ณ ๊ฐ์
product.stock -= item.quantity;
await product.save();
}
// ์ฃผ๋ฌธ ์์ฑ
const order = await Order.create({
user: req.user.id,
items: orderItems,
totalAmount,
shippingAddress,
paymentMethod
});
// ์ฅ๋ฐ๊ตฌ๋ ๋น์ฐ๊ธฐ
cart.items = [];
await cart.save();
res.status(201).json({
success: true,
message: '์ฃผ๋ฌธ์ด ์๋ฃ๋์์ต๋๋ค',
order
});
} catch (error) {
res.status(500).json({
success: false,
message: '์ฃผ๋ฌธ ์์ฑ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค',
error: error.message
});
}
};
// ๋ด ์ฃผ๋ฌธ ๋ชฉ๋ก ์กฐํ
exports.getMyOrders = async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
const orders = await Order.find({ user: req.user.id })
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit)
.populate('items.product', 'name images');
const total = await Order.countDocuments({ user: req.user.id });
res.status(200).json({
success: true,
count: orders.length,
total,
page,
totalPages: Math.ceil(total / limit),
orders
});
} catch (error) {
res.status(500).json({
success: false,
message: '์ฃผ๋ฌธ ๋ชฉ๋ก ์กฐํ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค',
error: error.message
});
}
};
// ์ฃผ๋ฌธ ์์ธ ์กฐํ
exports.getOrder = async (req, res) => {
try {
const order = await Order.findById(req.params.id)
.populate('items.product', 'name images')
.populate('user', 'name email phone');
if (!order) {
return res.status(404).json({
success: false,
message: '์ฃผ๋ฌธ์ ์ฐพ์ ์ ์์ต๋๋ค'
});
}
// ๋ณธ์ธ ์ฃผ๋ฌธ์ด๊ฑฐ๋ ๊ด๋ฆฌ์์ธ์ง ํ์ธ
if (order.user._id.toString() !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({
success: false,
message: '์ ๊ทผ ๊ถํ์ด ์์ต๋๋ค'
});
}
res.status(200).json({
success: true,
order
});
} catch (error) {
res.status(500).json({
success: false,
message: '์ฃผ๋ฌธ ์กฐํ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค',
error: error.message
});
}
};
// ์ฃผ๋ฌธ ์ทจ์
exports.cancelOrder = async (req, res) => {
try {
const order = await Order.findById(req.params.id);
if (!order) {
return res.status(404).json({
success: false,
message: '์ฃผ๋ฌธ์ ์ฐพ์ ์ ์์ต๋๋ค'
});
}
// ๋ณธ์ธ ์ฃผ๋ฌธ์ธ์ง ํ์ธ
if (order.user.toString() !== req.user.id) {
return res.status(403).json({
success: false,
message: '์ ๊ทผ ๊ถํ์ด ์์ต๋๋ค'
});
}
// ์ทจ์ ๊ฐ๋ฅํ ์ํ์ธ์ง ํ์ธ
if (order.orderStatus !== 'pending' && order.orderStatus !== 'confirmed') {
return res.status(400).json({
success: false,
message: '์ด๋ฏธ ๋ฐฐ์ก์ด ์์๋ ์ฃผ๋ฌธ์ ์ทจ์ํ ์ ์์ต๋๋ค'
});
}
// ์ฌ๊ณ ๋ณต๊ตฌ
for (const item of order.items) {
const product = await Product.findById(item.product);
if (product) {
product.stock += item.quantity;
await product.save();
}
}
order.orderStatus = 'cancelled';
await order.save();
res.status(200).json({
success: true,
message: '์ฃผ๋ฌธ์ด ์ทจ์๋์์ต๋๋ค',
order
});
} catch (error) {
res.status(500).json({
success: false,
message: '์ฃผ๋ฌธ ์ทจ์ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค',
error: error.message
});
}
};
// ์ฃผ๋ฌธ ์ํ ๋ณ๊ฒฝ (๊ด๋ฆฌ์ ์ ์ฉ)
exports.updateOrderStatus = async (req, res) => {
try {
const { orderStatus } = req.body;
const order = await Order.findById(req.params.id);
if (!order) {
return res.status(404).json({
success: false,
message: '์ฃผ๋ฌธ์ ์ฐพ์ ์ ์์ต๋๋ค'
});
}
order.orderStatus = orderStatus;
if (orderStatus === 'delivered') {
order.deliveredAt = Date.now();
}
await order.save();
res.status(200).json({
success: true,
message: '์ฃผ๋ฌธ ์ํ๊ฐ ๋ณ๊ฒฝ๋์์ต๋๋ค',
order
});
} catch (error) {
res.status(500).json({
success: false,
message: '์ฃผ๋ฌธ ์ํ ๋ณ๊ฒฝ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค',
error: error.message
});
}
};
// ์ ์ฒด ์ฃผ๋ฌธ ๋ชฉ๋ก ์กฐํ (๊ด๋ฆฌ์ ์ ์ฉ)
exports.getAllOrders = async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const skip = (page - 1) * limit;
const filter = {};
if (req.query.status) {
filter.orderStatus = req.query.status;
}
const orders = await Order.find(filter)
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit)
.populate('user', 'name email')
.populate('items.product', 'name');
const total = await Order.countDocuments(filter);
res.status(200).json({
success: true,
count: orders.length,
total,
page,
totalPages: Math.ceil(total / limit),
orders
});
} catch (error) {
res.status(500).json({
success: false,
message: '์ฃผ๋ฌธ ๋ชฉ๋ก ์กฐํ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค',
error: error.message
});
}
};
์ฃผ๋ฌธ ๋ผ์ฐํธ ์ค์
src/routes/orders.js:
const express = require('express');
const router = express.Router();
const {
createOrder,
getMyOrders,
getOrder,
cancelOrder,
updateOrderStatus,
getAllOrders
} = require('../controllers/orderController');
const { protect, authorize } = require('../middlewares/auth');
router.post('/', protect, createOrder);
router.get('/my', protect, getMyOrders);
router.get('/all', protect, authorize('admin'), getAllOrders);
router.get('/:id', protect, getOrder);
router.put('/:id/cancel', protect, cancelOrder);
router.put('/:id/status', protect, authorize('admin'), updateOrderStatus);
module.exports = router;
server.js์ ๋ฑ๋ก:
const orderRoutes = require('./src/routes/orders');
app.use('/api/orders', orderRoutes);
๐ ์ ๋ ฅ๊ฐ ๊ฒ์ฆ๊ณผ ์๋ฌ ์ฒ๋ฆฌ
API ์๋ฒ์์ ์ ๋ ฅ๊ฐ ๊ฒ์ฆ๊ณผ ์๋ฌ ์ฒ๋ฆฌ๋ ์ ๋ง ์ค์ํด! ๐ก๏ธ ์ฌ์ฉ์๊ฐ ์๋ชป๋ ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ด๋ ์๋ฒ๊ฐ ์์ ํ๊ฒ ์ฒ๋ฆฌํ ์ ์์ด์ผ ํ๊ฑฐ๋ .
์ ๋ ฅ๊ฐ ๊ฒ์ฆ ๋ฏธ๋ค์จ์ด
express-validator๋ฅผ ์ฌ์ฉํด์ ์
๋ ฅ๊ฐ์ ๊ฒ์ฆํด๋ณด์. src/utils/validators.js:
const { body, validationResult } = require('express-validator');
// ๊ฒ์ฆ ๊ฒฐ๊ณผ ํ์ธ ๋ฏธ๋ค์จ์ด
exports.validate = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: '์
๋ ฅ๊ฐ์ด ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค',
errors: errors.array()
});
}
next();
};
// ํ์๊ฐ์
๊ฒ์ฆ ๊ท์น
exports.registerValidation = [
body('email')
.isEmail()
.withMessage('์ ํจํ ์ด๋ฉ์ผ์ ์
๋ ฅํด์ฃผ์ธ์')
.normalizeEmail(),
body('password')
.isLength({ min: 6 })
.withMessage('๋น๋ฐ๋ฒํธ๋ ์ต์ 6์ ์ด์์ด์ด์ผ ํฉ๋๋ค'),
body('name')
.trim()
.notEmpty()
.withMessage('์ด๋ฆ์ ์
๋ ฅํด์ฃผ์ธ์'),
body('phone')
.matches(/^[0-9]{10,11}$/)
.withMessage('์ ํจํ ์ ํ๋ฒํธ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์')
];
// ์ํ ๋ฑ๋ก ๊ฒ์ฆ ๊ท์น
exports.productValidation = [
body('name')
.trim()
.notEmpty()
.withMessage('์ํ๋ช
์ ์
๋ ฅํด์ฃผ์ธ์')
.isLength({ max: 100 })
.withMessage('์ํ๋ช
์ 100์๋ฅผ ์ด๊ณผํ ์ ์์ต๋๋ค'),
body('description')
.trim()
.notEmpty()
.withMessage('์ํ ์ค๋ช
์ ์
๋ ฅํด์ฃผ์ธ์'),
body('price')
.isFloat({ min: 0 })
.withMessage('๊ฐ๊ฒฉ์ 0 ์ด์์ด์ด์ผ ํฉ๋๋ค'),
body('category')
.isIn(['์๋ฅ', '์ ์์ ํ', '์ํ', '๋์', '์ํ์ฉํ', '๊ธฐํ'])
.withMessage('์ ํจํ ์นดํ
๊ณ ๋ฆฌ๋ฅผ ์ ํํด์ฃผ์ธ์'),
body('stock')
.isInt({ min: 0 })
.withMessage('์ฌ๊ณ ๋ 0 ์ด์์ ์ ์์ฌ์ผ ํฉ๋๋ค')
];
// ์ฃผ๋ฌธ ์์ฑ ๊ฒ์ฆ ๊ท์น
exports.orderValidation = [
body('shippingAddress.name')
.trim()
.notEmpty()
.withMessage('์๋ น์ธ ์ด๋ฆ์ ์
๋ ฅํด์ฃผ์ธ์'),
body('shippingAddress.phone')
.matches(/^[0-9]{10,11}$/)
.withMessage('์ ํจํ ์ ํ๋ฒํธ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์'),
body('shippingAddress.zipCode')
.trim()
.notEmpty()
.withMessage('์ฐํธ๋ฒํธ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์'),
body('shippingAddress.street')
.trim()
.notEmpty()
.withMessage('์ฃผ์๋ฅผ ์
๋ ฅํด์ฃผ์ธ์'),
body('paymentMethod')
.isIn(['card', 'bank', 'virtual', 'mobile'])
.withMessage('์ ํจํ ๊ฒฐ์ ๋ฐฉ๋ฒ์ ์ ํํด์ฃผ์ธ์')
];
์ด์ ๋ผ์ฐํธ์์ ์ด ๊ฒ์ฆ ๊ท์น๋ค์ ์ฌ์ฉํ๋ฉด ๋ผ:
const { registerValidation, validate } = require('../utils/validators');
router.post('/register', registerValidation, validate, register);
์ ์ญ ์๋ฌ ํธ๋ค๋ฌ
src/middlewares/errorHandler.js๋ฅผ ๋ง๋ค์ด์ ๋ชจ๋ ์๋ฌ๋ฅผ ์ผ๊ด๋๊ฒ ์ฒ๋ฆฌํ์:
const errorHandler = (err, req, res, next) => {
let error = { ...err };
error.message = err.message;
// Mongoose ์ค๋ณต ํค ์๋ฌ
if (err.code === 11000) {
const field = Object.keys(err.keyValue)[0];
error.message = `์ด๋ฏธ ์กด์ฌํ๋ ${field}์
๋๋ค`;
error.statusCode = 400;
}
// Mongoose ์ ํจ์ฑ ๊ฒ์ฌ ์๋ฌ
if (err.name === 'ValidationError') {
const messages = Object.values(err.errors).map(e => e.message);
error.message = messages.join(', ');
error.statusCode = 400;
}
// Mongoose ObjectId ์๋ฌ
if (err.name === 'CastError') {
error.message = '์๋ชป๋ ID ํ์์
๋๋ค';
error.statusCode = 400;
}
// JWT ์๋ฌ
if (err.name === 'JsonWebTokenError') {
error.message = '์ ํจํ์ง ์์ ํ ํฐ์
๋๋ค';
error.statusCode = 401;
}
if (err.name === 'TokenExpiredError') {
error.message = 'ํ ํฐ์ด ๋ง๋ฃ๋์์ต๋๋ค';
error.statusCode = 401;
}
res.status(error.statusCode || 500).json({
success: false,
message: error.message || '์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค',
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
};
module.exports = errorHandler;
server.js์ ๋ง์ง๋ง์ ์๋ฌ ํธ๋ค๋ฌ๋ฅผ ๋ฑ๋กํด:
const errorHandler = require('./src/middlewares/errorHandler');
// ๋ชจ๋ ๋ผ์ฐํธ ๋ค์ ์์น
app.use(errorHandler);
โข ๊ฐ๋ฐ ํ๊ฒฝ์์๋ ์์ธํ ์๋ฌ ์ ๋ณด ์ ๊ณต
โข ์ด์ ํ๊ฒฝ์์๋ ๋ฏผ๊ฐํ ์ ๋ณด ๋ ธ์ถ ๋ฐฉ์ง
โข ์ ์ ํ HTTP ์ํ ์ฝ๋ ์ฌ์ฉ
โข ๋ก๊น ์์คํ ๊ตฌ์ถ (Winston, Morgan ๋ฑ)
๐ ์ฑ๋ฅ ์ต์ ํ์ ๋ณด์ ๊ฐํ
API ์๋ฒ๋ฅผ ์ค์ ๋ก ์ด์ํ๋ ค๋ฉด ์ฑ๋ฅ๊ณผ ๋ณด์์ ์ ๊ฒฝ ์จ์ผ ํด! โก
์ฑ๋ฅ ์ต์ ํ ๊ธฐ๋ฒ
1. ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ธ๋ฑ์ฑ
์์ฃผ ์กฐํ๋๋ ํ๋์ ์ธ๋ฑ์ค๋ฅผ ์ถ๊ฐํ๋ฉด ์ฟผ๋ฆฌ ์๋๊ฐ ์์ฒญ๋๊ฒ ๋นจ๋ผ์ ธ:
// Product ๋ชจ๋ธ์ ์ธ๋ฑ์ค ์ถ๊ฐ
productSchema.index({ name: 'text', description: 'text' });
productSchema.index({ category: 1, price: 1 });
productSchema.index({ createdAt: -1 });
// User ๋ชจ๋ธ์ ์ธ๋ฑ์ค ์ถ๊ฐ
userSchema.index({ email: 1 });
// Order ๋ชจ๋ธ์ ์ธ๋ฑ์ค ์ถ๊ฐ
orderSchema.index({ user: 1, createdAt: -1 });
orderSchema.index({ orderNumber: 1 });
2. ์๋ต ์์ถ
compression ๋ฏธ๋ค์จ์ด๋ฅผ ์ฌ์ฉํด์ ์๋ต ๋ฐ์ดํฐ๋ฅผ ์์ถํ์:
npm install compression
// server.js
const compression = require('compression');
app.use(compression());
3. ์บ์ฑ ์ ๋ต
์์ฃผ ๋ณ๊ฒฝ๋์ง ์๋ ๋ฐ์ดํฐ๋ Redis ๊ฐ์ ์บ์๋ฅผ ์ฌ์ฉํ๋ฉด ์ข์:
npm install redis
// src/config/redis.js
const redis = require('redis');
const client = redis.createClient({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379
});
client.on('error', (err) => console.error('Redis ์๋ฌ:', err));
module.exports = client;
4. ํ์ด์ง๋ค์ด์
๊ณผ ํ๋ ์ ํ
๋๋์ ๋ฐ์ดํฐ๋ฅผ ํ ๋ฒ์ ๋ณด๋ด์ง ๋ง๊ณ , ํ์ํ ํ๋๋ง ์ ํํด์ ๋ณด๋ด:
// ํ์ํ ํ๋๋ง ์ ํ
const products = await Product.find()
.select('name price images rating')
.limit(20);
// populateํ ๋๋ ํ๋ ์ ํ
const order = await Order.findById(id)
.populate('user', 'name email')
.populate('items.product', 'name price images');
๋ณด์ ๊ฐํ ๋ฐฉ๋ฒ
1. Helmet ์ฌ์ฉ
HTTP ํค๋๋ฅผ ์ค์ ํด์ ์ผ๋ฐ์ ์ธ ์น ์ทจ์ฝ์ ์ ๋ฐฉ์ดํด:
npm install helmet
// server.js
const helmet = require('helmet');
app.use(helmet());
2. Rate Limiting
API ๋จ์ฉ์ ๋ฐฉ์งํ๊ธฐ ์ํด ์์ฒญ ํ์๋ฅผ ์ ํํ์:
npm install express-rate-limit
// server.js
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15๋ถ
max: 100, // ์ต๋ 100๊ฐ ์์ฒญ
message: '๋๋ฌด ๋ง์ ์์ฒญ์ ๋ณด๋์ต๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.'
});
app.use('/api/', limiter);
3. XSS ๋ฐฉ์ง
์ฌ์ฉ์ ์
๋ ฅ์ sanitizeํด์ XSS ๊ณต๊ฒฉ์ ๋ฐฉ์ดํด:
npm install xss-clean
// server.js
const xss = require('xss-clean');
app.use(xss());
4. NoSQL Injection ๋ฐฉ์ง
MongoDB ์ฟผ๋ฆฌ ์ธ์ ์
์ ๋ฐฉ์งํ์:
npm install express-mongo-sanitize
// server.js
const mongoSanitize = require('express-mongo-sanitize');
app.use(mongoSanitize());
5. HTTPS ์ฌ์ฉ
์ด์ ํ๊ฒฝ์์๋ ๋ฐ๋์ HTTPS๋ฅผ ์ฌ์ฉํด์ผ ํด. Let's Encrypt๋ก ๋ฌด๋ฃ SSL ์ธ์ฆ์๋ฅผ ๋ฐ์ ์ ์์ด!
โ JWT ํ ํฐ์ ์ ์ ํ ๋ง๋ฃ ์๊ฐ ์ค์
โ ๋น๋ฐ๋ฒํธ ์ํธํ (bcrypt)
โ CORS ์ค์ ์ผ๋ก ํ์ฉ๋ ๋๋ฉ์ธ๋ง ์ ๊ทผ
โ SQL/NoSQL Injection ๋ฐฉ์ด
โ XSS ๊ณต๊ฒฉ ๋ฐฉ์ด
โ Rate Limiting ์ ์ฉ
โ HTTPS ์ฌ์ฉ
โ ์ ๊ธฐ์ ์ธ ์์กด์ฑ ์ ๋ฐ์ดํธ
๐ ๋ก๊น ๊ณผ ๋ชจ๋ํฐ๋ง
์ด์ ์ค์ธ API ์๋ฒ์ ์ํ๋ฅผ ํ์ ํ๋ ค๋ฉด ๋ก๊น ๊ณผ ๋ชจ๋ํฐ๋ง์ด ํ์์ผ! ๐
Winston์ผ๋ก ๋ก๊น ๊ตฌํ
winston์ Node.js์์ ๊ฐ์ฅ ๋ง์ด ์ฌ์ฉ๋๋ ๋ก๊น
๋ผ์ด๋ธ๋ฌ๋ฆฌ์ผ:
npm install winston
// src/config/logger.js
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.File({
filename: 'logs/error.log',
level: 'error'
}),
new winston.transports.File({
filename: 'logs/combined.log'
})
]
});
// ๊ฐ๋ฐ ํ๊ฒฝ์์๋ ์ฝ์์๋ ์ถ๋ ฅ
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}));
}
module.exports = logger;
์ด์ ์ปจํธ๋กค๋ฌ์์ ๋ก๊ฑฐ๋ฅผ ์ฌ์ฉํ ์ ์์ด:
const logger = require('../config/logger');
exports.createOrder = async (req, res) => {
try {
// ... ์ฃผ๋ฌธ ์์ฑ ๋ก์ง
logger.info('์ฃผ๋ฌธ ์์ฑ ์ฑ๊ณต', {
userId: req.user.id,
orderId: order._id,
totalAmount: order.totalAmount
});
res.status(201).json({ success: true, order });
} catch (error) {
logger.error('์ฃผ๋ฌธ ์์ฑ ์คํจ', {
userId: req.user.id,
error: error.message,
stack: error.stack
});
res.status(500).json({ success: false, message: '์ฃผ๋ฌธ ์์ฑ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค' });
}
};
Morgan์ผ๋ก HTTP ์์ฒญ ๋ก๊น
morgan์ HTTP ์์ฒญ์ ์๋์ผ๋ก ๋ก๊น
ํด์ค:
npm install morgan
// server.js
const morgan = require('morgan');
const logger = require('./src/config/logger');
// Morgan ์คํธ๋ฆผ ์ค์
const stream = {
write: (message) => logger.http(message.trim())
};
// ๊ฐ๋ฐ ํ๊ฒฝ์์๋ 'dev' ํฌ๋งท ์ฌ์ฉ
if (process.env.NODE_ENV === 'development') {
app.use(morgan('dev'));
} else {
app.use(morgan('combined', { stream }));
}
๐งช ํ ์คํธ ์์ฑํ๊ธฐ
์์ ์ ์ธ API๋ฅผ ๋ง๋ค๋ ค๋ฉด ํ ์คํธ๊ฐ ํ์์ผ! ๐งช Jest์ Supertest๋ฅผ ์ฌ์ฉํด์ ํ ์คํธ๋ฅผ ์์ฑํด๋ณด์.
ํ ์คํธ ํ๊ฒฝ ์ค์
npm install --save-dev jest supertest
// package.json์ ํ
์คํธ ์คํฌ๋ฆฝํธ ์ถ๊ฐ
{
"scripts": {
"test": "jest --watchAll --verbose",
"test:coverage": "jest --coverage"
},
"jest": {
"testEnvironment": "node",
"coveragePathIgnorePatterns": ["/node_modules/"]
}
}
API ํ ์คํธ ์์
tests/auth.test.js:
const request = require('supertest');
const app = require('../server');
const User = require('../src/models/User');
describe('์ธ์ฆ API ํ
์คํธ', () => {
beforeEach(async () => {
await User.deleteMany({});
});
describe('POST /api/auth/register', () => {
it('ํ์๊ฐ์
์ฑ๊ณต', async () => {
const res = await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com',
password: 'password123',
name: 'ํ๊ธธ๋',
phone: '01012345678'
});
expect(res.statusCode).toBe(201);
expect(res.body.success).toBe(true);
expect(res.body.token).toBeDefined();
expect(res.body.user.email).toBe('test@example.com');
});
it('์ค๋ณต ์ด๋ฉ์ผ๋ก ํ์๊ฐ์
์คํจ', async () => {
await User.create({
email: 'test@example.com',
password: 'password123',
name: 'ํ๊ธธ๋',
phone: '01012345678'
});
const res = await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com',
password: 'password456',
name: '๊น์ฒ ์',
phone: '01087654321'
});
expect(res.statusCode).toBe(400);
expect(res.body.success).toBe(false);
});
});
describe('POST /api/auth/login', () => {
beforeEach(async () => {
await User.create({
email: 'test@example.com',
password: 'password123',
name: 'ํ๊ธธ๋',
phone: '01012345678'
});
});
it('๋ก๊ทธ์ธ ์ฑ๊ณต', async () => {
const res = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'password123'
});
expect(res.statusCode).toBe(200);
expect(res.body.success).toBe(true);
expect(res.body.token).toBeDefined();
});
it('์๋ชป๋ ๋น๋ฐ๋ฒํธ๋ก ๋ก๊ทธ์ธ ์คํจ', async () => {
const res = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'wrongpassword'
});
expect(res.statusCode).toBe(401);
expect(res.body.success).toBe(false);
});
});
});
โข beforeEach๋ก ํ ์คํธ ์ ๋ฐ์ดํฐ ์ด๊ธฐํ
โข ์ฑ๊ณต ์ผ์ด์ค์ ์คํจ ์ผ์ด์ค ๋ชจ๋ ํ ์คํธ
โข ์ฃ์ง ์ผ์ด์ค๋ ๊ณ ๋ คํด์ ์์ฑ
โข ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง 80% ์ด์ ๋ชฉํ
๐ข ๋ฐฐํฌ์ ์ด์
๊ฐ๋ฐ์ด ๋๋ฌ์ผ๋ฉด ์ด์ ์ค์ ๋ก ๋ฐฐํฌํด์ผ์ง! ๐ ์ฌ๋ฌ ๋ฐฐํฌ ์ต์ ์ด ์์ด.
ํ๊ฒฝ ๋ณ์ ์ค์
์ด์ ํ๊ฒฝ์ .env ํ์ผ:
NODE_ENV=production
PORT=5000
MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/shopping-mall
JWT_SECRET=super-secret-production-key-change-this
JWT_EXPIRE=7d
REDIS_HOST=your-redis-host
REDIS_PORT=6379
PM2๋ก ํ๋ก์ธ์ค ๊ด๋ฆฌ
PM2๋ Node.js ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ด๋ฆฌํ๋ ํ๋ก์ธ์ค ๋งค๋์ ์ผ:
npm install -g pm2
// ecosystem.config.js
module.exports = {
apps: [{
name: 'shopping-mall-api',
script: './server.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production'
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss'
}]
};
// ์คํ
pm2 start ecosystem.config.js
pm2 save
pm2 startup
Docker ์ปจํ ์ด๋ํ
Docker๋ฅผ ์ฌ์ฉํ๋ฉด ์ด๋์๋ ๋์ผํ ํ๊ฒฝ์์ ์คํํ ์ ์์ด:
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 5000
CMD ["node", "server.js"]
# docker-compose.yml
version: '3.8'
services:
api:
build: .
ports:
- "5000:5000"
environment:
- NODE_ENV=production
- MONGODB_URI=mongodb://mongo:27017/shopping-mall
depends_on:
- mongo
restart: unless-stopped
mongo:
image: mongo:6
volumes:
- mongo-data:/data/db
restart: unless-stopped
volumes:
mongo-data:
ํด๋ผ์ฐ๋ ๋ฐฐํฌ ์ต์
1. Heroku - ๊ฐ๋จํ๊ณ ๋น ๋ฅธ ๋ฐฐํฌ
2. AWS EC2 - ์์ ํ ์ ์ด๊ถ
3. DigitalOcean - ๊ฐ์ฑ๋น ์ข์ VPS
4. Vercel/Netlify - ์๋ฒ๋ฆฌ์ค ํจ์๋ก ๋ฐฐํฌ
5. AWS Lambda - ์๋ฒ๋ฆฌ์ค ์ํคํ
์ฒ
โ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ฐฑ์ ์ค์
โ HTTPS ์ธ์ฆ์ ์ค์
โ ๋๋ฉ์ธ ์ฐ๊ฒฐ
โ ๋ชจ๋ํฐ๋ง ๋๊ตฌ ์ค์
โ ๋ก๊ทธ ์์ง ์์คํ ๊ตฌ์ถ
โ CI/CD ํ์ดํ๋ผ์ธ ๊ตฌ์ฑ
โ ์๋ฌ ์ถ์ ๋๊ตฌ (Sentry ๋ฑ)
โ ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง (New Relic ๋ฑ)
๐ก ์ค์ ํ๊ณผ ๋ฒ ์คํธ ํ๋ํฐ์ค
๋ง์ง๋ง์ผ๋ก ์ค์ ํ๋ก์ ํธ์์ ์ ์ฉํ ํ๋ค์ ๊ณต์ ํ ๊ฒ! โจ
API ๋ฌธ์ํ
Swagger๋ฅผ ์ฌ์ฉํด์ API ๋ฌธ์๋ฅผ ์๋์ผ๋ก ์์ฑํ์:
npm install swagger-jsdoc swagger-ui-express
// server.js
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const swaggerOptions = {
definition: {
openapi: '3.0.0',
info: {
title: '์ผํ๋ชฐ API',
version: '1.0.0',
description: 'Node.js์ Express๋ก ๋ง๋ ์ผํ๋ชฐ API ์๋ฒ'
},
servers: [
{
url: 'http://localhost:5000',
description: '๊ฐ๋ฐ ์๋ฒ'
}
]
},
apis: ['./src/routes/*.js']
};
const swaggerSpec = swaggerJsdoc(swaggerOptions);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
๋ฒ์ ๊ด๋ฆฌ
API ๋ฒ์ ์ ๊ด๋ฆฌํด์ ํ์ ํธํ์ฑ์ ์ ์งํ์:
// v1 ๋ผ์ฐํธ
app.use('/api/v1/auth', authRoutesV1);
app.use('/api/v1/products', productRoutesV1);
// v2 ๋ผ์ฐํธ (์๋ก์ด ๊ธฐ๋ฅ ์ถ๊ฐ)
app.use('/api/v2/auth', authRoutesV2);
app.use('/api/v2/products', productRoutesV2);
์ฝ๋ ๊ตฌ์กฐํ ํ
1. ์๋น์ค ๋ ์ด์ด ๋ถ๋ฆฌ
๋น์ฆ๋์ค ๋ก์ง์ ์ปจํธ๋กค๋ฌ์์ ๋ถ๋ฆฌํด์ ์ฌ์ฌ์ฉ์ฑ์ ๋์ฌ:
// src/services/orderService.js
class OrderService {
async createOrder(userId, orderData) {
// ๋น์ฆ๋์ค ๋ก์ง
}
async calculateTotal(items) {
// ๊ณ์ฐ ๋ก์ง
}
}
module.exports = new OrderService();
// ์ปจํธ๋กค๋ฌ์์ ์ฌ์ฉ
const orderService = require('../services/orderService');
exports.createOrder = async (req, res) => {
const order = await orderService.createOrder(req.user.id, req.body);
res.json({ success: true, order });
};
2. ์์ ๊ด๋ฆฌ
๋งค์ง ๋๋ฒ๋ ๋ฌธ์์ด์ ์์๋ก ๊ด๋ฆฌํด:
// src/constants/orderStatus.js
module.exports = {
PENDING: 'pending',
CONFIRMED: 'confirmed',
PREPARING: 'preparing',
SHIPPED: 'shipped',
DELIVERED: 'delivered',
CANCELLED: 'cancelled'
};
3. ์ ํธ๋ฆฌํฐ ํจ์
์์ฃผ ์ฌ์ฉํ๋ ๊ธฐ๋ฅ์ ์ ํธ๋ฆฌํฐ๋ก ๋ง๋ค์ด:
// src/utils/helpers.js
exports.generateOrderNumber = () => {
const date = new Date();
const dateStr = date.toISOString().slice(0, 10).replace(/-/g, '');
const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0');
return `ORD${dateStr}${random}`;
};
exports.calculateDiscountRate = (price, discountPrice) => {
if (!discountPrice || price <= discountPrice) return 0;
return Math.round(((price - discountPrice) / price) * 100);
};
์ฑ๋ฅ ๋ชจ๋ํฐ๋ง
๋๋ฆฐ ์ฟผ๋ฆฌ๋ฅผ ์ฐพ์์ ์ต์ ํํ์:
// MongoDB ์ฌ๋ก์ฐ ์ฟผ๋ฆฌ ๋ก๊น
mongoose.set('debug', (collectionName, method, query, doc) => {
logger.debug(`${collectionName}.${method}`, {
query: JSON.stringify(query),
doc: JSON.stringify(doc)
});
});
๐ ํ์ต ๋ฆฌ์์ค์ ๋ค์ ๋จ๊ณ
์ฌ๊ธฐ๊น์ง ๋ฐ๋ผ์๋ค๋ฉด ์ ๋ง ๋๋จํด! ๐ ์ด์ ๊ธฐ๋ณธ์ ์ธ ์ผํ๋ชฐ API ์๋ฒ๋ฅผ ๋ง๋ค ์ ์์ ๊ฑฐ์ผ.
์ถ๊ฐ๋ก ๊ตฌํํ๋ฉด ์ข์ ๊ธฐ๋ฅ๋ค
1. ๋ฆฌ๋ทฐ ์์คํ
โข ์ํ ๋ฆฌ๋ทฐ ์์ฑ/์์ /์ญ์
โข ํ์ ๊ณ์ฐ ๋ฐ ์
๋ฐ์ดํธ
โข ๋ฆฌ๋ทฐ ์ด๋ฏธ์ง ์
๋ก๋
2. ์์๋ฆฌ์คํธ
โข ์ฐํ๊ธฐ ๊ธฐ๋ฅ
โข ์์๋ฆฌ์คํธ ๊ด๋ฆฌ
โข ๊ฐ๊ฒฉ ๋ณ๋ ์๋ฆผ
3. ์ฟ ํฐ ์์คํ
โข ํ ์ธ ์ฟ ํฐ ๋ฐ๊ธ
โข ์ฟ ํฐ ์ ์ฉ ๋ฐ ๊ฒ์ฆ
โข ํ๋ก๋ชจ์
๊ด๋ฆฌ
4. ์ค์๊ฐ ์๋ฆผ
โข Socket.io๋ก ์ค์๊ฐ ์ฃผ๋ฌธ ์ํ ์๋ฆผ
โข ์ฌ์
๊ณ ์๋ฆผ
โข ํ๋ก๋ชจ์
ํธ์ ์๋ฆผ
5. ๊ฒฐ์ ์ฐ๋
โข ์์ํฌํธ, ํ ์คํ์ด๋จผ์ธ ๋ฑ PG์ฌ ์ฐ๋
โข ๊ฒฐ์ ๊ฒ์ฆ ๋ฐ ์ฒ๋ฆฌ
โข ํ๋ถ ์ฒ๋ฆฌ
6. ์ด๋ฏธ์ง ์
๋ก๋
โข Multer๋ก ํ์ผ ์
๋ก๋
โข AWS S3 ์ฐ๋
โข ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ง (Sharp)
์ถ์ฒ ํ์ต ์๋ฃ
๊ณต์ ๋ฌธ์:
โข Express.js ๊ณต์ ๋ฌธ์
โข Mongoose ๊ณต์ ๋ฌธ์
โข MongoDB ๊ณต์ ๋ฌธ์
์จ๋ผ์ธ ๊ฐ์:
โข Udemy์ Node.js ๊ฐ์๋ค
โข Nomad Coders์ ๋ฐฑ์๋ ๊ฐ์
โข ์ธํ๋ฐ์ Node.js ์ค์ ๊ฐ์
์ปค๋ฎค๋ํฐ:
โข Stack Overflow
โข Reddit์ r/node
โข ํ๊ตญ Node.js ์ปค๋ฎค๋ํฐ
๐ฏ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์, ์ ๋ง ๊ธด ์ฌ์ ์ด์์ง? ๐ Node.js์ Express๋ก ์ผํ๋ชฐ API ์๋ฒ๋ฅผ ๋ง๋๋ ๊ณผ์ ์ ์ฒ์๋ถํฐ ๋๊น์ง ์ดํด๋ดค์ด.
์ฐ๋ฆฌ๊ฐ ๋ค๋ฃฌ ๋ด์ฉ์ ์ ๋ฆฌํด๋ณด๋ฉด:
โข Express ์๋ฒ ๊ธฐ๋ณธ ์ค์
โข MongoDB ๋ฐ์ดํฐ ๋ชจ๋ธ ์ค๊ณ
โข JWT ๊ธฐ๋ฐ ์ธ์ฆ ์์คํ
โข ์ํ, ์ฅ๋ฐ๊ตฌ๋, ์ฃผ๋ฌธ ๊ด๋ฆฌ API
โข ์
๋ ฅ๊ฐ ๊ฒ์ฆ๊ณผ ์๋ฌ ์ฒ๋ฆฌ
โข ์ฑ๋ฅ ์ต์ ํ์ ๋ณด์
โข ๋ก๊น
, ํ
์คํธ, ๋ฐฐํฌ
์ด ๋ชจ๋ ๊ฑธ ํ ๋ฒ์ ์๋ฒฝํ๊ฒ ์ดํดํ๊ธฐ๋ ์ด๋ ค์. ์ฒ์ฒํ ํ๋์ฉ ๊ตฌํํด๋ณด๋ฉด์ ์ตํ๊ฐ๋ฉด ๋ผ! ๐ช
๊ฐ์ฅ ์ค์ํ ๊ฑด ์ง์ ์ฝ๋๋ฅผ ์์ฑํด๋ณด๋ ๊ฒ์ด์ผ. ์ด ๊ธ์ ๋ณด๋ฉด์ ๋ฐ๋ผ ๋ง๋ค์ด๋ณด๊ณ , ์์ ๋ง์ ๊ธฐ๋ฅ์ ์ถ๊ฐํด๋ณด๊ณ , ์๋ฌ๋ฅผ ๋ง๋๋ฉด ํด๊ฒฐํด๋ณด๋ฉด์ ์ค๋ ฅ์ด ๋์ด๋๋ ๊ฑฐ์ง.
๋ฐฑ์๋ ๊ฐ๋ฐ์ ์ ๋ง ๋งค๋ ฅ์ ์ธ ๋ถ์ผ์ผ. ์ฌ์ฉ์๊ฐ ๋ณด์ง ๋ชปํ๋ ๊ณณ์์ ๋ชจ๋ ๊ฑธ ์กฐ์จํ๊ณ , ๋ฐ์ดํฐ๋ฅผ ์์ ํ๊ฒ ๊ด๋ฆฌํ๊ณ , ๋น์ฆ๋์ค ๋ก์ง์ ๊ตฌํํ๋ ๊ฑฐ๋๊น. ์ผํ๋ชฐ API ์๋ฒ๋ฅผ ๋ง๋ค๋ฉด์ ๋ฐฐ์ด ์ง์์ ๋ค๋ฅธ ํ๋ก์ ํธ์๋ ์ถฉ๋ถํ ์์ฉํ ์ ์์ด!
ํน์ ๋งํ๋ ๋ถ๋ถ์ด ์๊ฑฐ๋ ๋ ๊น์ด ๋ฐฐ์ฐ๊ณ ์ถ๋ค๋ฉด, ๊ณต์ ๋ฌธ์๋ฅผ ์ฝ์ด๋ณด๊ฑฐ๋ ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํด๋ด. ๊ทธ๋ฆฌ๊ณ ์ค์ ๊ฒฝํ์ด ์๋ ๊ฐ๋ฐ์์๊ฒ ๋ฉํ ๋ง์ ๋ฐ๋ ๊ฒ๋ ์ ๋ง ํฐ ๋์์ด ๋ผ.
์, ์ด์ ๋น์ ์ฐจ๋ก์ผ! ๐ ์ด ๊ฐ์ด๋๋ฅผ ์ฐธ๊ณ ํด์ ๋ฉ์ง ์ผํ๋ชฐ API ์๋ฒ๋ฅผ ๋ง๋ค์ด๋ณด์. ํ์ดํ ! ๐ปโจ
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.