Skip to content

굿즈가 필요한 사람과 그렇지 않은 사람을 연결하는 실시간 경매 플랫폼

Notifications You must be signed in to change notification settings

goods-ending/goodsending-be

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

❤️‍🩹 GOODSENDING

GoodsEnding은 굿즈가 필요한 사람과 그렇지 않은 사람을 연결하는 실시간 경매 플랫폼으로, 원하는 굿즈를 쉽고 빠르게 사고팔 수 있는 공간을 제공합니다.

🔗 시연 영상 🔗 굿즈엔딩 소개 브로셔

🔗 GitHub 링크 🔗 GOODSENDING-FE GITHUB 🔗 GOODSENDING-BE GITHUB

🔗 프로젝트 서비스 링크 (현재 운영 중단되었습니다.)

📌 프로젝트 소개

📅 프로젝트 기간 : 2024.07.19 ~ 2024.08.16

굿즈엔딩 목업

🧸 판매하고 싶은 상품을 경매에 등록해보세요!

💸 실시간으로 경매에 참여하여 원하는 물건을 구매해보세요!

⌨️ 경매 참여한 유저들과 채팅으로 소통해보아요!

GoodsEnding은 더 이상 필요 없어진 굿즈를 실시간 경매를 통해 구매하고 판매할 수 있는 플랫폼입니다. 사용자는 간편하게 상품을 등록하고, 다른 사용자와 실시간 채팅으로 소통하며 경매에 참여할 수 있습니다.

🧬 프로젝트 아키텍처

아키텍처

경매 플로우

유저플로우

한 판매자가 사용하지 않는 굿즈를 '굿즈 엔딩' 플랫폼에 판매를 등록하여 경매가 시작되면 다른 유저들은 마음에 드는 가격으로 입찰을 시도할 수 있습니다. 마지막 입찰 후 5분간 추가 입찰이 없으면 자동으로 낙찰자가 선정되고, 주문 및 배송 프로세스가 진행되어 거래가 종료됩니다.

🔨 기술 스택

Frontend Backend CI / CD
Language: JavaScript (ES6)
- Framework: React
- State Management: Redux
- Build Tool: AWS Amplify
- Package Manager: yarn
- UI Library: Tailwind CSS
- HTTP Client: Axios
- Formatting: Prettier
- Version Control: Git
Language: Java 17
- Framework: Spring Boot
- Build Tool: Gradle
- DB: MySQL, Redis
- JPA
- Auth: JWT
- Spring Security
- Docker
- Cloud Storage Service: AWS S3
- WebSocket
- Spring Scheduler
- Query DSL
- SMTP
- Swagger
Deploy:
- AWS EC2
- Docker
- GitHub Actions
- AWS ECR
Communication:
- Slack
- GitHub
- Notion

🗄 ERD

erd-goodsending

🚏 API 설계

유저 API

Method Endpoint Description Request & Response Example
PUT /api/members/login 로그인
Request Headers
Content-Type: application/json
Request Body
{
  "email": "string",
  "password": "string"
}
Response (cookie)
  Refresh_Token: {JWT Token}
Response (header)
  Access_Token: {JWT Token}
PUT /api/members/{memberId}/password 비밀번호 변경
Request Headers
Authorization: Bearer {JWT Token}
Content-Type: application/json
Path Variable
Name        Description
memberId * integer($int64) (path)
Request Body
{
  "currentPassword": "string",
  "password": "string",
  "confirmPassword": "string"
}
▪️ Response (200 OK)
PUT /api/members/{memberId}/cash 캐시 충전
Request Headers
Authorization: Bearer {JWT Token}
Content-Type: application/json
Path Variable
Name        Description
memberId * integer($int64) (path)
Request Body
{
  "cash": 0
}
▪️ Response (200 OK)
POST /api/members/tokenReissue Access Token 재발급
Request (cookie)
Refresh_Token: string
Response (header)
Access_Token: {JWT Token}
POST /api/members/signup 회원 가입
Request Headers
Content-Type: application/json
Request Body
{
  "email": "string",
  "password": "string",
  "confirmPassword": "string",
  "code": "string"
}
▪️ Response (200 OK)
GET /api/members/validateAccessToken Access Token 만료 여부 확인
▪️ Response (200 OK)
GET /api/members/info 회원 정보 조회
Request Headers
Authorization: Bearer {JWT Token}
Content-Type: application/json
Response Body
{
  "memberId": 0,
  "email": "string",
  "cash": 0,
  "point": 0,
  "role": "ADMIN"
}
DELETE /api/members/logout 로그아웃
Request (cookie)
Refresh_Token: string
▪️ Response (200 OK)

상품 API

Method Endpoint Description Request & Response Example
GET /api/products/{productId} 경매 상품 상세 정보 조회 - 상품 아이디를 통해 선택한 상품의 상세 정보를 조회할 수 있다.
Path Variable
Name        Description
productId * integer($int64) (path)
Response (200 OK)
{
  "productId": 0,
  "memberId": 0,
  "name": "string",
  "price": 0,
  "introduction": "string",
  "startDateTime": "2024-10-01T02:53:43.859Z",
  "maxEndDateTime": "2024-10-01T02:53:43.859Z",
  "dynamicEndDateTime": "2024-10-01T02:53:43.859Z",
  "remainingExpiration": {
    "seconds": 0,
    "zero": true,
    "nano": 0,
    "negative": true,
    "units": [
      {
        "durationEstimated": true,
        "timeBased": true,
        "dateBased": true
      }
    ]
  },
  "bidMaxPrice": 0,
  "finalPrice": 0,
  "biddingCount": 0,
  "bidderCount": 0,
  "likeCount": 0,
  "status": "ONGOING",
  "productImages": [
    {
      "productImageId": 0,
      "url": "string",
      "productId": 0
    }
  ]
}
PUT /api/products/{productId} 경매 상품 수정 - 상품 아이디를 통해 상품명, 상품 소개, 경매시작일, 경매 시간대를 수정할 수 있습니다.
Request Headers
Authorization: Bearer {JWT Token}
Content-Type: application/json
Path Variable
Name        Description
productId * integer($int64) (path)
Request Body
application/json
{
  "requestDto": {
    "name": "string",
    "introduction": "string",
    "startDate": "2024-10-01",
    "auctionTime": "AFTERNOON"
  },
  "productImages": [
    "string"
  ]
}
Response (200 OK)
{
  "productId": 0,
  "memberId": 0,
  "name": "string",
  "price": 0,
  "introduction": "string",
  "likeCount": 0,
  "startDateTime": "2024-10-01T02:53:43.831Z",
  "maxEndDateTime": "2024-10-01T02:53:43.831Z",
  "productImages": [
    {
      "productImageId": 0,
      "url": "string",
      "productId": 0
    }
  ]
}
DELETE /api/products/{productId} 경매 상품 삭제 - 상품 아이디와 회원 아이디로 상품을 삭제할 수 있습니다.
Request Headers
Authorization: Bearer {JWT Token}
Content-Type: application/json
Path Variable
Name        Description
productId * integer($int64) (path)
Response (200 OK)
No response body
GET /api/products 경매 상품 검색 - 전체 조회일 경우 조건없이, 내가 등록한 상품 목록 조회 시 memberId를, 필터링 검색의 경우 openProduct, closedProduct, keyword 조건으로 상품 목록을 조회한다.
Request Parameters
Name                    Description
memberId                integer($int64) (query)
openProduct             boolean (query)
closedProduct           boolean (query)
keyword                 string (query)
cursorStatus            string (query) - Available values : ONGOING, UPCOMING, ENDED
cursorStartDateTime    string($date-time) (query)
cursorId               integer($int64) (query)
size                   integer($int32) (query) - Default value: 15
Response (200 OK)
{
  "first": true,
  "last": true,
  "size": 0,
  "content": [
    {
      "productId": 0,
      "name": "string",
      "price": 0,
      "finalPrice": 0,
      "startDateTime": "2024-10-01T02:53:43.838Z",
      "dynamicEndDateTime": "2024-10-01T02:53:43.838Z",
      "maxEndDateTime": "2024-10-01T02:53:43.838Z",
      "status": "ONGOING",
      "thumbnailUrl": "string"
    }
  ],
  "number": 0,
  "sort": [
    {
      "direction": "string",
      "nullHandling": "string",
      "ascending": true,
      "property": "string",
      "ignoreCase": true
    }
  ],
  "numberOfElements": 0,
  "pageable": {
    "offset": 0,
    "sort": [
      {
        "direction": "string",
        "nullHandling": "string",
        "ascending": true,
        "property": "string",
        "ignoreCase": true
      }
    ],
    "pageSize": 0,
    "paged": true,
    "pageNumber": 0,
    "unpaged": true
  },
  "empty": true
}
POST /api/products 경매 상품 등록 - 상품명, 판매가, 상품소개, 경매시작일, 경매시간대, 상품 이미지를 입력하면 상품을 등록할 수 있다.
Request Headers
Authorization: Bearer {JWT Token}
Content-Type: application/json
Request Body
application/json
{
  "requestDto": {
    "name": "string",
    "price": 0,
    "introduction": "string",
    "startDate": "2024-10-01",
    "auctionTime": "AFTERNOON"
  },
  "productImages": [
    "string"
  ]
}
Response (200 OK)
{
  "productId": 0,
  "memberId": 0,
  "name": "string",
  "price": 0,
  "introduction": "string",
  "likeCount": 0,
  "startDateTime": "2024-10-01T02:53:43.846Z",
  "maxEndDate": "2024-10-01T02:53:43.846Z",
  "productImages": [
    {
      "productImageId": 0,
      "url": "string",
      "productId": 0
    }
  ]
}
GET /api/products/top5/bidderCount 경매 상품 입찰자 수 TOP5 조회 - 입찰자 수를 기준으로 상위 top5 상품을 조회합니다.
Response (200 OK)
[
  {
    "productId": 0,
    "name": "string",
    "price": 0,
    "startDateTime": "2024-10-01T02:53:43.853Z",
    "maxEndDateTime": "2024-10-01T02:53:43.853Z",
    "status": "ONGOING",
    "thumbnailUrl": "string"
  }
]

입찰 API

Method Endpoint Description Request & Response Example
GET /api/bids 멤버별 입찰 내역 리스트를 조회합니다. 본인의 입찰 내역 리스트만 조회할 수 있습니다.
Request Headers
Authorization: Bearer {JWT Token}
Request Parameters
Name        Description
memberId * integer($int64) (query)
cursorId    integer($int64) (query)
pageSize    integer($int32) (query)
            Default value: 15
Response (200 OK)
{
  "first": true,
  "last": true,
  "size": 0,
  "content": [
    {
      "bidId": 0,
      "bidPrice": 0,
      "usePoint": 0,
      "memberId": 0,
      "bidStatus": "SUCCESSFUL",
      "orderResponse": {
        "orderId": 0,
        "sellerId": 0,
        "receiverName": "string",
        "receiverCellNumber": "string",
        "receiverAddress": "string",
        "deliveryDateTime": "2024-10-01T02:40:04.476Z",
        "confirmedDateTime": "2024-10-01T02:40:04.476Z",
        "status": "CANCELLED"
      },
      "productSummaryDto": {
        "productId": 0,
        "name": "string",
        "price": 0,
        "finalPrice": 0,
        "startDateTime": "2024-10-01T02:40:04.476Z",
        "dynamicEndDateTime": "2024-10-01T02:40:04.476Z",
        "maxEndDateTime": "2024-10-01T02:40:04.476Z",
        "status": "ONGOING",
        "thumbnailUrl": "string"
      },
      "useCash": 0
    }
  ],
  "number": 0,
  "sort": [
    {
      "direction": "string",
      "nullHandling": "string",
      "ascending": true,
      "property": "string",
      "ignoreCase": true
    }
  ],
  "numberOfElements": 0,
  "pageable": {
    "offset": 0,
    "sort": [
      {
        "direction": "string",
        "nullHandling": "string",
        "ascending": true,
        "property": "string",
        "ignoreCase": true
      }
    ],
    "pageSize": 0,
    "paged": true,
    "pageNumber": 0,
    "unpaged": true
  },
  "empty": true
}
POST /api/bids 입찰 신청 - 유저가 캐시와 포인트를 사용하여 입찰합니다.
Request Headers
Authorization: Bearer {JWT Token}
Content-Type: application/json
Request Body
{
  "bidPrice": 0,
  "usePoint": 0,
  "productId": 0
}
Response (200 OK)
{
  "bidId": 0,
  "price": 0,
  "usePoint": 0,
  "memberId": 0,
  "productId": 0,
  "biddingCount": 0,
  "bidderCount": 0,
  "remainDuration": {
    "seconds": 0,
    "zero": true,
    "nano": 0,
    "negative": true,
    "units": [
      {
        "durationEstimated": true,
        "timeBased": true,
        "dateBased": true
      }
    ]
  }
}

주문 API

Method Endpoint Description Request & Response Example
PUT /api/orders/{orderId}/receiver-info 주문 상품 수신자 정보 업데이트 - 주문 상품의 수신자명, 수신자연락처, 수신자 주소를 업데이트합니다.
Request Headers
Authorization: Bearer {JWT Token}
Content-Type: application/json
Path Variable
Name        Description
orderId *  integer($int64) (path)
Request Body
application/json
{
  "receiverName": "string",
  "receiverCellNumber": "string",
  "receiverAddress": "string"
}
Response (200 OK)
{
  "orderId": 0,
  "bidderId": 0,
  "receiverName": "string",
  "receiverCellNumber": "string",
  "receiverAddress": "string"
}
PUT /api/orders/{orderId}/delivery 주문 배송 출발 처리 - 판매자가 주문을 배송 출발 처리합니다.
Request Headers
Authorization: Bearer {JWT Token}
Path Variable
Name        Description
orderId *  integer($int64) (path)
Response (200 OK)
{
  "orderId": 0,
  "sellerId": 0,
  "receiverName": "string",
  "receiverCellNumber": "string",
  "receiverAddress": "string",
  "deliveryDateTime": "2024-10-01T02:57:14.028Z",
  "status": "CANCELLED"
}
PUT /api/orders/{orderId}/confirm 수신자가 거래를 확정합니다 - 수신자가 배송을 받은 후 거래확정을 합니다.
Request Headers
Authorization: Bearer {JWT Token}
Path Variable
Name        Description
orderId *  integer($int64) (path)
Response (200 OK)
{
  "orderId": 0,
  "sellerId": 0,
  "receiverName": "string",
  "receiverCellNumber": "string",
  "receiverAddress": "string",
  "deliveryDateTime": "2024-10-01T02:57:14.030Z",
  "confirmedDateTime": "2024-10-01T02:57:14.030Z",
  "status": "CANCELLED"
}
GET /api/orders 멤버별 판매 주문 내역 리스트를 조회합니다 - 본인의 판매 주문 내역 리스트만 조회할 수 있습니다.
Request Headers
Authorization: Bearer {JWT Token}
Request Parameters
Name        Description
memberId    * integer($int64) (query)
cursorId    integer($int64) (query)
pageSize    integer($int32) (query) - Default value: 15
Response (200 OK)
 {
  "first": true,
  "last": true,
  "size": 0,
  "content": [
    {
      "productSummaryDto": {
        "productId": 0,
        "name": "string",
        "price": 0,
        "finalPrice": 0,
        "startDateTime": "2024-10-01T02:57:14.035Z",
        "dynamicEndDateTime": "2024-10-01T02:57:14.035Z",
        "maxEndDateTime": "2024-10-01T02:57:14.035Z",
        "status": "ONGOING",
        "thumbnailUrl": "string"
      },
      "orderResponse": {
        "orderId": 0,
        "sellerId": 0,
        "receiverName": "string",
        "receiverCellNumber": "string",
        "receiverAddress": "string",
        "deliveryDateTime": "2024-10-01T02:57:14.035Z",
        "confirmedDateTime": "2024-10-01T02:57:14.035Z",
        "status": "CANCELLED"
      }
    }
  ],
  "number": 0,
  "sort": [
    {
      "direction": "string",
      "nullHandling": "string",
      "ascending": true,
      "property": "string",
      "ignoreCase": true
    }
  ],
  "numberOfElements": 0,
  "pageable": {
    "offset": 0,
    "sort": [
      {
        "direction": "string",
        "nullHandling": "string",
        "ascending": true,
        "property": "string",
        "ignoreCase": true
      }
    ],
    "pageSize": 0,
    "paged": true,
    "pageNumber": 0,
    "unpaged": true
  },
  "empty": true
}

채팅 API

Method Endpoint Description Request & Response Example
GET /api/product-message-histories 상품별 메시지 내역 조회 - 상품별 메시지 내역을 커서 기반 페이징으로 조회합니다.
Request Parameters
Name        Description
productId * integer($int64) (query)
size       integer($int32) (query)
            Default value: 15
cursorId   integer($int64) (query)
Response (200 OK)
{
  "first": true,
  "last": true,
  "size": 0,
  "content": [
    {
      "id": 0,
      "memberId": 0,
      "productId": 0,
      "message": "string",
      "type": "BID",
      "createdDateTime": "2024-10-01T02:50:21.671Z"
    }
  ],
  "number": 0,
  "sort": [
    {
      "direction": "string",
      "nullHandling": "string",
      "ascending": true,
      "property": "string",
      "ignoreCase": true
    }
  ],
  "numberOfElements": 0,
  "pageable": {
    "offset": 0,
    "sort": [
      {
        "direction": "string",
        "nullHandling": "string",
        "ascending": true,
        "property": "string",
        "ignoreCase": true
      }
    ],
    "pageSize": 0,
    "paged": true,
    "pageNumber": 0,
    "unpaged": true
  },
  "empty": true
}
STOMP /app/message 채팅 메시지 전송
Request Headers
Authorization: Bearer {JWT Token}
Content-Type: application/json
Message Content (application/json)
{
  "productId": 97,
  "message": "채팅메시지",
  "type": "GENERAL_CHAT"
}
Response (application/json)
{
  "memberId": 7,
  "productId": 97,
  "message": "채팅메시지",
  "price": 0,
  "biddingCount": 0,
  "bidderCount": 0,
  "type": "GENERAL_CHAT"
}
STOMP /topic/products/{productId} 제품 관련 채팅 메시지 수신 구독
Path Variable
Name        Description
productId *  integer($int64) (path)
Response (application/json)
{
  "memberId": 7,
  "productId": 97,
  "message": "채팅메시지",
  "price": 0,
  "biddingCount": 0,
  "bidderCount": 0,
  "type": "GENERAL_CHAT"
}

메일 API

Method Endpoint Description Request & Response Example
POST /api/members/sendMail 인증코드 발송
Request (application/json)
{
  "email": "string"
}
Response (200 OK)
string
GET /api/members/checkCode 인증코드 확인
Request Parameters
Name          Description
email *      string (query)
code *       string (query)
Response
200 OK

찜 API

Method Endpoint Description Request & Response Example
GET /api/likes 찜한 상품 목록 조회 페이징
Request Headers
Authorization: Bearer {JWT Token}
Content-Type: application/json
Request Parameters
Name          Description
page *       integer($int32) (query)
size *       integer($int32) (query)
sortBy *     string (query)
isAsc *      boolean (query)
Response (200 OK)
{
  "totalPages": 0,
  "totalElements": 0,
  "first": true,
  "last": true,
  "size": 0,
  "content": [
    {
      "productId": 0,
      "productName": "string",
      "price": 0,
      "startDateTime": "2024-10-01T02:37:43.478Z",
      "maxEndDateTime": "2024-10-01T02:37:43.478Z",
      "thumbnailUrl": "string",
      "likeCount": 0
    }
  ],
  "number": 0,
  "sort": [
    {
      "direction": "string",
      "nullHandling": "string",
      "ascending": true,
      "property": "string",
      "ignoreCase": true
    }
  ],
  "numberOfElements": 0,
  "pageable": {
    "offset": 0,
    "sort": [
      {
        "direction": "string",
        "nullHandling": "string",
        "ascending": true,
        "property": "string",
        "ignoreCase": true
      }
    ],
    "pageSize": 0,
    "paged": true,
    "pageNumber": 0,
    "unpaged": true
  },
  "empty": true
}
POST /api/likes 찜하기 토글
Request Headers
Authorization: Bearer {JWT Token}
Content-Type: application/json
Request
{
  "productId": 0,
  "press": true
}
Response (200 OK)
{
  "code": 0,
  "status": "100 CONTINUE",
  "message": "ALREADY_LIKE"
}
GET /api/likes/redis 찜하기 수 top5 상품 조회 redis
Response (200 OK)
[
  {
    "productId": 0,
    "name": "string",
    "price": 0,
    "startDateTime": "2024-10-01T02:37:43.481Z",
    "maxEndDateTime": "2024-10-01T02:37:43.481Z",
    "status": "ONGOING",
    "thumbnailUrl": "string"
  }
]
POST /api/likes/redis 찜하기 수 top5 상품 등록 redis
Request Headers
Authorization: Bearer {JWT Token}
Content-Type: application/json
Request
{
  "productId": 0,
  "press": true
}
Response (200 OK)
{
  "code": 0,
  "status": "100 CONTINUE",
  "message": "ALREADY_LIKE"
}
GET /api/likes/top5 찜하기 수 top5 상품 조회
Response (200 OK)
[
  {
    "productId": 0,
    "name": "string",
    "price": 0,
    "startDateTime": "2024-10-01T02:37:43.484Z",
    "maxEndDateTime": "2024-10-01T02:37:43.484Z",
    "status": "ONGOING",
    "thumbnailUrl": "string"
  }
]

📢 Communication

이슈를 활용한 git branch strategy

  1. 템플릿에 기반한 이슈작성
  2. 이슈번호로 브랜치 - [ ex) feature/#이슈번호 or feature/dev-#이슈번호 ] 생성
  3. 해당 브랜치에서 커밋할때 이슈번호 붙여서 커밋메세지 작성 ex) [#이슈번호] : OO기능 개발
  4. push
  5. 기능별 브랜치에서 작업 후 이슈단위로 Pull Request 수행
  • Project 관리

    Project

  • 이슈 관리

    이슈

    기능추가 이슈템플릿
          ## 어떤 기능인가요?
    
          - 추가하려는 기능에 대해 간결하게 설명해주세요
    
          ## 작업 상세 내용
    
          - [ ] TODO
          - [ ] TODO
          - [ ] TODO
    
          ## 참고할만한 자료 - 선택 (첨부 파일, 스크린샷 등)
    
    버그수정 이슈템플릿
      ## 어떤 버그인가요?
    
      - 어떤 버그인지 간결하게 설명해주세요
    
      ## 어떤 상황에서 발생한 버그인가요?
    
      - (가능하면) Given-When-Then 형식으로 서술해주세요
    
      ## 예상 결과
      
      - 예상했던 정상적인 결과가 어떤 것이었는지 설명해주세요
    
      ## 참고할만한 자료 - 선택 (첨부 파일, 스크린샷 등)
    
  • branch 종류

    • dev
    • feature/#{이슈번호} : dev에 기능을 추가하기 위한 branch
  • PR 관리 PR

    • PR템플릿
       ## 어떤 버그인가요?
      
       - 어떤 버그인지 간결하게 설명해주세요
      
       ## 어떤 상황에서 발생한 버그인가요?
      
       - (가능하면) Given-When-Then 형식으로 서술해주세요
      
       ## 예상 결과
      
       - 예상했던 정상적인 결과가 어떤 것이었는지 설명해주세요
      
       ## 참고할만한 자료 - 선택 (첨부 파일, 스크린샷 등)
      
    • 코드 리뷰 코드리뷰

Commit convention

commit

{Gitmoji} [#{이슈번호}] Title

Body
Gitmoji
🎉 Begin a project: 프로젝트 시작
✨ Features: 새 기능
🐛 Fix a bug: 버그 수정
🚚 Move or Rename resources(files, paths, routes): 자원 이동 및 수정
🔥Remove code or files: 코드 or 파일 삭제
📝 Add or Update documentation: 문서 생성 및 수정
💡 Add or Update comments: 주석 생성 및 수정
✏️ Fix typos
✅ Test
🔊 Add or Update logs
➕ Add a dependency
➖ Remove a dependency
♻️ Refactor code
⚡️Improve performance

💡 주요 기능 소개

👥 [사용자] 회원 가입

💡 실제 해당 메일 계정의 소유 여부를 검증하기 위해 인증코드를 발급하고 확인

  • 1️⃣ SecureRandom.getInstanceStrong() 난수 생성 메서드
  • - 생성되는 숫자는 6자리 밖에 안되지만 강력한 보안성을 위해 사용

  • 2️⃣ 네이버 SMTP (이메일을 전송할 때 사용되는 표준 통신 프로토콜)
  • - 한국 사용자에게 최적화된 SMTP 서버 이며, 국내 환경에 맞는 보안 설정과 사용성이 장점인 네이버 SMTP를 선택

  • 3️⃣ Redis
  • - 인증코드는 단순하게 확인만 하면 되는 정보이므로 인메모리 데이터 저장 구조인 Redis를 사용해서 빠르게 읽을 수 있고, 자동으로 만료되는 TTL 기능도 추가하여 메모리 사용을 효율적으로 관리할 수 있도록 적용

  • 4️⃣ 사용자 정의 애너테이션
  • - 사용자 정의 애너테이션을 만들어 정규 표현식 기반으로 비밀번호를 검증하고, 암호화 하여 DB에 저장되도록 설정

🔐 [사용자] 로그인 / 로그아웃

💡 Security를 적용하여, 인증 시 Access Token과 Refresh Token 발급

  • 1️⃣ 사용자 정보를 통한 JWT 토큰을 발급하는 방식으로 구현
  • 2️⃣ JWT 인증 필터를 이용하여 자동적으로 토큰의 유효성 검사를 하도록 설정
  • 3️⃣ JWT Access Token을 생성할 때 Refresh Token을 같이 생성해 Redis에 저장하고, API를 호출하기 전에 토큰이 만료되었는지 검사 후 만료되었으면 Redis에 저장된 Refresh Token을 확인 해 유효할 시 Access Token을 재발급하는 방법으로 사용하여 보안성을 강화하면서도 인증을 다시 하지 않아도 되도록 편의성을 갖추도록 구현
  • 4️⃣ 로그아웃 경우 쿠키와 Redis에 저장되어 있는 Refresh Token은 삭제되고, Access Token은 남은 만료 시간 만큼 Redis에 저장되어 재사용이 불가능 하도록 구현
🪪 [사용자] 마이페이지

💡 로그인 한 회원은 자신이 보유한 캐시 와 포인트를 확인 할 수 있고, 비밀번호 변경 및 캐시 충전 가능

  • @AuthenticationPrincipal 역할을 하는 사용자 정의 애너테이션 @memberId생성 인증된 사용자의 memberId를 쉽게 추출할 수 있고, 유지 보수 용이성과 코드 중복을 줄이기 위해 애너테이션 @memberId를 생성하여 사용
  • 추가적으로 구현하고 싶은 기능

  • 결제 API 사용해서 가상 결제를 추가적으로 구현해보고 싶습니다

💰 캐시 및 포인트

💡 서비스 수익화 및 관리를 위한 캐시 및 포인트 서비스

  • 1️⃣ 보증금 회수 및 환불
  • - 무분별한 상품 등록을 막고자 보증금 서비스 도입

    - 상품을 등록시 판매자의 캐시에서 보증금 지불

    - 보증금 금액: 경매 최소 가격의 5%(최소 3000원)

    - 무분별한 상품 등록을 막고자 보증금 서비스 도입

  • 2️⃣ 포인트 적립
  • - 확정금액(=낙찰자의 입찰가)의 2.5%의 포인트가 낙찰자 포인트에 적립

  • 3️⃣ 수수료 징수
  • - 상품의 주문이 거래 완료 시 서비스는 확정 금액(=낙찰자의 최종 입찰가)의 5%를 수수료로 징수 후 남은 금액을 판매자에게 지급

  • 추가적으로 구현하고 싶은 기능

  • 수수료, 보증금을 관리하는 관리자 기능

📋 [주문] 낙찰된 주문 관리

💡 낙찰된 주문 관리[배송지 정보 입력 ➡️ 배송 처리 ➡️ 거래 확정]

    구현한 기능

  • 1️⃣ 낙찰자의 배송받을 배송지주소, 연락처, 수신자명 업데이트
  • 2️⃣ 판매자의 배송지가 입력된 주문 배송처리
  • 3️⃣ 낙찰자의 배송받은 주문 확정
  • - 판매자에게 보증금을 환불하고 수수료를 제외한 수익을 캐시로 입금

  • 추가적으로 구현하고 싶은 기능

  • 각각의 진행 상황에서 기한내로 다음 상태로 넘어가지 않으면 주문을 자동 처리하는 기능

✨ [주문] 낙찰자 선정 및 주문 진행

💡 5분동안 추가 입찰이 없을 경우 낙찰자가 선정되어 주문 진행

    구현한 기능

  • 1️⃣ Redis : key expiration event
  • - 입찰 시 키의 만료시간 5분으로 정해지고 해당 키가 만료되면 key expiration event 가 발생하여 낙찰자 선정 및 주문 생성 로직이 실행

  • 2️⃣ 낙찰자 선정
  • - 최대 입찰금을 지불한 마지막 입찰자가 낙찰자

  • 3️⃣ 환불 처리
  • - 낙찰자를 제외한 나머지 입찰자는 지불한 캐시 및 포인트 환불 처리

📊 [인기 순위] 경매 상품 인기 순위

💡 경매 상품의 인기 순위를 입찰자와 좋아요 수를 기준으로 조회. 경매 시간대에는 입찰자, 경매 시간대가 아닌 경우 좋아요 수를 기준으로 인기 순위를 조회

    구현한 기능

  • 1️⃣ Redis를 이용한 캐싱 처리
  • - 초반에는 MySQL에서 입찰자, 좋아요 순으로 정렬하여 상위 5개의 상품을 조회하였지만, Redis의 ZSet을 사용하여 검색 성능을 개선

  • 2️⃣ QueryDSL을 이용한 DTO 가져오기
  • - 처음에는 DTO를 가져오는 것이 아니라 key(ranking),value(ProductId)만 저장해서 ProductId로 상품 정보를 다시 DB에서 찾는 과정이 있었는데 과도한 MySQL 접근 대신 QueryDSL로 성능을 개선

💬 [채팅] 실시간 경매 상품 채팅

💡 실시간 상품에 대한 채팅 참여 및 입찰,낙찰 내역을 확인

  • 1️⃣ Web Socket
  • - 실시간 양방향 데이터 송수신을 위한 웹소켓 활용

    - 커스텀 핸들러를 사용하여 SEND 시 유저를 식별

  • 2️⃣ STOMP
  • - WebSocket에 대한 불필요한 구현을 줄여, 명확하고 쉽게 구현

  • 3️⃣ 메시지 내역 저장
  • - 유저의 채팅메시지, 입찰 및 낙찰 메시지를 DB에 저장

  • 추가적으로 구현하고 싶은 기능

  • 관리자의 차단 기능 - 무분별한 채팅을 하는 회원이 존재하면 상품의 판매자가 해당 회원을 차단하면 채팅이 불가능하게 막을 수 있는 기능

❤️ [경매 상품] 찜

💡 서비스 수익화 및 관리를 위한 캐시 및 포인트 서비스

  • 1️⃣ 보증금 회수 및 환불
  • - 무분별한 상품 등록을 막고자 보증금 서비스 도입

    - 상품을 등록시 판매자의 캐시에서 보증금 지불

    - 보증금 금액: 경매 최소 가격의 5%(최소 3000원)

    - 무분별한 상품 등록을 막고자 보증금 서비스 도입

  • 2️⃣ 포인트 적립
  • - 확정금액(=낙찰자의 입찰가)의 2.5%의 포인트가 낙찰자 포인트에 적립

  • 3️⃣ 수수료 징수
  • - 상품의 주문이 거래 완료 시 서비스는 확정 금액(=낙찰자의 최종 입찰가)의 5%를 수수료로 징수 후 남은 금액을 판매자에게 지급

  • 추가적으로 구현하고 싶은 기능

  • 수수료, 보증금을 관리하는 관리자 기능

🔍 [경매 상품] 검색 및 필터링 기능

💡 사용자가 경매 상품을 검색할 때, 원하는 상품에 쉽고 빠르게 접근할 수 있는 검색 기능

  • 1️⃣ QueryDSL을 사용한 검색 기능
  • - 무분별한 상품 등록을 막고자 보증금 서비스 도입

    - 상품을 등록시 판매자의 캐시에서 보증금 지불

    - 보증금 금액: 경매 최소 가격의 5%(최소 3000원)

    - 무분별한 상품 등록을 막고자 보증금 서비스 도입

  • 2️⃣ 커서 기반 페이지네이션
  • - 확정금액(=낙찰자의 입찰가)의 2.5%의 포인트가 낙찰자 포인트에 적립

  • 3️⃣ 경매 진행 상품, 경매 진행 될 상품, 경매 종료 된 상품 순으로 사용자의 접근성을 고려하여 정렬
  • 4️⃣ 키워드, 경매 진행 상품 & 경매 진행 될 상품, 경매 종료 된 상품을 조건으로 원하는 상품에 빠르게 접근할 수 있도록 검색 결과를 제공
  • 추가적으로 구현하고 싶은 기능

  • MySQL Ngram을 적용하여 검색 성능을 향상하거나, 대용량 데이터 처리가 필요할 경우를 대비하여 Elastic Search를 도입하여 대용량 인덱스를 관리하고 싶습니다.

🧨 트러블 슈팅

🧨 QueryDSL을 통한 커서 페이징(no offset) 검색 최적화

❓문제 상황

커서 기반 페이지네이션 구현 시 cursor 판별을 위한 조회가 3회 발생

설정해주어야 할 경우의 수가 많아 코드 복잡성 증가

  • 원인
  • - 경매 상품의 상태(경매 진행 중, 경매 예정, 경매 종료)를 분류를 startDateTime과 maxEndDateTime로 판별

    - 상품 조회 마다 상태를 판별하기 위해 다수의 db 조회 발생

  • 해결 방법
  • - 경매 상태를 나타내는 ProductStatus 를 생성하여 경매 상태의 우선순위를 설정

    - Cursor를 status, startDateTime, id로 설정하여 설정한 정렬 기준에 맞게 조회

    - 리팩토링 전후 성능 테스트(사용자 수 1000명, 1번씩 요청)를 비교하였을 때, 오류 발생 비율 약 39.5% 감소

🧨 Redis Key Event Notification 기반 자동 경매 낙찰 ➡️ 기술 블로그

❓문제 상황

요구 사항 변경으로 인해, 마지막 입찰 5분 후 자동 낙찰자 선정되는 로직이 필요
➡ 단일 MySQL 데이터베이스 서버로는 실시간 입찰 데이터 처리와 만료 기반 자동 낙찰 로직을 효율적으로 처리하기 어려움이 존재

  • 변경된 요구사항
    1. 입찰 방식 변경: (비공개 입찰 → 실시간 공개 입찰 방식) 입찰가는 실시간으로 공개되며, 새로운 입찰자는 현재 최고 입찰가보다 높은 금액으로만 입찰 가능
    2. 입찰 마감 시간 갱신: 새로운 입찰이 발생할 때마다 해당 입찰 시간으로부터 5분 후로 입찰 마감 시간이 갱신(마감 시간은 최대 설정된 마감 시간을 초과하지 않음)
    3. 자동 낙찰자 선정: 마지막 입찰 이후 5분 동안 추가 입찰이 없을 경우, 자동으로 낙찰자 선정
  • 문제 및 원인
    1. 지속적인 스케줄러와 폴링의 비효율성: 실시간 공개 입찰로 전환하면서 시스템은 지속적인 데이터베이스 조회가 필요하게 되어, MySQL에 과도한 부하를 발생
    2. 타이밍 정확성의 문제: 지속적인 스케줄러만으로는 정확한 낙찰자 선정이 어려움
  • 해결 방법
    1. Web Socket을 활용한 입찰가 실시간 공개
    2. Redis Key Event 알람을 도입하여 입찰 데이터의 실시간 처리와 자동 낙찰을 구현
    3. 만료 설정: Redis의 만료 기능으로 5분 후 데이터가 자동으로 만료되게 설정
    4. 낙찰자 선정: Redis Key Event Notification 기술을 활용하여 만료된 데이터에 대해 자동 낙찰자를 선정하는 로직을 실행
  • 결과
    1. 기존 스케줄러 방식 대비 51.14% cpu usage 감소
🧨 프로파일 설정

❓문제 상황

Rapplication.yaml 을 복사하여 만든 application-cr.yaml 이 연결되지 않는 문제 발생

  • 원인
  • - SpringBoot 2.4 버전부터는 각 환경에 대한 application-**.yaml 파일을 생성해야 함

    - 또한, application-**.yaml 파일 내부에서 spring.profiles.active 를 사용할 수 없음

  • 해결 방법
  • - spring.profiles.active 관련 코드를 삭제

🧨 CORS(Cross Origin Resource Sharing)

❓문제 상황

백엔드 서버와 프론트 엔드 서버 연동 작업 중 API 요청 시 CORS 에러 발생

  • 원인
  • - CORS 설정을 추가해 주지 않아 발생

  • 해결 방법
  • - CORS 설정을 정의하고, SecurityFilterChain 메서드에 추가

    - setAllowedOrigins , setAllowedMethods , setExposedHeaders 속성을 적용할 때 와일드카드(“*”) 는 사용할 수 없으므로 명시적으로 지정

🧨 쿠키 속성 SameSite 설정

❓문제 상황

쿠키에 저장된 Refresh Token을 서버로 가져올 때, Postman에서는 성공하지만 프론트엔드에서 API 호출 시 Refresh Token이 전달되지 않아 null이 되면서 401 오류가 발생하는 문제

  • 원인
  • - SameSite를 설정해주지 않아 기본값인 Lax로 설정

  • 해결 방법
  • - SameSite 설정을 적용할 수 있는 ResponseCookie 사용

    - 속성 값은 None으로 적용

🧨 인기상품 Top 5의 상품 수정 문제

❓문제 상황

Top 5 상품의 정보를 조회할 때, 상품이 수정되면 Redis에 등록된 Top 5 상품 정보도 변경되어야 함 그래야 실시간 Top 5 상품정보가 조회될 수 있음

  • 원인
  • - Redis에 상품의 정보를 입력 해두고, 해당 product 엔티티의 정보 변경이 발생했을 때, Redis의 정보는 업데이트가 되지 않는 현상이 발생, 변경된 정보가 기존의 Redis에 등록된 정보와 다르기 때문에 오류 발생

  • 해결 방법
  • - productId 를 기준으로 변경 할 해당 DTO를 DB (queryDSL)에서 찾아서, 먼저 Redis ZSET 에서 삭제

    - 그 후 해당 엔티티 product의 정보를 업데이트하고, 해당 product 의 정보를 가진 Dto 를 생성함,

    - 그 후 생성한 productDto(변경된 정보)를 Redis ZSET에 다시 추가해서 업데이트

🧨 토큰 검증 예외처리

❓문제 상황

토큰 검증시 발생하는 여러가지 예외처리 부분에 CustomException을 적용해 주었지만 제대로 출력되지 않는 문제 발생

  • 원인
  • - JwtAuthenticationEntryPoint 클래스의 commence 메서드가 먼저 호출 되면서 CustomException이 아닌 commence 메서드에서 작성된 JSON 형식의 응답만 반환

  • 해결 방법
  • - try-catch로 감싸준 다음 CustomException 예외 발생 시 jwtAuthenticationEntryPoint 클래스의 AuthenticationException 예외로 변환하여 에러 메시지가 반환 되도록 적용

🧨 Cannot invoke "java.lang.Long.longValue()" because "current" is null with 낙관적락

❓문제 상황

트랜잭션 커밋이 정상적으로 되지 않는 상황

  • 원인
  • - 낙관적 락을 도입하면서 Entity 필드에 추가한 version 때문

    - 이전에 데이터를 프로시저를 통해 넣어주고 있었는데 version에 대한 값은 넣어주지 않음

    - version이 null로 들어가기 때문에 오류가 발생

  • 해결 방법
  • - version을 모두 0으로 직접 초기화 해주니 정상작동

🧨 배포한 서버 api 접근 불가

❓문제 상황

EC2 인스턴스에서 Spring Boot 서버를 실행하고 있는데, API에 접근할 수 없는 문제가 발생

  • 원인
  • - EC2 인스턴스의 보안 그룹에서 8080 포트가 허용되지 않았기 때문에 발생한 것으로 확인

    - 이로 인해 외부에서 EC2 인스턴스의 Spring Boot 서버에 접근이 불가능

  • 해결 방법
  • - 보안 그룹의 인바운드 규칙에 8080포트 번호를 허용해주어 해결

🧨 상속받은 클래스에 @EqualsAndHashCode 사용

❓문제 상황

상속받은 클래스에 @EqualsAndHashCode 사용했을 때 경고가 발생

  • 원인
  • - @EqualsAndHashCode(callSuper = true) 어노테이션을 붙여주지 않으면, 부모 클래스의 필드는 제외하고 EqualsAndHashCode를 생성해서 발생하는 warning

  • 해결 방법
  • - 자식 클래스의 필드만을 포함하고 싶기 때문에 @EqualsAndHashCode(callSuper = false)를 붙여 warning를 해결

👨🏻‍💻👩🏻‍💻 팀원 구성

이름 분담 GitHub
FE 류진식(Vice Leader) 로그인, 회원가입, websoket을 활용한 실시간 채팅, 상품 조회, 검색, 상세내역 보기, 경매 등록 상품 관리 https://github.com/ryujinsik
FE 정은주 상품 등록기능, 회원정보 조회, 캐시 충전, 비밀번호 변경, 마이페이지 https://github.com/25809637410
BE 박지은(Leader) CI/CD, 입찰, 낙찰, 주문 기능, Web실시간 채팅 https://github.com/je-pa
BE 김채린 경매 상품 CRUD, 경매 상품 목록 필터링 조회, Scheduler를 사용한 경매 상품 상태 변경, 입찰자 기준 상품 인기순위 Top5 조회 https://github.com/puclpu
BE 배근우 찜 하기, 찜 취소, 찜 리스트, 찜 인기순위 Top5 https://github.com/zz6331300zz
BE 이아람 로그인, 회원가입, 토큰 관리, 캐시 충전, 회원정보 조회 https://github.com/ramleeramlee

About

굿즈가 필요한 사람과 그렇지 않은 사람을 연결하는 실시간 경매 플랫폼

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 5

Languages