- 유저 이메일로 로그인&회원가입, jwt 토큰 발급
- 게시글 CRUD (게시글 생성, 수정, 삭제&복원, 검색)
⇒ 위에 명시된 기능을 제외한 다른 기능은 개인적으로 추가 정의하여 구현한 기능이며, 혼자 진행한 프로젝트입니다.
2022.07.20 ~ 2022.07.22(필수 기능 구현 기간) + α(추가 정의 구현 기간)
- express-validator를 통한 회원가입 입력값 검증
- 빈 값 확인
- 이메일ID 사용
- 비밀번호 3가지조합 8자리이상
- 이름 특수문자 입력 체크 - sql injection, xss 방어
- jwt를 이용한 로그인 구현
- 회원가입시 입력한 이메일로 메일을 보내 실제 본인 이메일이 맞는지 인증
- 접속한 적 없는 ip, os, device, browser로 접속 시 확인 메일 보내 검증
- 비밀번호 찾기 - 가입한 메일로 임시 비밀번호 생성해 발송
- 유저 검색 기능 - userId으로 검색 -> 이메일, 이름, 팔로우&팔로워수, 게시글 수, 게시글 목록
- 팔로잉 동작 기능
- 팔로우, 팔로워 목록 확인
- 회원정보 조회
- 회원정보 수정
- 좋아요 누른 게시글 조회
- 제목, 내용, 해시태그(option, 여러 개)를 입력받아 게시글 작성
- express-validator를 통한 게시글 입력값 검증
- 빈 값 확인
- 제목&내용 글자수 체크
- 모든 입력값 특수문자 입력 체크 - sql injection, xss 방어
- 작성한 게시글 수정
- 작성한 게시글 삭제
- 삭제한 게시글 복원
- 게시글 상세보기
- 게시글 상세보기를 하면 조회수 증가
- 사용자가 게시글 조회한 정보를 기록해 조회수는 사용자 당 1회만 증가하도록 구현
- 게시글에 좋아요 누르기
- 게시글에 좋아요를 눌렀던 적이 있으면 좋아요 취소
- 게시글 목록 및 검색
- 제목으로 검색 - 검색어 (띄어쓰기, 대소문자 구별 없이 검색되도록 구현)
- 작성일, 조회수, 좋아요 수로 정렬 (default: 작성일)
- 오름차순/내림차순으로 정렬할 것인지 선택 (default: desc)
- Filtering - 해당 해시태그를 가진 게시글만 보도록
- Pagination - 페이지 당 몇 개의 게시글을 볼 것인지 선택 (default: 10개)
- 팔로잉한 사람의 아직 읽지 않은 새로운 게시글 목록
- 게시글 댓글 달기
- 댓글 작성
- 댓글 수정
- 댓글 삭제
- 댓글 좋아요/좋아요 취소
- 게시글 댓글 목록
- winston을 통한 info, warn, error 로그 남기기
- docker를 사용한 환경 구축
- error나면 슬랙에 알림이 울리도록 구현
- 서버가 운영 중 멈추는 일이 없도록 모든 기능에 대한 예외처리
- DM 기능 구현 (1:1 채팅)
- swagger
users유저 정보 저장 테이블posts게시글 저장 테이블hashtags해시태그 저장 테이블postLogs게시글 본 기록 저장 테이블postLikes게시글 좋아요 기록 저장 테이블accessLogs유저 웹 접근 기록 저장 테이블followLogs팔로우 기록 저장 테이블comments게시글 댓글 저장 테이블commentLikes게시글 좋아요 기록 저장 테이블
이메일 본인 인증: 6자리 랜덤 인증 번호 저장 (expire: 180으로 3분간 저장)
key: { userId }
value: { 6자리 랜덤 인증 번호 }
비밀번호 찾기 기능: 임시 비밀번호 저장 (expire: 180으로 3분간 저장)
key: { userId }
value: { 임시 발급된 비밀번호(3가지 조합 8자리 이상) }
⇒ 두 기능이 같은 key를 사용하는데 이메일 본인 인증은 회원가입하는 경우 사용되고 비밀번호 찾기 기능은 보통 회원가입하고 한참 뒤 비밀번호를 잊어버렸을 경우 사용되기 때문에 두 기능이 3분 안에 값이 덮어씌워질 확률가 적다고 판단하여 같은 key를 사용하였다.
⇒ 또한 회원가입이 완료되고 3분안에 바로 비밀번호 찾기를 하는 경우, 회원가입에 쓰인 이메일 본인 인증 번호는 이미 인증을 완료하였기 때문에 더 이상 필요없어져 비밀번호 찾기를 할 때 임시 비밀번호로 값이 덮어씌워지더라고 문제가 없다 판단하였다.
POST /user/joinEmailConfirm
request {
"userId": "hwjddussls@naver.com"
}
response {
"message": "이메인 본인 확인 메일을 보냈습니다. 확인해주세요."
}
POST /user/joinRandomNumberConfirm
✔︎ 아이디 이메일 형식 확인
✔︎ 인증번호 6자리 숫자 확인
request {
"userId": "hwjddussls@naver.com",
"randomNum": "564534"
}
- 받은 인증번호가 redis에 저장된 인증번호와 동일한 경우
response {
"message": "인증번호가 확인되었습니다.",
"EmailConfirm": 1
}
- 받은 인증번호가 redis에 저장된 인증번호와 다른 경우
response {
"message": "인증번호가 올바르지 않습니다.",
"EmailConfirm": 0
}
- 시간초과(180초)로 redis에 인증번호가 없는 경우
response {
"message": "인증번호가 만료되었습니다.",
"EmailConfirm": 0
}
POST /user/join
✔︎ userId 이메일 형식 확인
✔︎ userPw 3가지 조합 8자 이상 확인
✔︎ userName 특수문자 제한 확인
✔︎ emailConfirm 이메일 본인 인증을 했는지 여부 (0: 인증X, 1: 인증O)
request {
"userId": "qwer1234@naver.com",
"userPw": "Qwer1234!",
"confirmPw": "Qwer1234!",
"userName": "골골",
"emailConfirm": 1
}
response {
"message": "회원가입 되었습니다."
}
POST /user/login
request {
"userId": "qwer1234@naver.com",
"userPw": "Qwer1234!"
}
response {
"message": "로그인 되었습니다.",
"token": token
}
사용자가 받은 메일에서 "본인이 맞습니까?"라는 물음에 예 또는 아니오 버튼을 누르면 서버로 오는 요청
GET /user/loginConfirm?answer=2&userId=qwer1234@naver.com&id=1
✔︎ 본인이 로그인한 것이 맞다고 응답한 경우 -> 해당 기록 인증 처리
GET /user/loginConfirm?answer=3&userId=qwer1234@naver.com&id=1
✔︎ 본인이 로그인한 것이 아니라고 응답한 경우 -> 접속 제한 처리
GET /user/findPw
request {
"userId": "qwer1234@naver.com"
}
- 없는 사용자인 경우
request {
"message": "해당 이메일로 가입된 적이 없습니다."
}
- 있는 사용자인 경우
request {
"message": "해당 메일로 임시 비밀번호를 발급했습니다. 해당 비밀번호는 3분간 유효하며 해당 비밀번호로 로그인하고 꼭 비밀번호를 수정하시기 바랍니다."
}
GET /user/search
request {
"search": "qwer12345@naver.com"
}
- 없는 유저인 경우
response {
"message": "해당 유저가 없습니다."
}
- 유저가 작성한 게시글이 없는 경우
response {
"message": "hwjddussls@naver.com으로 사용자 검색하였습니다.",
"userId": "hwjddussls@naver.com",
"userName": "골골",
"followers": 0,
"followings": 0,
"posts": 2,
"postList": "작성한 게시글이 없습니다."
}
- 게시글이 있는 경우
response {
"message": "hwjddussls@naver.com으로 사용자 검색하였습니다.",
"userId": "hwjddussls@naver.com",
"userName": "골골",
"followers": 0,
"followings": 0,
"posts": 2,
"postList": [
{
"id": 1,
"title": "강릉 여행",
"content": "너무 재미있었다.",
"likes": 0,
"views": 0,
"createdAt": "2022-08-08 03:31:56",
"updatedAt": "2022-08-08 03:31:56",
"hashtags.hashtags": "#맛집,#강릉,#카페,#주말"
},
{
"id": 2,
"title": "부산 여행",
"content": "너무 재미있었다.",
"likes": 0,
"views": 0,
"createdAt": "2022-08-08 03:33:11",
"updatedAt": "2022-08-08 03:33:11",
"hashtags.hashtags": "#맛집,#카페"
}
]
}
POST /user/follow
request {
"follow": "qwer1234@naver.com"
}
- 없는 user를 팔로우 하려는 경우
response {
"message": "없는 유저입니다.",
}
- 나를 팔로우 하려는 경우
response {
"message": "나를 팔로우할 수 없습니다.",
}
- 팔로우한 적 없는 경우
response {
"message": "팔로우 되었습니다.",
}
- 팔로우한 적 있는 경우
response {
"message": "팔로우 취소 되었습니다.",
}
GET /user/followingList
request {
"target": "qwer1234@naver.com"
}
- 팔로잉 목록이 없는 경우
response {
"message": "팔로잉 리스트가 없습니다.",
}
- 팔로잉 목록이 있는 경우
response {
"message": "팔로잉 리스트를 조회했습니다.",
"followingList": [
{
"userId": "hwjddussls@naver.com"
}
]
}
GET /user/followerList
request {
"target": "qwer1234@naver.com"
}
- 팔로우 목록이 없는 경우
response {
"message": "팔로우 리스트가 없습니다.",
}
- 팔로우 목록이 있는 경우
response {
"message": "팔로우 리스트를 조회했습니다.",
"followerList": [
{
"userId": "hwjddussls@naver.com"
}
]
}
GET /user/myInfo
response {
"message": "사용자 정보를 조회했습니다.",
"userId": "qwer1234@naver.com",
"userName": "골골",
"followers": 0,
"followings": 0
}
PUT /user/
request {
"userPw": "Qwer1234!",
"confirmPw": "Qwer1234!",
"userName": "골골이"
}
response {
"message": "사용자 정보를 수정했습니다.",
}
GET /user/likeList
response {
"message": "좋아요 누른 게시글을 조회했습니다.",
"myLikeList": [
{
"id": 9,
"title": "속초여행",
"content": "너무 즐거웠던 주말 속초 여행",
"likes": 1,
"views": 0,
"createdAt": "2022-07-21 06:03:49",
"updatedAt": "2022-07-21 06:03:49",
"hashtags.hashtags": "#맛집,#속초,#카페,#주말"
}
]
}
POST /post/
request {
"title": "속초여행",
"content": "너무 즐거웠던 주말 속초 여행",
"hashtags": "#맛집,#속초,#카페,#주말"
}
response {
"message": "포스팅 되었습니다."
}
PUT /post/:postId
✔︎ 작성자만 수정가능
request {
"title": "강릉여행",
"content": "너무 즐거웠던 주말 강릉 여행",
"hashtags": "#맛집,#강릉,#여행,#주말"
}
response {
"message": "포스팅 되었습니다."
}
PATCH /post/:postId
✔︎ 작성자만 삭제가능
response {
"message": "게시글이 삭제되었습니다. 삭제된 게시글은 복원할 수 있습니다."
}
GET /post/:postId
✔︎ 게시글 조회시 사용자 당 조회수 1회만 증가
response {
"message": "상세정보가 조회되었습니다.",
"detailInfo": {
"id": 4,
"userId": "qwer1234@naver.com",
"title": "강릉여행",
"content": "너무 즐거웠던 주말 강릉 여행",
"likes": 3,
"views": 10,
"createdAt": "2022-07-20 11:01:02",
"updatedAt": "2022-07-21 06:06:14",
"hashtags.hashtags": "#맛집,#강릉,#카페,#주말"
}
}
GET /post/deletedList
- 삭제된 게시물이 있을 경우
response {
"message": "삭제된 게시물을 조회했습니다.",
"deletedListInfo": [
{
"id": 8,
"title": "아무거나",
"content": "아무렇게나",
"likes": 1,
"views": 1,
"createdAt": "2022-07-24 03:43:49",
"updatedAt": "2022-07-24 09:06:11",
"hashtags.hashtags": "#대강"
}
]
}
- 삭제된 게시물이 없을 경우
response {
"message": "삭제된 게시물이 없습니다."
}
PATCH /post/restore/:postId
response {
"message": "게시글이 복원되었습니다."
}
PATCH /post/like/:postId
- 해당 게시글에 좋아요를 누른 적이 없는 경우
response {
"message": "해당 게시글에 좋아요를 표시했습니다."
}
- 이미 좋아요를 누른 게시글인 경우
response {
"message": "해당 게시글에 좋아요를 취소했습니다."
}
GET /post/list?search=여행&sort=views&orderBy=desc&hashtags=맛집,카페&perPage=5&page=1
- 검색결과가 있을 때
response {
"message": "게시글 목록을 조회했습니다.",
"filter": {
"search": "여행",
"sort": "views",
"orderBy": "desc",
"hashtags": "맛집,카페",
"perPage": 5,
"page": 1
},
"listInfo": [
{
"id": 4,
"userId": "qwer1234@naver.com",
"title": "강릉여행",
"content": "너무 즐거웠던 주말 강릉 여행",
"likes": 3,
"views": 10,
"createdAt": "2022-07-20 11:01:02",
"updatedAt": "2022-07-21 06:07:15",
"hashtags.hashtags": "#맛집,#강릉,#카페,#주말"
},
{
"id": 5,
"userId": "qwer1234@naver.com",
"title": "부산 여행",
"content": "너무 재미있었다.",
"likes": 0,
"views": 0,
"createdAt": "2022-07-21 09:28:38",
"updatedAt": "2022-07-21 09:28:38",
"hashtags.hashtags": "#맛집,#카페"
},
{
"id": 9,
"userId": "qwer1234@naver.com",
"title": "속초여행",
"content": "너무 즐거웠던 주말 속초 여행",
"likes": 0,
"views": 0,
"createdAt": "2022-07-21 06:03:49",
"updatedAt": "2022-07-21 06:03:49",
"hashtags.hashtags": "#맛집,#속초,#카페,#주말"
}
...
]
}
- 검색 결과가 없을 때
response {
"message": "게시글이 없습니다.",
"filter": {
"search": "여행",
"sort": "views",
"orderBy": "desc",
"hashtags": "맛집,카페,서울",
"perPage": 5,
"page": 1
}
}
GET /post/newList
- 읽지 않은 게시글이 있는 경우
response {
"message": "새로운 게시글이 있습니다.",
"newPostList": [
{
"id": 2,
"userId": "hwjddussls@naver.com",
"title": "해운대 여행",
"content": "너무 재미있었다.",
"likes": 0,
"views": 0,
"createdAt": "2022-08-08 03:33:11",
"updatedAt": "2022-08-09 12:57:57",
"hashtags.hashtags": "#바다,#해운대"
}
]
}
- 읽지 않은 게시글이 없는 경우
response {
"message": "새로운 게시글을 모두 읽었습니다.",
}
POST /post/comment/:postId
request {
"comment": "오오!",
}
response {
"message": "게시글에 댓글을 달았습니다."
}
✔︎ 작성자만 수정가능
PATCH /post/comment/:commentId
request {
"comment": "여행 좋아보여요!",
}
response {
"message": "댓글을 수정했습니다."
}
✔︎ 작성자만 삭제가능
DELETE /post/comment/:commentId
response {
"message": "댓글이 삭제되었습니다."
}
PATCH /post/comment/like/:commentId
- 해당 댓글(id: 1)에 좋아요를 누른 적이 없는 경우
response {
"message": "댓글에 좋아요를 표시했습니다."
}
- 이미 좋아요를 누른 댓글인 경우
response {
"message": "댓글에 좋아요를 취소했습니다."
}
GET /post/comment/:postId
- 게시글에 댓글이 있는 경우
response {
"message": "게시글 댓글 목록을 조회했습니다.",
"commentList": [
{
"id": 1,
"comment": "여행 좋아보여요!",
"likes": 1,
"createdAt": "2022-08-09T03:15:46.000Z",
"updatedAt": "2022-08-09T04:44:02.000Z",
"userId": "qwer1234@naver.com",
"postId": 1
}
...
]
}
- 게시글에 댓글이 없는 경우
response {
"message": "게시글에 댓글이 없습니다.",
}
200 : 성공
201 : 생성
400 : 입력값 형식이 맞지 않을 경우
401 : 인증토큰 문제
403 : 권한 없는 사용자가 수정, 삭제, 복원을 원할 경우
404 : 잘못된 경우, 요청 parameter가 없는 경우일 때
409 : 아이디 충돌
422 : 아이디나 비밀번호가 맞지 않을 경우
500 : 서버 에러 try catch
상세한 개발과정을 볼 수 있습니다.



