diff --git a/README.md b/README.md index 9a2c5dd0..4e2f2c9a 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,30 @@ [ERD](https://www.erdcloud.com/d/ZadArGCaQXFcxZuu8) ### 모듈 구조 #### api -- api-member -- api-event -- api-payment +- api-member +회원 도메인 +- api-event +공연 도메인 +- api-booking +결제 도메인 +#### batch +스프링 배치 모듈 #### core -- core-domain -- core-infra +- core-domain +JPA 엔티티, 리포지토리 +- core-infra +queryDsl, RDB 설정 파일 +- core-infra-es +elastic search 설정 파일, document, searchRepository +- core-security +spring security 설정 파일 +## 실행 방법 +1. git clone +2. RDB, 레디스 실행 ```docker-compose up -d``` +3. api-event 모듈로 이동 ```cd /api/api-event``` +4. 엘라스틱 서치 도커 이미지 빌드 ```docker build -t el:0.1 -f ./Dockerfile .``` +5. ELK 스택 실행 ```docker-compose up -d``` +6. api-booking, api-event, api-member 각 모듈에서 스프링 어플리케이션 실행 +## 테스트 방법 +- 통합 http 테스트는 /http/bingterpark.http에 있습니다. +- 어드민 플로우, 유저 플로우 http 코드를 위에서부터 하나씩 실행하시면 됩니다. diff --git a/api/api-booking/http/booking.http b/api/api-booking/http/booking.http index 31676658..d0a525ae 100644 --- a/api/api-booking/http/booking.http +++ b/api/api-booking/http/booking.http @@ -6,7 +6,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwic3ViIjoiam9obi5kb2VAZXhhb POST http://localhost:8082/api/v1/bookings/enter-queue Content-Type: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJpYXQiOjE3MDQ2OTE0OTEsImV4cCI6MTgwNDY4MzI5MSwiYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn0.wFNSz2uwRa35jP1KihNlTOewVLgMMeg3ADQ5Kztl3QQ -Booking-Session-Id: {세션아이디} +Booking-Session-Id: {sessionId} { "eventId": 1 @@ -15,14 +15,14 @@ Booking-Session-Id: {세션아이디} ### 대기열 조회 GET http://localhost:8082/api/v1/bookings/order-in-queue?eventId=1 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJpYXQiOjE3MDQ2OTE0OTEsImV4cCI6MTgwNDY4MzI5MSwiYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn0.wFNSz2uwRa35jP1KihNlTOewVLgMMeg3ADQ5Kztl3QQ -Booking-Session-Id: {세션아이디} +Booking-Session-Id: {sessionId} ### 예매 토큰 발급 POST http://localhost:8082/api/v1/bookings/issue-token Content-Type: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJpYXQiOjE3MDQ2OTE0OTEsImV4cCI6MTgwNDY4MzI5MSwiYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn0.wFNSz2uwRa35jP1KihNlTOewVLgMMeg3ADQ5Kztl3QQ -Booking-Session-Id: {세션아이디} +Booking-Session-Id: {sessionId} { "eventId": 1 @@ -32,7 +32,7 @@ Booking-Session-Id: {세션아이디} POST http://localhost:8082/api/v1/bookings/exit-queue Content-Type: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJpYXQiOjE3MDQ2OTE0OTEsImV4cCI6MTgwNDY4MzI5MSwiYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn0.wFNSz2uwRa35jP1KihNlTOewVLgMMeg3ADQ5Kztl3QQ -Booking-Session-Id: {세션아이디} +Booking-Session-Id: {sessionId} { "eventId": 1 @@ -43,8 +43,28 @@ GET http://localhost:8082/api/v1/seats?eventTimeId=1 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJpYXQiOjE3MDQ2OTE0OTEsImV4cCI6MTgwNDY4MzI5MSwiYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn0.wFNSz2uwRa35jP1KihNlTOewVLgMMeg3ADQ5Kztl3QQ Booking-Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJib29raW5nIiwiaWF0IjoxNzA0NzA0NTY0LCJleHAiOjQyMDAxNzA0NzA0NTY0LCJzZXNzaW9uSWQiOiJ7Pz8_Pz99In0.iKZaud5vfvsMzXmQzl1WaCweL9GL00U0iGzCg4_p3Zzei8Y7z19Ff_TTpxh9gLeB +### 좌석 선택 +POST http://localhost:8082/api/v1/seats/1/select +Content-Type: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJpYXQiOjE3MDQ2OTE0OTEsImV4cCI6MTgwNDY4MzI5MSwiYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn0.wFNSz2uwRa35jP1KihNlTOewVLgMMeg3ADQ5Kztl3QQ +Booking-Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJib29raW5nIiwiaWF0IjoxNzA0NzA0NTY0LCJleHAiOjQyMDAxNzA0NzA0NTY0LCJzZXNzaW9uSWQiOiJ7Pz8_Pz99In0.iKZaud5vfvsMzXmQzl1WaCweL9GL00U0iGzCg4_p3Zzei8Y7z19Ff_TTpxh9gLeB + +{ + "seatId": 1 +} + +### 좌석 선택 해제 +POST http://localhost:8082/api/v1/seats/1/deselect +Content-Type: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJpYXQiOjE3MDQ2OTE0OTEsImV4cCI6MTgwNDY4MzI5MSwiYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn0.wFNSz2uwRa35jP1KihNlTOewVLgMMeg3ADQ5Kztl3QQ +Booking-Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJib29raW5nIiwiaWF0IjoxNzA0NzA0NTY0LCJleHAiOjQyMDAxNzA0NzA0NTY0LCJzZXNzaW9uSWQiOiJ7Pz8_Pz99In0.iKZaud5vfvsMzXmQzl1WaCweL9GL00U0iGzCg4_p3Zzei8Y7z19Ff_TTpxh9gLeB + +{ + "seatId": 1 +} + ### 예매 ~ 결제 (브라우저에서 진행해 주세요) -GET http://localhost:8082/bookings/create +GET http://localhost:8082/bookings ### [Optional] 예매 이탈 POST http://localhost:8082/api/v1/bookings/bookingCreateTestId/exit @@ -52,7 +72,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwic3ViIjoiam9obi5kb2VAZXhhb Booking-Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJib29raW5nIiwiaWF0IjoxNzA0NzA0NTY0LCJleHAiOjQyMDAxNzA0NzA0NTY0LCJzZXNzaW9uSWQiOiJ7Pz8_Pz99In0.iKZaud5vfvsMzXmQzl1WaCweL9GL00U0iGzCg4_p3Zzei8Y7z19Ff_TTpxh9gLeB ### 예매 취소 (브라우저에서 생성된 예매번호로 진행해 주세요) -POST http://localhost:8082/api/v1/bookings/1704704631741/cancel +POST http://localhost:8082/api/v1/bookings/{bookingId}/cancel Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJpYXQiOjE3MDQ2OTE0OTEsImV4cCI6MTgwNDY4MzI5MSwiYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn0.wFNSz2uwRa35jP1KihNlTOewVLgMMeg3ADQ5Kztl3QQ Content-Type: application/json diff --git a/api/api-booking/src/main/java/com/pgms/apibooking/domain/seat/service/SeatService.java b/api/api-booking/src/main/java/com/pgms/apibooking/domain/seat/service/SeatService.java index a91a3dd5..a3575bb1 100644 --- a/api/api-booking/src/main/java/com/pgms/apibooking/domain/seat/service/SeatService.java +++ b/api/api-booking/src/main/java/com/pgms/apibooking/domain/seat/service/SeatService.java @@ -40,6 +40,12 @@ public List getSeats(SeatsGetRequest request) { public void selectSeat(Long seatId, Long memberId) { if (seatLockService.isSeatLocked(seatId)) { + Optional selectorIdOpt = seatLockService.getSelectorId(seatId); + + if (selectorIdOpt.isPresent() && selectorIdOpt.get().equals(memberId)) { + return; + } + throw new BookingException(BookingErrorCode.SEAT_BEING_BOOKED); } @@ -56,11 +62,11 @@ public void selectSeat(Long seatId, Long memberId) { public void deselectSeat(Long seatId, Long memberId) { Optional selectorIdOpt = seatLockService.getSelectorId(seatId); - if(selectorIdOpt.isEmpty()) { + if (selectorIdOpt.isEmpty()) { updateSeatStatusToAvailable(seatId); - throw new BookingException(BookingErrorCode.SEAT_SELECTION_EXPIRED); + return; } - + if (!selectorIdOpt.get().equals(memberId)) { throw new BookingException(BookingErrorCode.SEAT_SELECTED_BY_ANOTHER_MEMBER); } diff --git a/api/api-booking/src/main/resources/application.yml b/api/api-booking/src/main/resources/application.yml index 690e3380..ac93ac9d 100644 --- a/api/api-booking/src/main/resources/application.yml +++ b/api/api-booking/src/main/resources/application.yml @@ -14,7 +14,7 @@ spring: payment: toss: test-client-api-key: test_ck_Gv6LjeKD8aE1pdWDXXNw8wYxAdXy - test-secret-api-key: test # 노출되면 안됨!! 임시값 넣어놓음 + test-secret-api-key: test_sk_eqRGgYO1r5MNel9067jarQnN2Eya success-url: https://localhost:8080/api/v1/success fail-url: https://localhost:8080/api/v1/fail diff --git a/api/api-booking/src/main/resources/templates/booking_input.html b/api/api-booking/src/main/resources/templates/booking_input.html index d00b5cfb..f449fbad 100644 --- a/api/api-booking/src/main/resources/templates/booking_input.html +++ b/api/api-booking/src/main/resources/templates/booking_input.html @@ -121,10 +121,10 @@

주문 정보 입력

$.ajax({ type: "POST", - url: "/api/v1/bookings", // 실제 서버 엔드포인트에 맞게 수정 + url: "/api/v1/bookings/create", // 실제 서버 엔드포인트에 맞게 수정 contentType: "application/json", headers: { - // "Authorization": "Bearer " + "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJpYXQiOjE3MDQ2OTE0OTEsImV4cCI6MTgwNDY4MzI5MSwiYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn0.wFNSz2uwRa35jP1KihNlTOewVLgMMeg3ADQ5Kztl3QQ", + "Authorization": "Bearer " + "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJpYXQiOjE3MDQ2OTE0OTEsImV4cCI6MTgwNDY4MzI5MSwiYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn0.wFNSz2uwRa35jP1KihNlTOewVLgMMeg3ADQ5Kztl3QQ", "Booking-Authorization": "Bearer " + "eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJib29raW5nIiwiaWF0IjoxNzA0NzA0NTY0LCJleHAiOjQyMDAxNzA0NzA0NTY0LCJzZXNzaW9uSWQiOiJ7Pz8_Pz99In0.iKZaud5vfvsMzXmQzl1WaCweL9GL00U0iGzCg4_p3Zzei8Y7z19Ff_TTpxh9gLeB" }, data: JSON.stringify(orderData), diff --git a/api/api-event/http/event-search.http b/api/api-event/http/event-search.http index 4e1e2287..0283825c 100644 --- a/api/api-event/http/event-search.http +++ b/api/api-event/http/event-search.http @@ -1,5 +1,5 @@ ### 공연 키워드 검색 -GET http://localhost:8080/api/v1/events/search/keyword?page=1&size=10&keyword=드라큘라&startedAt=2023-01-01T12:00:00 +GET http://localhost:8080/api/v1/events/search/keyword?page=1&size=10&keyword=틱 &startedAt=2023-01-01T12:00:00 ### 실시간 인기 검색어 GET http://localhost:8080/api/v1/events/search/top-ten diff --git a/api/api-event/src/main/java/com/pgms/apievent/event/repository/EventCustomRepositoryImpl.java b/api/api-event/src/main/java/com/pgms/apievent/event/repository/EventCustomRepositoryImpl.java index add7e322..5608dbae 100644 --- a/api/api-event/src/main/java/com/pgms/apievent/event/repository/EventCustomRepositoryImpl.java +++ b/api/api-event/src/main/java/com/pgms/apievent/event/repository/EventCustomRepositoryImpl.java @@ -1,26 +1,24 @@ package com.pgms.apievent.event.repository; -import static com.pgms.coredomain.domain.booking.QBooking.*; -import static com.pgms.coredomain.domain.event.QEvent.*; -import static com.pgms.coredomain.domain.event.QEventReview.*; -import static com.pgms.coredomain.domain.event.QEventTime.*; - -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.util.List; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.support.PageableExecutionUtils; -import org.springframework.stereotype.Repository; - import com.pgms.apievent.event.dto.request.EventPageRequest; import com.pgms.apievent.event.dto.response.EventResponse; import com.pgms.coredomain.domain.event.GenreType; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; - import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; + +import static com.pgms.coredomain.domain.booking.QBooking.booking; +import static com.pgms.coredomain.domain.event.QEvent.event; +import static com.pgms.coredomain.domain.event.QEventReview.eventReview; +import static com.pgms.coredomain.domain.event.QEventTime.eventTime; @Repository @RequiredArgsConstructor @@ -55,7 +53,7 @@ public Page getEventsPageByGenreSortedByRanking(EventPageRequest JPAQuery countQuery = jpaQueryFactory.select(event.count()) .from(event) - .join(eventTime).fetchJoin() + .leftJoin(eventTime).fetchJoin() .on(eventTime.event.eq(event)) .leftJoin(booking).fetchJoin() .on(eventTime.id.eq(booking.time.id)) diff --git a/api/api-event/src/main/java/com/pgms/apievent/eventSearch/controller/EventSearchController.java b/api/api-event/src/main/java/com/pgms/apievent/eventSearch/controller/EventSearchController.java index a1c20b82..bfd126ef 100644 --- a/api/api-event/src/main/java/com/pgms/apievent/eventSearch/controller/EventSearchController.java +++ b/api/api-event/src/main/java/com/pgms/apievent/eventSearch/controller/EventSearchController.java @@ -4,10 +4,12 @@ import com.pgms.apievent.eventSearch.dto.request.EventKeywordSearchRequest; import com.pgms.apievent.eventSearch.dto.response.RecentTop10KeywordsResponse; import com.pgms.apievent.eventSearch.service.EventSearchService; +import com.pgms.apievent.exception.EventException; import com.pgms.coredomain.response.ApiResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; @@ -15,6 +17,8 @@ import java.util.List; +import static com.pgms.apievent.exception.EventErrorCode.BINDING_FAILED_EXCEPTION; + @RestController @RequestMapping("/api/v1/events/search") @RequiredArgsConstructor @@ -24,7 +28,12 @@ public class EventSearchController { @GetMapping("/keyword") public ResponseEntity searchEventsByKeyword( - @ModelAttribute @Valid EventKeywordSearchRequest eventKeywordSearchRequest) { + @ModelAttribute @Valid EventKeywordSearchRequest eventKeywordSearchRequest, + BindingResult bindingResult) { + if(bindingResult.hasErrors()){ + throw new EventException(BINDING_FAILED_EXCEPTION); + } + PageResponseDto response = eventSearchService.searchEventsByKeyword(eventKeywordSearchRequest); return ResponseEntity.ok(ApiResponse.ok(response)); } diff --git a/api/api-event/src/main/java/com/pgms/apievent/exception/EventErrorCode.java b/api/api-event/src/main/java/com/pgms/apievent/exception/EventErrorCode.java index 005b154d..7be79673 100644 --- a/api/api-event/src/main/java/com/pgms/apievent/exception/EventErrorCode.java +++ b/api/api-event/src/main/java/com/pgms/apievent/exception/EventErrorCode.java @@ -20,7 +20,8 @@ public enum EventErrorCode implements BaseErrorCode { REVIEWER_MISMATCH_EXCEPTION("REVIEWER MISMATCH", HttpStatus.BAD_REQUEST, "리뷰 작성자가 일치하지 않습니다."), EVENT_REVIEW_NOT_FOUND("EVENT REVIEW NOT FOUND", HttpStatus.NOT_FOUND, "존재하지 않는 공연 리뷰입니다."), UNSUPPORTED_FILE_EXTENSION("UNSUPPORTED FILE EXTENSION", HttpStatus.BAD_REQUEST, "지원되지 않는 파일 확장자입니다."), - S3_UPLOAD_FAILED_EXCEPTION("S3 UPLOAD FAILED", HttpStatus.INTERNAL_SERVER_ERROR, "S3에 파일 업로드를 실패했습니다."); + S3_UPLOAD_FAILED_EXCEPTION("S3 UPLOAD FAILED", HttpStatus.INTERNAL_SERVER_ERROR, "S3에 파일 업로드를 실패했습니다."), + BINDING_FAILED_EXCEPTION("BINDING FAILED", HttpStatus.BAD_REQUEST, "ModelAttribute 필드 바인딩이 실패했습니다."); private final String errorCode; private final HttpStatus status; diff --git a/api/api-event/src/main/java/com/pgms/apievent/exception/EventGlobalExceptionHandler.java b/api/api-event/src/main/java/com/pgms/apievent/exception/EventGlobalExceptionHandler.java index ee91e687..0bf6b9a4 100644 --- a/api/api-event/src/main/java/com/pgms/apievent/exception/EventGlobalExceptionHandler.java +++ b/api/api-event/src/main/java/com/pgms/apievent/exception/EventGlobalExceptionHandler.java @@ -1,10 +1,8 @@ package com.pgms.apievent.exception; -import static com.pgms.apievent.exception.EventErrorCode.*; - -import java.util.List; -import java.util.Objects; - +import com.pgms.coredomain.domain.common.BaseErrorCode; +import com.pgms.coredomain.response.ErrorResponse; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.BindingResult; @@ -16,20 +14,15 @@ import com.pgms.coredomain.domain.common.BaseErrorCode; import com.pgms.coredomain.response.ErrorResponse; import com.pgms.coresecurity.security.exception.SecurityCustomException; +import java.util.List; +import java.util.Objects; -import lombok.extern.slf4j.Slf4j; +import static com.pgms.apievent.exception.EventErrorCode.VALIDATION_FAILED; @Slf4j @RestControllerAdvice public class EventGlobalExceptionHandler { - @ExceptionHandler(Exception.class) - protected ResponseEntity handleGlobalException(Exception ex) { - log.error(">>>>> Internal Server Error : {}", ex); - ErrorResponse errorResponse = new ErrorResponse("INTERNAL SERVER ERROR", ex.getMessage()); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); - } - @ExceptionHandler(EventException.class) protected ResponseEntity handleEventCustomException(EventException ex) { log.warn(">>>>> Custom Exception : {}", ex); @@ -55,5 +48,11 @@ protected ResponseEntity handleSecurityCustomException(SecurityCu log.warn(">>>>> SecurityCustomException : {}", ex); BaseErrorCode errorCode = ex.getErrorCode(); return ResponseEntity.status(errorCode.getStatus()).body(errorCode.getErrorResponse()); + + @ExceptionHandler(Exception.class) + protected ResponseEntity handleGlobalException(Exception ex) { + log.error(">>>>> Internal Server Error : {}", ex); + ErrorResponse errorResponse = new ErrorResponse("INTERNAL SERVER ERROR", ex.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); } } diff --git a/core/core-domain/src/main/java/com/pgms/coredomain/domain/booking/Booking.java b/core/core-domain/src/main/java/com/pgms/coredomain/domain/booking/Booking.java index 4fd971c3..962e120d 100644 --- a/core/core-domain/src/main/java/com/pgms/coredomain/domain/booking/Booking.java +++ b/core/core-domain/src/main/java/com/pgms/coredomain/domain/booking/Booking.java @@ -140,7 +140,7 @@ public boolean isCancelable() { return this.cancel == null && !this.time.getEvent().isStarted() && this.payment.isCancelable() - && this.status == BookingStatus.WAITING_FOR_PAYMENT || this.status == BookingStatus.PAYMENT_COMPLETED; + && (this.status == BookingStatus.WAITING_FOR_PAYMENT || this.status == BookingStatus.PAYMENT_COMPLETED); } public boolean isPaid() { diff --git a/core/core-infra-es/src/main/java/com/pgms/coreinfraes/repository/EventSearchQueryRepository.java b/core/core-infra-es/src/main/java/com/pgms/coreinfraes/repository/EventSearchQueryRepository.java index 37c1a01e..b6f38a0c 100644 --- a/core/core-infra-es/src/main/java/com/pgms/coreinfraes/repository/EventSearchQueryRepository.java +++ b/core/core-infra-es/src/main/java/com/pgms/coreinfraes/repository/EventSearchQueryRepository.java @@ -1,12 +1,21 @@ package com.pgms.coreinfraes.repository; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - +import co.elastic.clients.elasticsearch._types.FieldValue; +import co.elastic.clients.elasticsearch._types.Script; +import co.elastic.clients.elasticsearch._types.ScriptLanguage; +import co.elastic.clients.elasticsearch._types.aggregations.Aggregation; +import co.elastic.clients.elasticsearch._types.aggregations.AggregationBuilders; +import co.elastic.clients.elasticsearch._types.aggregations.StringTermsBucket; +import co.elastic.clients.elasticsearch._types.query_dsl.Query; +import co.elastic.clients.elasticsearch._types.query_dsl.*; +import co.elastic.clients.json.JsonData; +import com.pgms.coreinfraes.document.AccessLogDocument; +import com.pgms.coreinfraes.document.EventDocument; +import com.pgms.coreinfraes.dto.EventDocumentResponse; +import com.pgms.coreinfraes.dto.EventKeywordSearchDto; +import com.pgms.coreinfraes.dto.TopTenSearchResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.elasticsearch.client.elc.ElasticsearchAggregations; @@ -18,34 +27,15 @@ import org.springframework.data.elasticsearch.core.SearchHits; import org.springframework.data.elasticsearch.core.document.Document; import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; -import org.springframework.data.elasticsearch.core.query.FetchSourceFilter; -import org.springframework.data.elasticsearch.core.query.ScriptData; -import org.springframework.data.elasticsearch.core.query.ScriptedField; -import org.springframework.data.elasticsearch.core.query.SourceFilter; -import org.springframework.data.elasticsearch.core.query.UpdateQuery; +import org.springframework.data.elasticsearch.core.query.*; import org.springframework.stereotype.Repository; -import com.pgms.coreinfraes.document.AccessLogDocument; -import com.pgms.coreinfraes.document.EventDocument; -import com.pgms.coreinfraes.dto.EventDocumentResponse; -import com.pgms.coreinfraes.dto.EventKeywordSearchDto; -import com.pgms.coreinfraes.dto.TopTenSearchResponse; - -import co.elastic.clients.elasticsearch._types.FieldValue; -import co.elastic.clients.elasticsearch._types.Script; -import co.elastic.clients.elasticsearch._types.ScriptLanguage; -import co.elastic.clients.elasticsearch._types.aggregations.Aggregation; -import co.elastic.clients.elasticsearch._types.aggregations.AggregationBuilders; -import co.elastic.clients.elasticsearch._types.aggregations.StringTermsBucket; -import co.elastic.clients.elasticsearch._types.query_dsl.FieldValueFactorModifier; -import co.elastic.clients.elasticsearch._types.query_dsl.FieldValueFactorScoreFunction; -import co.elastic.clients.elasticsearch._types.query_dsl.FunctionScore; -import co.elastic.clients.elasticsearch._types.query_dsl.Query; -import co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders; -import co.elastic.clients.elasticsearch._types.query_dsl.TermsQueryField; -import co.elastic.clients.json.JsonData; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; @Slf4j @Repository @@ -161,7 +151,7 @@ private NativeQuery getKeywordSearchNativeQuery(EventKeywordSearchDto eventKeywo Query multiQuery = QueryBuilders.multiMatch() .query(eventKeywordSearchDto.keyword()) - .fields("title^1", "title_chosung^1", "description^1", "genreType^1") + .fields("title.ngram^1", "title_chosung^1", "description^1", "genreType^1") .minimumShouldMatch(MINIMUM_SHOULD_MATCH_PERCENTAGE) .build()._toQuery(); diff --git a/core/core-infra-es/src/main/resources/es/event-mapping.json b/core/core-infra-es/src/main/resources/es/event-mapping.json index 3ecb9982..8e91c66c 100644 --- a/core/core-infra-es/src/main/resources/es/event-mapping.json +++ b/core/core-infra-es/src/main/resources/es/event-mapping.json @@ -5,10 +5,19 @@ }, "title": { "type": "text", - "analyzer": "korean", "copy_to": [ "title_chosung" - ] + ], + "fields" : { + "kor" : { + "type": "text", + "analyzer": "korean" + }, + "ngram" : { + "type": "text", + "analyzer": "ngram_analyzer" + } + } }, "title_chosung": { "type": "text", diff --git a/core/core-infra-es/src/main/resources/es/event-setting.json b/core/core-infra-es/src/main/resources/es/event-setting.json index 823ae2c3..44d40477 100644 --- a/core/core-infra-es/src/main/resources/es/event-setting.json +++ b/core/core-infra-es/src/main/resources/es/event-setting.json @@ -1,9 +1,16 @@ { + "index" : { + "max_ngram_diff": 5 + }, "analysis": { "analyzer": { "korean": { "type": "nori" }, + "ngram_analyzer" : { + "type": "custom", + "tokenizer" : "my_ngram" + }, "chosung": { "type": "custom", "tokenizer": "standard", @@ -12,6 +19,19 @@ "hanhinsam_chosung" ] } + }, + "tokenizer": { + "my_ngram": { + "type": "ngram", + "min_gram": "2", + "max_gram": "5", + "token_chars": [ + "letter", + "digit", + "whitespace", + "punctuation" + ] + } } } } diff --git a/core/core-infra/src/main/resources/data.sql b/core/core-infra/src/main/resources/data.sql index 734d6aa0..a0a6be73 100644 --- a/core/core-infra/src/main/resources/data.sql +++ b/core/core-infra/src/main/resources/data.sql @@ -6,13 +6,13 @@ VALUES ('고척스카이돔', '서울 구로구 경인로 430'); INSERT INTO event (title, description, running_time, started_at, ended_at, rating, genre, average_score, thumbnail, booking_started_at, booking_ended_at, event_hall_id) VALUES ('BLACKPINK WORLD TOUR [BORN PINK] FINALE IN SEOUL', 'BLACKPINK WORLD TOUR [BORN PINK] FINALE IN SEOUL', 120, - '2024-01-05T10:00:00', '2025-01-06T12:00:00', '15세 이상 관람가', 'CONCERT', 0.0, + '2025-01-01T10:00:00', '2025-01-01T12:00:00', '15세 이상 관람가', 'CONCERT', 0.0, 'https://ticketimage.interpark.com/Play/image/large/23/23011804_p.gif', '2023-12-21T09:00:00', '2024-12-31T11:00:00', 1); -- EventTime INSERT INTO event_time (round, started_at, ended_at, event_id) -VALUES (1, '2024-01-01T10:00:00', '2024-01-01T12:00:00', 1); +VALUES (1, '2025-01-01T10:00:00', '2025-01-01T12:00:00', 1); -- EventSeatArea INSERT INTO event_seat_area (price, area_type, event_id) @@ -23,10 +23,10 @@ VALUES (100000, 'S', 1), INSERT INTO event_seat (name, status, event_seat_area_id, event_time_id) VALUES ('A1', 'BEING_BOOKED', 1, 1) , ('A2', 'BEING_BOOKED', 1, 1) - , ('A3', 'BEING_BOOKED', 1, 1) - , ('E1', 'BEING_BOOKED', 2, 1) - , ('E2', 'BEING_BOOKED', 2, 1) - , ('E3', 'BEING_BOOKED', 2, 1); + , ('A3', 'AVAILABLE', 1, 1) + , ('E1', 'AVAILABLE', 2, 1) + , ('E2', 'AVAILABLE', 2, 1) + , ('E3', 'AVAILABLE', 2, 1); -- EventReview INSERT INTO event_review (score, content, event_id) diff --git a/http/bingterpark.http b/http/bingterpark.http new file mode 100644 index 00000000..868fab7c --- /dev/null +++ b/http/bingterpark.http @@ -0,0 +1,235 @@ +## 어드민 + +### 어드민 로그인 +POST http://localhost:8081/api/v1/auth/admin/login +Content-Type: application/json + +{ + "email": "admin@example.com", + "password": "user1234" +} + +### 공연장 등록 +POST http://localhost:8080/api/v1/event-halls +Content-Type: application/json + +{ + "name": "잠실 종합운동장", + "address": "서울특별시 송파구", + "eventHallSeatCreateRequests": [ + { + "name": "1A" + }, + { + "name": "2A" + }, + { + "name": "1B" + }, + { + "name": "1Z" + } + ] +} + +### 공연 등록 +POST http://localhost:8080/api/v1/events +Content-Type: application/json + +{ + "title": "히사이시 조 영화음악 콘서트", + "description": "영화보다 빛나는 영화 음악, 그 여운을 다시 한 번! 지브리 스튜디오 미야자키 히야오의 모든 애니메이션 음악을 담당하며 현존하는 최고의 작곡가&지휘자 히사이시 조", + "runningTime": 100, + "startedAt": "2024-02-05T12:00:00", + "endedAt": "2024-08-05T12:00:00", + "viewRating": "12", + "genreType": "CONCERT", + "bookingStartedAt": "2024-01-05T12:00:00", + "bookingEndedAt": "2024-02-01T12:00:00", + "eventHallId": 2 +} + +### 공연 회차 생성 +POST http://localhost:8080/api/v1/event-times/2 +Content-Type: application/json + +{ + "round": 1, + "startedAt": "2024-02-05T12:00:00", + "endedAt": "2024-08-05T12:00:00" +} + +### 공연 좌석 구역 생성 +POST http://localhost:8080/api/v1/events/2/seat-area +Content-Type: application/json + +{ + "requests": [ + { + "seatAreaType": "R", + "price": 1000 + }, + { + "seatAreaType": "S", + "price": 2000 + } + ] +} + +### 공연 좌석 생성 +POST http://localhost:8080/api/v1/event-seats/events/2 +Content-Type: application/json + +[ + { + "name": "1A", + "status": "AVAILABLE", + "eventSeatAreaId": 3 + }, + { + "name": "1B", + "status": "AVAILABLE", + "eventSeatAreaId": 3 + }, + { + "name": "2A", + "status": "AVAILABLE", + "eventSeatAreaId": 3 + }, + { + "name": "1A", + "status": "AVAILABLE", + "eventSeatAreaId": 4 + }, + { + "name": "1B", + "status": "AVAILABLE", + "eventSeatAreaId": 4 + } +] + +### 공연 회차 좌석 목록 조회 +GET http://localhost:8080/api/v1/event-seats/event-times/2 + +=== + +## 멤버 + +### 회원가입 +POST http://localhost:8081/api/v1/members/signup +Content-Type: application/json + +{ + "email": "test@test.com", + "password": "password", + "passwordConfirm": "password", + "name": "name", + "phoneNumber": "01012345678", + "birthDate": "20000101", + "gender": "MALE", + "streetAddress": "서울특별시 광진구 능동로", + "detailAddress": "A동 101호", + "zipCode": "12345" +} + +### 로그인 +POST http://localhost:8081/api/v1/auth/members/login +Content-Type: application/json + +{ + "email": "test@test.com", + "password": "password" +} + +### oauth 회원가입/로그인 (Optional) +http://localhost:8081/login + +### 내 정보 확인 +GET http://localhost:8081/api/v1/members/me +Authorization: Bearer {{accessToken}} + +### 공연 목록 - 리뷰순 +GET http://localhost:8080/api/v1/events/sort/ranking?page=1&size=10&genreType=CONCERT&dateOffset=7 + +### 키워드 검색 +GET http://localhost:8080/api/v1/events/search/keyword?page=1&size=10&keyword=드라큘라&startedAt=2023-01-01T12:00:00 + +### 실시간 인기 검색어 +GET http://localhost:8080/api/v1/events/search/top-ten + +### 공연 회차의 등급별 남은 좌석수 조회 +GET http://localhost:8080/api/v1/event-seats/event-times/2/available-numbers + +=== + +## 예매 + +### 세션 아이디 발급 +POST http://localhost:8082/api/v1/bookings/issue-session-id +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJpYXQiOjE3MDQ2OTE0OTEsImV4cCI6MTgwNDY4MzI5MSwiYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn0.wFNSz2uwRa35jP1KihNlTOewVLgMMeg3ADQ5Kztl3QQ + +### 대기열 진입 +POST http://localhost:8082/api/v1/bookings/enter-queue +Content-Type: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJpYXQiOjE3MDQ2OTE0OTEsImV4cCI6MTgwNDY4MzI5MSwiYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn0.wFNSz2uwRa35jP1KihNlTOewVLgMMeg3ADQ5Kztl3QQ +Booking-Session-Id: {sessionId} + +{ + "eventId": 1 +} + +### 대기열 조회 +GET http://localhost:8082/api/v1/bookings/order-in-queue?eventId=1 +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJpYXQiOjE3MDQ2OTE0OTEsImV4cCI6MTgwNDY4MzI5MSwiYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn0.wFNSz2uwRa35jP1KihNlTOewVLgMMeg3ADQ5Kztl3QQ +Booking-Session-Id: {sessionId} + + +### 예매 토큰 발급 +POST http://localhost:8082/api/v1/bookings/issue-token +Content-Type: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJpYXQiOjE3MDQ2OTE0OTEsImV4cCI6MTgwNDY4MzI5MSwiYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn0.wFNSz2uwRa35jP1KihNlTOewVLgMMeg3ADQ5Kztl3QQ +Booking-Session-Id: {sessionId} + +{ + "eventId": 1 +} + +### [Optional] 대기열 이탈 +POST http://localhost:8082/api/v1/bookings/exit-queue +Content-Type: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJpYXQiOjE3MDQ2OTE0OTEsImV4cCI6MTgwNDY4MzI5MSwiYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn0.wFNSz2uwRa35jP1KihNlTOewVLgMMeg3ADQ5Kztl3QQ +Booking-Session-Id: {sessionId} + +{ + "eventId": 1 +} + +### 좌석 목록 조회 +GET http://localhost:8082/api/v1/seats?eventTimeId=1 +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJpYXQiOjE3MDQ2OTE0OTEsImV4cCI6MTgwNDY4MzI5MSwiYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn0.wFNSz2uwRa35jP1KihNlTOewVLgMMeg3ADQ5Kztl3QQ +Booking-Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJib29raW5nIiwiaWF0IjoxNzA0NzA0NTY0LCJleHAiOjQyMDAxNzA0NzA0NTY0LCJzZXNzaW9uSWQiOiJ7Pz8_Pz99In0.iKZaud5vfvsMzXmQzl1WaCweL9GL00U0iGzCg4_p3Zzei8Y7z19Ff_TTpxh9gLeB + +### 예매 ~ 결제 승인 (브라우저에서 진행해 주세요) +GET http://localhost:8082/bookings + +### [Optional] 예매 이탈 +POST http://localhost:8082/api/v1/bookings/bookingCreateTestId/exit +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJpYXQiOjE3MDQ2OTE0OTEsImV4cCI6MTgwNDY4MzI5MSwiYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn0.wFNSz2uwRa35jP1KihNlTOewVLgMMeg3ADQ5Kztl3QQ +Booking-Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJib29raW5nIiwiaWF0IjoxNzA0NzA0NTY0LCJleHAiOjQyMDAxNzA0NzA0NTY0LCJzZXNzaW9uSWQiOiJ7Pz8_Pz99In0.iKZaud5vfvsMzXmQzl1WaCweL9GL00U0iGzCg4_p3Zzei8Y7z19Ff_TTpxh9gLeB + +### 예매 취소 (브라우저에서 생성된 예매 번호로 진행해 주세요) +POST http://localhost:8082/api/v1/bookings/bookingCancelTestId/cancel +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJpYXQiOjE3MDQ2OTE0OTEsImV4cCI6MTgwNDY4MzI5MSwiYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn0.wFNSz2uwRa35jP1KihNlTOewVLgMMeg3ADQ5Kztl3QQ +Content-Type: application/json + +{ + "cancelReason": "변심" +} + +### 내 예매 내역 목록 조회 +GET http://localhost:8082/api/v1/bookings +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJpYXQiOjE3MDQ2OTE0OTEsImV4cCI6MTgwNDY4MzI5MSwiYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn0.wFNSz2uwRa35jP1KihNlTOewVLgMMeg3ADQ5Kztl3QQ + +### 내 예매 내역 상세 조회 +GET http://localhost:8082/api/v1/bookings/{bookingId} +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJpYXQiOjE3MDQ2OTE0OTEsImV4cCI6MTgwNDY4MzI5MSwiYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn0.wFNSz2uwRa35jP1KihNlTOewVLgMMeg3ADQ5Kztl3QQ