- 테스케이스 어떻게 만들지? - Jest를 이용한 테스트 환경만들기
- MySQL을 직접 불러오지 않고 테스트를 할수 없을까? - Joi: schema validation을 적용시켜보자
- 정규표현식에 알맞는 데이터를 확인하려면 어떻게 할까? - 입력데이터가 정규표현식에 알맞는 데이터인지 확인하기
- raw데이터에서는 take()과 limit()이 적용되지 않아 pagination이 아닌 전체데이터가 리스폰스로 나와버렸다! 어떻게 해결해야되지? - pagination 구현과정과 리팩토링
- (고도화) Axios를 활용하여 OpenAPI 로부터 현재 날씨 데이터를 파싱하기
- Environment : Node.js
- Framework : NestJS
- Language : Typescript
- DB : MySQL
- ORM : TypeORM
- Unit-Test: Jest
- Etc: Github Action
- 이슈브랜치 자동생성
- Node.js 푸시후 특정브랜치에서의 자동빌드 (node 14, 16, 18 version)
- Axios & OpenAPI
Jest를 활용한 유닛테스트 케이스작성을 위한 환경세팅과 테스트코드 작성이 낯설고 어려웠습니다.
service.spec만 테스트하지 않으며, Repository단을 분리시켜 service.spec 테스트케이스 개선 및 controller.sepc 도 같이 테스트를 진행 할 예정입니다.
유저(Users) : 게시글(Posts) 관계를 1:N 으로 했습니다.
런타임 실행
$ npm run start
패키지 설치
$ npm install
URL | Method | 기능설명 |
---|---|---|
/api/posts | POST | 게시글 등록 |
/api/posts | GET | 게시글 조회 |
/api/posts/:postId | GET | 게시글 상세조회 |
/api/posts/:postId | PATCH | 게시글 수정 |
/api/posts/:postId | DELETE | 게시글 삭제 |
/api/posts/weather/today | GET | (테스트) 오늘 날씨상태 확인 |
조건
- 제목(최대 20자 제한)
- 본문(최대 200지 제한)
- 제목, 본문 모두 이모지 포함.
- URL:
[POST] localhost:3000/api/posts
- 응답데이터에 오늘 날씨 상태(weather) 추가
- Request
{
"title": "공개글",
"content": "공개글 테스트 본문",
"postType": "공개글"
}
- Response - StatusCode : 201
{
"identifiers": [
{
"postId": 20,
"userId": 2
}
],
"generatedMaps": [
{
"postId": 20,
"postType": "공개글",
"postPassword": null,
"userId": 2,
"dateColumns": {
"createdAt": "2022-11-07T00:47:16.000Z",
"updatedAt": "2022-11-07T00:47:16.080Z",
"deleteAt": null
}
}
],
"raw": {
"fieldCount": 0,
"affectedRows": 1,
"insertId": 20,
"info": "",
"serverStatus": 2,
"warningStatus": 0
}
}
- Request
{
"title": "공개글1dddddddddddddd1111111",
"content": "공개글 테스트 본문 입니다다다다 🎃🤖 입니다",
"postType": "공개글",
"postPassword": "abcdefg111yy1"
}
- Response
{
"statusCode": 400,
"message": ["title must be shorter than or equal to 20 characters"],
"error": "Bad Request"
}
- Request
{
"title": "공개글",
"content": "공개글 테스트 본문",
"postType": "자유게시판"
}
- Response
{
"statusCode": 400,
"message": ["postType must be a valid enum value"],
"error": "Bad Request"
}
- Request
{
"title": "",
"content": "공개글 테스트 본문",
"postType": "공개글"
}
- Response
{
"statusCode": 400,
"message": [
"title must be longer than or equal to 1 characters",
"title should not be empty"
],
"error": "Bad Request"
}
URL :
[PATCH] /api/posts/:postId
URL:
[PATCH] /api/posts/12
- Request
{
"title": "오늘도 화이팅",
"content": "화이팅~ 너무 무리하지 말아요~ 우리 모두 화이팅!"
}
- Response
{
"generatedMaps": [],
"raw": [],
"affected": 1
}
- Request
{
"title": "월요일입니다 헬요일입니다 으아아아악ㅇㅇㅇㅇㅇㅇㅇㅇㅇ",
"content": "월요일 입니다... 우리 모두 화이팅!!!!화이티이이이잉"
}
- Response
{
"statusCode": 400,
"message": ["title must be shorter than or equal to 20 characters"],
"error": "Bad Request"
}
- Request
{
"title": "",
"content": "월요일 입니다... 우리 모두 화이팅!!!!화이티이이이잉"
}
- Response
{
"statusCode": 400,
"message": ["title must be longer than or equal to 1 characters"],
"error": "Bad Request"
}
- Request
{
"title": "월요일",
"content": "화창한 월요일 아침입니다... 우리 모두 화이팅!"
}
- Response
{
"statusCode": 401,
"message": "접근권한이 없습니다.",
"error": "Unauthorized"
}
Soft Delete 방식으로 삭제했습니다.
URL :
/api/posts/:postId
URL :
localhost:3000/api/posts/17
-
Request
-
Response
{
"generatedMaps": [],
"raw": [],
"affected": 1
}
조건
- 비밀번호 설정 (6자이상, 숫자 1개이상 반드시 포함)
- 비밀번호 입력후 수정, 삭제 가능
- 비밀번호는 데이터베이스에 암호화된 형태로 저장
URL:
/api/posts/
- 오늘 날씨 상태 추가
- Request
{
"title": "비밀글",
"content": "비밀글 테스트 본문 입니다다다다 🎃🤖 입니다",
"postType": "비밀글",
"postPassword": "(비밀번호)"
}
- Response
{
"identifiers": [
{
"postId": 23,
"userId": 2
}
],
"generatedMaps": [
{
"postId": 23,
"postType": "비밀글",
"postPassword": "(암호화된값)",
"userId": 2,
"dateColumns": {
"createdAt": "2022-11-07T00:47:37.000Z",
"updatedAt": "2022-11-07T00:47:37.965Z",
"deleteAt": null
}
}
],
"raw": {
"fieldCount": 0,
"affectedRows": 1,
"insertId": 23,
"info": "",
"serverStatus": 2,
"warningStatus": 0
}
}
- Request
{
"title": "비밀글1",
"content": "비밀글 테스트 본문 입니다다다다 🎃🤖 입니다",
"postType": "비밀글",
"postPassword": "ayy1"
}
- Response
{
"statusCode": 400,
"success": false,
"timestamp": "2022-11-06T15:02:39.355Z",
"path": "/api/posts/",
"message": "비밀번호는 6자이상이며 최소 한개의 숫자가 있어야합니다."
}
- Request(Body)
{
"title": "비밀글 타이틀",
"content": "비밀글 테스트 본문 입니다다다다 🎃🤖 입니다",
"postType": "비밀글",
"postPassword": "abcdefghij"
}
- Response
{
"statusCode": 400,
"message": "비밀번호는 6자리 이상이며, 숫자는 최소 1개가 필요합니다.",
"error": "Bad Request"
}
URL :
[PATCH] /api/posts/:postId
- Request(Body)
{
"title": "비밀글 수정",
"content": "비밀글 수정했습니다",
"postPassword": "bank11brothers"
}
- Response
{
"generatedMaps": [],
"raw": [],
"affected": 1
}
- Request(Body)
{
"title": "3비밀글 수3정",
"content": "비밀글 수정했습니다",
"postPassword": "bank2brothers"
}
- Response
{
"statusCode": 400,
"message": "비밀번호가 일치하지 않습니다.",
"error": "Bad Request"
}
- Request(Body)
{
"title": "",
"content": "비밀글 수정했습니다",
"postPassword": "bank11brothers"
}
- Response
{
"statusCode": 400,
"message": ["title must be longer than or equal to 1 characters"],
"error": "Bad Request"
}
- Request(Body)
{
"title": "비밀글 수정",
"content": "비밀글 수정했습니다2222",
"postPassword": "bank11brothers"
}
- Response
{
"statusCode": 401,
"message": "접근권한이 없습니다.",
"error": "Unauthorized"
}
URL :
[DELETE] /api/posts/:postId
- Request(Body)
{
"postPassword": "bank11brothers"
}
- Response
{
"generatedMaps": [],
"raw": [],
"affected": 1
}
작성자가 아닌 계정으로 삭제할 때
- Response
{
"statusCode": 401,
"message": "접근권한이 없습니다.",
"error": "Unauthorized"
}
비밀번호가 일치하지 않을 때
- Request(Body)
{
"postPassword": "bank11"
}
- Response
{
"statusCode": 400,
"message": "비밀번호가 일치하지 않습니다.",
"error": "Bad Request"
}
사용자가 앱이나 웹에서 스크롤을 내릴 때마다 오래된 글들이 계속 로드되는 형태
- 게시글이 중복으로 나타나면 안됨.
- 추가 로드는 20개 단위
- URL :
[GET] /api/posts/
요청 URL :
http://localhost:3000/api/posts/
- Response
{
"list": [
{
"postId": 22,
"title": "비밀글",
"userId": 2,
"name": "Sheryl Schamberger"
},
{
"postId": 23,
"title": "비밀글",
"userId": 2,
"name": "Sheryl Schamberger"
},
{
"postId": 21,
"title": "비밀글",
"userId": 2,
"name": "Sheryl Schamberger"
},
{
"postId": 20,
"title": "공개글",
"userId": 2,
"name": "Sheryl Schamberger"
},
{
"postId": 18,
"title": "공개글",
"userId": 2,
"name": "Sheryl Schamberger"
},
{
"postId": 19,
"title": "공개글",
"userId": 2,
"name": "Sheryl Schamberger"
},
{
"postId": 16,
"title": "공개글",
"userId": 2,
"name": "Sheryl Schamberger"
},
{
"postId": 17,
"title": "공개글",
"userId": 2,
"name": "Sheryl Schamberger"
},
{
"postId": 15,
"title": "공개글",
"userId": 2,
"name": "Sheryl Schamberger"
},
{
"postId": 13,
"title": "공개글",
"userId": 2,
"name": "Sheryl Schamberger"
},
{
"postId": 14,
"title": "공개글",
"userId": 2,
"name": "Sheryl Schamberger"
},
{
"postId": 11,
"title": "공개글",
"userId": 1,
"name": "Carl Rath"
},
{
"postId": 12,
"title": "오늘도 화이팅",
"userId": 1,
"name": "Carl Rath"
},
{
"postId": 9,
"title": "공개글",
"userId": 1,
"name": "Carl Rath"
},
{
"postId": 10,
"title": "공개글",
"userId": 1,
"name": "Carl Rath"
},
{
"postId": 8,
"title": "공개글",
"userId": 1,
"name": "Carl Rath"
},
{
"postId": 7,
"title": "공개글",
"userId": 1,
"name": "Carl Rath"
},
{
"postId": 6,
"title": "공개글",
"userId": 1,
"name": "Carl Rath"
},
{
"postId": 4,
"title": "월요일",
"userId": 1,
"name": "Carl Rath"
},
{
"postId": 3,
"title": "비2밀글22",
"userId": 2,
"name": "Sheryl Schamberger"
}
],
"page": 1,
"pageSize": 20
}
-
Request :
http://localhost:3000/api/posts/?page=2&pageSize=10
-
Response
{
"list": [
{
"postId": 14,
"title": "공개글",
"userId": 2,
"name": "Sheryl Schamberger"
},
{
"postId": 11,
"title": "공개글",
"userId": 1,
"name": "Carl Rath"
},
{
"postId": 12,
"title": "오늘도 화이팅",
"userId": 1,
"name": "Carl Rath"
},
{
"postId": 9,
"title": "공개글",
"userId": 1,
"name": "Carl Rath"
},
{
"postId": 10,
"title": "공개글",
"userId": 1,
"name": "Carl Rath"
},
{
"postId": 8,
"title": "공개글",
"userId": 1,
"name": "Carl Rath"
},
{
"postId": 7,
"title": "공개글",
"userId": 1,
"name": "Carl Rath"
},
{
"postId": 6,
"title": "공개글",
"userId": 1,
"name": "Carl Rath"
},
{
"postId": 4,
"title": "월요일",
"userId": 1,
"name": "Carl Rath"
},
{
"postId": 3,
"title": "비2밀글22",
"userId": 2,
"name": "Sheryl Schamberger"
}
],
"page": 2,
"pageSize": 10
}
- 이전에 작성한 데이터들은 날씨상테(weather) 컬럼 값이 null 입니다.
-
Request :
http://localhost:3000/api/posts/1
-
Response
{
"postId": 1,
"postType": "공개글",
"title": "공개글2",
"content": "공개글 테스트 본문2",
"userId": 1,
"name": "Carl Rath"
}
- Request :
http://localhost:3000/api/posts/2
{
"postPassword": "bank11brothers"
}
- Response
{
"postId": 2,
"postType": "비밀글",
"title": "비밀글 수정",
"content": "비밀글 수정했습니다",
"userId": 1,
"name": "Carl Rath"
}
URL :
localhost:3000/api/posts/2
- Request
{
"postPassword": "bank2brothers"
}
- Response
{
"statusCode": 400,
"message": "비밀번호가 일치하지 않습니다.",
"error": "Bad Request"
}