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 |
| Method | Endpoint | Description | Request & Response Example |
|---|---|---|---|
| PUT | /api/members/login | 로그인 |
Request HeadersContent-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 HeadersAuthorization: Bearer {JWT Token}
Content-Type: application/json
Path VariableName Description memberId * integer($int64) (path) Request Body{
"currentPassword": "string",
"password": "string",
"confirmPassword": "string"
}
▪️ Response (200 OK)
|
| PUT | /api/members/{memberId}/cash | 캐시 충전 |
Request HeadersAuthorization: Bearer {JWT Token}
Content-Type: application/json
Path VariableName 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 HeadersContent-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 HeadersAuthorization: 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)
|
| Method | Endpoint | Description | Request & Response Example |
|---|---|---|---|
| GET | /api/products/{productId} | 경매 상품 상세 정보 조회 - 상품 아이디를 통해 선택한 상품의 상세 정보를 조회할 수 있다. |
Path VariableName 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 HeadersAuthorization: Bearer {JWT Token}
Content-Type: application/json
Path VariableName Description productId * integer($int64) (path) Request Bodyapplication/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 HeadersAuthorization: Bearer {JWT Token}
Content-Type: application/json
Path VariableName Description productId * integer($int64) (path) Response (200 OK)No response body |
| GET | /api/products | 경매 상품 검색 - 전체 조회일 경우 조건없이, 내가 등록한 상품 목록 조회 시 memberId를, 필터링 검색의 경우 openProduct, closedProduct, keyword 조건으로 상품 목록을 조회한다. |
Request ParametersName 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 HeadersAuthorization: Bearer {JWT Token}
Content-Type: application/json
Request Bodyapplication/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"
}
]
|
| Method | Endpoint | Description | Request & Response Example |
|---|---|---|---|
| GET | /api/bids | 멤버별 입찰 내역 리스트를 조회합니다. 본인의 입찰 내역 리스트만 조회할 수 있습니다. |
Request HeadersAuthorization: Bearer {JWT Token}
Request ParametersName 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 HeadersAuthorization: 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
}
]
}
}
|
| Method | Endpoint | Description | Request & Response Example |
|---|---|---|---|
| PUT | /api/orders/{orderId}/receiver-info | 주문 상품 수신자 정보 업데이트 - 주문 상품의 수신자명, 수신자연락처, 수신자 주소를 업데이트합니다. |
Request HeadersAuthorization: Bearer {JWT Token}
Content-Type: application/json
Path VariableName Description orderId * integer($int64) (path) Request Bodyapplication/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 HeadersAuthorization: Bearer {JWT Token}
Path VariableName 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 HeadersAuthorization: Bearer {JWT Token}
Path VariableName 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 HeadersAuthorization: Bearer {JWT Token}
Request ParametersName 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
}
|
| Method | Endpoint | Description | Request & Response Example |
|---|---|---|---|
| GET | /api/product-message-histories | 상품별 메시지 내역 조회 - 상품별 메시지 내역을 커서 기반 페이징으로 조회합니다. |
Request ParametersName 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 HeadersAuthorization: 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 VariableName Description productId * integer($int64) (path) Response (application/json){
"memberId": 7,
"productId": 97,
"message": "채팅메시지",
"price": 0,
"biddingCount": 0,
"bidderCount": 0,
"type": "GENERAL_CHAT"
}
|
| Method | Endpoint | Description | Request & Response Example |
|---|---|---|---|
| POST | /api/members/sendMail | 인증코드 발송 |
Request (application/json){
"email": "string"
}
Response (200 OK)string |
| GET | /api/members/checkCode | 인증코드 확인 |
Request ParametersName Description email * string (query) code * string (query) Response200 OK |
| Method | Endpoint | Description | Request & Response Example |
|---|---|---|---|
| GET | /api/likes | 찜한 상품 목록 조회 페이징 |
Request HeadersAuthorization: Bearer {JWT Token}
Content-Type: application/json
Request ParametersName 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 HeadersAuthorization: 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 HeadersAuthorization: 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"
}
]
|
- 템플릿에 기반한 이슈작성
- 이슈번호로 브랜치 - [ ex) feature/#이슈번호 or feature/dev-#이슈번호 ] 생성
- 해당 브랜치에서 커밋할때 이슈번호 붙여서 커밋메세지 작성 ex) [#이슈번호] : OO기능 개발
- push
- 기능별 브랜치에서 작업 후 이슈단위로 Pull Request 수행
-
Project 관리
-
이슈 관리
기능추가 이슈템플릿
## 어떤 기능인가요? - 추가하려는 기능에 대해 간결하게 설명해주세요 ## 작업 상세 내용 - [ ] TODO - [ ] TODO - [ ] TODO ## 참고할만한 자료 - 선택 (첨부 파일, 스크린샷 등)버그수정 이슈템플릿
## 어떤 버그인가요? - 어떤 버그인지 간결하게 설명해주세요 ## 어떤 상황에서 발생한 버그인가요? - (가능하면) Given-When-Then 형식으로 서술해주세요 ## 예상 결과 - 예상했던 정상적인 결과가 어떤 것이었는지 설명해주세요 ## 참고할만한 자료 - 선택 (첨부 파일, 스크린샷 등) -
branch 종류
- dev
- feature/#{이슈번호} : dev에 기능을 추가하기 위한 branch
-
-
PR템플릿
## 어떤 버그인가요? - 어떤 버그인지 간결하게 설명해주세요 ## 어떤 상황에서 발생한 버그인가요? - (가능하면) Given-When-Then 형식으로 서술해주세요 ## 예상 결과 - 예상했던 정상적인 결과가 어떤 것이었는지 설명해주세요 ## 참고할만한 자료 - 선택 (첨부 파일, 스크린샷 등)
-
{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() 난수 생성 메서드
- 2️⃣ 네이버 SMTP (이메일을 전송할 때 사용되는 표준 통신 프로토콜)
- 3️⃣ Redis
- 4️⃣ 사용자 정의 애너테이션
- 생성되는 숫자는 6자리 밖에 안되지만 강력한 보안성을 위해 사용
- 한국 사용자에게 최적화된 SMTP 서버 이며, 국내 환경에 맞는 보안 설정과 사용성이 장점인 네이버 SMTP를 선택
- 인증코드는 단순하게 확인만 하면 되는 정보이므로 인메모리 데이터 저장 구조인 Redis를 사용해서 빠르게 읽을 수 있고, 자동으로 만료되는 TTL 기능도 추가하여 메모리 사용을 효율적으로 관리할 수 있도록 적용
- 사용자 정의 애너테이션을 만들어 정규 표현식 기반으로 비밀번호를 검증하고, 암호화 하여 DB에 저장되도록 설정
🔐 [사용자] 로그인 / 로그아웃
- 1️⃣ 사용자 정보를 통한 JWT 토큰을 발급하는 방식으로 구현
- 2️⃣ JWT 인증 필터를 이용하여 자동적으로 토큰의 유효성 검사를 하도록 설정
- 3️⃣ JWT Access Token을 생성할 때 Refresh Token을 같이 생성해 Redis에 저장하고, API를 호출하기 전에 토큰이 만료되었는지 검사 후 만료되었으면 Redis에 저장된 Refresh Token을 확인 해 유효할 시 Access Token을 재발급하는 방법으로 사용하여 보안성을 강화하면서도 인증을 다시 하지 않아도 되도록 편의성을 갖추도록 구현
- 4️⃣ 로그아웃 경우 쿠키와 Redis에 저장되어 있는 Refresh Token은 삭제되고, Access Token은 남은 만료 시간 만큼 Redis에 저장되어 재사용이 불가능 하도록 구현
🪪 [사용자] 마이페이지
💰 캐시 및 포인트
📋 [주문] 낙찰된 주문 관리
✨ [주문] 낙찰자 선정 및 주문 진행
📊 [인기 순위] 경매 상품 인기 순위
💬 [채팅] 실시간 경매 상품 채팅
❤️ [경매 상품] 찜
🔍 [경매 상품] 검색 및 필터링 기능
- 1️⃣ QueryDSL을 사용한 검색 기능
- 2️⃣ 커서 기반 페이지네이션
- 3️⃣ 경매 진행 상품, 경매 진행 될 상품, 경매 종료 된 상품 순으로 사용자의 접근성을 고려하여 정렬
- 4️⃣ 키워드, 경매 진행 상품 & 경매 진행 될 상품, 경매 종료 된 상품을 조건으로 원하는 상품에 빠르게 접근할 수 있도록 검색 결과를 제공
- 무분별한 상품 등록을 막고자 보증금 서비스 도입
- 상품을 등록시 판매자의 캐시에서 보증금 지불
- 보증금 금액: 경매 최소 가격의 5%(최소 3000원)
- 무분별한 상품 등록을 막고자 보증금 서비스 도입
- 확정금액(=낙찰자의 입찰가)의 2.5%의 포인트가 낙찰자 포인트에 적립
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 데이터베이스 서버로는 실시간 입찰 데이터 처리와 만료 기반 자동 낙찰 로직을 효율적으로 처리하기 어려움이 존재
- 변경된 요구사항
- 입찰 방식 변경: (비공개 입찰 → 실시간 공개 입찰 방식) 입찰가는 실시간으로 공개되며, 새로운 입찰자는 현재 최고 입찰가보다 높은 금액으로만 입찰 가능
- 입찰 마감 시간 갱신: 새로운 입찰이 발생할 때마다 해당 입찰 시간으로부터 5분 후로 입찰 마감 시간이 갱신(마감 시간은 최대 설정된 마감 시간을 초과하지 않음)
- 자동 낙찰자 선정: 마지막 입찰 이후 5분 동안 추가 입찰이 없을 경우, 자동으로 낙찰자 선정
- 문제 및 원인
- 지속적인 스케줄러와 폴링의 비효율성: 실시간 공개 입찰로 전환하면서 시스템은 지속적인 데이터베이스 조회가 필요하게 되어, MySQL에 과도한 부하를 발생
- 타이밍 정확성의 문제: 지속적인 스케줄러만으로는 정확한 낙찰자 선정이 어려움
- 해결 방법
- 결과
- 기존 스케줄러 방식 대비 51.14% cpu usage 감소
🧨 프로파일 설정
🧨 CORS(Cross Origin Resource Sharing)
🧨 쿠키 속성 SameSite 설정
🧨 인기상품 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 낙관적락
🧨 배포한 서버 api 접근 불가
🧨 상속받은 클래스에 @EqualsAndHashCode 사용
| 이름 | 분담 | 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 |

















