(완료) 1.1주차 과제 만들기
(완료) 2. API 기능 정리
(완료) 3. 과제 정리 -> 요청 응답 정리(캡쳐)
(완료) 4. 깃허브 업로드
(완료) 5. 배포
- 상품 작성 API
- 상품명, 작성 내용, 작성자명, 비밀번호를 request에서 전달받기
- 상품은 두 가지 상태, 판매 중(
FOR_SALE)및 판매 완료(SOLD_OUT) 를 가질 수 있습니다.- 상품 등록 시 기본 상태는 판매 중(
FOR_SALE) 입니다.- 상품 목록 조회 API
- 상품명, 작성자명, 상품 상태, 작성 날짜 조회하기
- 상품 목록은 작성 날짜를 기준으로 내림차순(최신순) 정렬하기
- 상품 상세 조회 API
- 상품명, 작성 내용, 작성자명, 상품 상태, 작성 날짜 조회하기
- 상품 정보 수정 API
- 상품명, 작성 내용, 상품 상태, 비밀번호를 request에서 전달받기
- 수정할 상품과 비밀번호 일치 여부를 확인한 후, 동일할 때만 글이 수정되게 하기
- 선택한 상품이 존재하지 않을 경우, “상품 조회에 실패하였습니다." 메시지 반환하기
- 상품 삭제 API
- 비밀번호를 request에서 전달받기
- 수정할 상품과 비밀번호 일치 여부를 확인한 후, 동일할 때만 글이 삭제되게 하기
- 선택한 상품이 존재하지 않을 경우, “상품 조회에 실패하였습니다." 메시지 반환하기
/** 상품 등록 **/ //localhost:3000/api/goods POST router.post('/goods', async (req, res) => { const { goodsName, goodsContent, username, password } = req.body; if (!goodsName || !goodsContent) { return res.status(400).json({ errorMessage: "입력 값이 없어요!" }); }; const goodsMaxOrder = await Goods.findOne().sort('-goodsOrder').exec(); const goodsOrder = goodsMaxOrder ? goodsMaxOrder.goodsOrder + 1 : 1; //saleStatus 기본상태 : "FOR_SALE" const saleStatus = "FOR_SALE" const wroteDate = new Date(); const goods = new Goods({ goodsOrder, goodsName, goodsContent, username, password, saleStatus, wroteDate }); await goods.save(); return res.status(201).json({ goods: goods }); });부연설명
- 요청에서
goodsName,goodsContent,username,password를 받아옴const { goodsName, goodsContent, username, password } = req.body;
- 요청값에
goodsName와goodsContent이 없으면 입력값이 없다는 에러메시지 응답.if (!goodsName || !goodsContent) { return res.status(400).json({ errorMessage: "입력 값이 없어요!" }); };
- Goods 모델(스키마)를 내림차 정렬하여 맨 위의 하나를 찾아
goodMaxOrder에 할당한다.goodMaxOrder가 없으면 1부터 시작하는 순서를 가지고, 아니라면 +1하여 하나씩 증가시키는 값을 갖도록 해준다.const goodsMaxOrder = await Goods.findOne().sort('-goodsOrder').exec(); const goodsOrder = goodsMaxOrder ? goodsMaxOrder.goodsOrder + 1 : 1;
- 처음 상품등록하면 기본값으로
saleStatus는 "FOR_SALE",wroteDate는 작성한 날짜가 입력되도록 한다.const saleStatus = "FOR_SALE" const wroteDate = new Date();
- Goods 모델(스키마)의 변경사항을 변수에 할당하여 저장한다. 그 후 클라이언트에게 저장한 내용으로 응답.
const goods = new Goods({ goodsOrder, goodsName, goodsContent, username, password, saleStatus, wroteDate }); await goods.save(); return res.status(201).json({ goods: goods });
정보를 적고 send해서 post 완료.
스키마 기본값 설정
위에서는 라우터에서
saleStatus와wroteDate의 기본값을 설정해줬는데 스키마에서도 기본값을 설정해줄 수 있다!
/** 상품 목록 조회 **/ //localhost:3000/api/goods GET router.get('/goods', async (req, res) => { const goods = await Goods.find().select('goodsName username saleStatus wroteDate').sort('-wroteDate').exec(); return res.status(200).json({ goods }); });부연설명
- Goods 모델(스키마)에서 찾아서 목록을 조회해주는데
조회 시goodsName,username,saleStatus,wroteDate필드만 보이도록 하기 위해서 select를 사용했다. (select 사용법은 아래에 있다)
조회했던 값을 응답했다.
조회했을때 작성 일자순으로 내림차 정렬 확인.
/** 상품 상세 조회 **/ //localhost:3000/api/goods/:goodsId GET router.get('/goods/:goodsId', async (req, res) => { const { goodsId } = req.params; const goodsItem = await Goods.findById(goodsId).select('goodsName goodsContent username saleStatus wroteDate').sort('-wroteDate').exec(); return res.status(200).json({ goods: goodsItem }); });부연설명
- 상품 상세 조회하기 위해서 요청 params라는 경로 매개변수를 이용하여 해당 상품을 찾는다.
const { goodsId } = req.params;
- params에서 불러온
goodsId로 Goods 모델(스키마)에서 필요한 필드명들로 찾고 wroteDate를 이용해 내림차 정렬을 해준다. 정렬해준 값을 goodsItem에 할당하여 응답으로 보내준다.const goodsItem = await Goods.findById(goodsId).select('goodsName goodsContent username saleStatus wroteDate').sort('-wroteDate').exec(); return res.status(200).json({ goods: goodsItem });
주소창에 goodsId값을 넣고 조회 시 해당 상품 조회 가능
/** 상품 정보 수정 **/ //localhost:3000/api/goods/:goodsId PATCH router.patch('/goods/:goodsId', async (req, res) => { const { goodsName, goodsContent, saleStatus, password } = req.body; const { goodsId } = req.params; const currentGoods = await Goods.findById(goodsId).exec(); //선택한 상품 존재 X => 에러 메시지 응답 if (!currentGoods) { return res.status(404).json({ errorMessage: "상품조회에 실패하였습니다." }); }; //상품명, 비밀번호 일치 여부 확인 동일할 때만 수정되도록 if (password !== currentGoods.password) { return res.status(400).json({ errorMessage: "passaword가 맞지 않아요! " }); }; if (goodsName !== currentGoods.goodsName) { return res.status(400).json({ errorMessage: "상품 이름이 맞지않아요! " }); }; //상품 내용 변경 if (goodsContent) { currentGoods.goodsContent = goodsContent; }; if (saleStatus) { if (saleStatus !== "SOLD_OUT" && saleStatus !== "FOR_SALE") { return res.status(400).json({ errorMessage: "FOR_SALE 이나 SOLD_OUT만 적어주세요!" }); }; currentGoods.saleStatus = saleStatus; }; currentGoods.wroteDate = new Date(); await currentGoods.save(); return res.status(200).json({}) });부연설명
- 어떤 상품의 정보를 수정할지 확인해야하기때문에 요청 params에 있는
goodsId를 이용해 검색 후 요청 body에서 불러온goodsName,goodsContent,saleStatus,password를 사용할 것이다.router.patch('/goods/:goodsId', async (req, res) => { const { goodsName, goodsContent, saleStatus, password } = req.body; const { goodsId } = req.params;
goodsId로 검색한 상품인currentGoods검색했을 때 존재하지 않으면 에러메세지를 응답.if (!currentGoods) { return res.status(404).json({ errorMessage: "상품조회에 실패하였습니다." }); };
goodsId로 검색한 상품(currentGoods의 password)과 입력받은password와goodName이 동일하면 수정하도록 할건데 다른 경우를 찾아내야하기엔 조건문을 이용했다.const currentGoods = await Goods.findById(goodsId).exec(); if (password !== currentGoods.password) { return res.status(400).json({ errorMessage: "passaword가 맞지 않아요! " }); }; if (goodsName !== currentGoods.goodsName) { return res.status(400).json({ errorMessage: "상품 이름이 맞지않아요! " }); };
- 입력된 상품 내용
goodsContent과 상품 상태saleStatus가 있다면 수정하도록 했다. saleStatus의 경우 두가지값만 존재해야했기에 조건문을 달아줬다.(논리곱연산자를 사용한 이유는 조건이 긍정인 경우를 생각해봤을때saleStatus가 "SOLD_OUT"이거나 "FOR_SALE"로 수정 된다는 조건이었다. 그걸 코드로 표현하면
saleStatus === "SOLD_OUT" || saleStatus === "FOR_SALE"인데 이걸 부정으로 하면 논리합연산자가 논리곱연산자로 바뀌기 때문이다.)if (goodsContent) { currentGoods.goodsContent = goodsContent; }; if (saleStatus) { if (saleStatus !== "SOLD_OUT" && saleStatus !== "FOR_SALE") { return res.status(400).json({ errorMessage: "FOR_SALE 이나 SOLD_OUT만 적어주세요!" }); }; currentGoods.saleStatus = saleStatus; };
- 작성된 시점 날짜 변경 후 여태 수정한 내용 저장. 응답으로는 빈 객체 응답.
currentGoods.wroteDate = new Date(); await currentGoods.save(); return res.status(200).json({})
비밀번호가 맞지 않아 에러 메시지로 응답.(상품명코드의 위치만 바꾸면 상품명이 다를 때 먼저 에러메시지가 뜨겠다.)
비밀번호는 맞고 상품명이 다를 경우 에러메시지 응답.
상품명, 비밀번호 둘 다 맞을 때 성공했다는 응답. 응답해주는 json에 성공 메시지를 적어주면 훨씬 좋겠다!
바꿈! 쏘 큐트~!
목록 조회시 수정된 내용을 확인할 수 있다.
/** 상품 삭제 **/ //localhost:3000/api/goods/:goodsOrder DELETE router.delete('/goods/:goodsId', async (req, res) => { const { goodsId } = req.params; const { password } = req.body; const currentGoods = await Goods.findById(goodsId).exec(); if (!currentGoods) { return res.status(400).json({ errorMessage: "상품 조회에 실패하였습니다." }); }; if (password !== currentGoods.password) { return res.status(400).json({ errorMessage: "password가 맞지않아요!" }) } await Goods.deleteOne({ _id: currentGoods }).exec(); return res.status(200).json({}); })부연설명
- 삭제하기 위해서 goodsId를 통해 어떤 상품을 삭제하는지 확인해야한다. 그리고 password도 동일해야지 삭제할 수 있어서 password 또한 요청값에서 불러온다.
router.delete('/goods/:goodsId', async (req, res) => { const { goodsId } = req.params; const { password } = req.body;
- goodsId를 통해서 currentGoods값을 찾아서 currentGoods값이 존재하는지 안하는지 확인한다.
const currentGoods = await Goods.findById(goodsId).exec(); if (!currentGoods) { return res.status(400).json({ errorMessage: "상품 조회에 실패하였습니다." }); };3.password이 불러온 currentGoods의 password와 비교하여 틀리면 에러메시지를 보내고 아니라면 _id가 currentGoods인걸 하나 지운다. 그 후 응답으로 빈 객체를 보낸다.
if (password !== currentGoods.password) { return res.status(400).json({ errorMessage: "password가 맞지않아요!" }) } await Goods.deleteOne({ _id: currentGoods }).exec(); return res.status(200).json({});
비밀번호가 틀릴때 에러 메시지 응답
비밀번호 입력이 맞아서 빈 객체 응답.
상품 상세 조회시 null값으로 조회됨.(삭제완료)
router 파일 안에 (router코드 안 X) 유효성 검사할 객체를 정의해두고 validateAsync로 검사하면 된다. 그리고 검사한 결과validation도 객체이므로 그걸 req.body를 받아 사용하던 상수들한테 할당하면 된다. 그럼 일일이 조건문을 안달아도 된다!
import joi from 'joi';
const createGoodsSchema = joi.object({
goodsName: joi.string().min(1).max(10).required(),
goodsContent: joi.string().min(1).max(100).required(),
username: joi.string().min(1).max(10).required(),
password: joi.string().min(1).max(10).required()
});
>
/** 상품 등록 **/
//localhost:3000/api/goods POST
router.post('/goods', async (req, res) => {
// const { goodsName, goodsContent, username, password } = req.body;
>
const validation = await createGoodsSchema.validateAsync(req.body);
>
const { goodsName, goodsContent, username, password } = validation;



Joi의 데이터 유효성 검증을 실패한 코드에서 에러가 발생하는 경우에 대한 대응 방법
import express from 'express';
import mongoose from 'mongoose';
import Goods from '../schemas/goods.js';
import joi from 'joi';
>
const router = express.Router();
>
const createGoodsSchema = joi.object({
goodsName: joi.string().min(1).max(10).required(),
goodsContent: joi.string().min(1).max(100).required(),
username: joi.string().min(1).max(10).required(),
password: joi.string().min(1).max(10).required()
});
.
/** 상품 등록 **/
//localhost:3000/api/goods POST
router.post('/goods', async (req, res) => {
try {
const validation = await createGoodsSchema.validateAsync(req.body);
>
const { goodsName, goodsContent, username, password } = validation;
>
if (!goodsName || !goodsContent) {
return res.status(400).json({ errorMessage: "입력 값이 없어요!" });
};
>
const goodsMaxOrder = await Goods.findOne().sort('-goodsOrder').exec();
const goodsOrder = goodsMaxOrder ? goodsMaxOrder.goodsOrder + 1 : 1;
>
//saleStatus 기본상태 : "FOR_SALE"
// const saleStatus = "FOR_SALE"
// const wroteDate = new Date();
const goods = new Goods({ goodsOrder, goodsName, goodsContent, username, password });
await goods.save();
>
return res.status(201).json({ goods: goods });
} catch (error) {
console.error(error);
if(error.name === 'ValidationError') {
return res.status(400).json({ errorMessage : error.message })
}
return res.status(500).json({ errorMessage : '서버에 에러가 발생했습니다.💥 '})
}
});
try { } 안에 에러가 발생하지 않았을때 실행할 코드를 다 넣고,
catch (error ) { }안에 에러가 발생했을 때 실행할 코드를 넣으면 된다.
joi로 인해서 걸러서 발생한 ValidationError라면 validation의 message가 뜨도록 했다. (기존엔 그냥 에러라고만 뜸)


const goods = await Goods.find().select('goodsName username saleStatus wroteDate').sort('-wroteDate').exec();필요한 필드명(
goodsName,username,saleStatus,wroteDate)을 띄어쓰기 하나로 구분하여 select로 저렇게 하면 결과는 아래와 같이 나온다.
_id는 항상 나오는 값이다.
특정 필드를 빼고 싶으면 -를 붙이면 된다.const goods = await Goods.find().select('-_id goodsName username saleStatus wroteDate').sort('-wroteDate').exec();
REST(REpresentational State Transfer)
: HHTP의 장점을 최대한 활용할 수 있는 아키텍처
REST의 기본 원칙을 지킨 서비스 디자인을"RESTfull"이라고 부른다.
REST API의 구성
- 자원 : 자원 (URI)
- 행위 : 자원에 대한 행위 (HTTP 요청 메소드)
- 표현 : 자원에 대한 행위의 구체적 내용 (페이로드)
REST API 설계원칙
(다 URI에 대한 내용)
- 소문자 사용
- 언더바(_) 대신 하이픈(-) 사용
- 마지막에 슬래시 포함 X
- 행위 포함 X
- 파일 확장자 URL 포함 X
- 자원에는 명사만 사용(컨트롤 자원을 의미하는 경우 예외적으로 동사 사용)
[참고자료]https://cocoon1787.tistory.com/540

서버를 키고 post를 하려고 하니 에러가 발생했다. vscode의 에러 내용을 확인해보니 아래와 같았다.

중복된 키값이 있다는건데...스키마에서 내가 잘못만들었나 확인해보고(이상없었음), 검색해보니 나와 같은 사람이 있었다.

mongoDB 프로젝트 폴더에 들어가 index코너로 가면 내가 만든 필드_1로 되어 있는게 있었다. 그게 세개나 있었는데 그걸 다 지웠다. 그랬더니 post가 잘 작동되었다!
[참고자료]
몽고구스 쿼리
https://www.zerocho.com/category/MongoDB/post/59bd148b1474c800194b695a
https://domdom.tistory.com/100
깃허브의 레포가 public 인 경우는 아래와 같이 사용하면 된다.
git clone 다음에 해당 레포지토리 주소를 갖고와서 넣고 엔터치고,
깃허브의 아이디와 비번을 쓰면 된다
git clone https://github.com/username/repo.git
Username: <token>
Password:
허나!! 레포가 private인 경우 username에 아이디를 치고, 비번에 토큰를 넣어도 안된다. 그럴때 이방법으로 해서 해결봤다. 아래와 같이 <token> 대신 깃허브에서 발급받은 token을 넣으면 된다.
git clone https://<token>:x-oauth-basic@github.com/owner/repo.git (@다음부터해당 레포 주소)
또는
git clone https://<token>@github.com/owner/repo.git
그럼 토큰은 어디서 발급받냐고?

setting -> (맨 밑)Developer settings -> tokens(classic)으로 들어가 토큰을 받는다!
[참고자료]
https://github.blog/2012-09-21-easier-builds-and-deployments-using-git-over-https-and-oauth/
https://griffinchoidayday.tistory.com/29
https://earth-95.tistory.com/192