์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ›’ Node.js์™€ Express๋กœ ๋งŒ๋“œ๋Š” ์‡ผํ•‘๋ชฐ API ์„œ๋ฒ„ ์™„๋ฒฝ ๊ฐ€์ด๋“œ

๐Ÿ›’ Node.js์™€ Express๋กœ ๋งŒ๋“œ๋Š” ์‡ผํ•‘๋ชฐ API ์„œ๋ฒ„ ์™„๋ฒฝ ๊ฐ€์ด๋“œ

์‹ค์ „ ํ”„๋กœ์ ํŠธ๋กœ ๋ฐฐ์šฐ๋Š” ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์˜ ๋ชจ๋“  ๊ฒƒ

API Server Node Express Framework Client App Database MongoDB Request Query ์ƒํ’ˆ ๊ด€๋ฆฌ /api/products ์ฃผ๋ฌธ ์ฒ˜๋ฆฌ /api/orders ํšŒ์› ์ธ์ฆ /api/auth ์žฅ๋ฐ”๊ตฌ๋‹ˆ /api/cart ๊ฒฐ์ œ ์—ฐ๋™ /api/payment

๐ŸŽฏ ์™œ Node.js์™€ Express๋กœ ์‡ผํ•‘๋ชฐ API๋ฅผ ๋งŒ๋“ค๊นŒ?

์š”์ฆ˜ ์‡ผํ•‘๋ชฐ ํ•˜๋‚˜ ๋งŒ๋“ค๋ ค๊ณ  ํ•˜๋ฉด ์ •๋ง ๊ณ ๋ฏผ์ด ๋งŽ์•„์ง€์ง€? ๐Ÿค” ํ”„๋ก ํŠธ์—”๋“œ๋Š” React๋‚˜ Vue๋กœ ํ•˜๊ณ , ๋ชจ๋ฐ”์ผ ์•ฑ๋„ ๋”ฐ๋กœ ๋งŒ๋“ค์–ด์•ผ ํ•˜๊ณ ... ๊ทธ๋Ÿด ๋•Œ ํ•„์š”ํ•œ ๊ฒŒ ๋ฐ”๋กœ API ์„œ๋ฒ„์•ผ!

Node.js์™€ Express๋ฅผ ์„ ํƒํ•˜๋Š” ์ด์œ ๋Š” ๋ช…ํ™•ํ•ด. ์ฒซ์งธ, **JavaScript ํ•˜๋‚˜๋กœ ํ”„๋ก ํŠธ์™€ ๋ฐฑ์—”๋“œ๋ฅผ ๋ชจ๋‘ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ๋‹ค**๋Š” ์ ์ด ์—„์ฒญ๋‚œ ์žฅ์ ์ด์•ผ. ๋‘˜์งธ, **๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ์— ์ตœ์ ํ™”**๋˜์–ด ์žˆ์–ด์„œ ๋™์‹œ์— ๋งŽ์€ ์‚ฌ์šฉ์ž ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์ข‹์•„. ์…‹์งธ, **npm ์ƒํƒœ๊ณ„**๊ฐ€ ์–ด๋งˆ์–ด๋งˆํ•˜๊ฒŒ ๋ฐฉ๋Œ€ํ•ด์„œ ํ•„์š”ํ•œ ๊ธฐ๋Šฅ์€ ๋Œ€๋ถ€๋ถ„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์ง€.

๐Ÿ’ก ์‹ค์ œ ์‚ฌ๋ก€: ์•„๋งˆ์กด, ๋„ทํ”Œ๋ฆญ์Šค, ์šฐ๋ฒ„ ๊ฐ™์€ ๊ธ€๋กœ๋ฒŒ ๊ธฐ์—…๋“ค๋„ Node.js๋ฅผ ํ™œ์šฉํ•˜๊ณ  ์žˆ์–ด. ํŠนํžˆ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๊ฐ€ ์ค‘์š”ํ•œ ์„œ๋น„์Šค์—์„œ Node.js์˜ ์„ฑ๋Šฅ์€ ๊ฒ€์ฆ๋๋‹ค๊ณ  ๋ณผ ์ˆ˜ ์žˆ์ง€!

์žฌ๋Šฅ๋„ท(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
๐ŸŽ“ ํŒจํ‚ค์ง€ ์„ค๋ช…: โ€ข express: ์›น ํ”„๋ ˆ์ž„์›Œํฌ
โ€ข 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);
๐Ÿ’ก ํ•ต์‹ฌ ํฌ์ธํŠธ: โ€ข pre('save') ๋ฏธ๋“ค์›จ์–ด๋กœ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž๋™์œผ๋กœ ์•”ํ˜ธํ™”ํ•ด
โ€ข 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);
๐Ÿ”‘ JWT ์ธ์ฆ ํ๋ฆ„: 1. ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธํ•˜๋ฉด ์„œ๋ฒ„๊ฐ€ JWT ํ† ํฐ์„ ๋ฐœ๊ธ‰ํ•ด
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
    });
  }
};
๐Ÿ’ก ํ•„ํ„ฐ๋ง๊ณผ ํŽ˜์ด์ง€๋„ค์ด์…˜: ์ƒํ’ˆ ๋ชฉ๋ก API์—์„œ๋Š” ์นดํ…Œ๊ณ ๋ฆฌ, ๊ฐ€๊ฒฉ ๋ฒ”์œ„, ๊ฒ€์ƒ‰์–ด ๋“ฑ์œผ๋กœ ํ•„ํ„ฐ๋งํ•  ์ˆ˜ ์žˆ๊ณ , ์ •๋ ฌ ์˜ต์…˜๋„ ์ œ๊ณตํ•ด. ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ํ†ตํ•ด ํ•œ ๋ฒˆ์— 12๊ฐœ์”ฉ๋งŒ ๋ถˆ๋Ÿฌ์˜ค๋‹ˆ๊นŒ ์„ฑ๋Šฅ๋„ ์ข‹์•„์ง€๊ณ  ์‚ฌ์šฉ์ž ๊ฒฝํ—˜๋„ ๊ฐœ์„ ๋ผ! โšก

์ƒํ’ˆ ๋ผ์šฐํŠธ ์„ค์ •

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
    });
  }
};
โš ๏ธ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ: ์‹ค์ œ ์šด์˜ ํ™˜๊ฒฝ์—์„œ๋Š” ์ฃผ๋ฌธ ์ƒ์„ฑ ์‹œ ์žฌ๊ณ  ๊ฐ์†Œ์™€ ์ฃผ๋ฌธ ์ƒ์„ฑ์„ ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋ฌถ์–ด์•ผ ํ•ด. MongoDB์˜ ํŠธ๋žœ์žญ์…˜ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜, ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ๋กค๋ฐฑ ๋กœ์ง์„ ๊ตฌํ˜„ํ•ด์•ผ ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ์„ ๋ณด์žฅํ•  ์ˆ˜ ์žˆ์–ด!

์ฃผ๋ฌธ ๋ผ์šฐํŠธ ์„ค์ •

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 ์ปค๋ฎค๋‹ˆํ‹ฐ

๐Ÿ’ผ ์žฌ๋Šฅ๋„ท์—์„œ ๋” ๋ฐฐ์šฐ๊ธฐ: ์žฌ๋Šฅ๋„ท(https://www.jaenung.net)์—์„œ๋Š” ์‹ค์ „ ํ”„๋กœ์ ํŠธ ๊ฒฝํ—˜์ด ์žˆ๋Š” ๊ฐœ๋ฐœ์ž๋“ค์—๊ฒŒ 1:1 ๋ฉ˜ํ† ๋ง์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด. Node.js ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ๋ถ€ํ„ฐ ๋ฐฐํฌ, ์šด์˜๊นŒ์ง€ ์‹ค๋ฌด ๋…ธํ•˜์šฐ๋ฅผ ๋ฐฐ์šธ ์ˆ˜ ์žˆ์œผ๋‹ˆ ์ฐธ๊ณ ํ•ด๋ด! ๐Ÿš€

๐ŸŽฏ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€, ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์ง€? ๐Ÿ˜… Node.js์™€ Express๋กœ ์‡ผํ•‘๋ชฐ API ์„œ๋ฒ„๋ฅผ ๋งŒ๋“œ๋Š” ๊ณผ์ •์„ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ์‚ดํŽด๋ดค์–ด.

์šฐ๋ฆฌ๊ฐ€ ๋‹ค๋ฃฌ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•ด๋ณด๋ฉด:
โ€ข Express ์„œ๋ฒ„ ๊ธฐ๋ณธ ์„ค์ •
โ€ข MongoDB ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ์„ค๊ณ„
โ€ข JWT ๊ธฐ๋ฐ˜ ์ธ์ฆ ์‹œ์Šคํ…œ
โ€ข ์ƒํ’ˆ, ์žฅ๋ฐ”๊ตฌ๋‹ˆ, ์ฃผ๋ฌธ ๊ด€๋ฆฌ API
โ€ข ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ๊ณผ ์—๋Ÿฌ ์ฒ˜๋ฆฌ
โ€ข ์„ฑ๋Šฅ ์ตœ์ ํ™”์™€ ๋ณด์•ˆ
โ€ข ๋กœ๊น…, ํ…Œ์ŠคํŠธ, ๋ฐฐํฌ

์ด ๋ชจ๋“  ๊ฑธ ํ•œ ๋ฒˆ์— ์™„๋ฒฝํ•˜๊ฒŒ ์ดํ•ดํ•˜๊ธฐ๋Š” ์–ด๋ ค์›Œ. ์ฒœ์ฒœํžˆ ํ•˜๋‚˜์”ฉ ๊ตฌํ˜„ํ•ด๋ณด๋ฉด์„œ ์ตํ˜€๊ฐ€๋ฉด ๋ผ! ๐Ÿ’ช

๊ฐ€์žฅ ์ค‘์š”ํ•œ ๊ฑด ์ง์ ‘ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณด๋Š” ๊ฒƒ์ด์•ผ. ์ด ๊ธ€์„ ๋ณด๋ฉด์„œ ๋”ฐ๋ผ ๋งŒ๋“ค์–ด๋ณด๊ณ , ์ž์‹ ๋งŒ์˜ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด๋ณด๊ณ , ์—๋Ÿฌ๋ฅผ ๋งŒ๋‚˜๋ฉด ํ•ด๊ฒฐํ•ด๋ณด๋ฉด์„œ ์‹ค๋ ฅ์ด ๋Š˜์–ด๋‚˜๋Š” ๊ฑฐ์ง€.

๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์€ ์ •๋ง ๋งค๋ ฅ์ ์ธ ๋ถ„์•ผ์•ผ. ์‚ฌ์šฉ์ž๊ฐ€ ๋ณด์ง€ ๋ชปํ•˜๋Š” ๊ณณ์—์„œ ๋ชจ๋“  ๊ฑธ ์กฐ์œจํ•˜๊ณ , ๋ฐ์ดํ„ฐ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜๊ณ , ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜๋Š” ๊ฑฐ๋‹ˆ๊นŒ. ์‡ผํ•‘๋ชฐ API ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค๋ฉด์„œ ๋ฐฐ์šด ์ง€์‹์€ ๋‹ค๋ฅธ ํ”„๋กœ์ ํŠธ์—๋„ ์ถฉ๋ถ„ํžˆ ์‘์šฉํ•  ์ˆ˜ ์žˆ์–ด!

ํ˜น์‹œ ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ๊ฑฐ๋‚˜ ๋” ๊นŠ์ด ๋ฐฐ์šฐ๊ณ  ์‹ถ๋‹ค๋ฉด, ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฝ์–ด๋ณด๊ฑฐ๋‚˜ ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์งˆ๋ฌธํ•ด๋ด. ๊ทธ๋ฆฌ๊ณ  ์‹ค์ „ ๊ฒฝํ—˜์ด ์žˆ๋Š” ๊ฐœ๋ฐœ์ž์—๊ฒŒ ๋ฉ˜ํ† ๋ง์„ ๋ฐ›๋Š” ๊ฒƒ๋„ ์ •๋ง ํฐ ๋„์›€์ด ๋ผ.

์ž, ์ด์ œ ๋‹น์‹  ์ฐจ๋ก€์•ผ! ๐Ÿš€ ์ด ๊ฐ€์ด๋“œ๋ฅผ ์ฐธ๊ณ ํ•ด์„œ ๋ฉ‹์ง„ ์‡ผํ•‘๋ชฐ API ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž. ํ™”์ดํŒ…! ๐Ÿ’ปโœจ

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.