반응형
요약정리
전체 코드
동영상 강의 코드
app.js
더보기
const express = require("express");
const mongoose = require("mongoose");
mongoose.connect("mongodb://127.0.0.1/shopping-demo", {
useNewUrlParser: true,
useUnifiedTopology: true,
});
const db = mongoose.connection;
db.on("error", console.error.bind(console, "connection error:"));
const app = express();
const router = express.Router();
const User = require("./models/user");
// 회원가입 API
router.post("/users", async (req, res) => {
const { email, nickname, password, confirmPassword } = req.body;
if (password !== confirmPassword) {
res.status(400).send({
errorMessage: "패스워드가 패스워드 확인란과 다릅니다.",
});
return;
}
// email or nickname이 동일한게 이미 있는지 확인하기 위해 가져온다.
const existsUsers = await User.findOne({
$or: [{ email }, { nickname }],
});
if (existsUsers) {
// NOTE: 보안을 위해 인증 메세지는 자세히 설명하지 않는것을 원칙으로 한다.
// https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#authentication-responses
res.status(400).send({
errorMessage: "이메일 또는 닉네임이 이미 사용중입니다.",
});
return;
}
const user = new User({ email, nickname, password });
await user.save();
res.status(201).send({});
});
const jwt = require("jsonwebtoken");
router.post("/auth", async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
// NOTE: 인증 메세지는 자세히 설명하지 않는것을 원칙으로 한다: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#authentication-responses
if (!user || password !== user.password) {
res.status(400).send({
errorMessage: "이메일 또는 패스워드가 틀렸습니다.",
});
return;
}
res.send({
token: jwt.sign({ userId: user.userId }, "customized-secret-key"),
});
});
const authMiddleware = require("./middlewares/auth-middleware");
router.get("/users/me", authMiddleware, async (req, res) => {
res.send({ user: res.locals.user });
});
app.use("/api", express.urlencoded({ extended: false }), router);
app.use(express.static("assets"));
app.listen(8080, () => {
console.log("서버가 요청을 받을 준비가 됐어요");
});
models/user.js
더보기
const mongoose = require("mongoose");
const UserSchema = new mongoose.Schema({
email: String,
nickname: String,
password: String,
});
UserSchema.virtual("userId").get(function () {
return this._id.toHexString();
});
UserSchema.set("toJSON", {
virtuals: true,
});
module.exports = mongoose.model("User", UserSchema);
middlewares/auth-middlewares.js
더보기
const jwt = require("jsonwebtoken");
const User = require("../models/user");
module.exports = (req, res, next) => {
const { authorization } = req.headers;
const [authType, authToken] = (authorization || "").split(" ");
if (!authToken || authType !== "Bearer") {
res.status(401).send({
errorMessage: "로그인 후 이용 가능한 기능입니다.",
});
return;
}
try {
const { userId } = jwt.verify(authToken, "customized-secret-key");
User.findById(userId).then((user) => {
res.locals.user = user;
next();
});
} catch (err) {
res.status(401).send({
errorMessage: "로그인 후 이용 가능한 기능입니다.",
});
}
};
노션 코드
app.js
더보기
// app.js
const express = require("express");
const cookieParser = require("cookie-parser");
const goodsRouter = require("./routes/goods.js");
// const cartsRouter = require("./routes/carts.js"); // 현재 사용하지 않는 장바구니 라우터
const usersRouter = require("./routes/users.js");
const authRouter = require("./routes/auth.js");
// MongoDB 연결을 위한 스키마 설정 모듈을 가져옴
const connect = require("./schemas");
const app = express(); // express 애플리케이션을 생성
const port = 3000; // 서버가 사용할 포트 번호
connect(); // mongoose를 이용해 MongoDB에 연결
// 미들웨어 설정
app.use(express.json()); // JSON 요청 본문을 파싱합니다.
app.use(express.urlencoded({ extended: false })); // URL 인코딩된 데이터를 파싱
app.use(cookieParser()); // 쿠키를 파싱
app.use(express.static("assets")); // 정적 파일을 제공하는 미들웨어
// 라우트 미들웨어 설정
app.use("/api", [goodsRouter, usersRouter, authRouter]); // '/api' 경로로 시작하는 요청을 각 라우터에 위임
// 루트 URL에 대한 요청 처리
app.get("/", (req, res) => {
res.send("Hello World!"); // 루트 URL에 접근하면 'Hello World!'를 반환
});
// 서버 시작
app.listen(port, () => {
console.log(port, "포트로 서버가 열렸어요!"); // 서버가 포트 3000에서 시작되었음을 콘솔에 출력
});
schemas/cart.js
더보기
const mongoose = require("mongoose");
const cartSchema = new mongoose.Schema({
userId: {
type: String,
required: true,
},
goodsId: {
type: String,
required: true,
},
quantity: {
type: Number,
required: true,
},
});
module.exports = mongoose.model("Cart", cartSchema);
schemas/goods.js
더보기
const mongoose = require("mongoose");
const goodsSchema = new mongoose.Schema({
goodsId: {
type: String,
required: true,
unique: true,
},
name: {
type: String,
required: true,
unique: true,
},
thumbnailUrl: {
type: String,
},
category: {
type: String,
},
price: {
type: Number,
},
});
module.exports = mongoose.model("Goods", goodsSchema);
schemas/index.js
더보기
const mongoose = require("mongoose");
const connect = () => {
mongoose
.set("strictQuery", true)
.connect("mongodb://127.0.0.1:27017/spa_mall")
.catch((err) => console.log(err));
};
mongoose.connection.on("error", (err) => {
console.error("몽고디비 연결 에러", err);
});
module.exports = connect;
schemas/user.js
더보기
const mongoose = require("mongoose");
const UserSchema = new mongoose.Schema({
email: {
// email 필드
type: String,
required: true,
unique: true,
},
nickname: {
// nickname 필드
type: String,
required: true,
unique: true,
},
password: {
// password 필드
type: String,
required: true,
},
});
// 가상의 userId 값을 할당
UserSchema.virtual("userId").get(function () {
return this._id.toHexString();
});
// user 정보를 JSON으로 형변환 할 때 virtual 값이 출력되도록 설정
UserSchema.set("toJSON", {
virtuals: true,
});
module.exports = mongoose.model("User", UserSchema);
routes/auth.js
더보기
// routes/auth.js
// 필요한 모듈을 가져옴
const jwt = require("jsonwebtoken"); // JSON Web Tokens를 생성하고 검증하기 위한 모듈
const express = require("express");
const router = express.Router();
// MongoDB User 모델을 가져옴
const User = require("../schemas/user");
// 로그인 API
router.post("/auth", async (req, res) => {
// 요청 본문에서 이메일과 패스워드를 추출
const { email, password } = req.body;
// 이메일로 사용자를 찾습니다.
const user = await User.findOne({ email });
// 사용자가 존재하지 않거나 패스워드가 일치하지 않으면 오류 메시지를 반환
if (!user || password !== user.password) {
res.status(400).json({
errorMessage: "이메일 또는 패스워드가 틀렸습니다.",
});
return;
}
// 사용자를 위한 JWT를 생성
const token = jwt.sign({ userId: user.userId }, "customized-secret-key");
// 생성된 JWT를 쿠키에 저장
res.cookie("Authorization", `Bearer ${token}`);
// 생성된 JWT를 응답 본문에도 포함하여 반환
res.status(200).json({ token });
});
// 라우터 모듈을 내보냄
module.exports = router;
routes/goods.js
더보기
// routes/goods.js
const express = require("express");
const router = express.Router();
const authMiddleware = require("../middlewares/auth-middleware");
// 썬더 클라이언트로 등록할 상품목록
// const goods = [
// {
// goodsId: 4,
// name: "상품 4",
// thumbnailUrl: "https://cdn.pixabay.com/photo/2016/09/07/02/11/frogs-1650657_1280.jpg",
// category: "drink",
// price: 0.1,
// },
// {
// goodsId: 3,
// name: "상품 3",
// thumbnailUrl: "https://cdn.pixabay.com/photo/2016/09/07/02/12/frogs-1650658_1280.jpg",
// category: "drink",
// price: 2.2,
// },
// {
// goodsId: 2,
// name: "상품 2",
// thumbnailUrl: "https://cdn.pixabay.com/photo/2014/08/26/19/19/wine-428316_1280.jpg",
// category: "drink",
// price: 0.11,
// },
// {
// goodsId: 1,
// name: "상품 1",
// thumbnailUrl: "https://cdn.pixabay.com/photo/2016/09/07/19/54/wines-1652455_1280.jpg",
// category: "drink",
// price: 6.2,
// },
// ];
// MongoDB의 Goods 스키마를 가져옴
const Goods = require("../schemas/goods.js");
// MongoDB의 Cart 스키마를 가져옴
const Cart = require("../schemas/cart.js");
// 상품 목록 조회 API
router.get("/goods", async (req, res) => {
// 쿼리 파라미터에서 카테고리를 추출
const { category } = req.query;
// Goods 모델을 사용하여 데이터베이스에서 상품을 조회
const goods = await Goods.find(category ? { category } : {})
.sort("-date") // 최신순으로 정렬
.exec();
// 조회된 상품 데이터를 정리하여 응답
const results = goods.map((item) => {
return {
goodsId: item.goodsId,
name: item.name,
price: item.price,
thumbnailUrl: item.thumbnailUrl,
category: item.category,
};
});
res.json({ goods: results });
});
// 상품 등록 API
router.post("/goods", async (req, res) => {
const { goodsId, name, thumbnailUrl, category, price } = req.body; // 요청 본문에서 상품 정보 추출
// 동일한 goodsId를 가진 상품이 있는지 확인
const goods = await Goods.find({ goodsId });
if (goods.length) {
// 이미 존재하면 오류 메시지와 함께 400 상태 코드를 반환
return res.status(400).json({
success: false,
errorMessage: "이미 존재하는 GoodsId입니다.",
});
}
// 새 상품을 생성하고 데이터베이스에 저장
const createdGoods = await Goods.create({ goodsId, name, thumbnailUrl, category, price });
res.json({ goods: createdGoods });
});
// 상품 상세 조회 API
router.get("/goods/:goodsId", async (req, res) => {
// URL 파라미터에서 goodsId를 추출
const { goodsId } = req.params;
// 특정 상품 ID에 해당하는 상품을 조회
const goods = await Goods.findOne({ goodsId: goodsId }).exec();
// 해당 상품이 없으면 404 오류를 반환
if (!goods) return res.status(404).json({});
// 조회된 상품 정보를 응답
const result = {
goodsId: goods.goodsId,
name: goods.name,
price: goods.price,
thumbnailUrl: goods.thumbnailUrl,
category: goods.category,
};
res.json({ goods: result });
});
// ========== 장바구니 기능 미구현 ==========
// 장바구니 등록 API
router.post("/goods/:goodsId/cart", authMiddleware, async (req, res) => {
const { userId } = res.locals.user; // 인증된 사용자의 ID
const { goodsId } = req.params; // URL 파라미터에서 goodsId를 추출
const { quantity } = req.body; // 요청 본문에서 수량을 추출
// 동일한 상품이 장바구니에 있는지 확인
const existsCarts = await Cart.find({ userId, goodsId }).exec();
if (existsCarts.length) {
// 이미 존재하면 오류 메시지와 함께 400 상태 코드를 반환
return res.status(400).json({
success: false,
errorMessage: "이미 장바구니에 해당하는 상품이 존재합니다.",
});
}
// 장바구니에 상품을 추가
await Cart.create({ userId, goodsId, quantity });
res.json({ result: "success" });
});
// 장바구니 조회 API
router.get("/goods/cart", authMiddleware, async (req, res) => {
const { userId } = res.locals.user; // 인증된 사용자의 ID
// 사용자의 장바구니 항목을 조회
const carts = await Cart.find({ userId }).exec();
const goodsIds = carts.map((cart) => cart.goodsId);
// 장바구니에 있는 상품들의 정보를 조회
const goods = await Goods.find({ goodsId: goodsIds });
// 장바구니 항목과 상품 정보를 결합하여 응답
const results = carts.map((cart) => {
return {
quantity: cart.quantity,
goods: goods.find((item) => item.goodsId === cart.goodsId),
};
});
res.json({
carts: results,
});
});
// 장바구니 상품 수정 API
router.put("/goods/:goodsId/cart", authMiddleware, async (req, res) => {
const { userId } = res.locals.user; // 인증된 사용자의 ID
const { goodsId } = req.params; // URL 파라미터에서 goodsId 추출
const { quantity } = req.body; // 요청 본문에서 수량 추출
// 장바구니 항목이 존재하는지 확인
const existsCarts = await Cart.find({ userId, goodsId });
if (existsCarts.length) {
// 존재하면 해당 항목의 수량을 업데이트
await Cart.updateOne({ userId, goodsId: goodsId }, { $set: { quantity: quantity } });
}
res.status(200).json({ success: true });
});
// 장바구니 상품 삭제 API
router.delete("/goods/:goodsId/cart", authMiddleware, async (req, res) => {
const { userId } = res.locals.user; // 인증된 사용자의 ID
const { goodsId } = req.params; // URL 파라미터에서 goodsId 추출
// 장바구니 항목이 존재하는지 확인
const existsCarts = await Cart.find({ userId, goodsId });
if (existsCarts.length) {
// 존재하면 해당 항목을 삭제
await Cart.deleteOne({ userId, goodsId });
}
res.json({ result: "success" });
});
module.exports = router;
routes/users.js
더보기
// routes/users.js
const express = require("express");
const router = express.Router();
// MongoDB의 User 스키마를 가져옴
const User = require("../schemas/user");
// 사용자 인증을 위한 미들웨어
const authMiddleware = require("../middlewares/auth-middleware");
// 회원가입 API
router.post("/users", async (req, res) => {
// 요청 본문에서 필요한 정보를 추출
const { email, nickname, password, confirmPassword } = req.body;
// 입력된 비밀번호와 확인 비밀번호가 일치하는지 검사
if (password !== confirmPassword) {
// 일치하지 않으면 오류 메시지와 함께 400 상태 코드를 반환
res.status(400).json({
errorMessage: "패스워드가 패스워드 확인란과 다릅니다.",
});
return;
}
// 이메일 또는 닉네임이 이미 사용 중인지 검사
const existsUsers = await User.findOne({
$or: [{ email }, { nickname }],
});
if (existsUsers) {
// 이미 사용 중인 경우 오류 메시지와 함께 400 상태 코드를 반환
res.status(400).json({
errorMessage: "이메일 또는 닉네임이 이미 사용중입니다.",
});
return;
}
// 새로운 사용자를 생성하고 데이터베이스에 저장
const user = new User({ email, nickname, password });
await user.save();
// 생성이 완료되면 201 상태 코드를 반환
res.status(201).json({});
});
// 내 정보 조회 API
router.get("/users/me", authMiddleware, async (req, res) => {
// 인증 미들웨어를 통해 얻은 사용자 정보를 추출
const { email, nickname } = res.locals.user;
// 사용자 정보를 반환
res.status(200).json({
user: { email, nickname },
});
});
// 라우터 모듈을 내보냄
module.exports = router;
models/user.js
더보기
const mongoose = require("mongoose");
const UserSchema = new mongoose.Schema({
email: {
// email 필드
type: String,
required: true,
unique: true,
},
nickname: {
// nickname 필드
type: String,
required: true,
unique: true,
},
password: {
// password 필드
type: String,
required: true,
},
});
// 가상의 userId 값을 할당
UserSchema.virtual("userId").get(function () {
return this._id.toHexString();
});
// user 정보를 JSON으로 형변환 할 때 virtual 값이 출력되도록 설정
UserSchema.set("toJSON", {
virtuals: true,
});
module.exports = mongoose.model("User", UserSchema);
middlewares/auth-middleware.js
더보기
// middlewares/auth-middleware.js
const jwt = require("jsonwebtoken"); // JWT 처리를 위한 모듈
const User = require("../schemas/user"); // MongoDB User 모델
// 사용자 인증 미들웨어
module.exports = async (req, res, next) => {
// 요청에서 쿠키를 가져옴
const { Authorization } = req.cookies;
// 쿠키에서 인증 타입과 토큰을 분리
// 'Authorization' 쿠키가 없다면 빈 문자열을 기본값으로 사용
const [authType, authToken] = (Authorization ?? "").split(" ");
// 토큰이 없거나 인증 타입이 'Bearer'가 아니면 오류 메시지를 반환
if (!authToken || authType !== "Bearer") {
res.status(401).send({
errorMessage: "로그인 후 이용 가능한 기능입니다.",
});
return;
}
try {
// JWT를 검증하고, 토큰에 담긴 사용자 ID를 추출
const { userId } = jwt.verify(authToken, "customized-secret-key");
// 사용자 ID로 데이터베이스에서 사용자를 찾음
const user = await User.findById(userId);
// 찾은 사용자 정보를 응답 객체의 로컬 변수에 저장
res.locals.user = user;
// 다음 미들웨어를 실행합니다.
next();
} catch (err) {
// JWT 검증에 실패하면 오류를 로깅하고 오류 메시지를 반환
console.error(err);
res.status(401).send({
errorMessage: "로그인 후 이용 가능한 기능입니다.",
});
}
};
Error Handling?
Issue 1.
- 강의 미들웨어까지 진행 후 로그인 테스트를 해보면 에러 발생

Solution 1.
- 프론트에는 구현이 되어 있으나 서버엔 상품 CRUD가 존재하지 않음
강의에선 회원가입, 로그인만 알려주며 그 외 코드는 노션 자료에 있음
더보기
// 프론트 코드
// const socket = io.connect("/");
// socket.on("BUY_GOODS", function (data) {
// const { nickname, goodsId, goodsName, date } = data;
// makeBuyNotification(nickname, goodsName, goodsId, date);
// });
function initAuthenticatePage() {
// socket.emit("CHANGED_PAGE", `${location.pathname}${location.search}`);
}
function bindSamePageViewerCountEvent(callback) {
// socket.on("SAME_PAGE_VIEWER_COUNT", callback);
}
function postOrder(user, order) {
if (!order.length) {
return;
}
// socket.emit("BUY", {
// nickname: user.nickname,
// goodsId: order[0].goods.goodsId,
// goodsName:
// order.length > 1
// ? `${order[0].goods.name} 외 ${order.length - 1}개의 상품`
// : order[0].goods.name,
// });
}
function getSelf(callback) {
$.ajax({
type: "GET",
url: "/api/users/me",
headers: {
authorization: `Bearer ${localStorage.getItem("token")}`,
},
success: function (response) {
callback(response.user);
},
error: function (xhr, status, error) {
if (status == 401) {
alert("로그인이 필요합니다.");
} else {
localStorage.clear();
alert("알 수 없는 문제가 발생했습니다. 관리자에게 문의하세요.");
}
window.location.href = "/";
},
});
}
function getGoods(category, callback) {
$("#goodsList").empty();
$.ajax({
type: "GET",
url: `/api/goods${category ? "?category=" + category : ""}`,
headers: {
authorization: `Bearer ${localStorage.getItem("token")}`,
},
success: function (response) {
callback(response["goods"]);
},
});
}
function signOut() {
localStorage.clear();
window.location.href = "/";
}
function getGoodsDetail(goodsId, callback) {
$.ajax({
type: "GET",
url: `/api/goods/${goodsId}`,
headers: {
authorization: `Bearer ${localStorage.getItem("token")}`,
},
error: function (xhr, status, error) {
if (status == 401) {
alert("로그인이 필요합니다.");
} else if (status == 404) {
alert("존재하지 않는 상품입니다.");
} else {
alert("알 수 없는 문제가 발생했습니다. 관리자에게 문의하세요.");
}
window.location.href = "/goods";
},
success: function (response) {
callback(response.goods);
},
});
}
function makeBuyNotification(targetNickname, goodsName, goodsId, date) {
const messageHtml = `${targetNickname}님이 방금 <a href="/detail.html?goodsId=${goodsId}" class="alert-link">${goodsName}</a>을 구매했어요! <br /><small>(${date})</small>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">×</span>
</button>`;
const alt = $("#customerAlert");
if (alt.length) {
alt.html(messageHtml);
} else {
const htmlTemp = `<div class="alert alert-sparta alert-dismissible show fade" role="alert" id="customerAlert">${messageHtml}</div>`;
$("body").append(htmlTemp);
}
}
function addToCart(goodsId, quantity, callback) {
$.ajax({
type: "PUT",
url: `/api/goods/${goodsId}/cart`,
headers: {
authorization: `Bearer ${localStorage.getItem("token")}`,
},
data: {
quantity,
},
error: function (xhr, status, error) {
if (status == 400) {
alert("존재하지 않는 상품입니다.");
}
window.location.href = "/goods.html";
},
success: function () {
callback();
},
});
}
function buyLocation(params) {
sessionStorage.setItem("ordered", JSON.stringify(params));
location.href = "order.html";
}
function getCarts(callback) {
$.ajax({
type: "GET",
url: `/api/goods/cart`,
headers: {
authorization: `Bearer ${localStorage.getItem("token")}`,
},
success: function (response) {
callback(response.cart);
},
});
}
function deleteCart(goodsId, callback) {
$.ajax({
type: "DELETE",
url: `/api/goods/${goodsId}/cart`,
headers: {
authorization: `Bearer ${localStorage.getItem("token")}`,
},
success: function () {
callback();
},
});
}
Issue 2.
- CastError: Cast to Number failed for value "cart" (type string) at path "goodsId" for model "Goods"
스키마 컬럼 타입은 Number로 되어 있는데 왜 문자열을 집어넣냐고 출력되는 에러.. 상당히 길게 출력된다

Solution 2.
- 무지성 컬럼 타입 변경
에러가 출력되면 서버가 내려가서 임시로 변경해 줌

회고
정말.. 강의대로 했는데도 예상치 못한 에러들이 여기저기서 나왔어...
장바구니 기능도 구현해보고 싶었는데 새로운 개인 과제도 나와서 나중에 여유 시간 있을 때 한번 해봐야 될듯하다
세션은 이해가 되는데 JWT토큰은 아직 모호하다
좀 더 파봐야 될 듯
반응형
'내일배움캠프 > TIL' 카테고리의 다른 글
| TIL. 시퀄라이즈 정렬 (0) | 2023.11.12 |
|---|---|
| TIL. 노드 숙련 개인 과제 2일차 (0) | 2023.11.11 |
| TIL. 인증 & 알고리즘 스터디 1회차 (2) | 2023.11.09 |
| TIL. 할 일 메모 사이트 만들기 (1) | 2023.11.08 |
| TIL. Node.js 입문 복습 (1) | 2023.11.07 |
